diff --git a/src/app/shared/rete-editor/rete-editor.html b/src/app/shared/rete-editor/rete-editor.html index ea1f296..9b09975 100644 --- a/src/app/shared/rete-editor/rete-editor.html +++ b/src/app/shared/rete-editor/rete-editor.html @@ -78,4 +78,13 @@ } + + @if (initialTypesLoading()) { +
+
+ + Loading node types... +
+
+ } diff --git a/src/app/shared/rete-editor/rete-editor.ts b/src/app/shared/rete-editor/rete-editor.ts index 9a55c04..9e36299 100644 --- a/src/app/shared/rete-editor/rete-editor.ts +++ b/src/app/shared/rete-editor/rete-editor.ts @@ -35,8 +35,10 @@ export class ReteEditor implements OnChanges, OnDestroy { private viewReady = false; private loadVersion = 0; private suppressDirtyEvents = false; + private typesLoadingPromise: Promise | null = null; creatingEmptyBlock = false; creatingEmptyBlockType = ''; + initialTypesLoading = signal(false); editorMode = signal<'standard' | 'select'>('standard'); selectionBox = signal<{ left: number; top: number; width: number; height: number } | null>(null); private selectionPointerId: number | null = null; @@ -222,6 +224,8 @@ export class ReteEditor implements OnChanges, OnDestroy { const host = this.container?.nativeElement as HTMLElement | undefined; if (!host) return; + await this.ensureNodeTypesLoaded(); + const currentVersion = ++this.loadVersion; this.editorMode.set('standard'); this.selectionPointerId = null; @@ -493,4 +497,30 @@ export class ReteEditor implements OnChanges, OnDestroy { await area.zoom(nextZoom, 0, 0); } + + private async ensureNodeTypesLoaded() { + if (this.blocksService.hasLoadedBlockTypes() && this.containersService.hasLoadedContainerTypes()) { + this.initialTypesLoading.set(false); + return; + } + + if (this.typesLoadingPromise) { + this.initialTypesLoading.set(true); + await this.typesLoadingPromise; + this.initialTypesLoading.set(false); + return; + } + + this.initialTypesLoading.set(true); + this.typesLoadingPromise = Promise.all([ + this.blocksService.getAllBlocksTypes(), + this.containersService.getAllContainerTypes() + ]).then(() => undefined) + .finally(() => { + this.typesLoadingPromise = null; + this.initialTypesLoading.set(false); + }); + + await this.typesLoadingPromise; + } } diff --git a/src/app/shared/task-execution-viewer/task-execution-viewer.css b/src/app/shared/task-execution-viewer/task-execution-viewer.css index 2e27083..96acd41 100644 --- a/src/app/shared/task-execution-viewer/task-execution-viewer.css +++ b/src/app/shared/task-execution-viewer/task-execution-viewer.css @@ -90,36 +90,51 @@ word-break: break-word; } -.execution-output-badge { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 52px; - border: 1px solid #bfdbfe; - border-radius: 10px; - background: linear-gradient(180deg, #eff6ff 0%, #dbeafe 100%); - color: #1d4ed8; - line-height: 1.2; - padding: 8px 12px; - text-align: center; +.execution-output-group { + border: 1px solid #e2e8f0; + border-radius: 12px; + background: #f8fafc; + padding: 12px; } -.execution-output-title { +.execution-output-group-title { font-size: 14px; font-weight: 800; + color: #0f172a; } -.execution-output-subtitle { - margin-top: 2px; +.execution-output-group-list { + margin-top: 10px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.execution-output-item { + border: 1px solid #dbe7f5; + border-radius: 10px; + background: #ffffff; + padding: 10px 12px; +} + +.execution-output-item-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.execution-output-item-name { + min-width: 0; font-size: 11px; - font-weight: 600; + font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: #2563eb; } .execution-output-preview { + margin-top: 8px; white-space: pre-wrap; word-break: break-word; color: #0f172a; @@ -127,6 +142,28 @@ line-height: 1.45; } +.execution-output-view-btn { + flex: 0 0 auto; + width: 22px; + height: 22px; + border: 1px solid #bfdbfe; + border-radius: 999px; + background: #eff6ff; + color: #1d4ed8; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; +} + +.execution-output-view-btn:hover { + background: #dbeafe; +} + +.execution-output-view-btn i { + font-size: 11px; +} + .execution-log-card { border: 1px solid #e2e8f0; border-radius: 12px; diff --git a/src/app/shared/task-execution-viewer/task-execution-viewer.html b/src/app/shared/task-execution-viewer/task-execution-viewer.html index ec7ff37..1a9a64c 100644 --- a/src/app/shared/task-execution-viewer/task-execution-viewer.html +++ b/src/app/shared/task-execution-viewer/task-execution-viewer.html @@ -190,25 +190,31 @@ } @else {
- @if (!executionOutputs().length) { + @if (!executionOutputGroups().length) {
No execution output available.
} @else { - @for (output of executionOutputs(); track output.key) { -
-
-
{{ output.title }}
-
{{ output.subtitle }}
+ @for (group of executionOutputGroups(); track group.nodeTitle) { +
+
{{ group.nodeTitle }}
+
+ @for (output of group.outputs; track output.key) { +
+
+
{{ output.outputName }}
+ @if (output.isLong) { + + } +
+
{{ output.preview }}
+
+ }
-
{{ output.preview }}
- @if (output.isLong) { - - }
} } @@ -225,8 +231,8 @@
-

{{ outputModal.title }}

-

{{ outputModal.subtitle }}

+

{{ outputPreviewTitle(outputModal) }}

+

{{ outputPreviewSubtitle(outputModal) }}

diff --git a/src/app/shared/task-execution-viewer/task-execution-viewer.ts b/src/app/shared/task-execution-viewer/task-execution-viewer.ts index a0bc087..dd6ae82 100644 --- a/src/app/shared/task-execution-viewer/task-execution-viewer.ts +++ b/src/app/shared/task-execution-viewer/task-execution-viewer.ts @@ -37,13 +37,18 @@ import { firstValueFrom } from 'rxjs'; type ExecutionOutputEntry = { key: string; - title: string; - subtitle: string; + nodeTitle: string; + outputName: string; value: string; preview: string; isLong: boolean; }; +type ExecutionOutputGroup = { + nodeTitle: string; + outputs: ExecutionOutputEntry[]; +}; + type ExecutionLogEntryView = ExecutionEventLogEntry & { messageText: string; levelText: string; @@ -58,6 +63,7 @@ type ExecutionLogEntryView = ExecutionEventLogEntry & { export class TaskExecutionViewerComponent implements OnDestroy { private static readonly TEXT_INPUT_DEBOUNCE_MS = 1200; private static readonly EVENTS_POLL_INTERVAL_MS = 5000; + private static readonly OUTPUT_PREVIEW_LIMIT = 80; private taskExecutionsService = inject(TaskExecutionsService); private humanInteractionDialog = inject(HumanInteractionDialogService); private settingsDialog = inject(NodeSettingsDialogService); @@ -302,18 +308,36 @@ export class TaskExecutionViewerComponent implements OnDestroy { return Object.entries(resultMap) .map(([key, rawValue]) => { const value = this.stringifyOutputValue(rawValue); - const isLong = value.length > 160; + const isLong = value.length > TaskExecutionViewerComponent.OUTPUT_PREVIEW_LIMIT; const label = this.formatExecutionOutputLabel(key, steps); return { key, - title: label.title, - subtitle: label.subtitle, + nodeTitle: label.nodeTitle, + outputName: label.outputName, value, - preview: isLong ? `${value.slice(0, 160)}...` : value, + preview: isLong ? `${value.slice(0, TaskExecutionViewerComponent.OUTPUT_PREVIEW_LIMIT)}...` : value, isLong }; }) - .sort((a, b) => a.title.localeCompare(b.title) || a.subtitle.localeCompare(b.subtitle)); + .sort((a, b) => a.nodeTitle.localeCompare(b.nodeTitle) || a.outputName.localeCompare(b.outputName)); + }); + + readonly executionOutputGroups = computed(() => { + const groups = new Map(); + + for (const output of this.executionOutputs()) { + if (!groups.has(output.nodeTitle)) { + groups.set(output.nodeTitle, []); + } + groups.get(output.nodeTitle)!.push(output); + } + + return Array.from(groups.entries()) + .map(([nodeTitle, outputs]) => ({ + nodeTitle, + outputs + })) + .sort((a, b) => a.nodeTitle.localeCompare(b.nodeTitle)); }); readonly inputsReadOnly = computed(() => { @@ -440,6 +464,16 @@ export class TaskExecutionViewerComponent implements OnDestroy { this.outputPreviewModal.set(output); } + outputPreviewTitle(output: ExecutionOutputEntry | null): string { + if (!output) return 'Output'; + return output.nodeTitle; + } + + outputPreviewSubtitle(output: ExecutionOutputEntry | null): string { + if (!output) return ''; + return output.outputName; + } + closeOutputPreview(event?: Event) { event?.preventDefault(); event?.stopPropagation(); @@ -800,10 +834,10 @@ export class TaskExecutionViewerComponent implements OnDestroy { private formatExecutionOutputLabel( key: string, steps: Record - ): { title: string; subtitle: string } { + ): { nodeTitle: string; outputName: string } { const separatorIndex = key.indexOf(':'); if (separatorIndex < 0) { - return { title: key, subtitle: 'Execution output' }; + return { nodeTitle: 'Execution output', outputName: key }; } const nodeId = key.slice(0, separatorIndex); @@ -812,8 +846,8 @@ export class TaskExecutionViewerComponent implements OnDestroy { const nodeName = this.stepTitle(step).trim(); return { - title: nodeName || key, - subtitle: outputName || 'Execution output' + nodeTitle: nodeName || key, + outputName: outputName || 'Execution output' }; }