Support containers in task execution views
This commit is contained in:
parent
1c1121d423
commit
2a42817ef3
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
<span class="container-node__port-label">
|
||||
<span class="container-node__port-label container-node__port-label--input">
|
||||
@if (inputDisplayLabelParts(input.key).context; as context) {
|
||||
<span class="container-node__port-context">{{ context }}</span>
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
<div class="container-node__port-title">Outputs</div>
|
||||
@for (output of outputs; track output.key) {
|
||||
<div class="container-node__port-row container-node__port-row--right">
|
||||
<span class="container-node__port-label">
|
||||
<span class="container-node__port-label container-node__port-label--output">
|
||||
@if (outputDisplayLabelParts(output.key).context; as context) {
|
||||
<span class="container-node__port-context">{{ context }}</span>
|
||||
}
|
||||
|
|
@ -118,34 +118,21 @@
|
|||
<span class="container-node__spinner" aria-hidden="true"></span>
|
||||
<span>Updating container...</span>
|
||||
</div>
|
||||
} @else if (subFlowBlockCount > 0) {
|
||||
<div class="container-node__snapshot" aria-hidden="true">
|
||||
@for (node of subFlowSnapshotNodes; track node.id) {
|
||||
<span
|
||||
class="container-node__snapshot-node"
|
||||
[class.container-node__snapshot-node--container]="node.family === 'container'"
|
||||
[style.left.%]="node.left"
|
||||
[style.top.%]="node.top"
|
||||
[style.width.px]="node.width"
|
||||
[style.height.px]="node.height">
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="container-node__dropzone-main">
|
||||
<span>{{ subFlowBlockCount }} nodes</span>
|
||||
<span>{{ subFlowConnectionCount }} connections</span>
|
||||
</div>
|
||||
<div class="container-node__dropzone-note">
|
||||
Drop a new selection here to replace the embedded subflow.
|
||||
</div>
|
||||
} @else {
|
||||
<div class="container-node__dropzone-main">
|
||||
<span class="container-node__dropzone-empty">Drop selected nodes here</span>
|
||||
<span class="container-node__dropzone-empty">Drag a flow here or import from flow</span>
|
||||
</div>
|
||||
<div class="container-node__dropzone-note">
|
||||
Use the selection box in the editor, then drag the floating selection badge into this area.
|
||||
</div>
|
||||
}
|
||||
@if (subFlowBlockCount > 0 && !replaceConfirmOpen && !isAssigning) {
|
||||
<div class="container-node__dropzone-actions">
|
||||
<button type="button" class="container-node__subflow-view" (pointerdown)="$event.stopPropagation()" (click)="openSubflowPreview($event)">
|
||||
View Flow
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (!isReadonly && !replaceConfirmOpen && !isAssigning) {
|
||||
<div class="container-node__dropzone-actions">
|
||||
<button type="button" class="container-node__import" (pointerdown)="$event.stopPropagation()" (click)="importSubflow($event)">
|
||||
|
|
@ -160,30 +147,6 @@
|
|||
}
|
||||
</div>
|
||||
|
||||
@if (subFlowBlockCount > 0) {
|
||||
<div class="container-node__subflow-section">
|
||||
<div class="container-node__subflow-head">
|
||||
<div class="container-node__subflow-title">Subflow Content</div>
|
||||
<button type="button" class="container-node__subflow-view" (pointerdown)="$event.stopPropagation()" (click)="openSubflowPreview($event)">
|
||||
View Flow
|
||||
</button>
|
||||
</div>
|
||||
<div class="container-node__subflow-preview">
|
||||
@for (node of subFlowPreviewNodes; track node.id) {
|
||||
<div class="container-node__subflow-chip" [class.container-node__subflow-chip--container]="node.family === 'container'">
|
||||
<span class="container-node__subflow-chip-name">{{ node.name }}</span>
|
||||
<span class="container-node__subflow-chip-type">{{ node.type }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (hasMoreSubFlowNodes) {
|
||||
<div class="container-node__subflow-more">
|
||||
+{{ subFlowBlockCount - subFlowPreviewNodes.length }} more
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (assignmentErrorMessage) {
|
||||
<div class="container-node__message container-node__message--error">
|
||||
{{ assignmentErrorMessage }}
|
||||
|
|
|
|||
|
|
@ -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$/, ''));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,10 @@
|
|||
}
|
||||
<span class="llm-pill llm-pill-input">
|
||||
<span class="llm-pill-meta">
|
||||
<span class="llm-pill-name">{{ inputDisplayLabel(input.key) }}</span>
|
||||
@if (inputDisplayLabelParts(input.key).context; as context) {
|
||||
<span class="llm-pill-context">{{ context }}</span>
|
||||
}
|
||||
<span class="llm-pill-name">{{ inputDisplayLabelParts(input.key).name }}</span>
|
||||
<span class="llm-pill-kind">{{ inputKindLabel(input.key) }}</span>
|
||||
</span>
|
||||
</span>
|
||||
|
|
@ -114,7 +117,10 @@
|
|||
<div class="llm-row llm-row-output">
|
||||
<span class="llm-pill llm-pill-output" [ngClass]="outputPillClass(output.key)">
|
||||
<span class="llm-pill-meta llm-pill-meta-output">
|
||||
<span class="llm-pill-name">{{ outputDisplayLabel(output.key) }}</span>
|
||||
@if (outputDisplayLabelParts(output.key).context; as context) {
|
||||
<span class="llm-pill-context">{{ context }}</span>
|
||||
}
|
||||
<span class="llm-pill-name">{{ outputDisplayLabelParts(output.key).name }}</span>
|
||||
<span class="llm-pill-kind">{{ outputKindLabel(output.key) }}</span>
|
||||
</span>
|
||||
</span>
|
||||
|
|
@ -228,6 +234,17 @@
|
|||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (hasViewableSubflow()) {
|
||||
<div class="llm-subflow-section">
|
||||
<div class="llm-param-row-head">
|
||||
<div class="llm-param-key">Subflow</div>
|
||||
</div>
|
||||
<button type="button" class="llm-subflow-view" (pointerdown)="$event.stopPropagation()" (click)="openSubflowPreview($event)">
|
||||
View Flow
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<string, any> | 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<string, any> | 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<string, any> | 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<string, unknown>;
|
||||
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<string, unknown> => !!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<string, unknown> => !!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<string, unknown>;
|
||||
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<string, any> | null): Set<string> {
|
||||
|
|
|
|||
|
|
@ -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 || '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue