Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(editor): Indicate dirty nodes with yellow borders/connectors on canvas #13040

Draft
wants to merge 54 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
a2fba4a
feat(editor): indicate dirty nodes with yellow borders/connectors on …
autologie Feb 4, 2025
c94772b
refactorthe function to determine stale nodes and rename symbols
autologie Feb 5, 2025
665a639
add a safegard against infinite recursion
autologie Feb 5, 2025
f4c7b68
reuse calculated staleness in OutputPanel
autologie Feb 5, 2025
7b91858
Merge branch 'master' into sug-2-indicate-dirty-nodes-on-canvas
autologie Feb 7, 2025
7f11eba
add feature flag check for highlighting stale nodes
autologie Feb 7, 2025
ff38f90
add/update tooltips
autologie Feb 7, 2025
051b735
de-prioritize warning border color
autologie Feb 7, 2025
49eb5ae
rename 'stale' to 'dirty'
autologie Feb 7, 2025
4d72b06
add unit tests
autologie Feb 7, 2025
63520d5
revert describe.only()
autologie Feb 7, 2025
7de7d9f
change icon
autologie Feb 7, 2025
1c893d4
fix: if new partial execution is not enabled, nothing should change
autologie Feb 7, 2025
7313758
fix: nodes without runData should remain gray
autologie Feb 10, 2025
2cfa897
Merge branch 'master' into sug-2-indicate-dirty-nodes-on-canvas
autologie Feb 10, 2025
4bac7e1
refactor: use Set for tracking visited nodes
autologie Feb 11, 2025
3407147
fix: node injection/removal and toggling enabled/disabled should chan…
autologie Feb 11, 2025
466baee
refactor: use history as the place to store data for calculating dirt…
autologie Feb 11, 2025
86f37d6
Merge branch 'master' into sug-2-indicate-dirty-nodes-on-canvas
autologie Feb 12, 2025
0211987
remove duplicate store
autologie Feb 12, 2025
e9b71b3
cover all connection related dirtiness checks
autologie Feb 12, 2025
2846863
feat: updating pinned data makes downstream dirty
autologie Feb 12, 2025
0110c86
revert unrelated changes
autologie Feb 12, 2025
209a1bb
fix failing tests
autologie Feb 12, 2025
37ec0dd
fix: reflect dirtiness to strikethrough line
autologie Feb 12, 2025
8be6733
fix compile error
autologie Feb 12, 2025
ef16e1b
refactor
autologie Feb 13, 2025
7a90a46
add unit tests
autologie Feb 13, 2025
37c604a
add more tests
autologie Feb 14, 2025
d2d66d9
make dirty check work for sub nodes
autologie Feb 14, 2025
8b1c7d8
refactor test
autologie Feb 17, 2025
0d518c9
fix: output should not become dirty just by opening NDV
autologie Feb 17, 2025
f23e293
fix: should not crash if metadata for node does not exist
autologie Feb 17, 2025
b05dc6d
fix: warning icon is missing in stale output panel of an AI model node
autologie Feb 17, 2025
288fe58
Merge branch 'master' into sug-2-indicate-dirty-nodes-on-canvas
autologie Feb 17, 2025
13c8d1e
try different logic for highlighting dirty nodes
autologie Feb 18, 2025
d8f14c3
fix: send startNodes for now
autologie Feb 18, 2025
90e666e
change how dirtiness is shown
autologie Feb 18, 2025
92bee66
revert the condition to show icon in NDV
autologie Feb 19, 2025
52a4364
update tooltip text for dirty nodes
autologie Feb 19, 2025
8381774
Merge branch 'master' into sug-2-indicate-dirty-nodes-on-canvas
autologie Feb 19, 2025
e4c67d2
fix: handle sub nodes properly
autologie Feb 19, 2025
90f5944
change edge label color
autologie Feb 19, 2025
0ecbe75
do not change dirtiness when node is enabled
autologie Feb 19, 2025
1c632f6
make immediate downstream node dirty when pinned data changed
autologie Feb 19, 2025
e321529
fix label color
autologie Feb 19, 2025
cb84732
update dirtiness check for enabling sub node and removing a node
autologie Feb 19, 2025
f098ad3
Merge branch 'master' into sug-2-indicate-dirty-nodes-on-canvas
autologie Feb 19, 2025
ec25670
fix failing unit tests
autologie Feb 19, 2025
1a77ef4
add e2e test case
autologie Feb 20, 2025
b323833
fix typo
autologie Feb 20, 2025
33bd650
replace dirty node icon
autologie Feb 20, 2025
34a855e
fix: warning icon is missing in OutputPanel when there are multiple r…
autologie Feb 20, 2025
2d0fe73
feat: mark start node of the loop dirty
autologie Feb 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cypress/composables/ndv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ export function clickWorkflowCardContent(workflowName: string) {
getWorkflowCardContent(workflowName).click();
}

export function clickAssignmentCollectionAdd() {
cy.getByTestId('assignment-collection-drop-area').click();
}

export function assertNodeOutputHintExists() {
getNodeOutputHint().should('exist');
}
Expand Down
62 changes: 62 additions & 0 deletions cypress/e2e/40-manual-partial-execution.cy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
import {
clickAssignmentCollectionAdd,
clickGetBackToCanvas,
getNodeRunInfoStale,
getOutputTbodyCell,
} from '../composables/ndv';
import {
clickExecuteWorkflowButton,
getNodeByName,
getZoomToFitButton,
navigateToNewWorkflowPage,
openNode,
} from '../composables/workflow';
import { NDV, WorkflowPage } from '../pages';

const canvas = new WorkflowPage();
Expand Down Expand Up @@ -26,4 +39,53 @@ describe('Manual partial execution', () => {
ndv.getters.nodeRunTooltipIndicator().should('exist');
ndv.getters.outputRunSelector().should('not.exist'); // single run
});

describe('partial execution v2', () => {
beforeEach(() => {
cy.window().then((win) => {
win.localStorage.setItem('PartialExecution.version', '2');
});
navigateToNewWorkflowPage();
});

it('should execute from the first dirty node up to the current node', () => {
cy.createFixtureWorkflow('Test_workflow_partial_execution_v2.json');

getZoomToFitButton().click();

// First, execute the whole workflow
clickExecuteWorkflowButton();

getNodeByName('A').findChildByTestId('canvas-node-status-success').should('be.visible');
getNodeByName('B').findChildByTestId('canvas-node-status-success').should('be.visible');
getNodeByName('C').findChildByTestId('canvas-node-status-success').should('be.visible');
openNode('A');
getOutputTbodyCell(1, 0).invoke('text').as('before', { type: 'static' });
clickGetBackToCanvas();

// Change parameter of the node in the middle
openNode('B');
clickAssignmentCollectionAdd();
getNodeRunInfoStale().should('be.visible');
clickGetBackToCanvas();

getNodeByName('A').findChildByTestId('canvas-node-status-success').should('be.visible');
getNodeByName('B').findChildByTestId('canvas-node-status-warning').should('be.visible');
getNodeByName('C').findChildByTestId('canvas-node-status-success').should('be.visible');

// Partial execution
getNodeByName('C').findChildByTestId('execute-node-button').click();

getNodeByName('A').findChildByTestId('canvas-node-status-success').should('be.visible');
getNodeByName('B').findChildByTestId('canvas-node-status-success').should('be.visible');
getNodeByName('C').findChildByTestId('canvas-node-status-success').should('be.visible');
openNode('A');
getOutputTbodyCell(1, 0).invoke('text').as('after', { type: 'static' });

// Assert that 'A' ran only once by comparing its output
cy.get('@before').then((before) =>
cy.get('@after').then((after) => expect(before).to.equal(after)),
);
});
});
});
74 changes: 74 additions & 0 deletions cypress/fixtures/Test_workflow_partial_execution_v2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"nodes": [
{
"parameters": {
"rule": {
"interval": [{}]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [0, 0],
"id": "dcc1c5e1-c6c1-45f8-80d5-65c88d66d56e",
"name": "A"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "3d8f0810-84f0-41ce-a81b-0e7f04fd88cb",
"name": "",
"value": "",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [220, 0],
"id": "097ffa30-d37b-4de6-bd5c-ccd945f31df1",
"name": "B"
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [440, 0],
"id": "dc44e635-916f-4f76-a745-1add5762f730",
"name": "C"
}
],
"connections": {
"A": {
"main": [
[
{
"node": "B",
"type": "main",
"index": 0
}
]
]
},
"B": {
"main": [
[
{
"node": "C",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"instanceId": "b0d9447cff9c96796e4ac4f00fcd899b03cfac3ab3d4f748ae686d34881eae0c"
}
}
2 changes: 2 additions & 0 deletions packages/editor-ui/src/Interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,8 @@ export interface ITemplatesNode extends IVersionNode {

export interface INodeMetadata {
parametersLastUpdatedAt?: number;
pinnedDataLastUpdatedAt?: number;
pinnedDataLastRemovedAt?: number;
pristine: boolean;
}

Expand Down
12 changes: 11 additions & 1 deletion packages/editor-ui/src/components/InputPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import InputNodeSelect from './InputNodeSelect.vue';
import NodeExecuteButton from './NodeExecuteButton.vue';
import RunData from './RunData.vue';
import WireMeUp from './WireMeUp.vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';

type MappingMode = 'debugging' | 'mapping';

Expand Down Expand Up @@ -464,7 +465,16 @@ function activatePane() {
/>
</N8nTooltip>
<N8nText v-if="!readOnly" tag="div" size="small">
{{ i18n.baseText('ndv.input.noOutputData.hint') }}
<i18n-t keypath="ndv.input.noOutputData.hint">
<template #info>
<N8nTooltip placement="bottom">
<template #content>
{{ i18n.baseText('ndv.input.noOutputData.hint.tooltip') }}
</template>
<FontAwesomeIcon icon="question-circle" />
</N8nTooltip>
</template>
</i18n-t>
</N8nText>
</div>
<div v-else :class="$style.notConnected">
Expand Down
2 changes: 1 addition & 1 deletion packages/editor-ui/src/components/NodeSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ const credentialSelected = (updateInformation: INodeUpdatePropertiesInformation)

const nameChanged = (name: string) => {
if (node.value) {
historyStore.pushCommandToUndo(new RenameNodeCommand(node.value.name, name));
historyStore.pushCommandToUndo(new RenameNodeCommand(node.value.name, name, Date.now()));
}
valueChanged({
value: name,
Expand Down
15 changes: 14 additions & 1 deletion packages/editor-ui/src/components/OutputPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { useTelemetry } from '@/composables/useTelemetry';
import { useI18n } from '@/composables/useI18n';
import { waitingNodeTooltip } from '@/utils/executionUtils';
import { N8nRadioButtons, N8nText } from 'n8n-design-system';
import { useSettingsStore } from '@/stores/settings.store';
import { useNodeDirtiness } from '@/composables/useNodeDirtiness';

// Types

Expand Down Expand Up @@ -75,6 +77,8 @@ const uiStore = useUIStore();
const telemetry = useTelemetry();
const i18n = useI18n();
const { activeNode } = storeToRefs(ndvStore);
const settings = useSettingsStore();
const { dirtinessByName } = useNodeDirtiness();

// Composables

Expand Down Expand Up @@ -201,6 +205,11 @@ const staleData = computed(() => {
if (!node.value) {
return false;
}

if (settings.partialExecutionVersion === 2) {
return dirtinessByName.value[node.value.name] === 'parameters-updated';
}

const updatedAt = workflowsStore.getParametersLastUpdate(node.value.name);
if (!updatedAt || !runTaskData.value) {
return false;
Expand Down Expand Up @@ -352,7 +361,11 @@ const activatePane = () => {
{{ i18n.baseText(outputPanelEditMode.enabled ? 'ndv.output.edit' : 'ndv.output') }}
</span>
<RunInfo
v-if="hasNodeRun && !pinnedData.hasData.value && runsCount === 1"
v-if="
hasNodeRun &&
!pinnedData.hasData.value &&
(runsCount === 1 || (runsCount > 0 && staleData))
"
v-show="!outputPanelEditMode.enabled"
:task-data="runTaskData"
:has-stale-data="staleData"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { useNDVStore } from '@/stores/ndv.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useDocumentVisibility } from '@/composables/useDocumentVisibility';
import { N8nButton, N8nCallout, N8nNotice } from 'n8n-design-system';
import { isEqual } from 'lodash-es';

type Props = {
parameter: INodeProperties;
Expand Down Expand Up @@ -354,11 +355,14 @@ async function loadAndSetFieldsToMap(): Promise<void> {
}
return field;
});
state.paramValue = {
...state.paramValue,
schema: newSchema,
};
emitValueChanged();

if (!isEqual(newSchema, state.paramValue.schema)) {
state.paramValue = {
...state.paramValue,
schema: newSchema,
};
emitValueChanged();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ const edgeColor = computed(() => {
return 'var(--node-type-supplemental-color)';
} else if (props.selected) {
return 'var(--color-background-dark)';
} else if (status.value === 'warning') {
return 'var(--color-warning)';
} else {
return 'var(--color-foreground-xdark)';
}
Expand All @@ -87,7 +89,7 @@ const edgeClasses = computed(() => ({

const edgeLabelStyle = computed(() => ({
transform: `translate(0, ${isConnectorStraight.value ? '-100%' : '0%'})`,
color: edgeColor.value,
color: 'var(--color-text-base)',
}));

const isConnectorStraight = computed(() => renderData.value.isConnectorStraight);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ const runDataLabel = computed(() =>

const isHandlePlusVisible = computed(() => !isConnecting.value || isHovered.value);

const plusType = computed(() => (runDataTotal.value > 0 ? 'success' : 'default'));
const plusType = computed(() =>
renderOptions.value.dirtiness !== undefined
? 'warning'
: runDataTotal.value > 0
? 'success'
: 'default',
);

const plusLineSize = computed(
() =>
Expand Down Expand Up @@ -136,7 +142,7 @@ function onClickAdd() {
left: 50%;
transform: translate(-50%, -150%);
font-size: var(--font-size-xs);
color: var(--color-success);
color: var(--color-text-base);
}
</style>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const props = withDefaults(
handleClasses?: string;
plusSize?: number;
lineSize?: number;
type?: 'success' | 'secondary' | 'default';
type?: 'success' | 'warning' | 'secondary' | 'default';
}>(),
{
position: 'right',
Expand Down Expand Up @@ -163,6 +163,12 @@ function onClick(event: MouseEvent) {
}
}

&.warning {
.line {
stroke: var(--color-warning);
}
}

.plus {
&:hover {
cursor: pointer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const classes = computed(() => {
[$style.configurable]: renderOptions.value.configurable,
[$style.configuration]: renderOptions.value.configuration,
[$style.trigger]: renderOptions.value.trigger,
[$style.warning]: renderOptions.value.dirtiness !== undefined,
};
});

Expand Down Expand Up @@ -262,6 +263,10 @@ function openContextMenu(event: MouseEvent) {
border-color: var(--color-canvas-node-success-border-color, var(--color-success));
}

&.warning {
border-color: var(--color-warning);
}

&.error {
border-color: var(--color-canvas-node-error-border-color, var(--color-danger));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
<script setup lang="ts">
import { computed, useCssModule } from 'vue';
import { useCanvasNode } from '@/composables/useCanvasNode';
import { CanvasNodeRenderType } from '@/types';

const $style = useCssModule();

const { hasRunData } = useCanvasNode();
const { hasRunData, render } = useCanvasNode();

const classes = computed(() => {
return {
[$style.disabledStrikeThrough]: true,
[$style.success]: hasRunData.value,
[$style.warning]:
render.value.type === CanvasNodeRenderType.Default &&
render.value.options.dirtiness !== undefined,
};
});
</script>
Expand All @@ -31,4 +35,8 @@ const classes = computed(() => {
.success {
border-color: var(--color-success-light);
}

.warning {
border-color: var(--color-warning-tint-1);
}
</style>
Loading