mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(core): Improve debugging of sub-workflows (#11602)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
This commit is contained in:
parent
f4ca4b792f
commit
fd3254d587
29
cypress/composables/executions.ts
Normal file
29
cypress/composables/executions.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Getters
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const getExecutionsSidebar = () => cy.getByTestId('executions-sidebar');
|
||||||
|
|
||||||
|
export const getWorkflowExecutionPreviewIframe = () => cy.getByTestId('workflow-preview-iframe');
|
||||||
|
|
||||||
|
export const getExecutionPreviewBody = () =>
|
||||||
|
getWorkflowExecutionPreviewIframe()
|
||||||
|
.its('0.contentDocument.body')
|
||||||
|
.then((el) => cy.wrap(el));
|
||||||
|
|
||||||
|
export const getExecutionPreviewBodyNodes = () =>
|
||||||
|
getExecutionPreviewBody().findChildByTestId('canvas-node');
|
||||||
|
|
||||||
|
export const getExecutionPreviewBodyNodesByName = (name: string) =>
|
||||||
|
getExecutionPreviewBody().findChildByTestId('canvas-node').filter(`[data-name="${name}"]`).eq(0);
|
||||||
|
|
||||||
|
export function getExecutionPreviewOutputPanelRelatedExecutionLink() {
|
||||||
|
return getExecutionPreviewBody().findChildByTestId('related-execution-link');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const openExecutionPreviewNode = (name: string) =>
|
||||||
|
getExecutionPreviewBodyNodesByName(name).dblclick();
|
|
@ -48,10 +48,38 @@ export function getOutputTableRow(row: number) {
|
||||||
return getOutputTableRows().eq(row);
|
return getOutputTableRows().eq(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getOutputTableHeaders() {
|
||||||
|
return getOutputPanelDataContainer().find('table thead th');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOutputTableHeaderByText(text: string) {
|
||||||
|
return getOutputTableHeaders().contains(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOutputTbodyCell(row: number, col: number) {
|
||||||
|
return getOutputTableRows().eq(row).find('td').eq(col);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOutputRunSelector() {
|
||||||
|
return getOutputPanel().findChildByTestId('run-selector');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOutputRunSelectorInput() {
|
||||||
|
return getOutputRunSelector().find('input');
|
||||||
|
}
|
||||||
|
|
||||||
export function getOutputPanelTable() {
|
export function getOutputPanelTable() {
|
||||||
return getOutputPanelDataContainer().get('table');
|
return getOutputPanelDataContainer().get('table');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getOutputPanelItemsCount() {
|
||||||
|
return getOutputPanel().getByTestId('ndv-items-count');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOutputPanelRelatedExecutionLink() {
|
||||||
|
return getOutputPanel().getByTestId('related-execution-link');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Actions
|
* Actions
|
||||||
*/
|
*/
|
||||||
|
@ -90,3 +118,8 @@ export function setParameterSelectByContent(name: string, content: string) {
|
||||||
getParameterInputByName(name).realClick();
|
getParameterInputByName(name).realClick();
|
||||||
getVisibleSelect().find('.option-headline').contains(content).click();
|
getVisibleSelect().find('.option-headline').contains(content).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function changeOutputRunSelector(runName: string) {
|
||||||
|
getOutputRunSelector().click();
|
||||||
|
getVisibleSelect().find('.el-select-dropdown__item').contains(runName).click();
|
||||||
|
}
|
||||||
|
|
|
@ -76,6 +76,14 @@ export function getCanvasNodes() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSaveButton() {
|
||||||
|
return cy.getByTestId('workflow-save-button');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getZoomToFitButton() {
|
||||||
|
return cy.getByTestId('zoom-to-fit');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Actions
|
* Actions
|
||||||
*/
|
*/
|
||||||
|
@ -170,3 +178,19 @@ export function clickManualChatButton() {
|
||||||
export function openNode(nodeName: string) {
|
export function openNode(nodeName: string) {
|
||||||
getNodeByName(nodeName).dblclick();
|
getNodeByName(nodeName).dblclick();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function saveWorkflowOnButtonClick() {
|
||||||
|
cy.intercept('POST', '/rest/workflows').as('createWorkflow');
|
||||||
|
getSaveButton().should('contain', 'Save');
|
||||||
|
getSaveButton().click();
|
||||||
|
getSaveButton().should('contain', 'Saved');
|
||||||
|
cy.url().should('not.have.string', '/new');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pasteWorkflow(workflow: object) {
|
||||||
|
cy.get('body').paste(JSON.stringify(workflow));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clickZoomToFit() {
|
||||||
|
getZoomToFitButton().click();
|
||||||
|
}
|
||||||
|
|
|
@ -31,29 +31,31 @@ describe('NDV', () => {
|
||||||
|
|
||||||
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
|
|
||||||
ndv.getters.inputTableRow(1).realHover();
|
ndv.actions.dragMainPanelToRight();
|
||||||
|
ndv.getters.inputTableRow(1).realMouseMove(10, 1);
|
||||||
ndv.getters.outputTableRow(4).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.outputTableRow(4).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
|
|
||||||
ndv.getters.inputTableRow(2).realHover();
|
ndv.getters.inputTableRow(2).realMouseMove(10, 1);
|
||||||
ndv.getters.outputTableRow(2).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.outputTableRow(2).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
|
|
||||||
ndv.getters.inputTableRow(3).realHover();
|
ndv.getters.inputTableRow(3).realMouseMove(10, 1);
|
||||||
ndv.getters.outputTableRow(6).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.outputTableRow(6).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
|
|
||||||
// output to input
|
// output to input
|
||||||
ndv.getters.outputTableRow(1).realHover();
|
ndv.actions.dragMainPanelToLeft();
|
||||||
|
ndv.getters.outputTableRow(1).realMouseMove(10, 1);
|
||||||
ndv.getters.inputTableRow(4).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.inputTableRow(4).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
|
|
||||||
ndv.getters.outputTableRow(4).realHover();
|
ndv.getters.outputTableRow(4).realMouseMove(10, 1);
|
||||||
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
|
|
||||||
ndv.getters.outputTableRow(2).realHover();
|
ndv.getters.outputTableRow(2).realMouseMove(10, 1);
|
||||||
ndv.getters.inputTableRow(2).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.inputTableRow(2).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
|
|
||||||
ndv.getters.outputTableRow(6).realHover();
|
ndv.getters.outputTableRow(6).realMouseMove(10, 1);
|
||||||
ndv.getters.inputTableRow(3).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.inputTableRow(3).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
|
|
||||||
ndv.getters.outputTableRow(1).realHover();
|
ndv.getters.outputTableRow(1).realMouseMove(10, 1);
|
||||||
ndv.getters.inputTableRow(4).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.inputTableRow(4).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -75,31 +77,32 @@ describe('NDV', () => {
|
||||||
ndv.actions.switchInputMode('Table');
|
ndv.actions.switchInputMode('Table');
|
||||||
ndv.actions.switchOutputMode('Table');
|
ndv.actions.switchOutputMode('Table');
|
||||||
|
|
||||||
ndv.getters.backToCanvas().realHover(); // reset to default hover
|
ndv.getters.backToCanvas().realMouseMove(10, 1); // reset to default hover
|
||||||
ndv.getters.outputHoveringItem().should('not.exist');
|
ndv.getters.outputHoveringItem().should('not.exist');
|
||||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '1111');
|
ndv.getters.parameterExpressionPreview('value').should('include.text', '1111');
|
||||||
|
|
||||||
ndv.actions.selectInputNode('Set1');
|
ndv.actions.selectInputNode('Set1');
|
||||||
ndv.getters.backToCanvas().realHover(); // reset to default hover
|
ndv.getters.backToCanvas().realMouseMove(10, 1); // reset to default hover
|
||||||
|
|
||||||
ndv.getters.inputTableRow(1).should('have.text', '1000');
|
ndv.getters.inputTableRow(1).should('have.text', '1000');
|
||||||
|
|
||||||
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
|
|
||||||
ndv.getters.inputTableRow(1).realHover();
|
ndv.actions.dragMainPanelToRight();
|
||||||
cy.wait(50);
|
ndv.getters.inputTbodyCell(1, 0).realMouseMove(10, 1);
|
||||||
ndv.getters.outputHoveringItem().should('have.text', '1000');
|
ndv.getters.outputHoveringItem().should('have.text', '1000');
|
||||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
|
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
|
||||||
|
|
||||||
ndv.actions.selectInputNode('Sort');
|
ndv.actions.selectInputNode('Sort');
|
||||||
|
ndv.actions.dragMainPanelToLeft();
|
||||||
ndv.actions.changeOutputRunSelector('1 of 2 (6 items)');
|
ndv.actions.changeOutputRunSelector('1 of 2 (6 items)');
|
||||||
ndv.getters.backToCanvas().realHover(); // reset to default hover
|
ndv.getters.backToCanvas().realMouseMove(10, 1); // reset to default hover
|
||||||
|
|
||||||
ndv.getters.inputTableRow(1).should('have.text', '1111');
|
ndv.getters.inputTableRow(1).should('have.text', '1111');
|
||||||
|
|
||||||
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
ndv.getters.inputTableRow(1).realHover();
|
ndv.actions.dragMainPanelToRight();
|
||||||
cy.wait(50);
|
ndv.getters.inputTbodyCell(1, 0).realMouseMove(10, 1);
|
||||||
ndv.getters.outputHoveringItem().should('have.text', '1111');
|
ndv.getters.outputHoveringItem().should('have.text', '1111');
|
||||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '1111');
|
ndv.getters.parameterExpressionPreview('value').should('include.text', '1111');
|
||||||
});
|
});
|
||||||
|
@ -132,20 +135,22 @@ describe('NDV', () => {
|
||||||
|
|
||||||
ndv.getters.inputTableRow(1).should('have.text', '1111');
|
ndv.getters.inputTableRow(1).should('have.text', '1111');
|
||||||
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
|
|
||||||
|
ndv.actions.dragMainPanelToLeft();
|
||||||
ndv.getters.outputTableRow(1).should('have.text', '1111');
|
ndv.getters.outputTableRow(1).should('have.text', '1111');
|
||||||
ndv.getters.outputTableRow(1).realHover();
|
ndv.getters.outputTableRow(1).realMouseMove(10, 1);
|
||||||
|
|
||||||
ndv.getters.outputTableRow(3).should('have.text', '4444');
|
ndv.getters.outputTableRow(3).should('have.text', '4444');
|
||||||
ndv.getters.outputTableRow(3).realHover();
|
ndv.getters.outputTableRow(3).realMouseMove(10, 1);
|
||||||
|
|
||||||
ndv.getters.inputTableRow(3).should('have.text', '4444');
|
ndv.getters.inputTableRow(3).should('have.text', '4444');
|
||||||
ndv.getters.inputTableRow(3).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.inputTableRow(3).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
|
|
||||||
ndv.actions.changeOutputRunSelector('2 of 2 (6 items)');
|
ndv.actions.changeOutputRunSelector('2 of 2 (6 items)');
|
||||||
cy.wait(50);
|
|
||||||
|
|
||||||
ndv.getters.inputTableRow(1).should('have.text', '1000');
|
ndv.getters.inputTableRow(1).should('have.text', '1000');
|
||||||
ndv.getters.inputTableRow(1).realHover();
|
ndv.actions.dragMainPanelToRight();
|
||||||
|
ndv.getters.inputTableRow(1).realMouseMove(10, 1);
|
||||||
|
|
||||||
ndv.getters.outputTableRow(1).should('have.text', '1000');
|
ndv.getters.outputTableRow(1).should('have.text', '1000');
|
||||||
ndv.getters
|
ndv.getters
|
||||||
|
@ -155,7 +160,8 @@ describe('NDV', () => {
|
||||||
.should('equal', 'hovering-item');
|
.should('equal', 'hovering-item');
|
||||||
|
|
||||||
ndv.getters.outputTableRow(3).should('have.text', '2000');
|
ndv.getters.outputTableRow(3).should('have.text', '2000');
|
||||||
ndv.getters.outputTableRow(3).realHover();
|
ndv.actions.dragMainPanelToLeft();
|
||||||
|
ndv.getters.outputTableRow(3).realMouseMove(10, 1);
|
||||||
|
|
||||||
ndv.getters.inputTableRow(3).should('have.text', '2000');
|
ndv.getters.inputTableRow(3).should('have.text', '2000');
|
||||||
|
|
||||||
|
@ -175,14 +181,15 @@ describe('NDV', () => {
|
||||||
|
|
||||||
ndv.actions.switchOutputBranch('False Branch (2 items)');
|
ndv.actions.switchOutputBranch('False Branch (2 items)');
|
||||||
ndv.getters.outputTableRow(1).should('have.text', '8888');
|
ndv.getters.outputTableRow(1).should('have.text', '8888');
|
||||||
ndv.getters.outputTableRow(1).realHover();
|
ndv.actions.dragMainPanelToLeft();
|
||||||
|
ndv.getters.outputTableRow(1).realMouseMove(10, 1);
|
||||||
|
|
||||||
ndv.getters.inputTableRow(5).should('have.text', '8888');
|
ndv.getters.inputTableRow(5).should('have.text', '8888');
|
||||||
|
|
||||||
ndv.getters.inputTableRow(5).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.inputTableRow(5).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
|
|
||||||
ndv.getters.outputTableRow(2).should('have.text', '9999');
|
ndv.getters.outputTableRow(2).should('have.text', '9999');
|
||||||
ndv.getters.outputTableRow(2).realHover();
|
ndv.getters.outputTableRow(2).realMouseMove(10, 1);
|
||||||
|
|
||||||
ndv.getters.inputTableRow(6).should('have.text', '9999');
|
ndv.getters.inputTableRow(6).should('have.text', '9999');
|
||||||
|
|
||||||
|
@ -192,29 +199,35 @@ describe('NDV', () => {
|
||||||
|
|
||||||
workflowPage.actions.openNode('Set5');
|
workflowPage.actions.openNode('Set5');
|
||||||
|
|
||||||
|
ndv.actions.dragMainPanelToRight();
|
||||||
ndv.actions.switchInputBranch('True Branch');
|
ndv.actions.switchInputBranch('True Branch');
|
||||||
|
|
||||||
|
ndv.actions.dragMainPanelToLeft();
|
||||||
ndv.actions.changeOutputRunSelector('1 of 2 (2 items)');
|
ndv.actions.changeOutputRunSelector('1 of 2 (2 items)');
|
||||||
ndv.getters.outputTableRow(1).should('have.text', '8888');
|
ndv.getters.outputTableRow(1).should('have.text', '8888');
|
||||||
ndv.getters.outputTableRow(1).realHover();
|
ndv.getters.outputTableRow(1).realMouseMove(10, 1);
|
||||||
cy.wait(100);
|
|
||||||
ndv.getters.inputHoveringItem().should('not.exist');
|
ndv.getters.inputHoveringItem().should('not.exist');
|
||||||
|
|
||||||
ndv.getters.inputTableRow(1).should('have.text', '1111');
|
ndv.getters.inputTableRow(1).should('have.text', '1111');
|
||||||
ndv.getters.inputTableRow(1).realHover();
|
|
||||||
cy.wait(100);
|
ndv.actions.dragMainPanelToRight();
|
||||||
|
ndv.getters.inputTableRow(1).realMouseMove(10, 1);
|
||||||
ndv.getters.outputHoveringItem().should('not.exist');
|
ndv.getters.outputHoveringItem().should('not.exist');
|
||||||
|
|
||||||
ndv.actions.switchInputBranch('False Branch');
|
ndv.actions.switchInputBranch('False Branch');
|
||||||
ndv.getters.inputTableRow(1).should('have.text', '8888');
|
ndv.getters.inputTableRow(1).should('have.text', '8888');
|
||||||
ndv.getters.inputTableRow(1).realHover();
|
ndv.actions.dragMainPanelToRight();
|
||||||
|
ndv.getters.inputTableRow(1).realMouseMove(10, 1);
|
||||||
|
|
||||||
|
ndv.actions.dragMainPanelToLeft();
|
||||||
ndv.actions.changeOutputRunSelector('2 of 2 (4 items)');
|
ndv.actions.changeOutputRunSelector('2 of 2 (4 items)');
|
||||||
ndv.getters.outputTableRow(1).should('have.text', '1111');
|
ndv.getters.outputTableRow(1).should('have.text', '1111');
|
||||||
ndv.getters.outputTableRow(1).realHover();
|
ndv.getters.outputTableRow(1).realMouseMove(10, 1);
|
||||||
|
|
||||||
ndv.actions.changeOutputRunSelector('1 of 2 (2 items)');
|
ndv.actions.changeOutputRunSelector('1 of 2 (2 items)');
|
||||||
ndv.getters.inputTableRow(1).should('have.text', '8888');
|
ndv.getters.inputTableRow(1).should('have.text', '8888');
|
||||||
ndv.getters.inputTableRow(1).realHover();
|
ndv.actions.dragMainPanelToRight();
|
||||||
|
ndv.getters.inputTableRow(1).realMouseMove(10, 1);
|
||||||
ndv.getters.outputHoveringItem().should('have.text', '8888');
|
ndv.getters.outputHoveringItem().should('have.text', '8888');
|
||||||
// todo there's a bug here need to fix ADO-534
|
// todo there's a bug here need to fix ADO-534
|
||||||
// ndv.getters.outputHoveringItem().should('not.exist');
|
// ndv.getters.outputHoveringItem().should('not.exist');
|
||||||
|
|
140
cypress/e2e/47-subworkflow-debugging.cy.ts
Normal file
140
cypress/e2e/47-subworkflow-debugging.cy.ts
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
import {
|
||||||
|
getExecutionPreviewOutputPanelRelatedExecutionLink,
|
||||||
|
getExecutionsSidebar,
|
||||||
|
getWorkflowExecutionPreviewIframe,
|
||||||
|
openExecutionPreviewNode,
|
||||||
|
} from '../composables/executions';
|
||||||
|
import {
|
||||||
|
changeOutputRunSelector,
|
||||||
|
getOutputPanelItemsCount,
|
||||||
|
getOutputPanelRelatedExecutionLink,
|
||||||
|
getOutputRunSelectorInput,
|
||||||
|
getOutputTableHeaders,
|
||||||
|
getOutputTableRows,
|
||||||
|
getOutputTbodyCell,
|
||||||
|
} from '../composables/ndv';
|
||||||
|
import {
|
||||||
|
clickExecuteWorkflowButton,
|
||||||
|
clickZoomToFit,
|
||||||
|
getCanvasNodes,
|
||||||
|
navigateToNewWorkflowPage,
|
||||||
|
openNode,
|
||||||
|
pasteWorkflow,
|
||||||
|
saveWorkflowOnButtonClick,
|
||||||
|
} from '../composables/workflow';
|
||||||
|
import SUBWORKFLOW_DEBUGGING_EXAMPLE from '../fixtures/Subworkflow-debugging-execute-workflow.json';
|
||||||
|
|
||||||
|
describe('Subworkflow debugging', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
navigateToNewWorkflowPage();
|
||||||
|
pasteWorkflow(SUBWORKFLOW_DEBUGGING_EXAMPLE);
|
||||||
|
saveWorkflowOnButtonClick();
|
||||||
|
getCanvasNodes().should('have.length', 11);
|
||||||
|
clickZoomToFit();
|
||||||
|
|
||||||
|
clickExecuteWorkflowButton();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('can inspect sub executed workflow', () => {
|
||||||
|
it('(Run once with all items/ Wait for Sub-workflow completion) (default behavior)', () => {
|
||||||
|
openNode('Execute Workflow with param');
|
||||||
|
|
||||||
|
getOutputPanelItemsCount().should('contain.text', '2 items, 1 sub-execution');
|
||||||
|
getOutputPanelRelatedExecutionLink().should('contain.text', 'Inspect Sub-Execution');
|
||||||
|
getOutputPanelRelatedExecutionLink().should('have.attr', 'href');
|
||||||
|
|
||||||
|
// ensure workflow executed and waited on output
|
||||||
|
getOutputTableHeaders().should('have.length', 2);
|
||||||
|
getOutputTbodyCell(1, 0).should('have.text', 'world Natalie Moore');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('(Run once for each item/ Wait for Sub-workflow completion)', () => {
|
||||||
|
openNode('Execute Workflow with param1');
|
||||||
|
|
||||||
|
getOutputPanelItemsCount().should('contain.text', '2 items, 2 sub-execution');
|
||||||
|
getOutputPanelRelatedExecutionLink().should('not.exist');
|
||||||
|
|
||||||
|
// ensure workflow executed and waited on output
|
||||||
|
getOutputTableHeaders().should('have.length', 3);
|
||||||
|
getOutputTbodyCell(1, 0).find('a').should('have.attr', 'href');
|
||||||
|
getOutputTbodyCell(1, 1).should('have.text', 'world Natalie Moore');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('(Run once with all items/ Wait for Sub-workflow completion)', () => {
|
||||||
|
openNode('Execute Workflow with param2');
|
||||||
|
|
||||||
|
getOutputPanelItemsCount().should('not.exist');
|
||||||
|
getOutputPanelRelatedExecutionLink().should('contain.text', 'Inspect Sub-Execution');
|
||||||
|
getOutputPanelRelatedExecutionLink().should('have.attr', 'href');
|
||||||
|
|
||||||
|
// ensure workflow executed but returned same data as input
|
||||||
|
getOutputRunSelectorInput().should('have.value', '2 of 2 (3 items, 1 sub-execution)');
|
||||||
|
getOutputTableHeaders().should('have.length', 6);
|
||||||
|
getOutputTableHeaders().eq(0).should('have.text', 'uid');
|
||||||
|
getOutputTableRows().should('have.length', 4);
|
||||||
|
getOutputTbodyCell(1, 1).should('include.text', 'Jon_Ebert@yahoo.com');
|
||||||
|
|
||||||
|
changeOutputRunSelector('1 of 2 (2 items, 1 sub-execution)');
|
||||||
|
getOutputRunSelectorInput().should('have.value', '1 of 2 (2 items, 1 sub-execution)');
|
||||||
|
getOutputTableHeaders().should('have.length', 6);
|
||||||
|
getOutputTableHeaders().eq(0).should('have.text', 'uid');
|
||||||
|
getOutputTableRows().should('have.length', 3);
|
||||||
|
getOutputTbodyCell(1, 1).should('include.text', 'Terry.Dach@hotmail.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('(Run once for each item/ Wait for Sub-workflow completion)', () => {
|
||||||
|
openNode('Execute Workflow with param3');
|
||||||
|
|
||||||
|
// ensure workflow executed but returned same data as input
|
||||||
|
getOutputRunSelectorInput().should('have.value', '2 of 2 (3 items, 3 sub-executions)');
|
||||||
|
getOutputTableHeaders().should('have.length', 7);
|
||||||
|
getOutputTableHeaders().eq(1).should('have.text', 'uid');
|
||||||
|
getOutputTableRows().should('have.length', 4);
|
||||||
|
getOutputTbodyCell(1, 0).find('a').should('have.attr', 'href');
|
||||||
|
getOutputTbodyCell(1, 2).should('include.text', 'Jon_Ebert@yahoo.com');
|
||||||
|
|
||||||
|
changeOutputRunSelector('1 of 2 (2 items, 2 sub-executions)');
|
||||||
|
getOutputRunSelectorInput().should('have.value', '1 of 2 (2 items, 2 sub-executions)');
|
||||||
|
getOutputTableHeaders().should('have.length', 7);
|
||||||
|
getOutputTableHeaders().eq(1).should('have.text', 'uid');
|
||||||
|
getOutputTableRows().should('have.length', 3);
|
||||||
|
|
||||||
|
getOutputTbodyCell(1, 0).find('a').should('have.attr', 'href');
|
||||||
|
getOutputTbodyCell(1, 2).should('include.text', 'Terry.Dach@hotmail.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can inspect parent executions', () => {
|
||||||
|
cy.url().then((workflowUrl) => {
|
||||||
|
openNode('Execute Workflow with param');
|
||||||
|
|
||||||
|
getOutputPanelItemsCount().should('contain.text', '2 items, 1 sub-execution');
|
||||||
|
getOutputPanelRelatedExecutionLink().should('contain.text', 'Inspect Sub-Execution');
|
||||||
|
getOutputPanelRelatedExecutionLink().should('have.attr', 'href');
|
||||||
|
|
||||||
|
// ensure workflow executed and waited on output
|
||||||
|
getOutputTableHeaders().should('have.length', 2);
|
||||||
|
getOutputTbodyCell(1, 0).should('have.text', 'world Natalie Moore');
|
||||||
|
|
||||||
|
// cypress cannot handle new tabs so removing it
|
||||||
|
getOutputPanelRelatedExecutionLink().invoke('removeAttr', 'target').click();
|
||||||
|
|
||||||
|
getExecutionsSidebar().should('be.visible');
|
||||||
|
getWorkflowExecutionPreviewIframe().should('be.visible');
|
||||||
|
openExecutionPreviewNode('Execute Workflow Trigger');
|
||||||
|
|
||||||
|
getExecutionPreviewOutputPanelRelatedExecutionLink().should(
|
||||||
|
'include.text',
|
||||||
|
'Inspect Parent Execution',
|
||||||
|
);
|
||||||
|
|
||||||
|
getExecutionPreviewOutputPanelRelatedExecutionLink()
|
||||||
|
.invoke('removeAttr', 'target')
|
||||||
|
.click({ force: true });
|
||||||
|
|
||||||
|
cy.url().then((currentUrl) => {
|
||||||
|
expect(currentUrl === workflowUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
354
cypress/fixtures/Subworkflow-debugging-execute-workflow.json
Normal file
354
cypress/fixtures/Subworkflow-debugging-execute-workflow.json
Normal file
|
@ -0,0 +1,354 @@
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "08ce71ad998aeaade0abedb8dd96153d8eaa03fcb84cfccc1530095bf9ee478e"
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "4535ce3e-280e-49b0-8854-373472ec86d1",
|
||||||
|
"name": "When clicking ‘Test workflow’",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [80, 860]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"category": "randomData",
|
||||||
|
"randomDataSeed": "0",
|
||||||
|
"randomDataCount": 2
|
||||||
|
},
|
||||||
|
"id": "d7fba18a-d51f-4509-af45-68cd9425ac6b",
|
||||||
|
"name": "DebugHelper1",
|
||||||
|
"type": "n8n-nodes-base.debugHelper",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [280, 860]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"source": "parameter",
|
||||||
|
"workflowJson": "{\n \"meta\": {\n \"instanceId\": \"a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0\"\n },\n \"nodes\": [\n {\n \"parameters\": {},\n \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n \"typeVersion\": 1,\n \"position\": [\n 0,\n 0\n ],\n \"id\": \"00600a51-e63a-4b6e-93f5-f01d50a21e0c\",\n \"name\": \"Execute Workflow Trigger\"\n },\n {\n \"parameters\": {\n \"assignments\": {\n \"assignments\": [\n {\n \"id\": \"87ff01af-2e28-48da-ae6c-304040200b15\",\n \"name\": \"hello\",\n \"value\": \"=world {{ $json.firstname }} {{ $json.lastname }}\",\n \"type\": \"string\"\n }\n ]\n },\n \"includeOtherFields\": false,\n \"options\": {}\n },\n \"type\": \"n8n-nodes-base.set\",\n \"typeVersion\": 3.4,\n \"position\": [\n 280,\n 0\n ],\n \"id\": \"642219a1-d655-4a30-af5c-fcccbb690322\",\n \"name\": \"Edit Fields\"\n }\n ],\n \"connections\": {\n \"Execute Workflow Trigger\": {\n \"main\": [\n [\n {\n \"node\": \"Edit Fields\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n }\n },\n \"pinData\": {}\n}",
|
||||||
|
"mode": "each",
|
||||||
|
"options": {
|
||||||
|
"waitForSubWorkflow": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.executeWorkflow",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"position": [680, 1540],
|
||||||
|
"id": "f90a25da-dd89-4bf8-8f5b-bf8ee1de0b70",
|
||||||
|
"name": "Execute Workflow with param3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "c93f26bd-3489-467b-909e-6462e1463707",
|
||||||
|
"name": "uid",
|
||||||
|
"value": "={{ $json.uid }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3dd706ce-d925-4219-8531-ad12369972fe",
|
||||||
|
"name": "email",
|
||||||
|
"value": "={{ $json.email }}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [900, 1540],
|
||||||
|
"id": "3be57648-3be8-4b0f-abfa-8fdcafee804d",
|
||||||
|
"name": "Edit Fields8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"source": "parameter",
|
||||||
|
"workflowJson": "{\n \"meta\": {\n \"instanceId\": \"a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0\"\n },\n \"nodes\": [\n {\n \"parameters\": {},\n \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n \"typeVersion\": 1,\n \"position\": [\n 0,\n 0\n ],\n \"id\": \"00600a51-e63a-4b6e-93f5-f01d50a21e0c\",\n \"name\": \"Execute Workflow Trigger\"\n },\n {\n \"parameters\": {\n \"assignments\": {\n \"assignments\": [\n {\n \"id\": \"87ff01af-2e28-48da-ae6c-304040200b15\",\n \"name\": \"hello\",\n \"value\": \"=world {{ $json.firstname }} {{ $json.lastname }}\",\n \"type\": \"string\"\n }\n ]\n },\n \"includeOtherFields\": false,\n \"options\": {}\n },\n \"type\": \"n8n-nodes-base.set\",\n \"typeVersion\": 3.4,\n \"position\": [\n 280,\n 0\n ],\n \"id\": \"642219a1-d655-4a30-af5c-fcccbb690322\",\n \"name\": \"Edit Fields\"\n }\n ],\n \"connections\": {\n \"Execute Workflow Trigger\": {\n \"main\": [\n [\n {\n \"node\": \"Edit Fields\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n }\n },\n \"pinData\": {}\n}",
|
||||||
|
"options": {
|
||||||
|
"waitForSubWorkflow": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.executeWorkflow",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"position": [620, 1220],
|
||||||
|
"id": "dabc2356-3660-4d17-b305-936a002029ba",
|
||||||
|
"name": "Execute Workflow with param2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "c93f26bd-3489-467b-909e-6462e1463707",
|
||||||
|
"name": "uid",
|
||||||
|
"value": "={{ $json.uid }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3dd706ce-d925-4219-8531-ad12369972fe",
|
||||||
|
"name": "email",
|
||||||
|
"value": "={{ $json.email }}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [840, 1220],
|
||||||
|
"id": "9d2a9dda-e2a1-43e8-a66f-a8a555692e5f",
|
||||||
|
"name": "Edit Fields7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"source": "parameter",
|
||||||
|
"workflowJson": "{\n \"meta\": {\n \"instanceId\": \"a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0\"\n },\n \"nodes\": [\n {\n \"parameters\": {},\n \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n \"typeVersion\": 1,\n \"position\": [\n 0,\n 0\n ],\n \"id\": \"00600a51-e63a-4b6e-93f5-f01d50a21e0c\",\n \"name\": \"Execute Workflow Trigger\"\n },\n {\n \"parameters\": {\n \"assignments\": {\n \"assignments\": [\n {\n \"id\": \"87ff01af-2e28-48da-ae6c-304040200b15\",\n \"name\": \"hello\",\n \"value\": \"=world {{ $json.firstname }} {{ $json.lastname }}\",\n \"type\": \"string\"\n }\n ]\n },\n \"includeOtherFields\": false,\n \"options\": {}\n },\n \"type\": \"n8n-nodes-base.set\",\n \"typeVersion\": 3.4,\n \"position\": [\n 280,\n 0\n ],\n \"id\": \"642219a1-d655-4a30-af5c-fcccbb690322\",\n \"name\": \"Edit Fields\"\n }\n ],\n \"connections\": {\n \"Execute Workflow Trigger\": {\n \"main\": [\n [\n {\n \"node\": \"Edit Fields\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n }\n },\n \"pinData\": {}\n}",
|
||||||
|
"mode": "each",
|
||||||
|
"options": {
|
||||||
|
"waitForSubWorkflow": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.executeWorkflow",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"position": [560, 900],
|
||||||
|
"id": "07e47f60-622a-484c-ab24-35f6f2280595",
|
||||||
|
"name": "Execute Workflow with param1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "c93f26bd-3489-467b-909e-6462e1463707",
|
||||||
|
"name": "uid",
|
||||||
|
"value": "={{ $json.uid }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3dd706ce-d925-4219-8531-ad12369972fe",
|
||||||
|
"name": "email",
|
||||||
|
"value": "={{ $json.email }}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [760, 900],
|
||||||
|
"id": "80563d0a-0bab-444f-a04c-4041a505d78b",
|
||||||
|
"name": "Edit Fields6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"source": "parameter",
|
||||||
|
"workflowJson": "{\n \"meta\": {\n \"instanceId\": \"a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0\"\n },\n \"nodes\": [\n {\n \"parameters\": {},\n \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",\n \"typeVersion\": 1,\n \"position\": [\n 0,\n 0\n ],\n \"id\": \"00600a51-e63a-4b6e-93f5-f01d50a21e0c\",\n \"name\": \"Execute Workflow Trigger\"\n },\n {\n \"parameters\": {\n \"assignments\": {\n \"assignments\": [\n {\n \"id\": \"87ff01af-2e28-48da-ae6c-304040200b15\",\n \"name\": \"hello\",\n \"value\": \"=world {{ $json.firstname }} {{ $json.lastname }}\",\n \"type\": \"string\"\n }\n ]\n },\n \"includeOtherFields\": false,\n \"options\": {}\n },\n \"type\": \"n8n-nodes-base.set\",\n \"typeVersion\": 3.4,\n \"position\": [\n 280,\n 0\n ],\n \"id\": \"642219a1-d655-4a30-af5c-fcccbb690322\",\n \"name\": \"Edit Fields\"\n }\n ],\n \"connections\": {\n \"Execute Workflow Trigger\": {\n \"main\": [\n [\n {\n \"node\": \"Edit Fields\",\n \"type\": \"main\",\n \"index\": 0\n }\n ]\n ]\n }\n },\n \"pinData\": {}\n}",
|
||||||
|
"options": {
|
||||||
|
"waitForSubWorkflow": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.executeWorkflow",
|
||||||
|
"typeVersion": 1.1,
|
||||||
|
"position": [560, 580],
|
||||||
|
"id": "f04af481-f4d9-4d91-a60a-a377580e8393",
|
||||||
|
"name": "Execute Workflow with param"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "c93f26bd-3489-467b-909e-6462e1463707",
|
||||||
|
"name": "uid",
|
||||||
|
"value": "={{ $json.uid }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3dd706ce-d925-4219-8531-ad12369972fe",
|
||||||
|
"name": "email",
|
||||||
|
"value": "={{ $json.email }}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [760, 580],
|
||||||
|
"id": "80c10607-a0ac-4090-86a1-890da0a2aa52",
|
||||||
|
"name": "Edit Fields2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"content": "## Execute Workflow (Run once with all items/ DONT Wait for Sub-workflow completion)",
|
||||||
|
"height": 254.84308966329985,
|
||||||
|
"width": 457.58120569815793
|
||||||
|
},
|
||||||
|
"id": "534ef523-3453-4a16-9ff0-8ac9f025d47d",
|
||||||
|
"name": "Sticky Note5",
|
||||||
|
"type": "n8n-nodes-base.stickyNote",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [500, 1080]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"content": "## Execute Workflow (Run once with for each item/ DONT Wait for Sub-workflow completion) ",
|
||||||
|
"height": 284.59778445962905,
|
||||||
|
"width": 457.58120569815793
|
||||||
|
},
|
||||||
|
"id": "838f0fa3-5ee4-4d1a-afb8-42e009f1aa9e",
|
||||||
|
"name": "Sticky Note4",
|
||||||
|
"type": "n8n-nodes-base.stickyNote",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [580, 1400]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"category": "randomData",
|
||||||
|
"randomDataSeed": "1",
|
||||||
|
"randomDataCount": 3
|
||||||
|
},
|
||||||
|
"id": "86699a49-2aa7-488e-8ea9-828404c98f08",
|
||||||
|
"name": "DebugHelper",
|
||||||
|
"type": "n8n-nodes-base.debugHelper",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [320, 1120]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"content": "## Execute Workflow (Run once with for each item/ Wait for Sub-workflow completion) ",
|
||||||
|
"height": 284.59778445962905,
|
||||||
|
"width": 457.58120569815793
|
||||||
|
},
|
||||||
|
"id": "885d35f0-8ae6-45ec-821b-a82c27e7577a",
|
||||||
|
"name": "Sticky Note3",
|
||||||
|
"type": "n8n-nodes-base.stickyNote",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [480, 760]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"content": "## Execute Workflow (Run once with all items/ Wait for Sub-workflow completion) (default behavior)",
|
||||||
|
"height": 254.84308966329985,
|
||||||
|
"width": 457.58120569815793
|
||||||
|
},
|
||||||
|
"id": "505bd7f2-767e-41b8-9325-77300aed5883",
|
||||||
|
"name": "Sticky Note2",
|
||||||
|
"type": "n8n-nodes-base.stickyNote",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [460, 460]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"When clicking ‘Test workflow’": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "DebugHelper1",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "DebugHelper",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"DebugHelper1": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Execute Workflow with param3",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Execute Workflow with param2",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Execute Workflow with param1",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Execute Workflow with param",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Execute Workflow with param3": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Edit Fields8",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Execute Workflow with param2": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Edit Fields7",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Execute Workflow with param1": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Edit Fields6",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Execute Workflow with param": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Edit Fields2",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"DebugHelper": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Execute Workflow with param2",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Execute Workflow with param3",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pinData": {}
|
||||||
|
}
|
|
@ -323,6 +323,12 @@ export class NDV extends BasePage {
|
||||||
addItemToFixedCollection: (paramName: string) => {
|
addItemToFixedCollection: (paramName: string) => {
|
||||||
this.getters.fixedCollectionParameter(paramName).getByTestId('fixed-collection-add').click();
|
this.getters.fixedCollectionParameter(paramName).getByTestId('fixed-collection-add').click();
|
||||||
},
|
},
|
||||||
|
dragMainPanelToLeft: () => {
|
||||||
|
cy.drag('[data-test-id=panel-drag-button]', [-1000, 0], { moveTwice: true });
|
||||||
|
},
|
||||||
|
dragMainPanelToRight: () => {
|
||||||
|
cy.drag('[data-test-id=panel-drag-button]', [1000, 0], { moveTwice: true });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -177,6 +177,16 @@ Cypress.Commands.add('drag', (selector, pos, options) => {
|
||||||
pageY: newPosition.y,
|
pageY: newPosition.y,
|
||||||
force: true,
|
force: true,
|
||||||
});
|
});
|
||||||
|
if (options?.moveTwice) {
|
||||||
|
// first move like hover to trigger object to be visible
|
||||||
|
// like in main panel in ndv
|
||||||
|
element.trigger('mousemove', {
|
||||||
|
which: 1,
|
||||||
|
pageX: newPosition.x,
|
||||||
|
pageY: newPosition.y,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (options?.clickToFinish) {
|
if (options?.clickToFinish) {
|
||||||
// Click to finish the drag
|
// Click to finish the drag
|
||||||
// For some reason, mouseup isn't working when moving nodes
|
// For some reason, mouseup isn't working when moving nodes
|
||||||
|
|
|
@ -59,7 +59,13 @@ declare global {
|
||||||
drag(
|
drag(
|
||||||
selector: string | Chainable<JQuery<HTMLElement>>,
|
selector: string | Chainable<JQuery<HTMLElement>>,
|
||||||
target: [number, number],
|
target: [number, number],
|
||||||
options?: { abs?: boolean; index?: number; realMouse?: boolean; clickToFinish?: boolean },
|
options?: {
|
||||||
|
abs?: boolean;
|
||||||
|
index?: number;
|
||||||
|
realMouse?: boolean;
|
||||||
|
clickToFinish?: boolean;
|
||||||
|
moveTwice?: boolean;
|
||||||
|
},
|
||||||
): void;
|
): void;
|
||||||
draganddrop(
|
draganddrop(
|
||||||
draggableSelector: string,
|
draggableSelector: string,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
SupplyData,
|
SupplyData,
|
||||||
INodeParameterResourceLocator,
|
INodeParameterResourceLocator,
|
||||||
|
ExecuteWorkflowData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { BaseRetriever, type BaseRetrieverInput } from '@langchain/core/retrievers';
|
import { BaseRetriever, type BaseRetrieverInput } from '@langchain/core/retrievers';
|
||||||
|
@ -293,6 +294,8 @@ export class RetrieverWorkflow implements INodeType {
|
||||||
};
|
};
|
||||||
|
|
||||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||||
|
const workflowProxy = this.getWorkflowDataProxy(0);
|
||||||
|
|
||||||
class WorkflowRetriever extends BaseRetriever {
|
class WorkflowRetriever extends BaseRetriever {
|
||||||
lc_namespace = ['n8n-nodes-langchain', 'retrievers', 'workflow'];
|
lc_namespace = ['n8n-nodes-langchain', 'retrievers', 'workflow'];
|
||||||
|
|
||||||
|
@ -349,6 +352,9 @@ export class RetrieverWorkflow implements INodeType {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// same as current workflow
|
||||||
|
baseMetadata.workflowId = workflowProxy.$workflow.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawData: IDataObject = { query };
|
const rawData: IDataObject = { query };
|
||||||
|
@ -384,21 +390,29 @@ export class RetrieverWorkflow implements INodeType {
|
||||||
|
|
||||||
const items = [newItem] as INodeExecutionData[];
|
const items = [newItem] as INodeExecutionData[];
|
||||||
|
|
||||||
let receivedItems: INodeExecutionData[][];
|
let receivedData: ExecuteWorkflowData;
|
||||||
try {
|
try {
|
||||||
receivedItems = (await this.executeFunctions.executeWorkflow(
|
receivedData = await this.executeFunctions.executeWorkflow(
|
||||||
workflowInfo,
|
workflowInfo,
|
||||||
items,
|
items,
|
||||||
config?.getChild(),
|
config?.getChild(),
|
||||||
)) as INodeExecutionData[][];
|
{
|
||||||
|
parentExecution: {
|
||||||
|
executionId: workflowProxy.$execution.id,
|
||||||
|
workflowId: workflowProxy.$workflow.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Make sure a valid error gets returned that can by json-serialized else it will
|
// Make sure a valid error gets returned that can by json-serialized else it will
|
||||||
// not show up in the frontend
|
// not show up in the frontend
|
||||||
throw new NodeOperationError(this.executeFunctions.getNode(), error as Error);
|
throw new NodeOperationError(this.executeFunctions.getNode(), error as Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const receivedItems = receivedData.data?.[0] ?? [];
|
||||||
|
|
||||||
const returnData: Document[] = [];
|
const returnData: Document[] = [];
|
||||||
for (const [index, itemData] of receivedItems[0].entries()) {
|
for (const [index, itemData] of receivedItems.entries()) {
|
||||||
const pageContent = objectToString(itemData.json);
|
const pageContent = objectToString(itemData.json);
|
||||||
returnData.push(
|
returnData.push(
|
||||||
new Document({
|
new Document({
|
||||||
|
@ -406,6 +420,7 @@ export class RetrieverWorkflow implements INodeType {
|
||||||
metadata: {
|
metadata: {
|
||||||
...baseMetadata,
|
...baseMetadata,
|
||||||
itemIndex: index,
|
itemIndex: index,
|
||||||
|
executionId: receivedData.executionId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,8 +14,10 @@ import type {
|
||||||
ISupplyDataFunctions,
|
ISupplyDataFunctions,
|
||||||
SupplyData,
|
SupplyData,
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
|
ExecuteWorkflowData,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
INodeParameterResourceLocator,
|
INodeParameterResourceLocator,
|
||||||
|
ITaskMetadata,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeConnectionType, NodeOperationError, jsonParse } from 'n8n-workflow';
|
import { NodeConnectionType, NodeOperationError, jsonParse } from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -358,9 +360,14 @@ export class ToolWorkflow implements INodeType {
|
||||||
};
|
};
|
||||||
|
|
||||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||||
|
const workflowProxy = this.getWorkflowDataProxy(0);
|
||||||
|
|
||||||
const name = this.getNodeParameter('name', itemIndex) as string;
|
const name = this.getNodeParameter('name', itemIndex) as string;
|
||||||
const description = this.getNodeParameter('description', itemIndex) as string;
|
const description = this.getNodeParameter('description', itemIndex) as string;
|
||||||
|
|
||||||
|
let subExecutionId: string | undefined;
|
||||||
|
let subWorkflowId: string | undefined;
|
||||||
|
|
||||||
const useSchema = this.getNodeParameter('specifyInputSchema', itemIndex) as boolean;
|
const useSchema = this.getNodeParameter('specifyInputSchema', itemIndex) as boolean;
|
||||||
let tool: DynamicTool | DynamicStructuredTool | undefined = undefined;
|
let tool: DynamicTool | DynamicStructuredTool | undefined = undefined;
|
||||||
|
|
||||||
|
@ -396,11 +403,16 @@ export class ToolWorkflow implements INodeType {
|
||||||
) as INodeParameterResourceLocator;
|
) as INodeParameterResourceLocator;
|
||||||
workflowInfo.id = value as string;
|
workflowInfo.id = value as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subWorkflowId = workflowInfo.id;
|
||||||
} else if (source === 'parameter') {
|
} else if (source === 'parameter') {
|
||||||
// Read workflow from parameter
|
// Read workflow from parameter
|
||||||
const workflowJson = this.getNodeParameter('workflowJson', itemIndex) as string;
|
const workflowJson = this.getNodeParameter('workflowJson', itemIndex) as string;
|
||||||
try {
|
try {
|
||||||
workflowInfo.code = JSON.parse(workflowJson) as IWorkflowBase;
|
workflowInfo.code = JSON.parse(workflowJson) as IWorkflowBase;
|
||||||
|
|
||||||
|
// subworkflow is same as parent workflow
|
||||||
|
subWorkflowId = workflowProxy.$workflow.id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new NodeOperationError(
|
throw new NodeOperationError(
|
||||||
this.getNode(),
|
this.getNode(),
|
||||||
|
@ -440,13 +452,15 @@ export class ToolWorkflow implements INodeType {
|
||||||
|
|
||||||
const items = [newItem] as INodeExecutionData[];
|
const items = [newItem] as INodeExecutionData[];
|
||||||
|
|
||||||
let receivedData: INodeExecutionData;
|
let receivedData: ExecuteWorkflowData;
|
||||||
try {
|
try {
|
||||||
receivedData = (await this.executeWorkflow(
|
receivedData = await this.executeWorkflow(workflowInfo, items, runManager?.getChild(), {
|
||||||
workflowInfo,
|
parentExecution: {
|
||||||
items,
|
executionId: workflowProxy.$execution.id,
|
||||||
runManager?.getChild(),
|
workflowId: workflowProxy.$workflow.id,
|
||||||
)) as INodeExecutionData;
|
},
|
||||||
|
});
|
||||||
|
subExecutionId = receivedData.executionId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Make sure a valid error gets returned that can by json-serialized else it will
|
// Make sure a valid error gets returned that can by json-serialized else it will
|
||||||
// not show up in the frontend
|
// not show up in the frontend
|
||||||
|
@ -454,6 +468,7 @@ export class ToolWorkflow implements INodeType {
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: string | undefined = get(receivedData, [
|
const response: string | undefined = get(receivedData, [
|
||||||
|
'data',
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
'json',
|
'json',
|
||||||
|
@ -503,10 +518,25 @@ export class ToolWorkflow implements INodeType {
|
||||||
response = `There was an error: "${executionError.message}"`;
|
response = `There was an error: "${executionError.message}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let metadata: ITaskMetadata | undefined;
|
||||||
|
if (subExecutionId && subWorkflowId) {
|
||||||
|
metadata = {
|
||||||
|
subExecution: {
|
||||||
|
executionId: subExecutionId,
|
||||||
|
workflowId: subWorkflowId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (executionError) {
|
if (executionError) {
|
||||||
void this.addOutputData(NodeConnectionType.AiTool, index, executionError);
|
void this.addOutputData(NodeConnectionType.AiTool, index, executionError, metadata);
|
||||||
} else {
|
} else {
|
||||||
void this.addOutputData(NodeConnectionType.AiTool, index, [[{ json: { response } }]]);
|
void this.addOutputData(
|
||||||
|
NodeConnectionType.AiTool,
|
||||||
|
index,
|
||||||
|
[[{ json: { response } }]],
|
||||||
|
metadata,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,7 +10,12 @@ import type { Tool } from '@langchain/core/tools';
|
||||||
import { VectorStore } from '@langchain/core/vectorstores';
|
import { VectorStore } from '@langchain/core/vectorstores';
|
||||||
import { TextSplitter } from '@langchain/textsplitters';
|
import { TextSplitter } from '@langchain/textsplitters';
|
||||||
import type { BaseDocumentLoader } from 'langchain/dist/document_loaders/base';
|
import type { BaseDocumentLoader } from 'langchain/dist/document_loaders/base';
|
||||||
import type { IExecuteFunctions, INodeExecutionData, ISupplyDataFunctions } from 'n8n-workflow';
|
import type {
|
||||||
|
IExecuteFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
ISupplyDataFunctions,
|
||||||
|
ITaskMetadata,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import { NodeOperationError, NodeConnectionType } from 'n8n-workflow';
|
import { NodeOperationError, NodeConnectionType } from 'n8n-workflow';
|
||||||
|
|
||||||
import { logAiEvent, isToolsInstance, isBaseChatMemory, isBaseChatMessageHistory } from './helpers';
|
import { logAiEvent, isToolsInstance, isBaseChatMemory, isBaseChatMessageHistory } from './helpers';
|
||||||
|
@ -220,8 +225,24 @@ export function logWrapper(
|
||||||
arguments: [query, config],
|
arguments: [query, config],
|
||||||
})) as Array<Document<Record<string, any>>>;
|
})) as Array<Document<Record<string, any>>>;
|
||||||
|
|
||||||
|
const executionId: string | undefined = response[0]?.metadata?.executionId as string;
|
||||||
|
const workflowId: string | undefined = response[0]?.metadata?.workflowId as string;
|
||||||
|
|
||||||
|
const metadata: ITaskMetadata = {};
|
||||||
|
if (executionId && workflowId) {
|
||||||
|
metadata.subExecution = {
|
||||||
|
executionId,
|
||||||
|
workflowId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
logAiEvent(executeFunctions, 'ai-documents-retrieved', { query });
|
logAiEvent(executeFunctions, 'ai-documents-retrieved', { query });
|
||||||
executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]);
|
executeFunctions.addOutputData(
|
||||||
|
connectionType,
|
||||||
|
index,
|
||||||
|
[[{ json: { response } }]],
|
||||||
|
metadata,
|
||||||
|
);
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
"start:default": "cd bin && ./n8n",
|
"start:default": "cd bin && ./n8n",
|
||||||
"start:windows": "cd bin && n8n",
|
"start:windows": "cd bin && n8n",
|
||||||
"test": "pnpm test:sqlite",
|
"test": "pnpm test:sqlite",
|
||||||
|
"test:dev": "N8N_LOG_LEVEL=silent DB_TYPE=sqlite jest --watch",
|
||||||
"test:sqlite": "N8N_LOG_LEVEL=silent DB_TYPE=sqlite jest",
|
"test:sqlite": "N8N_LOG_LEVEL=silent DB_TYPE=sqlite jest",
|
||||||
"test:postgres": "N8N_LOG_LEVEL=silent DB_TYPE=postgresdb DB_POSTGRESDB_SCHEMA=alt_schema DB_TABLE_PREFIX=test_ jest --no-coverage",
|
"test:postgres": "N8N_LOG_LEVEL=silent DB_TYPE=postgresdb DB_POSTGRESDB_SCHEMA=alt_schema DB_TABLE_PREFIX=test_ jest --no-coverage",
|
||||||
"test:mysql": "N8N_LOG_LEVEL=silent DB_TYPE=mysqldb DB_TABLE_PREFIX=test_ jest --no-coverage",
|
"test:mysql": "N8N_LOG_LEVEL=silent DB_TYPE=mysqldb DB_TABLE_PREFIX=test_ jest --no-coverage",
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import type {
|
import type { IWorkflowBase } from 'n8n-workflow';
|
||||||
IExecuteWorkflowInfo,
|
import {
|
||||||
IWorkflowExecuteAdditionalData,
|
type IExecuteWorkflowInfo,
|
||||||
ExecuteWorkflowOptions,
|
type IWorkflowExecuteAdditionalData,
|
||||||
IRun,
|
type ExecuteWorkflowOptions,
|
||||||
|
type IRun,
|
||||||
|
type INodeExecutionData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import type PCancelable from 'p-cancelable';
|
import type PCancelable from 'p-cancelable';
|
||||||
import Container from 'typedi';
|
import Container from 'typedi';
|
||||||
|
@ -21,18 +23,37 @@ import { WorkflowStatisticsService } from '@/services/workflow-statistics.servic
|
||||||
import { SubworkflowPolicyChecker } from '@/subworkflows/subworkflow-policy-checker.service';
|
import { SubworkflowPolicyChecker } from '@/subworkflows/subworkflow-policy-checker.service';
|
||||||
import { Telemetry } from '@/telemetry';
|
import { Telemetry } from '@/telemetry';
|
||||||
import { PermissionChecker } from '@/user-management/permission-checker';
|
import { PermissionChecker } from '@/user-management/permission-checker';
|
||||||
import { executeWorkflow, getBase } from '@/workflow-execute-additional-data';
|
import { executeWorkflow, getBase, getRunData } from '@/workflow-execute-additional-data';
|
||||||
import { mockInstance } from '@test/mocking';
|
import { mockInstance } from '@test/mocking';
|
||||||
|
|
||||||
const run = mock<IRun>({
|
const EXECUTION_ID = '123';
|
||||||
data: { resultData: {} },
|
const LAST_NODE_EXECUTED = 'Last node executed';
|
||||||
|
|
||||||
|
const getMockRun = ({ lastNodeOutput }: { lastNodeOutput: Array<INodeExecutionData[] | null> }) =>
|
||||||
|
mock<IRun>({
|
||||||
|
data: {
|
||||||
|
resultData: {
|
||||||
|
runData: {
|
||||||
|
[LAST_NODE_EXECUTED]: [
|
||||||
|
{
|
||||||
|
startTime: 100,
|
||||||
|
data: {
|
||||||
|
main: lastNodeOutput,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
lastNodeExecuted: LAST_NODE_EXECUTED,
|
||||||
|
},
|
||||||
|
},
|
||||||
finished: true,
|
finished: true,
|
||||||
mode: 'manual',
|
mode: 'manual',
|
||||||
startedAt: new Date(),
|
startedAt: new Date(),
|
||||||
status: 'new',
|
status: 'new',
|
||||||
});
|
});
|
||||||
|
|
||||||
const cancelablePromise = mock<PCancelable<IRun>>({
|
const getCancelablePromise = async (run: IRun) =>
|
||||||
|
await mock<PCancelable<IRun>>({
|
||||||
then: jest
|
then: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementation(async (onfulfilled) => await Promise.resolve(run).then(onfulfilled)),
|
.mockImplementation(async (onfulfilled) => await Promise.resolve(run).then(onfulfilled)),
|
||||||
|
@ -45,19 +66,16 @@ const cancelablePromise = mock<PCancelable<IRun>>({
|
||||||
[Symbol.toStringTag]: 'PCancelable',
|
[Symbol.toStringTag]: 'PCancelable',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const processRunExecutionData = jest.fn();
|
||||||
|
|
||||||
jest.mock('n8n-core', () => ({
|
jest.mock('n8n-core', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
...jest.requireActual('n8n-core'),
|
...jest.requireActual('n8n-core'),
|
||||||
WorkflowExecute: jest.fn().mockImplementation(() => ({
|
WorkflowExecute: jest.fn().mockImplementation(() => ({
|
||||||
processRunExecutionData: jest.fn().mockReturnValue(cancelablePromise),
|
processRunExecutionData,
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../workflow-helpers', () => ({
|
|
||||||
...jest.requireActual('../workflow-helpers'),
|
|
||||||
getDataLastExecutedNodeData: jest.fn().mockReturnValue({ data: { main: [] } }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('WorkflowExecuteAdditionalData', () => {
|
describe('WorkflowExecuteAdditionalData', () => {
|
||||||
const variablesService = mockInstance(VariablesService);
|
const variablesService = mockInstance(VariablesService);
|
||||||
variablesService.getAllCached.mockResolvedValue([]);
|
variablesService.getAllCached.mockResolvedValue([]);
|
||||||
|
@ -95,17 +113,129 @@ describe('WorkflowExecuteAdditionalData', () => {
|
||||||
expect(eventService.emit).toHaveBeenCalledWith(eventName, payload);
|
expect(eventService.emit).toHaveBeenCalledWith(eventName, payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('`executeWorkflow` should set subworkflow execution as running', async () => {
|
describe('executeWorkflow', () => {
|
||||||
const executionId = '123';
|
const runWithData = getMockRun({ lastNodeOutput: [[{ json: { test: 1 } }]] });
|
||||||
workflowRepository.get.mockResolvedValue(mock<WorkflowEntity>({ id: executionId, nodes: [] }));
|
|
||||||
activeExecutions.add.mockResolvedValue(executionId);
|
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
workflowRepository.get.mockResolvedValue(
|
||||||
|
mock<WorkflowEntity>({ id: EXECUTION_ID, nodes: [] }),
|
||||||
|
);
|
||||||
|
activeExecutions.add.mockResolvedValue(EXECUTION_ID);
|
||||||
|
processRunExecutionData.mockReturnValue(getCancelablePromise(runWithData));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute workflow, return data and execution id', async () => {
|
||||||
|
const response = await executeWorkflow(
|
||||||
|
mock<IExecuteWorkflowInfo>(),
|
||||||
|
mock<IWorkflowExecuteAdditionalData>(),
|
||||||
|
mock<ExecuteWorkflowOptions>({ loadedWorkflowData: undefined, doNotWaitToFinish: false }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
data: runWithData.data.resultData.runData[LAST_NODE_EXECUTED][0].data!.main,
|
||||||
|
executionId: EXECUTION_ID,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute workflow, skip waiting', async () => {
|
||||||
|
const response = await executeWorkflow(
|
||||||
|
mock<IExecuteWorkflowInfo>(),
|
||||||
|
mock<IWorkflowExecuteAdditionalData>(),
|
||||||
|
mock<ExecuteWorkflowOptions>({ loadedWorkflowData: undefined, doNotWaitToFinish: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
data: [null],
|
||||||
|
executionId: EXECUTION_ID,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set sub workflow execution as running', async () => {
|
||||||
await executeWorkflow(
|
await executeWorkflow(
|
||||||
mock<IExecuteWorkflowInfo>(),
|
mock<IExecuteWorkflowInfo>(),
|
||||||
mock<IWorkflowExecuteAdditionalData>(),
|
mock<IWorkflowExecuteAdditionalData>(),
|
||||||
mock<ExecuteWorkflowOptions>({ loadedWorkflowData: undefined }),
|
mock<ExecuteWorkflowOptions>({ loadedWorkflowData: undefined }),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(executionRepository.setRunning).toHaveBeenCalledWith(executionId);
|
expect(executionRepository.setRunning).toHaveBeenCalledWith(EXECUTION_ID);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getRunData', () => {
|
||||||
|
it('should throw error to add trigger ndoe', async () => {
|
||||||
|
const workflow = mock<IWorkflowBase>({
|
||||||
|
id: '1',
|
||||||
|
name: 'test',
|
||||||
|
nodes: [],
|
||||||
|
active: false,
|
||||||
|
});
|
||||||
|
await expect(getRunData(workflow)).rejects.toThrowError('Missing node to start execution');
|
||||||
|
});
|
||||||
|
|
||||||
|
const workflow = mock<IWorkflowBase>({
|
||||||
|
id: '1',
|
||||||
|
name: 'test',
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
type: 'n8n-nodes-base.executeWorkflowTrigger',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
active: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default data', async () => {
|
||||||
|
expect(await getRunData(workflow)).toEqual({
|
||||||
|
executionData: {
|
||||||
|
executionData: {
|
||||||
|
contextData: {},
|
||||||
|
metadata: {},
|
||||||
|
nodeExecutionStack: [
|
||||||
|
{
|
||||||
|
data: { main: [[{ json: {} }]] },
|
||||||
|
metadata: { parentExecution: undefined },
|
||||||
|
node: workflow.nodes[0],
|
||||||
|
source: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
waitingExecution: {},
|
||||||
|
waitingExecutionSource: {},
|
||||||
|
},
|
||||||
|
resultData: { runData: {} },
|
||||||
|
startData: {},
|
||||||
|
},
|
||||||
|
executionMode: 'integrated',
|
||||||
|
workflowData: workflow,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return run data with input data and metadata', async () => {
|
||||||
|
const data = [{ json: { test: 1 } }];
|
||||||
|
const parentExecution = {
|
||||||
|
executionId: '123',
|
||||||
|
workflowId: '567',
|
||||||
|
};
|
||||||
|
expect(await getRunData(workflow, data, parentExecution)).toEqual({
|
||||||
|
executionData: {
|
||||||
|
executionData: {
|
||||||
|
contextData: {},
|
||||||
|
metadata: {},
|
||||||
|
nodeExecutionStack: [
|
||||||
|
{
|
||||||
|
data: { main: [data] },
|
||||||
|
metadata: { parentExecution },
|
||||||
|
node: workflow.nodes[0],
|
||||||
|
source: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
waitingExecution: {},
|
||||||
|
waitingExecutionSource: {},
|
||||||
|
},
|
||||||
|
resultData: { runData: {} },
|
||||||
|
startData: {},
|
||||||
|
},
|
||||||
|
executionMode: 'integrated',
|
||||||
|
workflowData: workflow,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import type { Scope } from '@n8n/permissions';
|
import type { Scope } from '@n8n/permissions';
|
||||||
import type { Application } from 'express';
|
import type { Application } from 'express';
|
||||||
import type { WorkflowExecute } from 'n8n-core';
|
|
||||||
import type {
|
import type {
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
|
@ -14,7 +13,6 @@ import type {
|
||||||
ITelemetryTrackProperties,
|
ITelemetryTrackProperties,
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
CredentialLoadingDetails,
|
CredentialLoadingDetails,
|
||||||
Workflow,
|
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
ExecutionStatus,
|
ExecutionStatus,
|
||||||
ExecutionSummary,
|
ExecutionSummary,
|
||||||
|
@ -300,12 +298,6 @@ export interface IWorkflowErrorData {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkflowExecuteProcess {
|
|
||||||
startedAt: Date;
|
|
||||||
workflow: Workflow;
|
|
||||||
workflowExecute: WorkflowExecute;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IWorkflowStatisticsDataLoaded {
|
export interface IWorkflowStatisticsDataLoaded {
|
||||||
dataLoaded: boolean;
|
dataLoaded: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,8 @@ import type {
|
||||||
ExecuteWorkflowOptions,
|
ExecuteWorkflowOptions,
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
EnvProviderState,
|
EnvProviderState,
|
||||||
|
ExecuteWorkflowData,
|
||||||
|
RelatedExecution,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
|
@ -45,11 +47,7 @@ import { CredentialsHelper } from '@/credentials-helper';
|
||||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||||
import type { AiEventMap, AiEventPayload } from '@/events/maps/ai.event-map';
|
import type { AiEventMap, AiEventPayload } from '@/events/maps/ai.event-map';
|
||||||
import { ExternalHooks } from '@/external-hooks';
|
import { ExternalHooks } from '@/external-hooks';
|
||||||
import type {
|
import type { IWorkflowErrorData, UpdateExecutionPayload } from '@/interfaces';
|
||||||
IWorkflowExecuteProcess,
|
|
||||||
IWorkflowErrorData,
|
|
||||||
UpdateExecutionPayload,
|
|
||||||
} from '@/interfaces';
|
|
||||||
import { NodeTypes } from '@/node-types';
|
import { NodeTypes } from '@/node-types';
|
||||||
import { Push } from '@/push';
|
import { Push } from '@/push';
|
||||||
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
|
||||||
|
@ -650,6 +648,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
||||||
export async function getRunData(
|
export async function getRunData(
|
||||||
workflowData: IWorkflowBase,
|
workflowData: IWorkflowBase,
|
||||||
inputData?: INodeExecutionData[],
|
inputData?: INodeExecutionData[],
|
||||||
|
parentExecution?: RelatedExecution,
|
||||||
): Promise<IWorkflowExecutionDataProcess> {
|
): Promise<IWorkflowExecutionDataProcess> {
|
||||||
const mode = 'integrated';
|
const mode = 'integrated';
|
||||||
|
|
||||||
|
@ -669,6 +668,7 @@ export async function getRunData(
|
||||||
data: {
|
data: {
|
||||||
main: [inputData],
|
main: [inputData],
|
||||||
},
|
},
|
||||||
|
metadata: { parentExecution },
|
||||||
source: null,
|
source: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -740,7 +740,41 @@ export async function executeWorkflow(
|
||||||
workflowInfo: IExecuteWorkflowInfo,
|
workflowInfo: IExecuteWorkflowInfo,
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
options: ExecuteWorkflowOptions,
|
options: ExecuteWorkflowOptions,
|
||||||
): Promise<Array<INodeExecutionData[] | null> | IWorkflowExecuteProcess> {
|
): Promise<ExecuteWorkflowData> {
|
||||||
|
const activeExecutions = Container.get(ActiveExecutions);
|
||||||
|
|
||||||
|
const workflowData =
|
||||||
|
options.loadedWorkflowData ??
|
||||||
|
(await getWorkflowData(workflowInfo, options.parentWorkflowId, options.parentWorkflowSettings));
|
||||||
|
|
||||||
|
const runData =
|
||||||
|
options.loadedRunData ??
|
||||||
|
(await getRunData(workflowData, options.inputData, options.parentExecution));
|
||||||
|
|
||||||
|
const executionId = await activeExecutions.add(runData);
|
||||||
|
|
||||||
|
const executionPromise = startExecution(
|
||||||
|
additionalData,
|
||||||
|
options,
|
||||||
|
executionId,
|
||||||
|
runData,
|
||||||
|
workflowData,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options.doNotWaitToFinish) {
|
||||||
|
return { executionId, data: [null] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return await executionPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startExecution(
|
||||||
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
|
options: ExecuteWorkflowOptions,
|
||||||
|
executionId: string,
|
||||||
|
runData: IWorkflowExecutionDataProcess,
|
||||||
|
workflowData: IWorkflowBase,
|
||||||
|
): Promise<ExecuteWorkflowData> {
|
||||||
const externalHooks = Container.get(ExternalHooks);
|
const externalHooks = Container.get(ExternalHooks);
|
||||||
await externalHooks.init();
|
await externalHooks.init();
|
||||||
|
|
||||||
|
@ -749,10 +783,6 @@ export async function executeWorkflow(
|
||||||
const eventService = Container.get(EventService);
|
const eventService = Container.get(EventService);
|
||||||
const executionRepository = Container.get(ExecutionRepository);
|
const executionRepository = Container.get(ExecutionRepository);
|
||||||
|
|
||||||
const workflowData =
|
|
||||||
options.loadedWorkflowData ??
|
|
||||||
(await getWorkflowData(workflowInfo, options.parentWorkflowId, options.parentWorkflowSettings));
|
|
||||||
|
|
||||||
const workflowName = workflowData ? workflowData.name : undefined;
|
const workflowName = workflowData ? workflowData.name : undefined;
|
||||||
const workflow = new Workflow({
|
const workflow = new Workflow({
|
||||||
id: workflowData.id,
|
id: workflowData.id,
|
||||||
|
@ -765,10 +795,6 @@ export async function executeWorkflow(
|
||||||
settings: workflowData.settings,
|
settings: workflowData.settings,
|
||||||
});
|
});
|
||||||
|
|
||||||
const runData = options.loadedRunData ?? (await getRunData(workflowData, options.inputData));
|
|
||||||
|
|
||||||
const executionId = await activeExecutions.add(runData);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A subworkflow execution in queue mode is not enqueued, but rather runs in the
|
* A subworkflow execution in queue mode is not enqueued, but rather runs in the
|
||||||
* same worker process as the parent execution. Hence ensure the subworkflow
|
* same worker process as the parent execution. Hence ensure the subworkflow
|
||||||
|
@ -890,7 +916,10 @@ export async function executeWorkflow(
|
||||||
|
|
||||||
activeExecutions.finalizeExecution(executionId, data);
|
activeExecutions.finalizeExecution(executionId, data);
|
||||||
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
|
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
|
||||||
return returnData!.data!.main;
|
return {
|
||||||
|
executionId,
|
||||||
|
data: returnData!.data!.main,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
activeExecutions.finalizeExecution(executionId, data);
|
activeExecutions.finalizeExecution(executionId, data);
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,8 @@
|
||||||
"lint": "eslint . --quiet",
|
"lint": "eslint . --quiet",
|
||||||
"lintfix": "eslint . --fix",
|
"lintfix": "eslint . --fix",
|
||||||
"watch": "tsc-watch -p tsconfig.build.json --onCompilationComplete \"tsc-alias -p tsconfig.build.json\"",
|
"watch": "tsc-watch -p tsconfig.build.json --onCompilationComplete \"tsc-alias -p tsconfig.build.json\"",
|
||||||
"test": "jest"
|
"test": "jest",
|
||||||
|
"test:dev": "jest --watch"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|
|
@ -39,6 +39,7 @@ import type {
|
||||||
BinaryHelperFunctions,
|
BinaryHelperFunctions,
|
||||||
CloseFunction,
|
CloseFunction,
|
||||||
ContextType,
|
ContextType,
|
||||||
|
ExecuteWorkflowData,
|
||||||
FieldType,
|
FieldType,
|
||||||
FileSystemHelperFunctions,
|
FileSystemHelperFunctions,
|
||||||
FunctionsBase,
|
FunctionsBase,
|
||||||
|
@ -78,6 +79,7 @@ import type {
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
ITaskDataConnections,
|
ITaskDataConnections,
|
||||||
|
ITaskMetadata,
|
||||||
ITriggerFunctions,
|
ITriggerFunctions,
|
||||||
IWebhookData,
|
IWebhookData,
|
||||||
IWebhookDescription,
|
IWebhookDescription,
|
||||||
|
@ -109,6 +111,7 @@ import type {
|
||||||
ISupplyDataFunctions,
|
ISupplyDataFunctions,
|
||||||
WebhookType,
|
WebhookType,
|
||||||
SchedulingFunctions,
|
SchedulingFunctions,
|
||||||
|
RelatedExecution,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import {
|
import {
|
||||||
NodeConnectionType,
|
NodeConnectionType,
|
||||||
|
@ -2721,6 +2724,7 @@ const addExecutionDataFunctions = async (
|
||||||
sourceNodeName: string,
|
sourceNodeName: string,
|
||||||
sourceNodeRunIndex: number,
|
sourceNodeRunIndex: number,
|
||||||
currentNodeRunIndex: number,
|
currentNodeRunIndex: number,
|
||||||
|
metadata?: ITaskMetadata,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (connectionType === NodeConnectionType.Main) {
|
if (connectionType === NodeConnectionType.Main) {
|
||||||
throw new ApplicationError('Setting type is not supported for main connection', {
|
throw new ApplicationError('Setting type is not supported for main connection', {
|
||||||
|
@ -2746,6 +2750,7 @@ const addExecutionDataFunctions = async (
|
||||||
if (taskData === undefined) {
|
if (taskData === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
taskData.metadata = metadata;
|
||||||
}
|
}
|
||||||
taskData = taskData!;
|
taskData = taskData!;
|
||||||
|
|
||||||
|
@ -3622,6 +3627,12 @@ export function getExecuteFunctions(
|
||||||
itemIndex,
|
itemIndex,
|
||||||
),
|
),
|
||||||
getExecuteData: () => executeData,
|
getExecuteData: () => executeData,
|
||||||
|
setMetadata: (metadata: ITaskMetadata): void => {
|
||||||
|
executeData.metadata = {
|
||||||
|
...(executeData.metadata ?? {}),
|
||||||
|
...metadata,
|
||||||
|
};
|
||||||
|
},
|
||||||
continueOnFail: () => {
|
continueOnFail: () => {
|
||||||
return continueOnFail(node);
|
return continueOnFail(node);
|
||||||
},
|
},
|
||||||
|
@ -3643,23 +3654,28 @@ export function getExecuteFunctions(
|
||||||
workflowInfo: IExecuteWorkflowInfo,
|
workflowInfo: IExecuteWorkflowInfo,
|
||||||
inputData?: INodeExecutionData[],
|
inputData?: INodeExecutionData[],
|
||||||
parentCallbackManager?: CallbackManager,
|
parentCallbackManager?: CallbackManager,
|
||||||
): Promise<any> {
|
options?: {
|
||||||
|
doNotWaitToFinish?: boolean;
|
||||||
|
parentExecution?: RelatedExecution;
|
||||||
|
},
|
||||||
|
): Promise<ExecuteWorkflowData> {
|
||||||
return await additionalData
|
return await additionalData
|
||||||
.executeWorkflow(workflowInfo, additionalData, {
|
.executeWorkflow(workflowInfo, additionalData, {
|
||||||
|
...options,
|
||||||
parentWorkflowId: workflow.id?.toString(),
|
parentWorkflowId: workflow.id?.toString(),
|
||||||
inputData,
|
inputData,
|
||||||
parentWorkflowSettings: workflow.settings,
|
parentWorkflowSettings: workflow.settings,
|
||||||
node,
|
node,
|
||||||
parentCallbackManager,
|
parentCallbackManager,
|
||||||
})
|
})
|
||||||
.then(
|
.then(async (result) => {
|
||||||
async (result) =>
|
const data = await Container.get(BinaryDataService).duplicateBinaryData(
|
||||||
await Container.get(BinaryDataService).duplicateBinaryData(
|
|
||||||
workflow.id,
|
workflow.id,
|
||||||
additionalData.executionId!,
|
additionalData.executionId!,
|
||||||
result,
|
result.data,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
return { ...result, data };
|
||||||
|
});
|
||||||
},
|
},
|
||||||
getContext(type: ContextType): IContextObject {
|
getContext(type: ContextType): IContextObject {
|
||||||
return NodeHelpers.getContext(runExecutionData, type, node);
|
return NodeHelpers.getContext(runExecutionData, type, node);
|
||||||
|
@ -3853,6 +3869,7 @@ export function getExecuteFunctions(
|
||||||
connectionType: NodeConnectionType,
|
connectionType: NodeConnectionType,
|
||||||
currentNodeRunIndex: number,
|
currentNodeRunIndex: number,
|
||||||
data: INodeExecutionData[][] | ExecutionBaseError,
|
data: INodeExecutionData[][] | ExecutionBaseError,
|
||||||
|
metadata?: ITaskMetadata,
|
||||||
): void {
|
): void {
|
||||||
addExecutionDataFunctions(
|
addExecutionDataFunctions(
|
||||||
'output',
|
'output',
|
||||||
|
@ -3864,6 +3881,7 @@ export function getExecuteFunctions(
|
||||||
node.name,
|
node.name,
|
||||||
runIndex,
|
runIndex,
|
||||||
currentNodeRunIndex,
|
currentNodeRunIndex,
|
||||||
|
metadata,
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
`There was a problem logging output data of node "${this.getNode().name}": ${
|
`There was a problem logging output data of node "${this.getNode().name}": ${
|
||||||
|
@ -3972,7 +3990,11 @@ export function getSupplyDataFunctions(
|
||||||
workflowInfo: IExecuteWorkflowInfo,
|
workflowInfo: IExecuteWorkflowInfo,
|
||||||
inputData?: INodeExecutionData[],
|
inputData?: INodeExecutionData[],
|
||||||
parentCallbackManager?: CallbackManager,
|
parentCallbackManager?: CallbackManager,
|
||||||
) =>
|
options?: {
|
||||||
|
doNotWaitToFinish?: boolean;
|
||||||
|
parentExecution?: RelatedExecution;
|
||||||
|
},
|
||||||
|
): Promise<ExecuteWorkflowData> =>
|
||||||
await additionalData
|
await additionalData
|
||||||
.executeWorkflow(workflowInfo, additionalData, {
|
.executeWorkflow(workflowInfo, additionalData, {
|
||||||
parentWorkflowId: workflow.id?.toString(),
|
parentWorkflowId: workflow.id?.toString(),
|
||||||
|
@ -3980,15 +4002,16 @@ export function getSupplyDataFunctions(
|
||||||
parentWorkflowSettings: workflow.settings,
|
parentWorkflowSettings: workflow.settings,
|
||||||
node,
|
node,
|
||||||
parentCallbackManager,
|
parentCallbackManager,
|
||||||
|
...options,
|
||||||
})
|
})
|
||||||
.then(
|
.then(async (result) => {
|
||||||
async (result) =>
|
const data = await Container.get(BinaryDataService).duplicateBinaryData(
|
||||||
await Container.get(BinaryDataService).duplicateBinaryData(
|
|
||||||
workflow.id,
|
workflow.id,
|
||||||
additionalData.executionId!,
|
additionalData.executionId!,
|
||||||
result,
|
result.data,
|
||||||
),
|
);
|
||||||
),
|
return { ...result, data };
|
||||||
|
}),
|
||||||
getNodeOutputs() {
|
getNodeOutputs() {
|
||||||
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||||
return NodeHelpers.getNodeOutputs(workflow, node, nodeType.description).map((output) => {
|
return NodeHelpers.getNodeOutputs(workflow, node, nodeType.description).map((output) => {
|
||||||
|
@ -4143,6 +4166,7 @@ export function getSupplyDataFunctions(
|
||||||
connectionType: NodeConnectionType,
|
connectionType: NodeConnectionType,
|
||||||
currentNodeRunIndex: number,
|
currentNodeRunIndex: number,
|
||||||
data: INodeExecutionData[][],
|
data: INodeExecutionData[][],
|
||||||
|
metadata?: ITaskMetadata,
|
||||||
): void {
|
): void {
|
||||||
addExecutionDataFunctions(
|
addExecutionDataFunctions(
|
||||||
'output',
|
'output',
|
||||||
|
@ -4154,6 +4178,7 @@ export function getSupplyDataFunctions(
|
||||||
node.name,
|
node.name,
|
||||||
runIndex,
|
runIndex,
|
||||||
currentNodeRunIndex,
|
currentNodeRunIndex,
|
||||||
|
metadata,
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
`There was a problem logging output data of node "${this.getNode().name}": ${
|
`There was a problem logging output data of node "${this.getNode().name}": ${
|
||||||
|
|
|
@ -408,7 +408,10 @@ export class WorkflowExecute {
|
||||||
let metaRunData: ITaskMetadata;
|
let metaRunData: ITaskMetadata;
|
||||||
for (const nodeName of Object.keys(metadata)) {
|
for (const nodeName of Object.keys(metadata)) {
|
||||||
for ([index, metaRunData] of metadata[nodeName].entries()) {
|
for ([index, metaRunData] of metadata[nodeName].entries()) {
|
||||||
runData[nodeName][index].metadata = metaRunData;
|
runData[nodeName][index].metadata = {
|
||||||
|
...(runData[nodeName][index].metadata ?? {}),
|
||||||
|
...metaRunData,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1448,6 +1451,7 @@ export class WorkflowExecute {
|
||||||
startTime,
|
startTime,
|
||||||
executionTime: new Date().getTime() - startTime,
|
executionTime: new Date().getTime() - startTime,
|
||||||
source: !executionData.source ? [] : executionData.source.main,
|
source: !executionData.source ? [] : executionData.source.main,
|
||||||
|
metadata: executionData.metadata,
|
||||||
executionStatus: 'success',
|
executionStatus: 'success',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import type {
|
||||||
IContextObject,
|
IContextObject,
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
ISourceData,
|
ISourceData,
|
||||||
|
ITaskMetadata,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { ApplicationError, NodeHelpers } from 'n8n-workflow';
|
import { ApplicationError, NodeHelpers } from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -298,4 +299,33 @@ describe('ExecuteSingleContext', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('setMetadata', () => {
|
||||||
|
it('sets metadata on execution data', () => {
|
||||||
|
const context = new ExecuteSingleContext(
|
||||||
|
workflow,
|
||||||
|
node,
|
||||||
|
additionalData,
|
||||||
|
mode,
|
||||||
|
runExecutionData,
|
||||||
|
runIndex,
|
||||||
|
connectionInputData,
|
||||||
|
inputData,
|
||||||
|
itemIndex,
|
||||||
|
executeData,
|
||||||
|
abortSignal,
|
||||||
|
);
|
||||||
|
|
||||||
|
const metadata: ITaskMetadata = {
|
||||||
|
subExecution: {
|
||||||
|
workflowId: '123',
|
||||||
|
executionId: 'xyz',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(context.getExecuteData().metadata?.subExecution).toEqual(undefined);
|
||||||
|
context.setMetadata(metadata);
|
||||||
|
expect(context.getExecuteData().metadata?.subExecution).toEqual(metadata.subExecution);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
||||||
ContextType,
|
ContextType,
|
||||||
AiEvent,
|
AiEvent,
|
||||||
ISourceData,
|
ISourceData,
|
||||||
|
ITaskMetadata,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import {
|
import {
|
||||||
ApplicationError,
|
ApplicationError,
|
||||||
|
@ -85,6 +86,13 @@ export class ExecuteSingleContext extends NodeExecutionContext implements IExecu
|
||||||
this.abortSignal?.addEventListener('abort', fn);
|
this.abortSignal?.addEventListener('abort', fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMetadata(metadata: ITaskMetadata): void {
|
||||||
|
this.executeData.metadata = {
|
||||||
|
...(this.executeData.metadata ?? {}),
|
||||||
|
...metadata,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
continueOnFail() {
|
continueOnFail() {
|
||||||
return continueOnFail(this.node);
|
return continueOnFail(this.node);
|
||||||
}
|
}
|
||||||
|
|
|
@ -182,6 +182,10 @@ export interface IAiDataContent {
|
||||||
metadata: {
|
metadata: {
|
||||||
executionTime: number;
|
executionTime: number;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
|
subExecution?: {
|
||||||
|
workflowId: string;
|
||||||
|
executionId: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,6 +206,10 @@ export interface ITableData {
|
||||||
columns: string[];
|
columns: string[];
|
||||||
data: GenericValue[][];
|
data: GenericValue[][];
|
||||||
hasJson: { [key: string]: boolean };
|
hasJson: { [key: string]: boolean };
|
||||||
|
metadata: {
|
||||||
|
hasExecutionIds: boolean;
|
||||||
|
data: Array<INodeExecutionData['metadata'] | undefined>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple version of n8n-workflow.Workflow
|
// Simple version of n8n-workflow.Workflow
|
||||||
|
|
|
@ -35,7 +35,7 @@ const onDragStart = () => {
|
||||||
@dragend="onDragEnd"
|
@dragend="onDragEnd"
|
||||||
>
|
>
|
||||||
<template #default="{ isDragging }">
|
<template #default="{ isDragging }">
|
||||||
<div :class="{ [$style.dragButton]: true }">
|
<div :class="{ [$style.dragButton]: true }" data-test-id="panel-drag-button">
|
||||||
<span
|
<span
|
||||||
v-if="canMoveLeft"
|
v-if="canMoveLeft"
|
||||||
:class="{ [$style.leftArrow]: true, [$style.visible]: isDragging }"
|
:class="{ [$style.leftArrow]: true, [$style.visible]: isDragging }"
|
||||||
|
|
|
@ -8,10 +8,17 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { waitFor } from '@testing-library/vue';
|
import { waitFor } from '@testing-library/vue';
|
||||||
import type { INodeExecutionData } from 'n8n-workflow';
|
import type { INodeExecutionData, ITaskData, ITaskMetadata } from 'n8n-workflow';
|
||||||
import { setActivePinia } from 'pinia';
|
import { setActivePinia } from 'pinia';
|
||||||
import { useNodeTypesStore } from '../stores/nodeTypes.store';
|
import { useNodeTypesStore } from '../stores/nodeTypes.store';
|
||||||
|
|
||||||
|
const MOCK_EXECUTION_URL = 'execution.url/123';
|
||||||
|
|
||||||
|
const { trackOpeningRelatedExecution, resolveRelatedExecutionUrl } = vi.hoisted(() => ({
|
||||||
|
trackOpeningRelatedExecution: vi.fn(),
|
||||||
|
resolveRelatedExecutionUrl: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('vue-router', () => {
|
vi.mock('vue-router', () => {
|
||||||
return {
|
return {
|
||||||
useRouter: () => ({}),
|
useRouter: () => ({}),
|
||||||
|
@ -20,6 +27,13 @@ vi.mock('vue-router', () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock('@/composables/useExecutionHelpers', () => ({
|
||||||
|
useExecutionHelpers: () => ({
|
||||||
|
trackOpeningRelatedExecution,
|
||||||
|
resolveRelatedExecutionUrl,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
const nodes = [
|
const nodes = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
|
@ -32,50 +46,55 @@ const nodes = [
|
||||||
] as INodeUi[];
|
] as INodeUi[];
|
||||||
|
|
||||||
describe('RunData', () => {
|
describe('RunData', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
resolveRelatedExecutionUrl.mockReturnValue('execution.url/123');
|
||||||
|
});
|
||||||
|
|
||||||
it("should render pin button in output panel disabled when there's binary data", () => {
|
it("should render pin button in output panel disabled when there's binary data", () => {
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render({
|
||||||
[
|
defaultRunItems: [
|
||||||
{
|
{
|
||||||
json: {},
|
json: {},
|
||||||
binary: {
|
binary: {
|
||||||
data: {
|
data: {
|
||||||
fileName: 'test.xyz',
|
fileName: 'test.xyz',
|
||||||
mimeType: 'application/octet-stream',
|
mimeType: 'application/octet-stream',
|
||||||
|
data: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'binary',
|
displayMode: 'binary',
|
||||||
);
|
});
|
||||||
|
|
||||||
expect(getByTestId('ndv-pin-data')).toBeInTheDocument();
|
expect(getByTestId('ndv-pin-data')).toBeInTheDocument();
|
||||||
expect(getByTestId('ndv-pin-data')).toHaveAttribute('disabled');
|
expect(getByTestId('ndv-pin-data')).toHaveAttribute('disabled');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not render pin button in input panel when there's binary data", () => {
|
it("should not render pin button in input panel when there's binary data", () => {
|
||||||
const { queryByTestId } = render(
|
const { queryByTestId } = render({
|
||||||
[
|
defaultRunItems: [
|
||||||
{
|
{
|
||||||
json: {},
|
json: {},
|
||||||
binary: {
|
binary: {
|
||||||
data: {
|
data: {
|
||||||
fileName: 'test.xyz',
|
fileName: 'test.xyz',
|
||||||
mimeType: 'application/octet-stream',
|
mimeType: 'application/octet-stream',
|
||||||
|
data: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'binary',
|
displayMode: 'binary',
|
||||||
undefined,
|
paneType: 'input',
|
||||||
'input',
|
});
|
||||||
);
|
|
||||||
|
|
||||||
expect(queryByTestId('ndv-pin-data')).not.toBeInTheDocument();
|
expect(queryByTestId('ndv-pin-data')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render data correctly even when "item.json" has another "json" key', async () => {
|
it('should render data correctly even when "item.json" has another "json" key', async () => {
|
||||||
const { getByText, getAllByTestId, getByTestId } = render(
|
const { getByText, getAllByTestId, getByTestId } = render({
|
||||||
[
|
defaultRunItems: [
|
||||||
{
|
{
|
||||||
json: {
|
json: {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
@ -95,8 +114,8 @@ describe('RunData', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'schema',
|
displayMode: 'schema',
|
||||||
);
|
});
|
||||||
|
|
||||||
await userEvent.click(getByTestId('ndv-pin-data'));
|
await userEvent.click(getByTestId('ndv-pin-data'));
|
||||||
await waitFor(() => getAllByTestId('run-data-schema-item'), { timeout: 1000 });
|
await waitFor(() => getAllByTestId('run-data-schema-item'), { timeout: 1000 });
|
||||||
|
@ -105,8 +124,8 @@ describe('RunData', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render view and download buttons for PDFs', async () => {
|
it('should render view and download buttons for PDFs', async () => {
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render({
|
||||||
[
|
defaultRunItems: [
|
||||||
{
|
{
|
||||||
json: {},
|
json: {},
|
||||||
binary: {
|
binary: {
|
||||||
|
@ -114,12 +133,13 @@ describe('RunData', () => {
|
||||||
fileName: 'test.pdf',
|
fileName: 'test.pdf',
|
||||||
fileType: 'pdf',
|
fileType: 'pdf',
|
||||||
mimeType: 'application/pdf',
|
mimeType: 'application/pdf',
|
||||||
|
data: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'binary',
|
displayMode: 'binary',
|
||||||
);
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(getByTestId('ndv-view-binary-data')).toBeInTheDocument();
|
expect(getByTestId('ndv-view-binary-data')).toBeInTheDocument();
|
||||||
|
@ -129,20 +149,21 @@ describe('RunData', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render a view button for unknown content-type', async () => {
|
it('should not render a view button for unknown content-type', async () => {
|
||||||
const { getByTestId, queryByTestId } = render(
|
const { getByTestId, queryByTestId } = render({
|
||||||
[
|
defaultRunItems: [
|
||||||
{
|
{
|
||||||
json: {},
|
json: {},
|
||||||
binary: {
|
binary: {
|
||||||
data: {
|
data: {
|
||||||
fileName: 'test.xyz',
|
fileName: 'test.xyz',
|
||||||
mimeType: 'application/octet-stream',
|
mimeType: 'application/octet-stream',
|
||||||
|
data: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'binary',
|
displayMode: 'binary',
|
||||||
);
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(queryByTestId('ndv-view-binary-data')).not.toBeInTheDocument();
|
expect(queryByTestId('ndv-view-binary-data')).not.toBeInTheDocument();
|
||||||
|
@ -152,25 +173,32 @@ describe('RunData', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render pin data button when there is no output data', async () => {
|
it('should not render pin data button when there is no output data', async () => {
|
||||||
const { queryByTestId } = render([], 'table');
|
const { queryByTestId } = render({ defaultRunItems: [], displayMode: 'table' });
|
||||||
expect(queryByTestId('ndv-pin-data')).not.toBeInTheDocument();
|
expect(queryByTestId('ndv-pin-data')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disable pin data button when data is pinned', async () => {
|
it('should disable pin data button when data is pinned', async () => {
|
||||||
const { getByTestId } = render([], 'table', [{ json: { name: 'Test' } }]);
|
const { getByTestId } = render({
|
||||||
|
defaultRunItems: [],
|
||||||
|
displayMode: 'table',
|
||||||
|
pinnedData: [{ json: { name: 'Test' } }],
|
||||||
|
});
|
||||||
const pinDataButton = getByTestId('ndv-pin-data');
|
const pinDataButton = getByTestId('ndv-pin-data');
|
||||||
expect(pinDataButton).toBeDisabled();
|
expect(pinDataButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should enable pin data button when data is not pinned', async () => {
|
it('should enable pin data button when data is not pinned', async () => {
|
||||||
const { getByTestId } = render([{ json: { name: 'Test' } }], 'table');
|
const { getByTestId } = render({
|
||||||
|
defaultRunItems: [{ json: { name: 'Test' } }],
|
||||||
|
displayMode: 'table',
|
||||||
|
});
|
||||||
const pinDataButton = getByTestId('ndv-pin-data');
|
const pinDataButton = getByTestId('ndv-pin-data');
|
||||||
expect(pinDataButton).toBeEnabled();
|
expect(pinDataButton).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render pagination on binary tab', async () => {
|
it('should not render pagination on binary tab', async () => {
|
||||||
const { queryByTestId } = render(
|
const { queryByTestId } = render({
|
||||||
Array.from({ length: 11 }).map((_, i) => ({
|
defaultRunItems: Array.from({ length: 11 }).map((_, i) => ({
|
||||||
json: {
|
json: {
|
||||||
data: {
|
data: {
|
||||||
id: i,
|
id: i,
|
||||||
|
@ -180,17 +208,19 @@ describe('RunData', () => {
|
||||||
binary: {
|
binary: {
|
||||||
data: {
|
data: {
|
||||||
a: 'b',
|
a: 'b',
|
||||||
|
data: '',
|
||||||
|
mimeType: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
'binary',
|
displayMode: 'binary',
|
||||||
);
|
});
|
||||||
expect(queryByTestId('ndv-data-pagination')).not.toBeInTheDocument();
|
expect(queryByTestId('ndv-data-pagination')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render pagination with binary data on non-binary tab', async () => {
|
it('should render pagination with binary data on non-binary tab', async () => {
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render({
|
||||||
Array.from({ length: 11 }).map((_, i) => ({
|
defaultRunItems: Array.from({ length: 11 }).map((_, i) => ({
|
||||||
json: {
|
json: {
|
||||||
data: {
|
data: {
|
||||||
id: i,
|
id: i,
|
||||||
|
@ -200,20 +230,177 @@ describe('RunData', () => {
|
||||||
binary: {
|
binary: {
|
||||||
data: {
|
data: {
|
||||||
a: 'b',
|
a: 'b',
|
||||||
|
data: '',
|
||||||
|
mimeType: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
'json',
|
displayMode: 'json',
|
||||||
);
|
});
|
||||||
expect(getByTestId('ndv-data-pagination')).toBeInTheDocument();
|
expect(getByTestId('ndv-data-pagination')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
const render = (
|
it('should render sub-execution link in header', async () => {
|
||||||
outputData: unknown[],
|
const metadata: ITaskMetadata = {
|
||||||
displayMode: IRunDataDisplayMode,
|
subExecution: {
|
||||||
pinnedData?: INodeExecutionData[],
|
workflowId: 'xyz',
|
||||||
paneType: NodePanelType = 'output',
|
executionId: '123',
|
||||||
) => {
|
},
|
||||||
|
subExecutionsCount: 1,
|
||||||
|
};
|
||||||
|
const { getByTestId } = render({
|
||||||
|
defaultRunItems: [
|
||||||
|
{
|
||||||
|
json: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayMode: 'table',
|
||||||
|
paneType: 'output',
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByTestId('related-execution-link')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('related-execution-link')).toHaveTextContent('Inspect Sub-Execution 123');
|
||||||
|
expect(resolveRelatedExecutionUrl).toHaveBeenCalledWith(metadata);
|
||||||
|
expect(getByTestId('related-execution-link')).toHaveAttribute('href', MOCK_EXECUTION_URL);
|
||||||
|
|
||||||
|
expect(getByTestId('ndv-items-count')).toHaveTextContent('1 item, 1 sub-execution');
|
||||||
|
|
||||||
|
getByTestId('related-execution-link').click();
|
||||||
|
expect(trackOpeningRelatedExecution).toHaveBeenCalledWith(metadata, 'table');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render parent-execution link in header', async () => {
|
||||||
|
const metadata: ITaskMetadata = {
|
||||||
|
parentExecution: {
|
||||||
|
workflowId: 'xyz',
|
||||||
|
executionId: '123',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { getByTestId } = render({
|
||||||
|
defaultRunItems: [
|
||||||
|
{
|
||||||
|
json: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayMode: 'table',
|
||||||
|
paneType: 'output',
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByTestId('related-execution-link')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('related-execution-link')).toHaveTextContent('Inspect Parent Execution 123');
|
||||||
|
expect(resolveRelatedExecutionUrl).toHaveBeenCalledWith(metadata);
|
||||||
|
expect(getByTestId('related-execution-link')).toHaveAttribute('href', MOCK_EXECUTION_URL);
|
||||||
|
|
||||||
|
expect(getByTestId('ndv-items-count')).toHaveTextContent('1 item');
|
||||||
|
|
||||||
|
getByTestId('related-execution-link').click();
|
||||||
|
expect(trackOpeningRelatedExecution).toHaveBeenCalledWith(metadata, 'table');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render sub-execution link in header with multiple items', async () => {
|
||||||
|
const metadata: ITaskMetadata = {
|
||||||
|
subExecution: {
|
||||||
|
workflowId: 'xyz',
|
||||||
|
executionId: '123',
|
||||||
|
},
|
||||||
|
subExecutionsCount: 3,
|
||||||
|
};
|
||||||
|
const { getByTestId } = render({
|
||||||
|
defaultRunItems: [
|
||||||
|
{
|
||||||
|
json: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
json: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayMode: 'json',
|
||||||
|
paneType: 'output',
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByTestId('related-execution-link')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('related-execution-link')).toHaveTextContent('Inspect Sub-Execution 123');
|
||||||
|
expect(resolveRelatedExecutionUrl).toHaveBeenCalledWith(metadata);
|
||||||
|
expect(getByTestId('related-execution-link')).toHaveAttribute('href', MOCK_EXECUTION_URL);
|
||||||
|
|
||||||
|
expect(getByTestId('ndv-items-count')).toHaveTextContent('2 items, 3 sub-executions');
|
||||||
|
|
||||||
|
getByTestId('related-execution-link').click();
|
||||||
|
expect(trackOpeningRelatedExecution).toHaveBeenCalledWith(metadata, 'json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render sub-execution link in header with multiple runs', async () => {
|
||||||
|
const metadata: ITaskMetadata = {
|
||||||
|
subExecution: {
|
||||||
|
workflowId: 'xyz',
|
||||||
|
executionId: '123',
|
||||||
|
},
|
||||||
|
subExecutionsCount: 3,
|
||||||
|
};
|
||||||
|
const { getByTestId, queryByTestId } = render({
|
||||||
|
runs: [
|
||||||
|
{
|
||||||
|
startTime: new Date().getTime(),
|
||||||
|
executionTime: new Date().getTime(),
|
||||||
|
data: {
|
||||||
|
main: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
source: [null],
|
||||||
|
metadata,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: new Date().getTime(),
|
||||||
|
executionTime: new Date().getTime(),
|
||||||
|
data: {
|
||||||
|
main: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
source: [null],
|
||||||
|
metadata,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayMode: 'json',
|
||||||
|
paneType: 'output',
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByTestId('related-execution-link')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('related-execution-link')).toHaveTextContent('Inspect Sub-Execution 123');
|
||||||
|
|
||||||
|
expect(queryByTestId('ndv-items-count')).not.toBeInTheDocument();
|
||||||
|
expect(getByTestId('run-selector')).toBeInTheDocument();
|
||||||
|
|
||||||
|
getByTestId('related-execution-link').click();
|
||||||
|
expect(trackOpeningRelatedExecution).toHaveBeenCalledWith(metadata, 'json');
|
||||||
|
});
|
||||||
|
|
||||||
|
const render = ({
|
||||||
|
defaultRunItems,
|
||||||
|
displayMode,
|
||||||
|
pinnedData,
|
||||||
|
paneType = 'output',
|
||||||
|
metadata,
|
||||||
|
runs,
|
||||||
|
}: {
|
||||||
|
defaultRunItems?: INodeExecutionData[];
|
||||||
|
displayMode: IRunDataDisplayMode;
|
||||||
|
pinnedData?: INodeExecutionData[];
|
||||||
|
paneType?: NodePanelType;
|
||||||
|
metadata?: ITaskMetadata;
|
||||||
|
runs?: ITaskData[];
|
||||||
|
}) => {
|
||||||
|
const defaultRun: ITaskData = {
|
||||||
|
startTime: new Date().getTime(),
|
||||||
|
executionTime: new Date().getTime(),
|
||||||
|
data: {
|
||||||
|
main: [defaultRunItems ?? [{ json: {} }]],
|
||||||
|
},
|
||||||
|
source: [null],
|
||||||
|
metadata,
|
||||||
|
};
|
||||||
|
|
||||||
const pinia = createTestingPinia({
|
const pinia = createTestingPinia({
|
||||||
stubActions: false,
|
stubActions: false,
|
||||||
initialState: {
|
initialState: {
|
||||||
|
@ -246,16 +433,7 @@ describe('RunData', () => {
|
||||||
data: {
|
data: {
|
||||||
resultData: {
|
resultData: {
|
||||||
runData: {
|
runData: {
|
||||||
'Test Node': [
|
'Test Node': runs ?? [defaultRun],
|
||||||
{
|
|
||||||
startTime: new Date().getTime(),
|
|
||||||
executionTime: new Date().getTime(),
|
|
||||||
data: {
|
|
||||||
main: [outputData],
|
|
||||||
},
|
|
||||||
source: [null],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type {
|
||||||
INodeOutputConfiguration,
|
INodeOutputConfiguration,
|
||||||
IRunData,
|
IRunData,
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
|
ITaskMetadata,
|
||||||
NodeError,
|
NodeError,
|
||||||
NodeHint,
|
NodeHint,
|
||||||
Workflow,
|
Workflow,
|
||||||
|
@ -77,6 +78,7 @@ import {
|
||||||
} from 'n8n-design-system';
|
} from 'n8n-design-system';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useExecutionHelpers } from '@/composables/useExecutionHelpers';
|
||||||
|
|
||||||
const LazyRunDataTable = defineAsyncComponent(
|
const LazyRunDataTable = defineAsyncComponent(
|
||||||
async () => await import('@/components/RunDataTable.vue'),
|
async () => await import('@/components/RunDataTable.vue'),
|
||||||
|
@ -180,6 +182,7 @@ const nodeHelpers = useNodeHelpers();
|
||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const { trackOpeningRelatedExecution, resolveRelatedExecutionUrl } = useExecutionHelpers();
|
||||||
|
|
||||||
const node = toRef(props, 'node');
|
const node = toRef(props, 'node');
|
||||||
|
|
||||||
|
@ -315,7 +318,9 @@ const workflowRunData = computed(() => {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
const dataCount = computed(() => getDataCount(props.runIndex, currentOutputIndex.value));
|
const dataCount = computed(() =>
|
||||||
|
getDataCount(props.runIndex, currentOutputIndex.value, connectionType.value),
|
||||||
|
);
|
||||||
|
|
||||||
const unfilteredDataCount = computed(() =>
|
const unfilteredDataCount = computed(() =>
|
||||||
pinnedData.data.value ? pinnedData.data.value.length : rawInputData.value.length,
|
pinnedData.data.value ? pinnedData.data.value.length : rawInputData.value.length,
|
||||||
|
@ -506,6 +511,28 @@ const pinButtonDisabled = computed(
|
||||||
readOnlyEnv.value,
|
readOnlyEnv.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const activeTaskMetadata = computed((): ITaskMetadata | null => {
|
||||||
|
if (!node.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return workflowRunData.value?.[node.value.name]?.[props.runIndex]?.metadata ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasReleatedExectuion = computed((): boolean => {
|
||||||
|
return Boolean(
|
||||||
|
activeTaskMetadata.value?.subExecution || activeTaskMetadata.value?.parentExecution,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasInputOverwrite = computed((): boolean => {
|
||||||
|
if (!node.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const taskData = nodeHelpers.getNodeTaskData(node.value, props.runIndex);
|
||||||
|
return Boolean(taskData?.inputOverride);
|
||||||
|
});
|
||||||
|
|
||||||
watch(node, (newNode, prevNode) => {
|
watch(node, (newNode, prevNode) => {
|
||||||
if (newNode?.id === prevNode?.id) return;
|
if (newNode?.id === prevNode?.id) return;
|
||||||
init();
|
init();
|
||||||
|
@ -953,6 +980,10 @@ function onDisplayModeChange(newDisplayMode: IRunDataDisplayMode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRunLabel(option: number) {
|
function getRunLabel(option: number) {
|
||||||
|
if (!node.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let itemsCount = 0;
|
let itemsCount = 0;
|
||||||
for (let i = 0; i <= maxOutputIndex.value; i++) {
|
for (let i = 0; i <= maxOutputIndex.value; i++) {
|
||||||
itemsCount += getPinDataOrLiveData(getRawInputData(option - 1, i)).length;
|
itemsCount += getPinDataOrLiveData(getRawInputData(option - 1, i)).length;
|
||||||
|
@ -961,7 +992,18 @@ function getRunLabel(option: number) {
|
||||||
adjustToNumber: itemsCount,
|
adjustToNumber: itemsCount,
|
||||||
interpolate: { count: itemsCount },
|
interpolate: { count: itemsCount },
|
||||||
});
|
});
|
||||||
const itemsLabel = itemsCount > 0 ? ` (${items})` : '';
|
|
||||||
|
const metadata = workflowRunData.value?.[node.value.name]?.[option - 1]?.metadata ?? null;
|
||||||
|
const subexecutions = metadata?.subExecutionsCount
|
||||||
|
? i18n.baseText('ndv.output.andSubExecutions', {
|
||||||
|
adjustToNumber: metadata.subExecutionsCount,
|
||||||
|
interpolate: {
|
||||||
|
count: metadata.subExecutionsCount,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const itemsLabel = itemsCount > 0 ? ` (${items}${subexecutions})` : '';
|
||||||
return option + i18n.baseText('ndv.output.of') + (maxRunIndex.value + 1) + itemsLabel;
|
return option + i18n.baseText('ndv.output.of') + (maxRunIndex.value + 1) + itemsLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1185,6 +1227,22 @@ function onSearchClear() {
|
||||||
document.dispatchEvent(new KeyboardEvent('keyup', { key: '/' }));
|
document.dispatchEvent(new KeyboardEvent('keyup', { key: '/' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getExecutionLinkLabel(task: ITaskMetadata): string | undefined {
|
||||||
|
if (task.parentExecution) {
|
||||||
|
return i18n.baseText('runData.openParentExecution', {
|
||||||
|
interpolate: { id: task.parentExecution.executionId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.subExecution) {
|
||||||
|
return i18n.baseText('runData.openSubExecution', {
|
||||||
|
interpolate: { id: task.subExecution.executionId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({ enterEditMode });
|
defineExpose({ enterEditMode });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1311,11 +1369,12 @@ defineExpose({ enterEditMode });
|
||||||
v-show="!editMode.enabled"
|
v-show="!editMode.enabled"
|
||||||
:class="$style.runSelector"
|
:class="$style.runSelector"
|
||||||
>
|
>
|
||||||
|
<div :class="$style.runSelectorInner">
|
||||||
<slot v-if="inputSelectLocation === 'runs'" name="input-select"></slot>
|
<slot v-if="inputSelectLocation === 'runs'" name="input-select"></slot>
|
||||||
|
|
||||||
<N8nSelect
|
<N8nSelect
|
||||||
:model-value="runIndex"
|
:model-value="runIndex"
|
||||||
:class="$style.runSelectorInner"
|
:class="$style.runSelectorSelect"
|
||||||
size="small"
|
size="small"
|
||||||
teleported
|
teleported
|
||||||
data-test-id="run-selector"
|
data-test-id="run-selector"
|
||||||
|
@ -1349,6 +1408,21 @@ defineExpose({ enterEditMode });
|
||||||
<slot name="run-info"></slot>
|
<slot name="run-info"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
v-if="
|
||||||
|
activeTaskMetadata && hasReleatedExectuion && !(paneType === 'input' && hasInputOverwrite)
|
||||||
|
"
|
||||||
|
:class="$style.relatedExecutionInfo"
|
||||||
|
data-test-id="related-execution-link"
|
||||||
|
:href="resolveRelatedExecutionUrl(activeTaskMetadata)"
|
||||||
|
target="_blank"
|
||||||
|
@click.stop="trackOpeningRelatedExecution(activeTaskMetadata, displayMode)"
|
||||||
|
>
|
||||||
|
<N8nIcon icon="external-link-alt" size="xsmall" />
|
||||||
|
{{ getExecutionLinkLabel(activeTaskMetadata) }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<slot v-if="!displaysMultipleNodes" name="before-data" />
|
<slot v-if="!displaysMultipleNodes" name="before-data" />
|
||||||
|
|
||||||
<N8nCallout
|
<N8nCallout
|
||||||
|
@ -1400,13 +1474,37 @@ defineExpose({ enterEditMode });
|
||||||
}}
|
}}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
<N8nText v-else :class="$style.itemsText">
|
<N8nText v-else :class="$style.itemsText">
|
||||||
|
<span>
|
||||||
{{
|
{{
|
||||||
$locale.baseText('ndv.output.items', {
|
$locale.baseText('ndv.output.items', {
|
||||||
adjustToNumber: dataCount,
|
adjustToNumber: dataCount,
|
||||||
interpolate: { count: dataCount },
|
interpolate: { count: dataCount },
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
</span>
|
||||||
|
<span v-if="activeTaskMetadata?.subExecutionsCount">
|
||||||
|
{{
|
||||||
|
$locale.baseText('ndv.output.andSubExecutions', {
|
||||||
|
adjustToNumber: activeTaskMetadata.subExecutionsCount,
|
||||||
|
interpolate: { count: activeTaskMetadata.subExecutionsCount },
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
</N8nText>
|
</N8nText>
|
||||||
|
|
||||||
|
<a
|
||||||
|
v-if="
|
||||||
|
activeTaskMetadata && hasReleatedExectuion && !(paneType === 'input' && hasInputOverwrite)
|
||||||
|
"
|
||||||
|
:class="$style.relatedExecutionInfo"
|
||||||
|
data-test-id="related-execution-link"
|
||||||
|
:href="resolveRelatedExecutionUrl(activeTaskMetadata)"
|
||||||
|
target="_blank"
|
||||||
|
@click.stop="trackOpeningRelatedExecution(activeTaskMetadata, displayMode)"
|
||||||
|
>
|
||||||
|
<N8nIcon icon="external-link-alt" size="xsmall" />
|
||||||
|
{{ getExecutionLinkLabel(activeTaskMetadata) }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="dataContainerRef" :class="$style.dataContainer" data-test-id="ndv-data-container">
|
<div ref="dataContainerRef" :class="$style.dataContainer" data-test-id="ndv-data-container">
|
||||||
|
@ -1877,6 +1975,7 @@ defineExpose({ enterEditMode });
|
||||||
padding-left: var(--spacing-s);
|
padding-left: var(--spacing-s);
|
||||||
padding-right: var(--spacing-s);
|
padding-right: var(--spacing-s);
|
||||||
padding-bottom: var(--spacing-s);
|
padding-bottom: var(--spacing-s);
|
||||||
|
flex-flow: wrap;
|
||||||
|
|
||||||
.itemsText {
|
.itemsText {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
@ -1898,24 +1997,31 @@ defineExpose({ enterEditMode });
|
||||||
}
|
}
|
||||||
|
|
||||||
.runSelector {
|
.runSelector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-flow: wrap;
|
||||||
padding-left: var(--spacing-s);
|
padding-left: var(--spacing-s);
|
||||||
padding-right: var(--spacing-s);
|
padding-right: var(--spacing-s);
|
||||||
padding-bottom: var(--spacing-s);
|
margin-bottom: var(--spacing-s);
|
||||||
display: flex;
|
gap: var(--spacing-3xs);
|
||||||
gap: var(--spacing-4xs);
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
:global(.el-input--suffix .el-input__inner) {
|
:global(.el-input--suffix .el-input__inner) {
|
||||||
padding-right: var(--spacing-l);
|
padding-right: var(--spacing-l);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search {
|
.runSelectorInner {
|
||||||
margin-left: auto;
|
display: flex;
|
||||||
|
gap: var(--spacing-4xs);
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.runSelectorInner {
|
.runSelectorSelect {
|
||||||
max-width: 172px;
|
max-width: 205px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
|
@ -2075,6 +2181,15 @@ defineExpose({ enterEditMode });
|
||||||
.schema {
|
.schema {
|
||||||
padding: 0 var(--spacing-s);
|
padding: 0 var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.relatedExecutionInfo {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
margin-left: var(--spacing-3xs);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -60,6 +60,7 @@ function getReferencedData(
|
||||||
metadata: {
|
metadata: {
|
||||||
executionTime: taskData.executionTime,
|
executionTime: taskData.executionTime,
|
||||||
startTime: taskData.startTime,
|
startTime: taskData.startTime,
|
||||||
|
subExecution: taskData.metadata?.subExecution,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import NodeIcon from '@/components/NodeIcon.vue';
|
import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
import AiRunContentBlock from './AiRunContentBlock.vue';
|
import AiRunContentBlock from './AiRunContentBlock.vue';
|
||||||
|
import { useExecutionHelpers } from '@/composables/useExecutionHelpers';
|
||||||
|
|
||||||
interface RunMeta {
|
interface RunMeta {
|
||||||
startTimeMs: number;
|
startTimeMs: number;
|
||||||
|
@ -18,6 +19,10 @@ interface RunMeta {
|
||||||
node: INodeTypeDescription | null;
|
node: INodeTypeDescription | null;
|
||||||
type: 'input' | 'output';
|
type: 'input' | 'output';
|
||||||
connectionType: NodeConnectionType;
|
connectionType: NodeConnectionType;
|
||||||
|
subExecution?: {
|
||||||
|
workflowId: string;
|
||||||
|
executionId: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
inputData: IAiData;
|
inputData: IAiData;
|
||||||
|
@ -27,6 +32,8 @@ const props = defineProps<{
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
|
const { trackOpeningRelatedExecution, resolveRelatedExecutionUrl } = useExecutionHelpers();
|
||||||
|
|
||||||
type TokenUsageData = {
|
type TokenUsageData = {
|
||||||
completionTokens: number;
|
completionTokens: number;
|
||||||
promptTokens: number;
|
promptTokens: number;
|
||||||
|
@ -75,6 +82,7 @@ function extractRunMeta(run: IAiDataContent) {
|
||||||
node: nodeType,
|
node: nodeType,
|
||||||
type: run.inOut,
|
type: run.inOut,
|
||||||
connectionType: run.type,
|
connectionType: run.type,
|
||||||
|
subExecution: run.metadata?.subExecution,
|
||||||
};
|
};
|
||||||
|
|
||||||
return runMeta;
|
return runMeta;
|
||||||
|
@ -131,6 +139,22 @@ const outputError = computed(() => {
|
||||||
}}
|
}}
|
||||||
</n8n-tooltip>
|
</n8n-tooltip>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="runMeta?.subExecution">
|
||||||
|
<a
|
||||||
|
:href="resolveRelatedExecutionUrl(runMeta)"
|
||||||
|
target="_blank"
|
||||||
|
@click.stop="trackOpeningRelatedExecution(runMeta, 'ai')"
|
||||||
|
>
|
||||||
|
<N8nIcon icon="external-link-alt" size="xsmall" />
|
||||||
|
{{
|
||||||
|
$locale.baseText('runData.openSubExecution', {
|
||||||
|
interpolate: {
|
||||||
|
id: runMeta.subExecution?.executionId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li v-if="(consumedTokensSum?.totalTokens ?? 0) > 0" :class="$style.tokensUsage">
|
<li v-if="(consumedTokensSum?.totalTokens ?? 0) > 0" :class="$style.tokensUsage">
|
||||||
{{
|
{{
|
||||||
$locale.baseText('runData.aiContentBlock.tokens', {
|
$locale.baseText('runData.aiContentBlock.tokens', {
|
||||||
|
|
|
@ -13,8 +13,9 @@ import MappingPill from './MappingPill.vue';
|
||||||
import TextWithHighlights from './TextWithHighlights.vue';
|
import TextWithHighlights from './TextWithHighlights.vue';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { N8nInfoTip, N8nTooltip, N8nTree } from 'n8n-design-system';
|
import { N8nIconButton, N8nInfoTip, N8nTooltip, N8nTree } from 'n8n-design-system';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useExecutionHelpers } from '@/composables/useExecutionHelpers';
|
||||||
|
|
||||||
const MAX_COLUMNS_LIMIT = 40;
|
const MAX_COLUMNS_LIMIT = 40;
|
||||||
|
|
||||||
|
@ -63,6 +64,7 @@ const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
const { trackOpeningRelatedExecution, resolveRelatedExecutionUrl } = useExecutionHelpers();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
hoveringItem,
|
hoveringItem,
|
||||||
|
@ -116,6 +118,18 @@ function isHoveringRow(row: number): boolean {
|
||||||
return pairedItemMappings.value[itemNodeId].has(hoveringItemId);
|
return pairedItemMappings.value[itemNodeId].has(hoveringItemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showExecutionLink(index: number) {
|
||||||
|
if (index === activeRow.value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeRow.value === null) {
|
||||||
|
return index === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function onMouseEnterCell(e: MouseEvent) {
|
function onMouseEnterCell(e: MouseEvent) {
|
||||||
const target = e.target;
|
const target = e.target;
|
||||||
if (target && props.mappingEnabled) {
|
if (target && props.mappingEnabled) {
|
||||||
|
@ -304,6 +318,11 @@ function convertToTable(inputData: INodeExecutionData[]): ITableData {
|
||||||
let leftEntryColumns: string[], entryRows: GenericValue[];
|
let leftEntryColumns: string[], entryRows: GenericValue[];
|
||||||
// Go over all entries
|
// Go over all entries
|
||||||
let entry: IDataObject;
|
let entry: IDataObject;
|
||||||
|
|
||||||
|
const metadata: ITableData['metadata'] = {
|
||||||
|
hasExecutionIds: false,
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
const hasJson: { [key: string]: boolean } = {};
|
const hasJson: { [key: string]: boolean } = {};
|
||||||
inputData.forEach((data) => {
|
inputData.forEach((data) => {
|
||||||
if (!data.hasOwnProperty('json')) {
|
if (!data.hasOwnProperty('json')) {
|
||||||
|
@ -322,6 +341,13 @@ function convertToTable(inputData: INodeExecutionData[]): ITableData {
|
||||||
leftEntryColumns = entryColumns;
|
leftEntryColumns = entryColumns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.metadata?.subExecution) {
|
||||||
|
metadata.data.push(data.metadata);
|
||||||
|
metadata.hasExecutionIds = true;
|
||||||
|
} else {
|
||||||
|
metadata.data.push(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
// Go over all the already existing column-keys
|
// Go over all the already existing column-keys
|
||||||
tableColumns.forEach((key) => {
|
tableColumns.forEach((key) => {
|
||||||
if (entry.hasOwnProperty(key)) {
|
if (entry.hasOwnProperty(key)) {
|
||||||
|
@ -368,6 +394,7 @@ function convertToTable(inputData: INodeExecutionData[]): ITableData {
|
||||||
hasJson,
|
hasJson,
|
||||||
columns: tableColumns,
|
columns: tableColumns,
|
||||||
data: resultTableData,
|
data: resultTableData,
|
||||||
|
metadata,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -390,6 +417,9 @@ watch(focusedMappableInput, (curr) => {
|
||||||
<table v-if="tableData.columns && tableData.columns.length === 0" :class="$style.table">
|
<table v-if="tableData.columns && tableData.columns.length === 0" :class="$style.table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th v-if="tableData.metadata.hasExecutionIds" :class="$style.executionLinkRowHeader">
|
||||||
|
<!-- column for execution link -->
|
||||||
|
</th>
|
||||||
<th :class="$style.emptyCell"></th>
|
<th :class="$style.emptyCell"></th>
|
||||||
<th :class="$style.tableRightMargin"></th>
|
<th :class="$style.tableRightMargin"></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -400,6 +430,37 @@ watch(focusedMappableInput, (curr) => {
|
||||||
:key="index1"
|
:key="index1"
|
||||||
:class="{ [$style.hoveringRow]: isHoveringRow(index1) }"
|
:class="{ [$style.hoveringRow]: isHoveringRow(index1) }"
|
||||||
>
|
>
|
||||||
|
<td
|
||||||
|
v-if="tableData.metadata.hasExecutionIds"
|
||||||
|
:data-row="index1"
|
||||||
|
:class="$style.executionLinkCell"
|
||||||
|
@mouseenter="onMouseEnterCell"
|
||||||
|
@mouseleave="onMouseLeaveCell"
|
||||||
|
>
|
||||||
|
<N8nTooltip
|
||||||
|
:content="
|
||||||
|
i18n.baseText('runData.table.inspectSubExecution', {
|
||||||
|
interpolate: {
|
||||||
|
id: `${tableData.metadata.data[index1]?.subExecution.executionId}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
"
|
||||||
|
placement="left"
|
||||||
|
:hide-after="0"
|
||||||
|
>
|
||||||
|
<N8nIconButton
|
||||||
|
v-if="tableData.metadata.data[index1]"
|
||||||
|
v-show="showExecutionLink(index1)"
|
||||||
|
type="secondary"
|
||||||
|
icon="external-link-alt"
|
||||||
|
data-test-id="debug-sub-execution"
|
||||||
|
size="mini"
|
||||||
|
:href="resolveRelatedExecutionUrl(tableData.metadata.data[index1])"
|
||||||
|
target="_blank"
|
||||||
|
@click="trackOpeningRelatedExecution(tableData.metadata.data[index1], 'table')"
|
||||||
|
/>
|
||||||
|
</N8nTooltip>
|
||||||
|
</td>
|
||||||
<td
|
<td
|
||||||
:data-row="index1"
|
:data-row="index1"
|
||||||
:data-col="0"
|
:data-col="0"
|
||||||
|
@ -415,6 +476,9 @@ watch(focusedMappableInput, (curr) => {
|
||||||
<table v-else :class="$style.table">
|
<table v-else :class="$style.table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th v-if="tableData.metadata.hasExecutionIds" :class="$style.executionLinkRowHeader">
|
||||||
|
<!-- column for execution link -->
|
||||||
|
</th>
|
||||||
<th v-for="(column, i) in tableData.columns || []" :key="column">
|
<th v-for="(column, i) in tableData.columns || []" :key="column">
|
||||||
<N8nTooltip placement="bottom-start" :disabled="!mappingEnabled" :show-after="1000">
|
<N8nTooltip placement="bottom-start" :disabled="!mappingEnabled" :show-after="1000">
|
||||||
<template #content>
|
<template #content>
|
||||||
|
@ -502,6 +566,40 @@ watch(focusedMappableInput, (curr) => {
|
||||||
:class="{ [$style.hoveringRow]: isHoveringRow(index1) }"
|
:class="{ [$style.hoveringRow]: isHoveringRow(index1) }"
|
||||||
:data-test-id="isHoveringRow(index1) ? 'hovering-item' : undefined"
|
:data-test-id="isHoveringRow(index1) ? 'hovering-item' : undefined"
|
||||||
>
|
>
|
||||||
|
<td
|
||||||
|
v-if="tableData.metadata.hasExecutionIds"
|
||||||
|
:data-row="index1"
|
||||||
|
:class="$style.executionLinkCell"
|
||||||
|
@mouseenter="onMouseEnterCell"
|
||||||
|
@mouseleave="onMouseLeaveCell"
|
||||||
|
>
|
||||||
|
<N8nTooltip
|
||||||
|
:content="
|
||||||
|
i18n.baseText('runData.table.inspectSubExecution', {
|
||||||
|
interpolate: {
|
||||||
|
id: `${tableData.metadata.data[index1]?.subExecution.executionId}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
"
|
||||||
|
placement="left"
|
||||||
|
:hide-after="0"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="tableData.metadata.data[index1]"
|
||||||
|
v-show="showExecutionLink(index1)"
|
||||||
|
:href="resolveRelatedExecutionUrl(tableData.metadata.data[index1])"
|
||||||
|
target="_blank"
|
||||||
|
@click="trackOpeningRelatedExecution(tableData.metadata.data[index1], 'table')"
|
||||||
|
>
|
||||||
|
<N8nIconButton
|
||||||
|
type="secondary"
|
||||||
|
icon="external-link-alt"
|
||||||
|
data-test-id="debug-sub-execution"
|
||||||
|
size="mini"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</N8nTooltip>
|
||||||
|
</td>
|
||||||
<td
|
<td
|
||||||
v-for="(data, index2) in row"
|
v-for="(data, index2) in row"
|
||||||
:key="index2"
|
:key="index2"
|
||||||
|
@ -736,4 +834,12 @@ watch(focusedMappableInput, (curr) => {
|
||||||
.warningTooltip {
|
.warningTooltip {
|
||||||
color: var(--color-warning);
|
color: var(--color-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.executionLinkCell {
|
||||||
|
padding: var(--spacing-3xs) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.executionLinkRowHeader {
|
||||||
|
width: var(--spacing-m);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
import { ref, computed, useCssModule } from 'vue';
|
import { ref, computed, useCssModule } from 'vue';
|
||||||
import type { ExecutionSummary } from 'n8n-workflow';
|
import type { ExecutionSummary } from 'n8n-workflow';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { VIEWS, WAIT_TIME_UNLIMITED } from '@/constants';
|
import { WAIT_TIME_UNLIMITED } from '@/constants';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { convertToDisplayDate } from '@/utils/formatters/dateFormatter';
|
import { convertToDisplayDate } from '@/utils/formatters/dateFormatter';
|
||||||
import { i18n as locale } from '@/plugins/i18n';
|
import { i18n as locale } from '@/plugins/i18n';
|
||||||
import ExecutionsTime from '@/components/executions/ExecutionsTime.vue';
|
import ExecutionsTime from '@/components/executions/ExecutionsTime.vue';
|
||||||
|
@ -35,7 +34,6 @@ const props = withDefaults(
|
||||||
|
|
||||||
const style = useCssModule();
|
const style = useCssModule();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const router = useRouter();
|
|
||||||
const executionHelpers = useExecutionHelpers();
|
const executionHelpers = useExecutionHelpers();
|
||||||
|
|
||||||
const isStopping = ref(false);
|
const isStopping = ref(false);
|
||||||
|
@ -138,11 +136,7 @@ function formatDate(fullDate: Date | string | number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayExecution() {
|
function displayExecution() {
|
||||||
const route = router.resolve({
|
executionHelpers.openExecutionInNewTab(props.execution.id, props.execution.workflowId);
|
||||||
name: VIEWS.EXECUTION_PREVIEW,
|
|
||||||
params: { name: props.execution.workflowId, executionId: props.execution.id },
|
|
||||||
});
|
|
||||||
window.open(route.href, '_blank');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStopExecution() {
|
function onStopExecution() {
|
||||||
|
|
|
@ -3,6 +3,22 @@ import type { ExecutionSummary } from 'n8n-workflow';
|
||||||
import { i18n } from '@/plugins/i18n';
|
import { i18n } from '@/plugins/i18n';
|
||||||
import { convertToDisplayDate } from '@/utils/formatters/dateFormatter';
|
import { convertToDisplayDate } from '@/utils/formatters/dateFormatter';
|
||||||
|
|
||||||
|
const { resolve, track } = vi.hoisted(() => ({
|
||||||
|
resolve: vi.fn(),
|
||||||
|
track: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('vue-router', () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
resolve,
|
||||||
|
}),
|
||||||
|
RouterLink: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/composables/useTelemetry', () => ({
|
||||||
|
useTelemetry: () => ({ track }),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('useExecutionHelpers()', () => {
|
describe('useExecutionHelpers()', () => {
|
||||||
describe('getUIDetails()', () => {
|
describe('getUIDetails()', () => {
|
||||||
it.each([
|
it.each([
|
||||||
|
@ -68,4 +84,70 @@ describe('useExecutionHelpers()', () => {
|
||||||
).toEqual(false);
|
).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('openExecutionInNewTab', () => {
|
||||||
|
const executionId = '123';
|
||||||
|
const workflowId = 'xyz';
|
||||||
|
const href = 'test.com';
|
||||||
|
global.window.open = vi.fn();
|
||||||
|
|
||||||
|
it('opens execution in new tab', () => {
|
||||||
|
const { openExecutionInNewTab } = useExecutionHelpers();
|
||||||
|
resolve.mockReturnValue({ href });
|
||||||
|
openExecutionInNewTab(executionId, workflowId);
|
||||||
|
expect(window.open).toHaveBeenCalledWith(href, '_blank');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('trackOpeningRelatedExecution', () => {
|
||||||
|
it('tracks sub execution click', () => {
|
||||||
|
const { trackOpeningRelatedExecution } = useExecutionHelpers();
|
||||||
|
|
||||||
|
trackOpeningRelatedExecution(
|
||||||
|
{ subExecution: { executionId: '123', workflowId: 'xyz' } },
|
||||||
|
'table',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(track).toHaveBeenCalledWith('User clicked inspect sub-workflow', { view: 'table' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tracks parent execution click', () => {
|
||||||
|
const { trackOpeningRelatedExecution } = useExecutionHelpers();
|
||||||
|
|
||||||
|
trackOpeningRelatedExecution(
|
||||||
|
{ parentExecution: { executionId: '123', workflowId: 'xyz' } },
|
||||||
|
'json',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(track).toHaveBeenCalledWith('User clicked parent execution button', { view: 'json' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolveRelatedExecutionUrl', () => {
|
||||||
|
it('resolves sub execution url', () => {
|
||||||
|
const fullPath = 'test.com';
|
||||||
|
resolve.mockReturnValue({ fullPath });
|
||||||
|
const { resolveRelatedExecutionUrl } = useExecutionHelpers();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveRelatedExecutionUrl({ subExecution: { executionId: '123', workflowId: 'xyz' } }),
|
||||||
|
).toEqual(fullPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves parent execution url', () => {
|
||||||
|
const fullPath = 'test.com';
|
||||||
|
resolve.mockReturnValue({ fullPath });
|
||||||
|
const { resolveRelatedExecutionUrl } = useExecutionHelpers();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveRelatedExecutionUrl({ parentExecution: { executionId: '123', workflowId: 'xyz' } }),
|
||||||
|
).toEqual(fullPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty if no related execution url', () => {
|
||||||
|
const { resolveRelatedExecutionUrl } = useExecutionHelpers();
|
||||||
|
|
||||||
|
expect(resolveRelatedExecutionUrl({})).toEqual('');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import type { ExecutionSummary } from 'n8n-workflow';
|
import type { ExecutionSummary, RelatedExecution } from 'n8n-workflow';
|
||||||
import { convertToDisplayDate } from '@/utils/formatters/dateFormatter';
|
import { convertToDisplayDate } from '@/utils/formatters/dateFormatter';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { VIEWS } from '@/constants';
|
||||||
|
import { useTelemetry } from './useTelemetry';
|
||||||
|
import type { IRunDataDisplayMode } from '@/Interface';
|
||||||
|
|
||||||
export interface IExecutionUIData {
|
export interface IExecutionUIData {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -14,6 +18,8 @@ export interface IExecutionUIData {
|
||||||
|
|
||||||
export function useExecutionHelpers() {
|
export function useExecutionHelpers() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
function getUIDetails(execution: ExecutionSummary): IExecutionUIData {
|
function getUIDetails(execution: ExecutionSummary): IExecutionUIData {
|
||||||
const status = {
|
const status = {
|
||||||
|
@ -69,9 +75,57 @@ export function useExecutionHelpers() {
|
||||||
return ['crashed', 'error'].includes(execution.status) && !execution.retrySuccessId;
|
return ['crashed', 'error'].includes(execution.status) && !execution.retrySuccessId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openExecutionInNewTab(executionId: string, workflowId: string): void {
|
||||||
|
const route = router.resolve({
|
||||||
|
name: VIEWS.EXECUTION_PREVIEW,
|
||||||
|
params: { name: workflowId, executionId },
|
||||||
|
});
|
||||||
|
|
||||||
|
window.open(route.href, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRelatedExecutionUrl(metadata: {
|
||||||
|
parentExecution?: RelatedExecution;
|
||||||
|
subExecution?: RelatedExecution;
|
||||||
|
}): string {
|
||||||
|
const info = metadata.parentExecution || metadata.subExecution;
|
||||||
|
if (!info) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { workflowId, executionId } = info;
|
||||||
|
|
||||||
|
return router.resolve({
|
||||||
|
name: VIEWS.EXECUTION_PREVIEW,
|
||||||
|
params: { name: workflowId, executionId },
|
||||||
|
}).fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackOpeningRelatedExecution(
|
||||||
|
metadata: { parentExecution?: RelatedExecution; subExecution?: RelatedExecution },
|
||||||
|
view: IRunDataDisplayMode,
|
||||||
|
) {
|
||||||
|
const info = metadata.parentExecution || metadata.subExecution;
|
||||||
|
if (!info) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
telemetry.track(
|
||||||
|
metadata.parentExecution
|
||||||
|
? 'User clicked parent execution button'
|
||||||
|
: 'User clicked inspect sub-workflow',
|
||||||
|
{
|
||||||
|
view,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getUIDetails,
|
getUIDetails,
|
||||||
formatDate,
|
formatDate,
|
||||||
isExecutionRetriable,
|
isExecutionRetriable,
|
||||||
|
openExecutionInNewTab,
|
||||||
|
trackOpeningRelatedExecution,
|
||||||
|
resolveRelatedExecutionUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -566,6 +566,29 @@ export function useNodeHelpers() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNodeTaskData(node: INodeUi | null, runIndex = 0) {
|
||||||
|
if (node === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (workflowsStore.getWorkflowExecution === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const executionData = workflowsStore.getWorkflowExecution.data;
|
||||||
|
if (!executionData?.resultData) {
|
||||||
|
// unknown status
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const runData = executionData.resultData.runData;
|
||||||
|
|
||||||
|
const taskData = get(runData, [node.name, runIndex]);
|
||||||
|
if (!taskData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return taskData;
|
||||||
|
}
|
||||||
|
|
||||||
function getNodeInputData(
|
function getNodeInputData(
|
||||||
node: INodeUi | null,
|
node: INodeUi | null,
|
||||||
runIndex = 0,
|
runIndex = 0,
|
||||||
|
@ -583,22 +606,8 @@ export function useNodeHelpers() {
|
||||||
runIndex = runIndex - 1;
|
runIndex = runIndex - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node === null) {
|
const taskData = getNodeTaskData(node, runIndex);
|
||||||
return [];
|
if (taskData === null) {
|
||||||
}
|
|
||||||
if (workflowsStore.getWorkflowExecution === null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const executionData = workflowsStore.getWorkflowExecution.data;
|
|
||||||
if (!executionData?.resultData) {
|
|
||||||
// unknown status
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const runData = executionData.resultData.runData;
|
|
||||||
|
|
||||||
const taskData = get(runData, [node.name, runIndex]);
|
|
||||||
if (!taskData) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1282,5 +1291,6 @@ export function useNodeHelpers() {
|
||||||
removeConnectionByConnectionInfo,
|
removeConnectionByConnectionInfo,
|
||||||
addPinDataConnections,
|
addPinDataConnections,
|
||||||
removePinDataConnections,
|
removePinDataConnections,
|
||||||
|
getNodeTaskData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -966,6 +966,7 @@
|
||||||
"ndv.output.branch": "Branch",
|
"ndv.output.branch": "Branch",
|
||||||
"ndv.output.executing": "Executing node...",
|
"ndv.output.executing": "Executing node...",
|
||||||
"ndv.output.items": "{count} item | {count} items",
|
"ndv.output.items": "{count} item | {count} items",
|
||||||
|
"ndv.output.andSubExecutions": ", {count} sub-execution | , {count} sub-executions",
|
||||||
"ndv.output.noOutputData.message": "n8n stops executing the workflow when a node has no output data. You can change this default behaviour via",
|
"ndv.output.noOutputData.message": "n8n stops executing the workflow when a node has no output data. You can change this default behaviour via",
|
||||||
"ndv.output.noOutputData.message.settings": "Settings",
|
"ndv.output.noOutputData.message.settings": "Settings",
|
||||||
"ndv.output.noOutputData.message.settingsOption": "> \"Always Output Data\".",
|
"ndv.output.noOutputData.message.settingsOption": "> \"Always Output Data\".",
|
||||||
|
@ -1564,6 +1565,8 @@
|
||||||
"resourceMapper.addAllFields": "Add All {fieldWord}",
|
"resourceMapper.addAllFields": "Add All {fieldWord}",
|
||||||
"resourceMapper.removeAllFields": "Remove All {fieldWord}",
|
"resourceMapper.removeAllFields": "Remove All {fieldWord}",
|
||||||
"resourceMapper.refreshFieldList": "Refresh {fieldWord} List",
|
"resourceMapper.refreshFieldList": "Refresh {fieldWord} List",
|
||||||
|
"runData.openSubExecution": "Inspect Sub-Execution {id}",
|
||||||
|
"runData.openParentExecution": "Inspect Parent Execution {id}",
|
||||||
"runData.emptyItemHint": "This is an item, but it's empty.",
|
"runData.emptyItemHint": "This is an item, but it's empty.",
|
||||||
"runData.emptyArray": "[empty array]",
|
"runData.emptyArray": "[empty array]",
|
||||||
"runData.emptyString": "[empty]",
|
"runData.emptyString": "[empty]",
|
||||||
|
@ -1611,6 +1614,7 @@
|
||||||
"runData.showBinaryData": "View",
|
"runData.showBinaryData": "View",
|
||||||
"runData.startTime": "Start Time",
|
"runData.startTime": "Start Time",
|
||||||
"runData.table": "Table",
|
"runData.table": "Table",
|
||||||
|
"runData.table.inspectSubExecution": "Inspect sub-execution {id}",
|
||||||
"runData.pindata.learnMore": "Learn more",
|
"runData.pindata.learnMore": "Learn more",
|
||||||
"runData.pindata.thisDataIsPinned": "This data is pinned.",
|
"runData.pindata.thisDataIsPinned": "This data is pinned.",
|
||||||
"runData.pindata.unpin": "Unpin",
|
"runData.pindata.unpin": "Unpin",
|
||||||
|
@ -2623,6 +2627,8 @@
|
||||||
"executionUsage.button.upgrade": "Upgrade plan",
|
"executionUsage.button.upgrade": "Upgrade plan",
|
||||||
"executionUsage.expired.text": "Your trial is over. Upgrade now to keep your data.",
|
"executionUsage.expired.text": "Your trial is over. Upgrade now to keep your data.",
|
||||||
"executionUsage.ranOutOfExecutions.text": "You’re out of executions. Upgrade your plan to keep automating.",
|
"executionUsage.ranOutOfExecutions.text": "You’re out of executions. Upgrade your plan to keep automating.",
|
||||||
|
"openExecution.missingExeuctionId.title": "Could not find execution",
|
||||||
|
"openExecution.missingExeuctionId.message": "Make sure this workflow saves executions via the settings",
|
||||||
"type.string": "String",
|
"type.string": "String",
|
||||||
"type.number": "Number",
|
"type.number": "Number",
|
||||||
"type.dateTime": "Date & Time",
|
"type.dateTime": "Date & Time",
|
||||||
|
|
|
@ -25,6 +25,7 @@ const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { callDebounced } = useDebounce();
|
const { callDebounced } = useDebounce();
|
||||||
|
|
||||||
const workflowHelpers = useWorkflowHelpers({ router });
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
|
|
||||||
|
@ -100,6 +101,16 @@ async function fetchExecution() {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.showError(error, i18n.baseText('nodeView.showError.openExecution.title'));
|
toast.showError(error, i18n.baseText('nodeView.showError.openExecution.title'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!currentExecution.value) {
|
||||||
|
toast.showMessage({
|
||||||
|
type: 'error',
|
||||||
|
title: i18n.baseText('openExecution.missingExeuctionId.title'),
|
||||||
|
message: i18n.baseText('openExecution.missingExeuctionId.message'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDocumentVisibilityChange() {
|
function onDocumentVisibilityChange() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
||||||
import type {
|
import type {
|
||||||
|
ExecuteWorkflowData,
|
||||||
IExecuteFunctions,
|
IExecuteFunctions,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
INodeType,
|
INodeType,
|
||||||
|
@ -209,8 +210,11 @@ export class ExecuteWorkflow implements INodeType {
|
||||||
const mode = this.getNodeParameter('mode', 0, false) as string;
|
const mode = this.getNodeParameter('mode', 0, false) as string;
|
||||||
const items = this.getInputData();
|
const items = this.getInputData();
|
||||||
|
|
||||||
|
const workflowProxy = this.getWorkflowDataProxy(0);
|
||||||
|
const currentWorkflowId = workflowProxy.$workflow.id as string;
|
||||||
|
|
||||||
if (mode === 'each') {
|
if (mode === 'each') {
|
||||||
let returnData: INodeExecutionData[][] = [];
|
const returnData: INodeExecutionData[][] = [];
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
try {
|
try {
|
||||||
|
@ -222,14 +226,28 @@ export class ExecuteWorkflow implements INodeType {
|
||||||
const workflowInfo = await getWorkflowInfo.call(this, source, i);
|
const workflowInfo = await getWorkflowInfo.call(this, source, i);
|
||||||
|
|
||||||
if (waitForSubWorkflow) {
|
if (waitForSubWorkflow) {
|
||||||
const workflowResult: INodeExecutionData[][] = await this.executeWorkflow(
|
const executionResult: ExecuteWorkflowData = await this.executeWorkflow(
|
||||||
workflowInfo,
|
workflowInfo,
|
||||||
[items[i]],
|
[items[i]],
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
parentExecution: {
|
||||||
|
executionId: workflowProxy.$execution.id,
|
||||||
|
workflowId: workflowProxy.$workflow.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
const workflowResult = executionResult.data as INodeExecutionData[][];
|
||||||
|
|
||||||
for (const [outputIndex, outputData] of workflowResult.entries()) {
|
for (const [outputIndex, outputData] of workflowResult.entries()) {
|
||||||
for (const item of outputData) {
|
for (const item of outputData) {
|
||||||
item.pairedItem = { item: i };
|
item.pairedItem = { item: i };
|
||||||
|
item.metadata = {
|
||||||
|
subExecution: {
|
||||||
|
executionId: executionResult.executionId,
|
||||||
|
workflowId: workflowInfo.id ?? currentWorkflowId,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (returnData[outputIndex] === undefined) {
|
if (returnData[outputIndex] === undefined) {
|
||||||
|
@ -239,8 +257,32 @@ export class ExecuteWorkflow implements INodeType {
|
||||||
returnData[outputIndex].push(...outputData);
|
returnData[outputIndex].push(...outputData);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
void this.executeWorkflow(workflowInfo, [items[i]]);
|
const executionResult: ExecuteWorkflowData = await this.executeWorkflow(
|
||||||
returnData = [items];
|
workflowInfo,
|
||||||
|
[items[i]],
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
doNotWaitToFinish: true,
|
||||||
|
parentExecution: {
|
||||||
|
executionId: workflowProxy.$execution.id,
|
||||||
|
workflowId: workflowProxy.$workflow.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (returnData.length === 0) {
|
||||||
|
returnData.push([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
returnData[0].push({
|
||||||
|
...items[i],
|
||||||
|
metadata: {
|
||||||
|
subExecution: {
|
||||||
|
workflowId: workflowInfo.id ?? currentWorkflowId,
|
||||||
|
executionId: executionResult.executionId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.continueOnFail()) {
|
if (this.continueOnFail()) {
|
||||||
|
@ -258,6 +300,10 @@ export class ExecuteWorkflow implements INodeType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setMetadata({
|
||||||
|
subExecutionsCount: items.length,
|
||||||
|
});
|
||||||
|
|
||||||
return returnData;
|
return returnData;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
@ -268,15 +314,32 @@ export class ExecuteWorkflow implements INodeType {
|
||||||
) as boolean;
|
) as boolean;
|
||||||
const workflowInfo = await getWorkflowInfo.call(this, source);
|
const workflowInfo = await getWorkflowInfo.call(this, source);
|
||||||
|
|
||||||
|
const executionResult: ExecuteWorkflowData = await this.executeWorkflow(
|
||||||
|
workflowInfo,
|
||||||
|
items,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
doNotWaitToFinish: !waitForSubWorkflow,
|
||||||
|
parentExecution: {
|
||||||
|
executionId: workflowProxy.$execution.id,
|
||||||
|
workflowId: workflowProxy.$workflow.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.setMetadata({
|
||||||
|
subExecution: {
|
||||||
|
executionId: executionResult.executionId,
|
||||||
|
workflowId: workflowInfo.id ?? (workflowProxy.$workflow.id as string),
|
||||||
|
},
|
||||||
|
subExecutionsCount: 1,
|
||||||
|
});
|
||||||
|
|
||||||
if (!waitForSubWorkflow) {
|
if (!waitForSubWorkflow) {
|
||||||
void this.executeWorkflow(workflowInfo, items);
|
|
||||||
return [items];
|
return [items];
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowResult: INodeExecutionData[][] = await this.executeWorkflow(
|
const workflowResult = executionResult.data as INodeExecutionData[][];
|
||||||
workflowInfo,
|
|
||||||
items,
|
|
||||||
);
|
|
||||||
|
|
||||||
const fallbackPairedItemData = generatePairedItemData(items.length);
|
const fallbackPairedItemData = generatePairedItemData(items.length);
|
||||||
|
|
||||||
|
|
|
@ -484,6 +484,7 @@ export interface ISourceDataConnections {
|
||||||
|
|
||||||
export interface IExecuteData {
|
export interface IExecuteData {
|
||||||
data: ITaskDataConnections;
|
data: ITaskDataConnections;
|
||||||
|
metadata?: ITaskMetadata;
|
||||||
node: INode;
|
node: INode;
|
||||||
source: ITaskDataConnectionsSource | null;
|
source: ITaskDataConnectionsSource | null;
|
||||||
}
|
}
|
||||||
|
@ -936,6 +937,7 @@ export type ContextType = 'flow' | 'node';
|
||||||
|
|
||||||
type BaseExecutionFunctions = FunctionsBaseWithRequiredKeys<'getMode'> & {
|
type BaseExecutionFunctions = FunctionsBaseWithRequiredKeys<'getMode'> & {
|
||||||
continueOnFail(): boolean;
|
continueOnFail(): boolean;
|
||||||
|
setMetadata(metadata: ITaskMetadata): void;
|
||||||
evaluateExpression(expression: string, itemIndex: number): NodeParameterValueType;
|
evaluateExpression(expression: string, itemIndex: number): NodeParameterValueType;
|
||||||
getContext(type: ContextType): IContextObject;
|
getContext(type: ContextType): IContextObject;
|
||||||
getExecuteData(): IExecuteData;
|
getExecuteData(): IExecuteData;
|
||||||
|
@ -953,7 +955,11 @@ export type IExecuteFunctions = ExecuteFunctions.GetNodeParameterFn &
|
||||||
workflowInfo: IExecuteWorkflowInfo,
|
workflowInfo: IExecuteWorkflowInfo,
|
||||||
inputData?: INodeExecutionData[],
|
inputData?: INodeExecutionData[],
|
||||||
parentCallbackManager?: CallbackManager,
|
parentCallbackManager?: CallbackManager,
|
||||||
): Promise<any>;
|
options?: {
|
||||||
|
doNotWaitToFinish?: boolean;
|
||||||
|
parentExecution?: RelatedExecution;
|
||||||
|
},
|
||||||
|
): Promise<ExecuteWorkflowData>;
|
||||||
getInputConnectionData(
|
getInputConnectionData(
|
||||||
inputName: NodeConnectionType,
|
inputName: NodeConnectionType,
|
||||||
itemIndex: number,
|
itemIndex: number,
|
||||||
|
@ -976,6 +982,7 @@ export type IExecuteFunctions = ExecuteFunctions.GetNodeParameterFn &
|
||||||
connectionType: NodeConnectionType,
|
connectionType: NodeConnectionType,
|
||||||
currentNodeRunIndex: number,
|
currentNodeRunIndex: number,
|
||||||
data: INodeExecutionData[][] | ExecutionError,
|
data: INodeExecutionData[][] | ExecutionError,
|
||||||
|
metadata?: ITaskMetadata,
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
nodeHelpers: NodeHelperFunctions;
|
nodeHelpers: NodeHelperFunctions;
|
||||||
|
@ -1208,6 +1215,9 @@ export interface INodeExecutionData {
|
||||||
binary?: IBinaryKeyData;
|
binary?: IBinaryKeyData;
|
||||||
error?: NodeApiError | NodeOperationError;
|
error?: NodeApiError | NodeOperationError;
|
||||||
pairedItem?: IPairedItemData | IPairedItemData[] | number;
|
pairedItem?: IPairedItemData | IPairedItemData[] | number;
|
||||||
|
metadata?: {
|
||||||
|
subExecution: RelatedExecution;
|
||||||
|
};
|
||||||
index?: number;
|
index?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1545,6 +1555,11 @@ export interface ITriggerResponse {
|
||||||
manualTriggerResponse?: Promise<INodeExecutionData[][]>;
|
manualTriggerResponse?: Promise<INodeExecutionData[][]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExecuteWorkflowData {
|
||||||
|
executionId: string;
|
||||||
|
data: Array<INodeExecutionData[] | null>;
|
||||||
|
}
|
||||||
|
|
||||||
export type WebhookSetupMethodNames = 'checkExists' | 'create' | 'delete';
|
export type WebhookSetupMethodNames = 'checkExists' | 'create' | 'delete';
|
||||||
|
|
||||||
export namespace MultiPartFormData {
|
export namespace MultiPartFormData {
|
||||||
|
@ -2133,8 +2148,16 @@ export interface ITaskSubRunMetadata {
|
||||||
runIndex: number;
|
runIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RelatedExecution {
|
||||||
|
executionId: string;
|
||||||
|
workflowId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ITaskMetadata {
|
export interface ITaskMetadata {
|
||||||
subRun?: ITaskSubRunMetadata[];
|
subRun?: ITaskSubRunMetadata[];
|
||||||
|
parentExecution?: RelatedExecution;
|
||||||
|
subExecution?: RelatedExecution;
|
||||||
|
subExecutionsCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The data that gets returned when a node runs
|
// The data that gets returned when a node runs
|
||||||
|
@ -2261,6 +2284,8 @@ export interface ExecuteWorkflowOptions {
|
||||||
loadedRunData?: IWorkflowExecutionDataProcess;
|
loadedRunData?: IWorkflowExecutionDataProcess;
|
||||||
parentWorkflowSettings?: IWorkflowSettings;
|
parentWorkflowSettings?: IWorkflowSettings;
|
||||||
parentCallbackManager?: CallbackManager;
|
parentCallbackManager?: CallbackManager;
|
||||||
|
doNotWaitToFinish?: boolean;
|
||||||
|
parentExecution?: RelatedExecution;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiEvent =
|
export type AiEvent =
|
||||||
|
@ -2294,7 +2319,7 @@ export interface IWorkflowExecuteAdditionalData {
|
||||||
workflowInfo: IExecuteWorkflowInfo,
|
workflowInfo: IExecuteWorkflowInfo,
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
options: ExecuteWorkflowOptions,
|
options: ExecuteWorkflowOptions,
|
||||||
) => Promise<any>;
|
) => Promise<ExecuteWorkflowData>;
|
||||||
executionId?: string;
|
executionId?: string;
|
||||||
restartExecutionId?: string;
|
restartExecutionId?: string;
|
||||||
hooks?: WorkflowHooks;
|
hooks?: WorkflowHooks;
|
||||||
|
|
Loading…
Reference in a new issue