mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-10 06:34:05 -08:00
feat(editor): Add execution indication to canvas v2 (no-changelog) (#9984)
This commit is contained in:
parent
32ddcee782
commit
90e3f56a9d
|
@ -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
|
||||||
|
|
|
@ -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}`]: {
|
||||||
|
|
|
@ -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 }]]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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)';
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }],
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
Loading…
Reference in a new issue