From 2a42817ef3c17e817a3a2a8971bfaf035715e669 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Mon, 16 Mar 2026 16:50:44 +0100 Subject: [PATCH] Support containers in task execution views --- src/app/models/task-execution.ts | 22 ++- .../nodes/container-node/container-node.css | 138 ++++-------------- .../nodes/container-node/container-node.html | 57 ++------ .../nodes/container-node/container-node.ts | 65 +-------- .../nodes/task-step-node/task-step-node.css | 38 +++++ .../nodes/task-step-node/task-step-node.html | 21 ++- .../nodes/task-step-node/task-step-node.ts | 127 +++++++++++++++- .../task-execution-viewer.ts | 87 +++++++---- 8 files changed, 297 insertions(+), 258 deletions(-) diff --git a/src/app/models/task-execution.ts b/src/app/models/task-execution.ts index 2c53caf..b0d22aa 100644 --- a/src/app/models/task-execution.ts +++ b/src/app/models/task-execution.ts @@ -1,4 +1,4 @@ -import { FlowBlock, FlowBlockConnection, FlowPort } from './flow'; +import { FlowBlock, FlowBlockConnection, FlowContainer, FlowNode, FlowPort } from './flow'; export type TaskExecutionStatus = 'CREATED' | 'READY' | 'RUNNING' | 'WAITING' | 'SUCCESS' | 'ERROR'; export type TaskExecutionStatusGroup = 'INIT' | 'RUNNING' | 'FINAL'; @@ -38,7 +38,9 @@ export type TaskExecutionContext = { }; export type TaskExecutionStep = { - block: FlowBlock; + node?: FlowNode; + block?: FlowBlock; + container?: FlowContainer; id: string; inputs: TaskExecutionStepInput[]; outputs: TaskExecutionStepOutput[]; @@ -89,3 +91,19 @@ export function normalizeExecutionStatus(status: string | null | undefined): Tas if (normalized === 'ERROR' || normalized === 'FAILED') return 'ERROR'; return 'CREATED'; } + +export function getTaskExecutionStepNode(step: TaskExecutionStep | null | undefined): FlowNode | null { + if (!step) return null; + if (step.node) { + return step.node.nodeFamily === 'container' + ? { ...step.node, nodeFamily: 'container' } + : { ...step.node, nodeFamily: 'block' }; + } + if (step.container) { + return { ...step.container, nodeFamily: 'container' }; + } + if (step.block) { + return { ...step.block, nodeFamily: 'block' }; + } + return null; +} diff --git a/src/app/shared/nodes/container-node/container-node.css b/src/app/shared/nodes/container-node/container-node.css index 1e7a0ce..b5fef64 100644 --- a/src/app/shared/nodes/container-node/container-node.css +++ b/src/app/shared/nodes/container-node/container-node.css @@ -213,13 +213,29 @@ display: inline-flex; flex-direction: column; align-items: flex-start; + flex: 1 1 0; + min-width: 0; min-height: 36px; padding: 4px 10px; border-radius: 14px; - background: #e2e8f0; - color: #0f172a; + border: 1px solid transparent; font-size: 12px; font-weight: 600; + box-sizing: border-box; +} + +.container-node__port-label--input { + background: #ecfdf3; + color: #166534; + border-color: #bbf7d0; +} + +.container-node__port-label--output { + background: #fff1f2; + color: #9f1239; + border-color: #fecdd3; + align-items: flex-end; + text-align: right; } .container-node__port-context { @@ -243,7 +259,7 @@ .container-node__dropzone { margin: 0 14px 14px; - min-height: 152px; + min-height: 128px; border: 2px dashed #94a3b8; border-radius: 18px; padding: 18px 14px; @@ -256,41 +272,6 @@ text-align: center; } -.container-node__snapshot { - position: relative; - width: 100%; - height: 74px; - border-radius: 14px; - border: 1px solid rgba(148, 163, 184, 0.3); - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.88) 0%, rgba(226, 232, 240, 0.56) 100%); - overflow: hidden; -} - -.container-node__snapshot::before { - content: ""; - position: absolute; - inset: 0; - background-image: - linear-gradient(rgba(148, 163, 184, 0.12) 1px, transparent 1px), - linear-gradient(90deg, rgba(148, 163, 184, 0.12) 1px, transparent 1px); - background-size: 18px 18px; -} - -.container-node__snapshot-node { - position: absolute; - z-index: 1; - display: block; - border-radius: 5px; - background: linear-gradient(135deg, #0f766e 0%, #14b8a6 100%); - box-shadow: 0 2px 4px rgba(15, 23, 42, 0.16); -} - -.container-node__snapshot-node--container { - background: linear-gradient(135deg, #0284c7 0%, #38bdf8 100%); - border-radius: 6px; -} - .container-node__dropzone--active { border-color: #0f766e; background: @@ -308,6 +289,7 @@ font-size: 14px; font-weight: 700; color: #0f172a; + flex-wrap: wrap; } .container-node__replace-confirm { @@ -372,9 +354,12 @@ display: flex; justify-content: center; margin-top: 2px; + gap: 10px; + flex-wrap: wrap; } -.container-node__import { +.container-node__import, +.container-node__subflow-view { display: inline-flex; align-items: center; justify-content: center; @@ -387,85 +372,16 @@ color: #f8fafc; font-size: 11px; font-weight: 700; +} + +.container-node__import { box-shadow: 0 10px 18px rgba(15, 118, 110, 0.18); } -.container-node__subflow-preview { - display: grid; - gap: 8px; - margin-top: 4px; - text-align: left; -} - -.container-node__subflow-chip { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - padding: 8px 10px; - border: 1px solid rgba(148, 163, 184, 0.38); - border-radius: 12px; - background: rgba(255, 255, 255, 0.86); -} - -.container-node__subflow-chip--container { - border-color: rgba(14, 165, 233, 0.34); - background: rgba(224, 242, 254, 0.72); -} - -.container-node__subflow-chip-name { - min-width: 0; - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 12px; - font-weight: 700; - color: #0f172a; -} - -.container-node__subflow-chip-type { - font-size: 11px; - font-weight: 600; - color: #475569; - white-space: nowrap; -} - -.container-node__subflow-more { - font-size: 11px; - font-weight: 700; - color: #0f766e; - text-align: center; -} - .container-node__dropzone-empty { font-size: 16px; } -.container-node__subflow-section { - margin: 0 14px 14px; - border: 1px solid #dbe2ea; - border-radius: 16px; - padding: 12px; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(248, 250, 252, 0.96)); -} - -.container-node__subflow-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - margin-bottom: 10px; -} - -.container-node__subflow-title { - font-size: 11px; - font-weight: 800; - letter-spacing: 0.08em; - text-transform: uppercase; - color: #475569; -} - .container-node__subflow-view { border: 1px solid #bae6fd; border-radius: 999px; diff --git a/src/app/shared/nodes/container-node/container-node.html b/src/app/shared/nodes/container-node/container-node.html index 856c091..877e473 100644 --- a/src/app/shared/nodes/container-node/container-node.html +++ b/src/app/shared/nodes/container-node/container-node.html @@ -57,7 +57,7 @@ }" [emit]="emit"> - + @if (inputDisplayLabelParts(input.key).context; as context) { {{ context }} } @@ -72,7 +72,7 @@
Outputs
@for (output of outputs; track output.key) {
- + @if (outputDisplayLabelParts(output.key).context; as context) { {{ context }} } @@ -118,34 +118,21 @@ Updating container...
- } @else if (subFlowBlockCount > 0) { - -
- {{ subFlowBlockCount }} nodes - {{ subFlowConnectionCount }} connections -
-
- Drop a new selection here to replace the embedded subflow. -
} @else {
- Drop selected nodes here + Drag a flow here or import from flow
Use the selection box in the editor, then drag the floating selection badge into this area.
} + @if (subFlowBlockCount > 0 && !replaceConfirmOpen && !isAssigning) { +
+ +
+ } @if (!isReadonly && !replaceConfirmOpen && !isAssigning) {
- @if (subFlowBlockCount > 0) { -
-
-
Subflow Content
- -
-
- @for (node of subFlowPreviewNodes; track node.id) { -
- {{ node.name }} - {{ node.type }} -
- } - @if (hasMoreSubFlowNodes) { -
- +{{ subFlowBlockCount - subFlowPreviewNodes.length }} more -
- } -
-
- } - @if (assignmentErrorMessage) {
{{ assignmentErrorMessage }} diff --git a/src/app/shared/nodes/container-node/container-node.ts b/src/app/shared/nodes/container-node/container-node.ts index 3ecfdc2..d846053 100644 --- a/src/app/shared/nodes/container-node/container-node.ts +++ b/src/app/shared/nodes/container-node/container-node.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common'; import { Component, HostBinding, Input, inject } from '@angular/core'; -import { currentFlowPortValueKind, flowValueKindLabel, FlowBlock, FlowContainer, FlowData, FlowNode } from '@models/flow'; +import { currentFlowPortValueKind, flowValueKindLabel, FlowBlock, FlowContainer, FlowData } from '@models/flow'; import { NodeSettingsDialogService } from '@services/dialogs/node-settings-dialog'; import { ContainersService } from '@services/containers/containers'; import { FieldRetriever } from '@services/retriever/field-retriever'; @@ -8,7 +8,6 @@ import { ClassicPreset } from 'rete'; import { ReteModule } from 'rete-angular-plugin/21'; import { SubflowPreviewDialogService } from '@services/dialogs/subflow-preview-dialog'; import { EditorStateHolder } from '@stores/flow-editor'; -import { pathToLabel } from '../node-utility'; import { CONTAINER_SUBFLOW_DRAG_MIME } from './container-node-drag'; import { firstValueFrom } from 'rxjs'; @@ -111,55 +110,6 @@ export class ContainerNodeComponent { return (this.subFlow?.blocks?.length ?? 0) + (this.subFlow?.containers?.length ?? 0); } - get subFlowConnectionCount() { - return this.subFlow?.connections?.length ?? 0; - } - - get subFlowPreviewNodes() { - const subFlow = this.subFlow; - if (!subFlow) return []; - return [...(subFlow.blocks ?? []), ...(subFlow.containers ?? [])] - .slice(0, 6) - .map((node) => this.toPreviewNode(node)); - } - - get hasMoreSubFlowNodes() { - return this.subFlowBlockCount > this.subFlowPreviewNodes.length; - } - - get subFlowSnapshotNodes() { - const subFlow = this.subFlow; - if (!subFlow) return []; - - const allNodes = [...(subFlow.blocks ?? []), ...(subFlow.containers ?? [])]; - if (!allNodes.length) return []; - - const positioned = allNodes.map((node) => ({ - id: node.id, - family: node.nodeFamily === 'container' ? 'container' : 'block', - x: typeof node.position?.x === 'number' ? node.position.x : 0, - y: typeof node.position?.y === 'number' ? node.position.y : 0, - width: node.nodeFamily === 'container' ? 24 : 18, - height: node.nodeFamily === 'container' ? 14 : 12 - })); - - const minX = Math.min(...positioned.map((node) => node.x)); - const minY = Math.min(...positioned.map((node) => node.y)); - const maxX = Math.max(...positioned.map((node) => node.x + node.width)); - const maxY = Math.max(...positioned.map((node) => node.y + node.height)); - const spanX = Math.max(1, maxX - minX); - const spanY = Math.max(1, maxY - minY); - - return positioned.map((node) => ({ - id: node.id, - family: node.family, - left: 6 + ((node.x - minX) / spanX) * 100, - top: 6 + ((node.y - minY) / spanY) * 52, - width: node.width, - height: node.height - })); - } - get replaceConfirmOpen() { return Array.isArray(this.replaceConfirmSelection) && this.replaceConfirmSelection.length > 0; } @@ -432,15 +382,6 @@ export class ContainerNodeComponent { return ports.find((item: any) => item?.name === key) ?? null; } - private toPreviewNode(node: FlowNode) { - return { - id: node.id, - name: String(node.name ?? node.typeName ?? 'Node'), - type: this.nodeTypeLabel(String(node.typeName ?? 'Node')), - family: node.nodeFamily === 'container' ? 'container' : 'block' - }; - } - private normalizeSubFlowBlocks(raw: unknown): FlowBlock[] { if (!Array.isArray(raw)) return []; @@ -502,8 +443,4 @@ export class ContainerNodeComponent { }; } - private nodeTypeLabel(typeName: string) { - if (typeName === 'HumanInteractionBlock') return 'Human Task'; - return pathToLabel(typeName.replace(/Block$/, '')); - } } diff --git a/src/app/shared/nodes/task-step-node/task-step-node.css b/src/app/shared/nodes/task-step-node/task-step-node.css index 8d23617..8882241 100644 --- a/src/app/shared/nodes/task-step-node/task-step-node.css +++ b/src/app/shared/nodes/task-step-node/task-step-node.css @@ -349,6 +349,17 @@ white-space: nowrap; } +.llm-pill-context { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 9px; + line-height: 1; + font-weight: 700; + opacity: 0.7; +} + .llm-pill-kind { font-size: 10px; line-height: 1; @@ -663,3 +674,30 @@ text-overflow: ellipsis; white-space: nowrap; } + +.llm-subflow-section { + display: flex; + flex-direction: column; + gap: 6px; + padding: 10px 12px; + border: 1px solid #dbe7f5; + border-radius: 12px; + background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%); +} + +.llm-subflow-view { + align-self: flex-start; + border: 1px solid #bfdbfe; + border-radius: 999px; + background: #eff6ff; + color: #1d4ed8; + font-size: 11px; + font-weight: 700; + line-height: 1; + padding: 7px 10px; + cursor: pointer; +} + +.llm-subflow-view:hover { + background: #dbeafe; +} diff --git a/src/app/shared/nodes/task-step-node/task-step-node.html b/src/app/shared/nodes/task-step-node/task-step-node.html index 07f18f9..e96c657 100644 --- a/src/app/shared/nodes/task-step-node/task-step-node.html +++ b/src/app/shared/nodes/task-step-node/task-step-node.html @@ -100,7 +100,10 @@ } - {{ inputDisplayLabel(input.key) }} + @if (inputDisplayLabelParts(input.key).context; as context) { + {{ context }} + } + {{ inputDisplayLabelParts(input.key).name }} {{ inputKindLabel(input.key) }} @@ -114,7 +117,10 @@
- {{ outputDisplayLabel(output.key) }} + @if (outputDisplayLabelParts(output.key).context; as context) { + {{ context }} + } + {{ outputDisplayLabelParts(output.key).name }} {{ outputKindLabel(output.key) }} @@ -228,6 +234,17 @@ }
} + + @if (hasViewableSubflow()) { +
+
+
Subflow
+
+ +
+ }
diff --git a/src/app/shared/nodes/task-step-node/task-step-node.ts b/src/app/shared/nodes/task-step-node/task-step-node.ts index 9c15127..8b6a819 100644 --- a/src/app/shared/nodes/task-step-node/task-step-node.ts +++ b/src/app/shared/nodes/task-step-node/task-step-node.ts @@ -2,8 +2,10 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectorRef, Component, HostBinding, Input, inject } from '@angular/core'; import { ClassicPreset } from 'rete'; import { ReteModule } from 'rete-angular-plugin/21'; -import { FlowPort } from '@models/flow'; +import { FlowBlock, FlowContainer, FlowData, FlowPort } from '@models/flow'; import { BlocksService } from '@services/blocks/blocks'; +import { ContainersService } from '@services/containers/containers'; +import { SubflowPreviewDialogService } from '@services/dialogs/subflow-preview-dialog'; import { HumanInteractionDialogService } from '@services/dialogs/human-interaction-dialog'; import { TaskExecutionsService } from '@services/task-executions/task-executions'; import { @@ -56,6 +58,11 @@ type MainContentView = { parts: { text: string; isDynamicInput: boolean }[]; }; +type PortLabelParts = { + context: string | null; + name: string; +}; + @Component({ selector: 'app-task-step-node', imports: [CommonModule, ReteModule], @@ -67,6 +74,8 @@ type MainContentView = { }) export class TaskStepNodeComponent { private blocksService = inject(BlocksService); + private containersService = inject(ContainersService); + private subflowPreview = inject(SubflowPreviewDialogService); private cdr = inject(ChangeDetectorRef); private humanInteractionDialog = inject(HumanInteractionDialogService); private taskExecutionsService = inject(TaskExecutionsService); @@ -120,6 +129,7 @@ export class TaskStepNodeComponent { this.name = toStringOrNull(config['name']) ?? this.name; const arrayFieldPaths = new Set(this.arrayFieldDefinitions.map((definition) => definition.path)); const primitiveEntries = flattenPrimitiveValues(config) + .filter((entry) => !this.shouldHideConfigPath(entry.path)) .filter((entry) => !arrayFieldPaths.has(entry.path)); const visibleEntries = primitiveEntries.filter((entry) => this.isPathVisible(entry.path)); const mainContentEntries = visibleEntries @@ -225,10 +235,27 @@ export class TaskStepNodeComponent { return this.portKindLabel('output', outputKey); } + inputDisplayLabelParts(inputKey: string): PortLabelParts { + return this.toPortLabelParts(this.inputDisplayLabel(inputKey)); + } + + outputDisplayLabelParts(outputKey: string): PortLabelParts { + return this.toPortLabelParts(this.outputDisplayLabel(outputKey)); + } + hasMainContent(): boolean { return this.mainContentFields.length > 0; } + isContainerNode(): boolean { + return this.data?.data?.nodeFamily === 'container' || this.blockType === 'GenericContainer'; + } + + hasViewableSubflow(): boolean { + const subFlow = this.subFlow(); + return !!subFlow && ((subFlow.blocks?.length ?? 0) > 0 || (subFlow.containers?.length ?? 0) > 0 || (subFlow.connections?.length ?? 0) > 0); + } + formatDynamicInputToken(token: string): string { const match = token.match(/^\$\{\{\s*([^}]+?)\s*\}\}$/); return match ? match[1] : token; @@ -367,6 +394,14 @@ export class TaskStepNodeComponent { }); } + openSubflowPreview(event?: Event) { + event?.preventDefault(); + event?.stopPropagation(); + const subFlow = this.subFlow(); + if (!subFlow) return; + this.subflowPreview.open(subFlow, `${this.name || this.nodeTitle()} subflow`); + } + private get blockConfiguration(): Record | null { return this.data?.data?.specificConfiguration ?? null; } @@ -414,6 +449,29 @@ export class TaskStepNodeComponent { return port?.name ?? key; } + private toPortLabelParts(label: string): PortLabelParts { + if (!this.isContainerNode()) { + return { + context: null, + name: label + }; + } + + const trimmed = String(label ?? '').trim(); + const separatorIndex = trimmed.lastIndexOf('.'); + if (separatorIndex <= 0 || separatorIndex >= trimmed.length - 1) { + return { + context: null, + name: trimmed + }; + } + + return { + context: trimmed.slice(0, separatorIndex), + name: trimmed.slice(separatorIndex + 1) + }; + } + private portTypeLabel(port: FlowPort): string { const type = String(port.type ?? 'TEXT').toUpperCase(); return port.multiple ? `${type}[]` : type; @@ -441,8 +499,13 @@ export class TaskStepNodeComponent { const type = this.blockType; if (!type) return; - const blockType = await this.blocksService.getBlockType(type); - this.blockSchema = (blockType?.schema ?? null) as Record | null; + const nodeFamily = this.data?.data?.nodeFamily === 'container' || type === 'GenericContainer' + ? 'container' + : 'block'; + const typeDescriptor = nodeFamily === 'container' + ? await this.containersService.getContainerType(type) + : await this.blocksService.getBlockType(type); + this.blockSchema = (typeDescriptor?.schema ?? null) as Record | null; this.variablePlaceholderPaths = this.extractVariablePlaceholderPaths(this.blockSchema); this.arrayFieldDefinitions = this.extractArrayFieldDefinitions(this.blockSchema); this.mainContentPaths = this.extractMainContentPaths(this.blockSchema); @@ -522,7 +585,63 @@ export class TaskStepNodeComponent { }; walk(schema, ''); - return definitions; + return this.isContainerNode() + ? definitions.filter((definition) => !definition.path.startsWith('subFlow.')) + : definitions; + } + + private subFlow(): FlowData | null { + if (!this.isContainerNode()) return null; + + const raw = this.blockConfiguration?.['subFlow']; + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null; + + const candidate = raw as Record; + const blocks = this.normalizeSubFlowBlocks(candidate['blocks']); + const containers = this.normalizeSubFlowContainers(candidate['containers']); + const connections = Array.isArray(candidate['connections']) + ? candidate['connections'].filter((item): item is FlowData['connections'][number] => !!item && typeof item === 'object') + : []; + + if (!blocks.length && !containers.length && !connections.length) return null; + return { blocks, containers, connections }; + } + + private normalizeSubFlowBlocks(raw: unknown): FlowBlock[] { + if (!Array.isArray(raw)) return []; + + return raw + .filter((item): item is Record => !!item && typeof item === 'object' && !Array.isArray(item)) + .map((item) => ({ + ...item, + nodeFamily: 'block', + position: this.normalizePosition(item['position']) + })) as FlowBlock[]; + } + + private normalizeSubFlowContainers(raw: unknown): FlowContainer[] { + if (!Array.isArray(raw)) return []; + + return raw + .filter((item): item is Record => !!item && typeof item === 'object' && !Array.isArray(item)) + .map((item) => ({ + ...item, + nodeFamily: 'container', + position: this.normalizePosition(item['position']) + })) as FlowContainer[]; + } + + private normalizePosition(raw: unknown): { x: number; y: number } | undefined { + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined; + const value = raw as Record; + const x = typeof value['x'] === 'number' ? value['x'] : Number(value['x']); + const y = typeof value['y'] === 'number' ? value['y'] : Number(value['y']); + if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined; + return { x, y }; + } + + private shouldHideConfigPath(path: string): boolean { + return this.isContainerNode() && (path === 'subFlow' || path.startsWith('subFlow.')); } private extractMainContentPaths(schema: Record | null): Set { 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 c1a667c..9fac432 100644 --- a/src/app/shared/task-execution-viewer/task-execution-viewer.ts +++ b/src/app/shared/task-execution-viewer/task-execution-viewer.ts @@ -3,8 +3,17 @@ import { Component, computed, effect, inject, input, OnDestroy, signal } from '@ import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { areFlowValueKindsCompatible, FlowBlockConnection, FlowData, normalizeFlowPortValueKinds } from '@models/flow'; import { + areFlowValueKindsCompatible, + FlowBlock, + FlowBlockConnection, + FlowContainer, + FlowData, + FlowNode, + normalizeFlowPortValueKinds +} from '@models/flow'; +import { + getTaskExecutionStepNode, getExecutionStatusGroup, TaskExecution, TaskExecutionAuthorizationRequirement, @@ -118,31 +127,45 @@ export class TaskExecutionViewerComponent implements OnDestroy { const contextWarnings = this.execution()?.context.warnings ?? {}; const waitingSteps = this.execution()?.context.waitingSteps ?? []; const steps = this.stepsArray(); - const blocks = steps.map((step, index) => ({ - ...step.block, - nodeFamily: 'block' as const, - specificConfiguration: { - ...(step.block.specificConfiguration ?? {}), - __executionId: this.execution()?.id ?? null, - __executionNodeId: step.id, - __stepStatus: step.status, - __executionStatusGroup: executionStatusGroup, - __isWaitingStep: waitingSteps.includes(step.id), - __executionInputs: this.getExecutionInputValues(step, contextInputs), - __connectedInputs: this.getConnectedInputs(step), - __executionOutputs: this.getExecutionOutputValues(step, contextResults), - __connectedOutputs: this.getConnectedOutputs(step), - __executionErrors: this.getExecutionErrors(step.id, contextErrors), - __executionWarnings: this.getExecutionWarnings(step.id, contextWarnings) - }, - position: step.block.position ?? { - x: 120 + (index % 3) * 340, - y: 100 + Math.floor(index / 3) * 220 + const blocks: FlowBlock[] = []; + const containers: FlowContainer[] = []; + + for (const [index, step] of steps.entries()) { + const stepNode = getTaskExecutionStepNode(step); + if (!stepNode) continue; + + const executionNode: FlowNode = { + ...stepNode, + nodeFamily: stepNode.nodeFamily === 'container' ? 'container' : 'block', + specificConfiguration: { + ...(stepNode.specificConfiguration ?? {}), + __executionId: this.execution()?.id ?? null, + __executionNodeId: step.id, + __stepStatus: step.status, + __executionStatusGroup: executionStatusGroup, + __isWaitingStep: waitingSteps.includes(step.id), + __executionInputs: this.getExecutionInputValues(step, contextInputs), + __connectedInputs: this.getConnectedInputs(step), + __executionOutputs: this.getExecutionOutputValues(step, contextResults), + __connectedOutputs: this.getConnectedOutputs(step), + __executionErrors: this.getExecutionErrors(step.id, contextErrors), + __executionWarnings: this.getExecutionWarnings(step.id, contextWarnings) + }, + position: stepNode.position ?? { + x: 120 + (index % 3) * 340, + y: 100 + Math.floor(index / 3) * 220 + } + }; + + if (executionNode.nodeFamily === 'container') { + containers.push(executionNode); + } else { + blocks.push(executionNode); } - })); + } const connections = this.getExecutionConnections(steps); - return { blocks, containers: [], connections }; + return { blocks, containers, connections }; }); readonly formattedDuration = computed(() => { @@ -239,7 +262,7 @@ export class TaskExecutionViewerComponent implements OnDestroy { key, nodeId: step.id, inputName, - title: step.block.name, + title: this.stepTitle(step), subtitle: inputName, type: String(input.descriptor?.type ?? 'TEXT').toUpperCase(), multiple: Boolean(input.descriptor?.multiple), @@ -460,10 +483,10 @@ export class TaskExecutionViewerComponent implements OnDestroy { const nodeId = key.slice(0, separatorIndex); const outputName = key.slice(separatorIndex + 1); const step = steps[nodeId]; - const blockName = step?.block?.name?.trim(); + const nodeName = this.stepTitle(step).trim(); return { - title: blockName || key, + title: nodeName || key, subtitle: outputName || 'Execution output' }; } @@ -535,9 +558,9 @@ export class TaskExecutionViewerComponent implements OnDestroy { connections.push({ id, - sourceId: source.sourceStep.block.id, + sourceId: this.stepNodeId(source.sourceStep), sourceName: source.sourceOutputName, - targetId: targetStep.block.id, + targetId: this.stepNodeId(targetStep), targetName: input.descriptor.name }); } @@ -687,4 +710,12 @@ export class TaskExecutionViewerComponent implements OnDestroy { return String(value); } } + + private stepTitle(step: TaskExecutionStep | null | undefined): string { + return getTaskExecutionStepNode(step)?.name?.trim() || step?.id || 'Step'; + } + + private stepNodeId(step: TaskExecutionStep | null | undefined): string { + return getTaskExecutionStepNode(step)?.id || step?.id || ''; + } }