feat(editor): Add execution indication to canvas v2 (no-changelog) (#9984)

This commit is contained in:
Elias Meire 2024-07-10 10:36:00 +02:00 committed by GitHub
parent 32ddcee782
commit 90e3f56a9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 260 additions and 120 deletions

View file

@ -85,6 +85,7 @@
--color-node-executing-background: var(--color-primary-tint-3); --color-node-executing-background: var(--color-primary-tint-3);
--color-node-executing-other-background: var(--color-primary-tint-3); --color-node-executing-other-background: var(--color-primary-tint-3);
--color-node-pinned-border: var(--color-secondary); --color-node-pinned-border: var(--color-secondary);
--color-node-running-border: var(--color-primary);
--node-type-main-color: var(--prim-gray-490); --node-type-main-color: var(--prim-gray-490);
// Sticky // Sticky

View file

@ -10,7 +10,7 @@ export function createCanvasNodeData({
inputs = [], inputs = [],
outputs = [], outputs = [],
connections = { input: {}, output: {} }, connections = { input: {}, output: {} },
execution = {}, execution = { running: false },
issues = { items: [], visible: false }, issues = { items: [], visible: false },
pinnedData = { count: 0, visible: false }, pinnedData = { count: 0, visible: false },
runData = { count: 0, visible: false }, runData = { count: 0, visible: false },
@ -55,7 +55,7 @@ export function createCanvasNodeProps({
label = 'Test Node', label = 'Test Node',
selected = false, selected = false,
data = {}, data = {},
} = {}) { }: { id?: string; label?: string; selected?: boolean; data?: Partial<CanvasElementData> } = {}) {
return { return {
id, id,
label, label,
@ -69,7 +69,7 @@ export function createCanvasNodeProvide({
label = 'Test Node', label = 'Test Node',
selected = false, selected = false,
data = {}, data = {},
} = {}) { }: { id?: string; label?: string; selected?: boolean; data?: Partial<CanvasElementData> } = {}) {
const props = createCanvasNodeProps({ id, label, selected, data }); const props = createCanvasNodeProps({ id, label, selected, data });
return { return {
[`${CanvasNodeKey}`]: { [`${CanvasNodeKey}`]: {

View file

@ -107,6 +107,7 @@ describe('Canvas', () => {
}); });
await fireEvent.mouseUp(node, { view: window }); await fireEvent.mouseUp(node, { view: window });
expect(emitted()['update:node:position']).toEqual([['1', { x: 100, y: 100 }]]); // Snap to 16px grid: 100 -> 96
expect(emitted()['update:node:position']).toEqual([['1', { x: 96, y: 96 }]]);
}); });
}); });

View file

@ -126,6 +126,8 @@ function onClickPane(event: MouseEvent) {
:apply-changes="false" :apply-changes="false"
fit-view-on-init fit-view-on-init
pan-on-scroll pan-on-scroll
snap-to-grid
:snap-grid="[16, 16]"
:min-zoom="0.2" :min-zoom="0.2"
:max-zoom="2" :max-zoom="2"
data-test-id="canvas" data-test-id="canvas"

View file

@ -47,30 +47,4 @@ const { elements, connections } = useCanvasMapping({ workflow, workflowObject })
position: relative; position: relative;
display: block; display: block;
} }
.executionButtons {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
left: 50%;
transform: translateX(-50%);
bottom: var(--spacing-l);
width: auto;
@media (max-width: $breakpoint-2xs) {
bottom: 150px;
}
button {
display: flex;
justify-content: center;
align-items: center;
margin-left: 0.625rem;
&:first-child {
margin: 0;
}
}
}
</style> </style>

View file

@ -1,21 +1,25 @@
import { fireEvent } from '@testing-library/vue'; import { fireEvent } from '@testing-library/vue';
import CanvasEdge from './CanvasEdge.vue'; import CanvasEdge, { type CanvasEdgeProps } from './CanvasEdge.vue';
import { createComponentRenderer } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render';
import { createTestingPinia } from '@pinia/testing'; import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia'; import { setActivePinia } from 'pinia';
import { Position } from '@vue-flow/core';
const renderComponent = createComponentRenderer(CanvasEdge, { const DEFAULT_PROPS = {
props: { sourceX: 0,
sourceX: 0, sourceY: 0,
sourceY: 0, sourcePosition: Position.Top,
sourcePosition: 'top', targetX: 100,
targetX: 100, targetY: 100,
targetY: 100, targetPosition: Position.Bottom,
targetPosition: 'bottom', data: {
data: { status: undefined,
status: undefined, source: { index: 0, type: 'main' },
}, target: { index: 0, type: 'main' },
}, },
} satisfies Partial<CanvasEdgeProps>;
const renderComponent = createComponentRenderer(CanvasEdge, {
props: DEFAULT_PROPS,
}); });
beforeEach(() => { beforeEach(() => {
@ -42,4 +46,28 @@ describe('CanvasEdge', () => {
stroke: 'var(--color-foreground-xdark)', stroke: 'var(--color-foreground-xdark)',
}); });
}); });
it('should correctly style a running connection', () => {
const { container } = renderComponent({
props: { ...DEFAULT_PROPS, data: { ...DEFAULT_PROPS.data, status: 'running' } },
});
const edge = container.querySelector('.vue-flow__edge-path');
expect(edge).toHaveStyle({
stroke: 'var(--color-primary)',
});
});
it('should correctly style a pinned connection', () => {
const { container } = renderComponent({
props: { ...DEFAULT_PROPS, data: { ...DEFAULT_PROPS.data, status: 'pinned' } },
});
const edge = container.querySelector('.vue-flow__edge-path');
expect(edge).toHaveStyle({
stroke: 'var(--color-secondary)',
});
});
}); });

View file

@ -4,16 +4,17 @@ import type { Connection, EdgeProps } from '@vue-flow/core';
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from '@vue-flow/core'; import { BaseEdge, EdgeLabelRenderer, getBezierPath } from '@vue-flow/core';
import CanvasEdgeToolbar from './CanvasEdgeToolbar.vue'; import CanvasEdgeToolbar from './CanvasEdgeToolbar.vue';
import { computed, useCssModule } from 'vue'; import { computed, useCssModule } from 'vue';
import type { CanvasConnectionData } from '@/types';
const emit = defineEmits<{ const emit = defineEmits<{
delete: [connection: Connection]; delete: [connection: Connection];
}>(); }>();
const props = defineProps< export type CanvasEdgeProps = EdgeProps<CanvasConnectionData> & {
EdgeProps & { hovered?: boolean;
hovered?: boolean; };
}
>(); const props = defineProps<CanvasEdgeProps>();
const $style = useCssModule(); const $style = useCssModule();
@ -27,6 +28,8 @@ const statusColor = computed(() => {
return 'var(--color-success)'; return 'var(--color-success)';
} else if (status.value === 'pinned') { } else if (status.value === 'pinned') {
return 'var(--color-secondary)'; return 'var(--color-secondary)';
} else if (status.value === 'running') {
return 'var(--color-primary)';
} else { } else {
return 'var(--color-foreground-xdark)'; return 'var(--color-foreground-xdark)';
} }

View file

@ -53,11 +53,14 @@ describe('CanvasNode', () => {
...createCanvasNodeProps({ ...createCanvasNodeProps({
data: { data: {
inputs: [ inputs: [
{ type: NodeConnectionType.Main }, { type: NodeConnectionType.Main, index: 0 },
{ type: NodeConnectionType.Main }, { type: NodeConnectionType.Main, index: 0 },
{ type: NodeConnectionType.Main }, { type: NodeConnectionType.Main, index: 0 },
],
outputs: [
{ type: NodeConnectionType.Main, index: 0 },
{ type: NodeConnectionType.Main, index: 0 },
], ],
outputs: [{ type: NodeConnectionType.Main }, { type: NodeConnectionType.Main }],
}, },
}), }),
}, },

View file

@ -89,10 +89,10 @@ describe('CanvasNodeConfigurable', () => {
...createCanvasNodeProvide({ ...createCanvasNodeProvide({
data: { data: {
inputs: [ inputs: [
{ type: NodeConnectionType.Main }, { type: NodeConnectionType.Main, index: 0 },
{ type: NodeConnectionType.AiTool }, { type: NodeConnectionType.AiTool, index: 0 },
{ type: NodeConnectionType.AiDocument, required: true }, { type: NodeConnectionType.AiDocument, index: 0, required: true },
{ type: NodeConnectionType.AiMemory, required: true }, { type: NodeConnectionType.AiMemory, index: 0, required: true },
], ],
}, },
}), }),

View file

@ -32,7 +32,7 @@ describe('CanvasNodeDefault', () => {
provide: { provide: {
...createCanvasNodeProvide({ ...createCanvasNodeProvide({
data: { data: {
outputs: [{ type: NodeConnectionType.Main }], outputs: [{ type: NodeConnectionType.Main, index: 0 }],
}, },
}), }),
}, },
@ -50,9 +50,9 @@ describe('CanvasNodeDefault', () => {
...createCanvasNodeProvide({ ...createCanvasNodeProvide({
data: { data: {
outputs: [ outputs: [
{ type: NodeConnectionType.Main }, { type: NodeConnectionType.Main, index: 0 },
{ type: NodeConnectionType.Main }, { type: NodeConnectionType.Main, index: 0 },
{ type: NodeConnectionType.Main }, { type: NodeConnectionType.Main, index: 0 },
], ],
}, },
}), }),
@ -118,4 +118,17 @@ describe('CanvasNodeDefault', () => {
expect(getByText('Test Node').closest('.node')).not.toHaveClass('disabled'); expect(getByText('Test Node').closest('.node')).not.toHaveClass('disabled');
}); });
}); });
describe('running', () => {
it('should apply running class when node is running', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({ data: { execution: { running: true } } }),
},
},
});
expect(getByText('Test Node').closest('.node')).toHaveClass('running');
});
});
}); });

View file

@ -17,6 +17,7 @@ const {
isDisabled, isDisabled,
isSelected, isSelected,
hasPinnedData, hasPinnedData,
executionRunning,
hasRunData, hasRunData,
hasIssues, hasIssues,
} = useCanvasNode(); } = useCanvasNode();
@ -34,6 +35,7 @@ const classes = computed(() => {
[$style.success]: hasRunData.value, [$style.success]: hasRunData.value,
[$style.error]: hasIssues.value, [$style.error]: hasIssues.value,
[$style.pinned]: hasPinnedData.value, [$style.pinned]: hasPinnedData.value,
[$style.running]: executionRunning.value,
}; };
}); });
@ -94,6 +96,11 @@ const styles = computed(() => {
&.disabled { &.disabled {
border-color: var(--color-canvas-node-disabled-border-color, var(--color-foreground-base)); border-color: var(--color-canvas-node-disabled-border-color, var(--color-foreground-base));
} }
&.running {
background-color: var(--color-node-executing-background);
border-color: var(--color-canvas-node-running-border-color, var(--color-node-running-border));
}
} }
.label { .label {
@ -108,7 +115,7 @@ const styles = computed(() => {
.statusIcons { .statusIcons {
position: absolute; position: absolute;
top: calc(var(--canvas-node--height) - 24px); bottom: var(--spacing-2xs);
right: var(--spacing-xs); right: var(--spacing-2xs);
} }
</style> </style>

View file

@ -0,0 +1,40 @@
import CanvasNodeStatusIcons from './CanvasNodeStatusIcons.vue';
import { createComponentRenderer } from '@/__tests__/render';
import { createCanvasNodeProvide } from '@/__tests__/data';
import { createTestingPinia } from '@pinia/testing';
const renderComponent = createComponentRenderer(CanvasNodeStatusIcons, {
pinia: createTestingPinia(),
});
describe('CanvasNodeStatusIcons', () => {
it('should render correctly for a pinned node', () => {
const { getByTestId } = renderComponent({
global: {
provide: createCanvasNodeProvide({ data: { pinnedData: { count: 5, visible: true } } }),
},
});
expect(getByTestId('canvas-node-status-pinned')).toHaveTextContent('5');
});
it('should render correctly for a running node', () => {
const { getByTestId } = renderComponent({
global: {
provide: createCanvasNodeProvide({ data: { execution: { running: true } } }),
},
});
expect(getByTestId('canvas-node-status-running')).toBeInTheDocument();
});
it('should render correctly for a node that ran successfully', () => {
const { getByTestId } = renderComponent({
global: {
provide: createCanvasNodeProvide({ data: { runData: { count: 15, visible: true } } }),
},
});
expect(getByTestId('canvas-node-status-success')).toHaveTextContent('15');
});
});

View file

@ -13,6 +13,7 @@ const {
hasIssues, hasIssues,
executionStatus, executionStatus,
executionWaiting, executionWaiting,
executionRunning,
hasRunData, hasRunData,
runDataCount, runDataCount,
} = useCanvasNode(); } = useCanvasNode();
@ -21,46 +22,62 @@ const hideNodeIssues = computed(() => false); // @TODO Implement this
</script> </script>
<template> <template>
<div :class="$style.canvasNodeStatusIcons"> <div
<div v-if="hasIssues && !hideNodeIssues" :class="$style.issues" data-test-id="node-issues"> v-if="hasIssues && !hideNodeIssues"
<N8nTooltip :show-after="500" placement="bottom"> :class="[$style.status, $style.issues]"
<template #content> data-test-id="node-issues"
<TitledList :title="`${$locale.baseText('node.issues')}:`" :items="issues" /> >
</template> <N8nTooltip :show-after="500" placement="bottom">
<FontAwesomeIcon icon="exclamation-triangle" /> <template #content>
</N8nTooltip> <TitledList :title="`${$locale.baseText('node.issues')}:`" :items="issues" />
</div> </template>
<div v-else-if="executionWaiting || executionStatus === 'waiting'" class="waiting"> <FontAwesomeIcon icon="exclamation-triangle" />
<N8nTooltip placement="bottom"> </N8nTooltip>
<template #content> </div>
<div v-text="executionWaiting"></div> <div
</template> v-else-if="executionWaiting || executionStatus === 'waiting'"
<FontAwesomeIcon icon="clock" /> :class="[$style.status, $style.waiting]"
</N8nTooltip> >
</div> <N8nTooltip placement="bottom">
<span <template #content>
v-else-if="hasPinnedData && !nodeHelpers.isProductionExecutionPreview.value" <div v-text="executionWaiting"></div>
:class="$style.pinnedData" </template>
> <FontAwesomeIcon icon="clock" />
<FontAwesomeIcon icon="thumbtack" /> </N8nTooltip>
<span v-if="pinnedDataCount > 1" class="items-count"> {{ pinnedDataCount }}</span> </div>
</span> <div
<span v-else-if="executionStatus === 'unknown'"> v-else-if="hasPinnedData && !nodeHelpers.isProductionExecutionPreview.value"
<!-- Do nothing, unknown means the node never executed --> data-test-id="canvas-node-status-pinned"
</span> :class="[$style.status, $style.pinnedData]"
<span v-else-if="hasRunData" :class="$style.runData"> >
<FontAwesomeIcon icon="check" /> <FontAwesomeIcon icon="thumbtack" />
<span v-if="runDataCount > 1" :class="$style.itemsCount"> {{ runDataCount }}</span> <span v-if="pinnedDataCount > 1" :class="$style.count"> {{ pinnedDataCount }}</span>
</span> </div>
<div v-else-if="executionStatus === 'unknown'">
<!-- Do nothing, unknown means the node never executed -->
</div>
<div
v-else-if="executionRunning || executionStatus === 'running'"
data-test-id="canvas-node-status-running"
:class="[$style.status, $style.running]"
>
<FontAwesomeIcon icon="sync-alt" spin />
</div>
<div
v-else-if="hasRunData"
data-test-id="canvas-node-status-success"
:class="[$style.status, $style.runData]"
>
<FontAwesomeIcon icon="check" />
<span v-if="runDataCount > 1" :class="$style.count"> {{ runDataCount }}</span>
</div> </div>
</template> </template>
<style lang="scss" module> <style lang="scss" module>
.canvasNodeStatusIcons { .status {
display: flex; display: flex;
flex-direction: row;
align-items: center; align-items: center;
justify-content: flex-end; gap: var(--spacing-5xs);
} }
.runData { .runData {
@ -72,12 +89,16 @@ const hideNodeIssues = computed(() => false); // @TODO Implement this
color: var(--color-secondary); color: var(--color-secondary);
} }
.running {
color: var(--color-primary);
}
.issues { .issues {
color: var(--color-danger); color: var(--color-danger);
cursor: default; cursor: default;
} }
.itemsCount { .count {
font-size: var(--font-size-s); font-size: var(--font-size-s);
} }
</style> </style>

View file

@ -15,6 +15,7 @@ import {
} from '@/__tests__/mocks'; } from '@/__tests__/mocks';
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE } from '@/constants'; import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE } from '@/constants';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useWorkflowsStore } from '../stores/workflows.store';
beforeEach(() => { beforeEach(() => {
const pinia = createPinia(); const pinia = createPinia();
@ -80,6 +81,7 @@ describe('useCanvasMapping', () => {
disabled: false, disabled: false,
execution: { execution: {
status: 'new', status: 'new',
running: false,
waiting: undefined, waiting: undefined,
}, },
issues: { issues: {
@ -137,6 +139,27 @@ describe('useCanvasMapping', () => {
expect(elements.value[0]?.data?.disabled).toEqual(true); expect(elements.value[0]?.data?.disabled).toEqual(true);
}); });
it('should handle execution state', () => {
const manualTriggerNode = mockNode({
name: 'Manual Trigger',
type: MANUAL_TRIGGER_NODE_TYPE,
disabled: true,
});
const workflow = mock<IWorkflowDb>({
nodes: [manualTriggerNode],
});
const workflowObject = createTestWorkflowObject(workflow);
useWorkflowsStore().addExecutingNode(manualTriggerNode.name);
const { elements } = useCanvasMapping({
workflow: ref(workflow),
workflowObject: ref(workflowObject) as Ref<Workflow>,
});
expect(elements.value[0]?.data?.execution.running).toEqual(true);
});
it('should handle input and output connections', () => { it('should handle input and output connections', () => {
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2); const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
const workflow = mock<IWorkflowDb>({ const workflow = mock<IWorkflowDb>({
@ -217,6 +240,7 @@ describe('useCanvasMapping', () => {
target: setNode.id, target: setNode.id,
targetHandle: `inputs/${NodeConnectionType.Main}/0`, targetHandle: `inputs/${NodeConnectionType.Main}/0`,
type: 'canvas-edge', type: 'canvas-edge',
animated: false,
}, },
]); ]);
}); });
@ -264,6 +288,7 @@ describe('useCanvasMapping', () => {
target: setNode.id, target: setNode.id,
targetHandle: `inputs/${NodeConnectionType.AiTool}/0`, targetHandle: `inputs/${NodeConnectionType.AiTool}/0`,
type: 'canvas-edge', type: 'canvas-edge',
animated: false,
}, },
{ {
data: { data: {
@ -285,6 +310,7 @@ describe('useCanvasMapping', () => {
target: setNode.id, target: setNode.id,
targetHandle: `inputs/${NodeConnectionType.AiDocument}/1`, targetHandle: `inputs/${NodeConnectionType.AiDocument}/1`,
type: 'canvas-edge', type: 'canvas-edge',
animated: false,
}, },
]); ]);
}); });

View file

@ -110,6 +110,13 @@ export function useCanvasMapping({
}, {}), }, {}),
); );
const nodeExecutionRunningById = computed(() =>
workflow.value.nodes.reduce<Record<string, boolean>>((acc, node) => {
acc[node.id] = workflowsStore.isNodeExecuting(node.name);
return acc;
}, {}),
);
const nodeExecutionStatusById = computed(() => const nodeExecutionStatusById = computed(() =>
workflow.value.nodes.reduce<Record<string, ExecutionStatus>>((acc, node) => { workflow.value.nodes.reduce<Record<string, ExecutionStatus>>((acc, node) => {
acc[node.id] = acc[node.id] =
@ -221,6 +228,7 @@ export function useCanvasMapping({
execution: { execution: {
status: nodeExecutionStatusById.value[node.id], status: nodeExecutionStatusById.value[node.id],
waiting: nodeExecutionWaitingById.value[node.id], waiting: nodeExecutionWaitingById.value[node.id],
running: nodeExecutionRunningById.value[node.id],
}, },
runData: { runData: {
count: nodeExecutionRunDataById.value[node.id]?.length ?? 0, count: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
@ -255,6 +263,7 @@ export function useCanvasMapping({
data, data,
type, type,
label, label,
animated: data.status === 'running',
}; };
}); });
}); });
@ -266,7 +275,12 @@ export function useCanvasMapping({
let status: CanvasConnectionData['status']; let status: CanvasConnectionData['status'];
if (fromNode) { if (fromNode) {
if (nodePinnedDataById.value[fromNode.id] && nodeExecutionRunDataById.value[fromNode.id]) { if (nodeExecutionRunningById.value[fromNode.id]) {
status = 'running';
} else if (
nodePinnedDataById.value[fromNode.id] &&
nodeExecutionRunDataById.value[fromNode.id]
) {
status = 'pinned'; status = 'pinned';
} else if (nodeHasIssuesById.value[fromNode.id]) { } else if (nodeHasIssuesById.value[fromNode.id]) {
status = 'error'; status = 'error';

View file

@ -1,5 +1,6 @@
import { useCanvasNode } from '@/composables/useCanvasNode'; import { useCanvasNode } from '@/composables/useCanvasNode';
import { inject, ref } from 'vue'; import { inject, ref } from 'vue';
import type { CanvasNodeInjectionData } from '../types';
vi.mock('vue', async () => { vi.mock('vue', async () => {
const actual = await vi.importActual('vue'); const actual = await vi.importActual('vue');
@ -27,38 +28,38 @@ describe('useCanvasNode', () => {
expect(result.hasIssues.value).toBe(false); expect(result.hasIssues.value).toBe(false);
expect(result.executionStatus.value).toBeUndefined(); expect(result.executionStatus.value).toBeUndefined();
expect(result.executionWaiting.value).toBeUndefined(); expect(result.executionWaiting.value).toBeUndefined();
expect(result.executionRunning.value).toBe(false);
}); });
it('should return node data when node is provided', () => { it('should return node data when node is provided', () => {
const node = { const node = {
data: { data: ref({
value: { id: 'node1',
id: 'node1', type: 'nodeType1',
type: 'nodeType1', typeVersion: 1,
typeVersion: 1, disabled: true,
disabled: true, inputs: [{ type: 'main', index: 0 }],
inputs: ['input1'], outputs: [{ type: 'main', index: 0 }],
outputs: ['output1'], connections: { input: { '0': [] }, output: {} },
connections: { input: { '0': ['node2'] }, output: {} }, issues: { items: ['issue1'], visible: true },
issues: { items: ['issue1'], visible: true }, execution: { status: 'running', waiting: 'waiting', running: true },
execution: { status: 'running', waiting: false }, runData: { count: 1, visible: true },
runData: { count: 1, visible: true }, pinnedData: { count: 1, visible: true },
pinnedData: { count: 1, visible: true }, renderType: 'default',
renderType: 'default', }),
}, id: ref('1'),
},
label: ref('Node 1'), label: ref('Node 1'),
selected: ref(true), selected: ref(true),
}; } satisfies Partial<CanvasNodeInjectionData>;
vi.mocked(inject).mockReturnValue(node); vi.mocked(inject).mockReturnValue(node);
const result = useCanvasNode(); const result = useCanvasNode();
expect(result.label.value).toBe('Node 1'); expect(result.label.value).toBe('Node 1');
expect(result.inputs.value).toEqual(['input1']); expect(result.inputs.value).toEqual([{ type: 'main', index: 0 }]);
expect(result.outputs.value).toEqual(['output1']); expect(result.outputs.value).toEqual([{ type: 'main', index: 0 }]);
expect(result.connections.value).toEqual({ input: { '0': ['node2'] }, output: {} }); expect(result.connections.value).toEqual({ input: { '0': [] }, output: {} });
expect(result.isDisabled.value).toBe(true); expect(result.isDisabled.value).toBe(true);
expect(result.isSelected.value).toBe(true); expect(result.isSelected.value).toBe(true);
expect(result.pinnedDataCount.value).toBe(1); expect(result.pinnedDataCount.value).toBe(1);
@ -68,6 +69,7 @@ describe('useCanvasNode', () => {
expect(result.issues.value).toEqual(['issue1']); expect(result.issues.value).toEqual(['issue1']);
expect(result.hasIssues.value).toBe(true); expect(result.hasIssues.value).toBe(true);
expect(result.executionStatus.value).toBe('running'); expect(result.executionStatus.value).toBe('running');
expect(result.executionWaiting.value).toBe(false); expect(result.executionWaiting.value).toBe('waiting');
expect(result.executionRunning.value).toBe(true);
}); });
}); });

View file

@ -21,7 +21,9 @@ export function useCanvasNode() {
connections: { input: {}, output: {} }, connections: { input: {}, output: {} },
issues: { items: [], visible: false }, issues: { items: [], visible: false },
pinnedData: { count: 0, visible: false }, pinnedData: { count: 0, visible: false },
execution: {}, execution: {
running: false,
},
runData: { count: 0, visible: false }, runData: { count: 0, visible: false },
renderType: 'default', renderType: 'default',
}, },
@ -45,6 +47,7 @@ export function useCanvasNode() {
const executionStatus = computed(() => data.value.execution.status); const executionStatus = computed(() => data.value.execution.status);
const executionWaiting = computed(() => data.value.execution.waiting); const executionWaiting = computed(() => data.value.execution.waiting);
const executionRunning = computed(() => data.value.execution.running);
const runDataCount = computed(() => data.value.runData.count); const runDataCount = computed(() => data.value.runData.count);
const hasRunData = computed(() => data.value.runData.visible); const hasRunData = computed(() => data.value.runData.visible);
@ -65,5 +68,6 @@ export function useCanvasNode() {
hasIssues, hasIssues,
executionStatus, executionStatus,
executionWaiting, executionWaiting,
executionRunning,
}; };
} }

View file

@ -58,6 +58,7 @@ export interface CanvasElementData {
execution: { execution: {
status?: ExecutionStatus; status?: ExecutionStatus;
waiting?: string; waiting?: string;
running: boolean;
}; };
runData: { runData: {
count: number; count: number;
@ -72,7 +73,7 @@ export interface CanvasConnectionData {
source: CanvasConnectionPort; source: CanvasConnectionPort;
target: CanvasConnectionPort; target: CanvasConnectionPort;
fromNodeName?: string; fromNodeName?: string;
status?: 'success' | 'error' | 'pinned'; status?: 'success' | 'error' | 'pinned' | 'running';
} }
export type CanvasConnection = DefaultEdge<CanvasConnectionData>; export type CanvasConnection = DefaultEdge<CanvasConnectionData>;