From 96083caa507565dc50cb6f3d7dc2d08887e8a06e Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Wed, 2 Aug 2023 14:37:19 +0200 Subject: [PATCH 01/51] feat(editor): Debug execution --- .../editor-ui/src/__tests__/router.test.ts | 26 ++++++ packages/editor-ui/src/constants.ts | 1 + packages/editor-ui/src/router.ts | 79 +++++++++++-------- .../editor-ui/src/stores/workflows.store.ts | 42 ++++------ packages/editor-ui/src/views/NodeView.vue | 60 +++++++------- 5 files changed, 119 insertions(+), 89 deletions(-) create mode 100644 packages/editor-ui/src/__tests__/router.test.ts diff --git a/packages/editor-ui/src/__tests__/router.test.ts b/packages/editor-ui/src/__tests__/router.test.ts new file mode 100644 index 0000000000000..8b8c3b107f02d --- /dev/null +++ b/packages/editor-ui/src/__tests__/router.test.ts @@ -0,0 +1,26 @@ +import { createPinia, setActivePinia } from 'pinia'; +import { createComponentRenderer } from '@/__tests__/render'; +import router from '@/router'; +import { VIEWS } from '@/constants'; + +const App = { + template: '
', +}; +const renderComponent = createComponentRenderer(App); + +describe('router', () => { + beforeAll(() => { + const pinia = createPinia(); + setActivePinia(pinia); + renderComponent({ pinia }); + }); + + test.each([ + ['/', VIEWS.WORKFLOWS], + ['/workflow/R9JFXwkUCL1jZBuw/executions/29021', VIEWS.EXECUTION_PREVIEW], + ['/workflow/R9JFXwkUCL1jZBuw/debug/29021', VIEWS.EXECUTION_DEBUG], + ])('should resolve %s to %s', async (path, expected) => { + await router.push(path); + expect(router.currentRoute.value.name).toBe(expected); + }); +}); diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 7cc42b68e19f8..115998fcb95a4 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -348,6 +348,7 @@ export const enum VIEWS { COLLECTION = 'TemplatesCollectionView', EXECUTIONS = 'Executions', EXECUTION_PREVIEW = 'ExecutionPreview', + EXECUTION_DEBUG = 'ExecutionPreview', EXECUTION_HOME = 'ExecutionsLandingPage', TEMPLATE = 'TemplatesWorkflowView', TEMPLATES = 'TemplatesSearchView', diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index b8403a1d2a396..b5f0c69e0b292 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -207,10 +207,6 @@ export const routes = [ }, }, }, - { - path: '/workflow', - redirect: '/workflow/new', - }, { path: '/workflows', name: VIEWS.WORKFLOWS, @@ -227,25 +223,8 @@ export const routes = [ }, }, { - path: '/workflow/new', - name: VIEWS.NEW_WORKFLOW, - components: { - default: NodeView, - header: MainHeader, - sidebar: MainSidebar, - }, - meta: { - nodeView: true, - permissions: { - allow: { - loginStatus: [LOGIN_STATUS.LoggedIn], - }, - }, - }, - }, - { - path: '/workflow/:name', - name: VIEWS.WORKFLOW, + path: '/workflow/:name/debug/:executionId', + name: VIEWS.EXECUTION_DEBUG, components: { default: NodeView, header: MainHeader, @@ -309,6 +288,41 @@ export const routes = [ }, ], }, + { + path: '/workflows/templates/:id', + name: VIEWS.TEMPLATE_IMPORT, + components: { + default: NodeView, + header: MainHeader, + sidebar: MainSidebar, + }, + meta: { + templatesEnabled: true, + getRedirect: getTemplatesRedirect, + permissions: { + allow: { + loginStatus: [LOGIN_STATUS.LoggedIn], + }, + }, + }, + }, + { + path: '/workflow/new', + name: VIEWS.NEW_WORKFLOW, + components: { + default: NodeView, + header: MainHeader, + sidebar: MainSidebar, + }, + meta: { + nodeView: true, + permissions: { + allow: { + loginStatus: [LOGIN_STATUS.LoggedIn], + }, + }, + }, + }, { path: '/workflows/demo', name: VIEWS.DEMO, @@ -324,16 +338,15 @@ export const routes = [ }, }, { - path: '/workflows/templates/:id', - name: VIEWS.TEMPLATE_IMPORT, + path: '/workflow/:name', + name: VIEWS.WORKFLOW, components: { default: NodeView, header: MainHeader, sidebar: MainSidebar, }, meta: { - templatesEnabled: true, - getRedirect: getTemplatesRedirect, + nodeView: true, permissions: { allow: { loginStatus: [LOGIN_STATUS.LoggedIn], @@ -341,6 +354,10 @@ export const routes = [ }, }, }, + { + path: '/workflow', + redirect: '/workflow/new', + }, { path: '/signin', name: VIEWS.SIGNIN, @@ -471,7 +488,7 @@ export const routes = [ shouldDeny: () => { const settingsStore = useSettingsStore(); return ( - settingsStore.settings.hideUsagePage === true || + settingsStore.settings.hideUsagePage || settingsStore.settings.deployment?.type === 'cloud' ); }, @@ -548,7 +565,7 @@ export const routes = [ deny: { shouldDeny: () => { const settingsStore = useSettingsStore(); - return settingsStore.isPublicApiEnabled === false; + return !settingsStore.isPublicApiEnabled; }, }, }, @@ -641,7 +658,7 @@ export const routes = [ deny: { shouldDeny: () => { const settingsStore = useSettingsStore(); - return settingsStore.isCommunityNodesFeatureEnabled === false; + return !settingsStore.isCommunityNodesFeatureEnabled; }, }, }, @@ -658,7 +675,7 @@ export const routes = [ pageCategory: 'settings', getProperties(route: RouteLocation) { return { - feature: route.params['featureId'], + feature: route.params.featureId, }; }, }, diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index 931704259d2aa..8174717fa5186 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -32,6 +32,7 @@ import type { IWorkflowDb, IWorkflowsMap, WorkflowsState, + NodeMetadataMap, } from '@/Interface'; import { defineStore } from 'pinia'; import type { @@ -84,7 +85,6 @@ import { useNDVStore } from './ndv.store'; import { useNodeTypesStore } from './nodeTypes.store'; import { useUsersStore } from '@/stores/users.store'; import { useSettingsStore } from '@/stores/settings.store'; -import type { NodeMetadataMap } from '@/Interface'; const defaults: Omit & { settings: NonNullable } = { name: '', @@ -157,25 +157,15 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { workflowTriggerNodes(): INodeUi[] { return this.workflow.nodes.filter((node: INodeUi) => { const nodeTypesStore = useNodeTypesStore(); - const nodeType = nodeTypesStore.getNodeType( - node.type as string, - node.typeVersion as number, - ); - return nodeType && nodeType.group.includes('trigger'); + const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion); + return nodeType?.group.includes('trigger'); }); }, currentWorkflowHasWebhookNode(): boolean { return !!this.workflow.nodes.find((node: INodeUi) => !!node.webhookId); }, getWorkflowRunData(): IRunData | null { - if ( - !this.workflowExecutionData || - !this.workflowExecutionData.data || - !this.workflowExecutionData.data.resultData - ) { - return null; - } - return this.workflowExecutionData.data.resultData.runData; + return this.workflowExecutionData?.data?.resultData.runData || null; }, getWorkflowResultDataByNodeName() { return (nodeName: string): ITaskData[] | null => { @@ -260,13 +250,12 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { return this.workflowExecutionData ? this.workflowExecutionData.executedNode : undefined; }, getParametersLastUpdate(): (name: string) => number | undefined { - return (nodeName: string) => - this.nodeMetadata[nodeName] && this.nodeMetadata[nodeName].parametersLastUpdatedAt; + return (nodeName: string) => this.nodeMetadata[nodeName]?.parametersLastUpdatedAt; }, isNodePristine(): (name: string) => boolean { return (nodeName: string) => - this.nodeMetadata[nodeName] === undefined || this.nodeMetadata[nodeName].pristine === true; + this.nodeMetadata[nodeName] === undefined || this.nodeMetadata[nodeName].pristine; }, // Executions getters getExecutionDataById(): (id: string) => IExecutionsSummary | undefined { @@ -452,7 +441,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { this.workflow.usedCredentials = data; this.usedCredentials = data.reduce<{ [name: string]: IUsedCredential }>( (accu, credential) => { - accu[credential.id!] = credential; + accu[credential.id] = credential; return accu; }, {}, @@ -488,7 +477,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { const nodeCredentials: INodeCredentials | undefined = (node as unknown as INode) .credentials; - if (!nodeCredentials || !nodeCredentials[data.type]) { + if (!nodeCredentials?.[data.type]) { return; } @@ -763,7 +752,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { break; } // Add the new connection if it does not exist already - if (connectionExists === false) { + if (!connectionExists) { this.workflow.connections[sourceData.node][sourceData.type][sourceData.index].push( destinationData, ); @@ -859,8 +848,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { // If node has any WorkflowResultData rename also that one that the data // does still get displayed also after node got renamed if ( - this.workflowExecutionData !== null && - this.workflowExecutionData.data && + this.workflowExecutionData?.data && this.workflowExecutionData.data.resultData.runData.hasOwnProperty(nameData.old) ) { this.workflowExecutionData.data.resultData.runData[nameData.new] = @@ -920,11 +908,11 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { return false; } - const node = this.workflow.nodes[nodeIndex!]; + const node = this.workflow.nodes[nodeIndex]; if (nodeIssueData.value === null) { // Remove the value if one exists - if (node.issues === undefined || node.issues[nodeIssueData.type] === undefined) { + if (node.issues?.[nodeIssueData.type] === undefined) { // No values for type exist so nothing has to get removed return true; } @@ -1098,7 +1086,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { }, addNodeExecutionData(pushData: IPushDataNodeExecuteAfter): void { - if (this.workflowExecutionData === null || !this.workflowExecutionData.data) { + if (!this.workflowExecutionData?.data) { throw new Error('The "workflowExecutionData" is not initialized!'); } if (this.workflowExecutionData.data.resultData.runData[pushData.nodeName] === undefined) { @@ -1120,7 +1108,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { this.workflowExecutionPairedItemMappings = getPairedItemsMapping(this.workflowExecutionData); }, clearNodeExecutionData(nodeName: string): void { - if (this.workflowExecutionData === null || !this.workflowExecutionData.data) { + if (!this.workflowExecutionData?.data) { return; } @@ -1139,7 +1127,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { }, pinDataByNodeName(nodeName: string): INodeExecutionData[] | undefined { - if (!this.workflow.pinData || !this.workflow.pinData[nodeName]) return undefined; + if (!this.workflow.pinData?.[nodeName]) return undefined; return this.workflow.pinData[nodeName].map((item) => item.json) as INodeExecutionData[]; }, diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index f6e6162f407a6..f4eb5583fa754 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -185,7 +185,7 @@ import { EVENT_CONNECTION_MOVED, INTERCEPT_BEFORE_DROP, } from '@jsplumb/core'; -import type { MessageBoxInputData } from 'element-plus'; +import type { MessageBoxInputData, ElNotification } from 'element-plus'; import { FIRST_ONBOARDING_PROMPT_TIMEOUT, @@ -315,7 +315,6 @@ import { N8nPlusEndpointType, EVENT_PLUS_ENDPOINT_CLICK, } from '@/plugins/endpoints/N8nPlusEndpointType'; -import type { ElNotification } from 'element-plus'; import { sourceControlEventBus } from '@/event-bus/source-control'; interface AddNodeOptions { @@ -562,7 +561,7 @@ export default defineComponent({ workflowClasses() { const returnClasses = []; if (this.ctrlKeyPressed || this.moveCanvasKeyPressed) { - if (this.uiStore.nodeViewMoveInProgress === true) { + if (this.uiStore.nodeViewMoveInProgress) { returnClasses.push('move-in-process'); } else { returnClasses.push('move-active'); @@ -1067,7 +1066,7 @@ export default defineComponent({ lastSelectedNode.name, ); } - } else if (e.key === 'a' && this.isCtrlKeyPressed(e) === true) { + } else if (e.key === 'a' && this.isCtrlKeyPressed(e)) { // Select all nodes e.stopPropagation(); e.preventDefault(); @@ -1317,7 +1316,7 @@ export default defineComponent({ const workflow = this.getCurrentWorkflow(); const childNodes = workflow.getChildNodes(sourceNodeName); for (const nodeName of childNodes) { - const node = this.workflowsStore.nodesByName[nodeName] as INodeUi; + const node = this.workflowsStore.nodesByName[nodeName]; const oldPosition = node.position; if (node.position[0] < sourceNode.position[0]) { @@ -1487,7 +1486,7 @@ export default defineComponent({ const currentTab = getNodeViewTab(this.$route); if (currentTab === MAIN_HEADER_TABS.WORKFLOW) { let workflowData: IWorkflowDataUpdate | undefined; - if (this.editAllowedCheck() === false) { + if (!this.editAllowedCheck()) { return; } // Check if it is an URL which could contain workflow data @@ -1763,11 +1762,9 @@ export default defineComponent({ parameters: {}, }; - const credentialPerType = - nodeTypeData.credentials && - nodeTypeData.credentials - .map((type) => this.credentialsStore.getUsableCredentialByType(type.name)) - .flat(); + const credentialPerType = nodeTypeData.credentials + ?.map((type) => this.credentialsStore.getUsableCredentialByType(type.name)) + .flat(); if (credentialPerType && credentialPerType.length === 1) { const defaultCredential = credentialPerType[0]; @@ -1804,10 +1801,7 @@ export default defineComponent({ return newNodeData; } - if ( - Object.keys(authDisplayOptions).length === 1 && - authDisplayOptions['authentication'] - ) { + if (Object.keys(authDisplayOptions).length === 1 && authDisplayOptions.authentication) { // ignore complex case when there's multiple dependencies newNodeData.credentials = credentials; @@ -1941,7 +1935,7 @@ export default defineComponent({ newNodeData.name = this.uniqueNodeName(localizedName); - if (nodeTypeData.webhooks && nodeTypeData.webhooks.length) { + if (nodeTypeData.webhooks?.length) { newNodeData.webhookId = uuid(); } @@ -1991,9 +1985,8 @@ export default defineComponent({ targetNodeName: string, targetNodeOuputIndex: number, ): IConnection | undefined { - const nodeConnections = ( - this.workflowsStore.outgoingConnectionsByNodeName(sourceNodeName) as INodeConnections - ).main; + const nodeConnections = + this.workflowsStore.outgoingConnectionsByNodeName(sourceNodeName).main; if (nodeConnections) { const connections: IConnection[] | null = nodeConnections[sourceNodeOutputIndex]; @@ -2075,7 +2068,7 @@ export default defineComponent({ if (lastSelectedNode) { await this.$nextTick(); - if (lastSelectedConnection && lastSelectedConnection.__meta) { + if (lastSelectedConnection?.__meta) { this.__deleteJSPlumbConnection(lastSelectedConnection, trackHistory); const targetNodeName = lastSelectedConnection.__meta.targetNodeName; @@ -2409,8 +2402,8 @@ export default defineComponent({ const { top, left, right, bottom } = element.getBoundingClientRect(); const [x, y] = NodeViewUtils.getMousePosition(e); if (top <= y && bottom >= y && left - inputMargin <= x && right >= x) { - const nodeName = (element as HTMLElement).dataset['name'] as string; - const node = this.workflowsStore.getNodeByName(nodeName) as INodeUi | null; + const nodeName = (element as HTMLElement).dataset.name as string; + const node = this.workflowsStore.getNodeByName(nodeName); if (node) { const nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion); if (nodeType && nodeType.inputs && nodeType.inputs.length === 1) { @@ -2459,7 +2452,7 @@ export default defineComponent({ .forEach((endpoint) => setTimeout(() => endpoint.instance.revalidate(endpoint.element), 0)); }, onPlusEndpointClick(endpoint: Endpoint) { - if (endpoint && endpoint.__meta) { + if (endpoint?.__meta) { this.insertNodeAfterSelected({ sourceId: endpoint.__meta.nodeId, index: endpoint.__meta.index, @@ -2553,7 +2546,6 @@ export default defineComponent({ this.stopLoading(); }, async tryToAddWelcomeSticky(): Promise { - const newWorkflow = this.workflowData; this.canvasStore.zoomToFit(); }, async initView(): Promise { @@ -2613,6 +2605,13 @@ export default defineComponent({ // Open existing workflow await this.openWorkflow(workflow); } + + if (this.$route.name === VIEWS.EXECUTION_DEBUG) { + console.log('debugging'); + console.log(workflow); + const execData = await this.workflowsStore.getExecution(this.$route.params.executionId); + console.log(execData); + } } else if (this.$route.meta?.nodeView === true) { // Create new workflow await this.newWorkflow(); @@ -2735,8 +2734,7 @@ export default defineComponent({ const nodeTypeData = this.nodeTypesStore.getNodeType(node.type, node.typeVersion); if ( - nodeTypeData && - nodeTypeData.maxNodes !== undefined && + nodeTypeData?.maxNodes !== undefined && this.getNodeTypeCount(node.type) >= nodeTypeData.maxNodes ) { this.showMaxNodeTypeError(nodeTypeData); @@ -2891,7 +2889,7 @@ export default defineComponent({ }) { const pinData = this.workflowsStore.getPinData; - if (pinData && pinData[name]) return; + if (pinData?.[name]) return; const sourceNodeName = name; const sourceNode = this.workflowsStore.getNodeByName(sourceNodeName); @@ -2936,7 +2934,7 @@ export default defineComponent({ if (output.isArtificialRecoveredEventItem) { NodeViewUtils.recoveredConnection(connection); - } else if ((!output || !output.total) && !output.isArtificialRecoveredEventItem) { + } else if (!output?.total && !output.isArtificialRecoveredEventItem) { NodeViewUtils.resetConnection(connection); } else { NodeViewUtils.addConnectionOutputSuccess(connection, output); @@ -2948,7 +2946,7 @@ export default defineComponent({ sourceNodeName, parseInt(sourceOutputIndex, 10), ); - if (endpoint && endpoint.endpoint) { + if (endpoint?.endpoint) { const output = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0]; if (output && output.total > 0) { @@ -3238,7 +3236,7 @@ export default defineComponent({ ); }, async addNodes(nodes: INodeUi[], connections?: IConnections, trackHistory = false) { - if (!nodes || !nodes.length) { + if (!nodes?.length) { return; } @@ -3728,7 +3726,7 @@ export default defineComponent({ const mode = this.nodeCreatorStore.selectedView === TRIGGER_NODE_CREATOR_VIEW ? 'trigger' : 'regular'; - if (createNodeActive === true) this.nodeCreatorStore.setOpenSource(source); + if (createNodeActive) this.nodeCreatorStore.setOpenSource(source); void this.$externalHooks().run('nodeView.createNodeActiveChanged', { source, mode, From 0d9508dafc3ff1605dc037a57b7496d0bbebf44b Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Wed, 2 Aug 2023 15:55:25 +0200 Subject: [PATCH 02/51] test: add workflow routes tests --- packages/editor-ui/src/__tests__/router.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/__tests__/router.test.ts b/packages/editor-ui/src/__tests__/router.test.ts index 8b8c3b107f02d..04e264474674b 100644 --- a/packages/editor-ui/src/__tests__/router.test.ts +++ b/packages/editor-ui/src/__tests__/router.test.ts @@ -17,10 +17,15 @@ describe('router', () => { test.each([ ['/', VIEWS.WORKFLOWS], + ['/workflow', VIEWS.NEW_WORKFLOW], + ['/workflow/new', VIEWS.NEW_WORKFLOW], + ['/workflow/R9JFXwkUCL1jZBuw', VIEWS.WORKFLOW], ['/workflow/R9JFXwkUCL1jZBuw/executions/29021', VIEWS.EXECUTION_PREVIEW], ['/workflow/R9JFXwkUCL1jZBuw/debug/29021', VIEWS.EXECUTION_DEBUG], - ])('should resolve %s to %s', async (path, expected) => { + ['/workflows/templates/R9JFXwkUCL1jZBuw', VIEWS.TEMPLATE_IMPORT], + ['/workflows/demo', VIEWS.DEMO], + ])('should resolve %s to %s', async (path, name) => { await router.push(path); - expect(router.currentRoute.value.name).toBe(expected); + expect(router.currentRoute.value.name).toBe(name); }); }); From 25fd6aa93e683fc3da6936d26329a7ebda8191fc Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Wed, 2 Aug 2023 16:07:25 +0200 Subject: [PATCH 03/51] fix(editor): debug route name --- packages/editor-ui/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index b3572fe824845..e561decfd5323 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -347,7 +347,7 @@ export const enum VIEWS { COLLECTION = 'TemplatesCollectionView', EXECUTIONS = 'Executions', EXECUTION_PREVIEW = 'ExecutionPreview', - EXECUTION_DEBUG = 'ExecutionPreview', + EXECUTION_DEBUG = 'ExecutionDebug', EXECUTION_HOME = 'ExecutionsLandingPage', TEMPLATE = 'TemplatesWorkflowView', TEMPLATES = 'TemplatesSearchView', From dfc0943c218eef4dac3faf09c6cc8f24d7f3192e Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 3 Aug 2023 11:44:58 +0200 Subject: [PATCH 04/51] feat(editor): pin execution data --- packages/editor-ui/src/Interface.ts | 2 +- .../editor-ui/src/mixins/genericHelpers.ts | 9 ++- .../src/plugins/i18n/locales/en.json | 4 ++ packages/editor-ui/src/views/NodeView.vue | 71 ++++++++++++++++--- 4 files changed, 74 insertions(+), 12 deletions(-) diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index d38bef1f124bc..1d5d5c3949069 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -716,7 +716,7 @@ export interface ITimeoutHMS { seconds: number; } -export type WorkflowTitleStatus = 'EXECUTING' | 'IDLE' | 'ERROR'; +export type WorkflowTitleStatus = 'EXECUTING' | 'IDLE' | 'ERROR' | 'DEBUG'; export type ExtractActionKeys = T extends SimplifiedNodeType ? T['name'] : never; diff --git a/packages/editor-ui/src/mixins/genericHelpers.ts b/packages/editor-ui/src/mixins/genericHelpers.ts index d0aac24107ce3..49799d832f18b 100644 --- a/packages/editor-ui/src/mixins/genericHelpers.ts +++ b/packages/editor-ui/src/mixins/genericHelpers.ts @@ -19,9 +19,12 @@ export const genericHelpers = defineComponent({ computed: { ...mapStores(useSourceControlStore), isReadOnlyRoute(): boolean { - return ![VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW, VIEWS.LOG_STREAMING_SETTINGS].includes( - this.$route.name as VIEWS, - ); + return ![ + VIEWS.WORKFLOW, + VIEWS.NEW_WORKFLOW, + VIEWS.LOG_STREAMING_SETTINGS, + VIEWS.EXECUTION_DEBUG, + ].includes(this.$route.name as VIEWS); }, readOnlyEnv(): boolean { return this.sourceControlStore.preferences.branchReadOnly; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 03069264808f7..48aca5e431936 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -853,6 +853,10 @@ "nodeView.confirmMessage.receivedCopyPasteData.confirmButtonText": "Yes, import", "nodeView.confirmMessage.receivedCopyPasteData.headline": "Import Workflow?", "nodeView.confirmMessage.receivedCopyPasteData.message": "Workflow will be imported from
{plainTextData}", + "nodeView.confirmMessage.debug.cancelButtonText": "Cancel", + "nodeView.confirmMessage.debug.confirmButtonText": "Proceed", + "nodeView.confirmMessage.debug.headline": "Debug workflow with execution data", + "nodeView.confirmMessage.debug.message": "Some nodes in this workflow have pinned data. This will be overwritten with the data from the execution. Do you want to proceed?", "nodeView.couldntImportWorkflow": "Could not import workflow", "nodeView.deletesTheCurrentExecutionData": "Deletes the current execution data", "nodeView.executesTheWorkflowFromATriggerNode": "Runs the workflow, starting from a Trigger node", diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 4c982ef98e38d..37be9a00ab615 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -753,6 +753,60 @@ export default defineComponent({ this.nodeCreatorStore.setShowScrim(true); this.onToggleNodeCreator({ source, createNodeActive: true }); }, + async pinExecutionData( + workflow: IWorkflowDb, + execution: IExecutionResponse | undefined, + ): Promise { + // If no execution data is available, return the workflow as is + if (!execution?.data?.resultData) { + return workflow; + } + + const { runData, pinData } = execution.data.resultData; + + // Get nodes from execution data and apply their pinned data or the first execution data + const executionNodesData = Object.entries(runData).map(([name, data]) => ({ + name, + data: pinData?.[name] ?? data?.[0].data?.main[0], + })); + const workflowPinnedNodeNames = Object.keys(workflow.pinData ?? {}); + + // Check if any of the workflow nodes have pinned data already + if (executionNodesData.some((eNode) => workflowPinnedNodeNames.includes(eNode.name))) { + const overWritePinnedDataConfirm = await this.confirm( + this.$locale.baseText('nodeView.confirmMessage.debug.message'), + this.$locale.baseText('nodeView.confirmMessage.debug.headline'), + { + type: 'warning', + confirmButtonText: this.$locale.baseText( + 'nodeView.confirmMessage.debug.confirmButtonText', + ), + cancelButtonText: this.$locale.baseText( + 'nodeView.confirmMessage.debug.cancelButtonText', + ), + dangerouslyUseHTMLString: true, + }, + ); + + if (overWritePinnedDataConfirm !== MODAL_CONFIRM) { + return workflow; + } + } + + // Overwrite the workflow pinned data with the execution data + workflow.pinData = executionNodesData.reduce( + (acc, { name, data }) => { + // Only add data if it exists and the node is in the workflow + if (acc && data && workflow.nodes.some((node) => node.name === name)) { + acc[name] = data; + } + return acc; + }, + {} as IWorkflowDb['pinData'], + ); + + return workflow; + }, async openExecution(executionId: string) { this.startLoading(); this.resetWorkspace(); @@ -2608,15 +2662,16 @@ export default defineComponent({ if (workflow) { this.titleSet(workflow.name, 'IDLE'); - // Open existing workflow - await this.openWorkflow(workflow); - } - if (this.$route.name === VIEWS.EXECUTION_DEBUG) { - console.log('debugging'); - console.log(workflow); - const execData = await this.workflowsStore.getExecution(this.$route.params.executionId); - console.log(execData); + if (this.$route.name === VIEWS.EXECUTION_DEBUG) { + this.titleSet(workflow.name, 'DEBUG'); + const execution = await this.workflowsStore.getExecution( + this.$route.params.executionId as string, + ); + workflow = await this.pinExecutionData(workflow, execution); + } + + await this.openWorkflow(workflow); } } else if (this.$route.meta?.nodeView === true) { // Create new workflow From d374bb4e71040f3241b9f5e16ade9f57f699c739 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 3 Aug 2023 11:50:17 +0200 Subject: [PATCH 05/51] fix(editor): type fix --- packages/editor-ui/src/views/NodeView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 37be9a00ab615..6d41919427940 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -756,7 +756,7 @@ export default defineComponent({ async pinExecutionData( workflow: IWorkflowDb, execution: IExecutionResponse | undefined, - ): Promise { + ): Promise { // If no execution data is available, return the workflow as is if (!execution?.data?.resultData) { return workflow; From a84d7f74ce7d4574d834911fcddc6a81c82a46d8 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 3 Aug 2023 12:52:31 +0200 Subject: [PATCH 06/51] fix(editor): make debugging a composable --- packages/editor-ui/src/composables/index.ts | 1 + .../src/composables/useExecutionDebugging.ts | 63 +++++++++++++++++++ packages/editor-ui/src/views/NodeView.vue | 56 +---------------- 3 files changed, 66 insertions(+), 54 deletions(-) create mode 100644 packages/editor-ui/src/composables/useExecutionDebugging.ts diff --git a/packages/editor-ui/src/composables/index.ts b/packages/editor-ui/src/composables/index.ts index 6f2ac9255f351..d0d33421d18f5 100644 --- a/packages/editor-ui/src/composables/index.ts +++ b/packages/editor-ui/src/composables/index.ts @@ -12,3 +12,4 @@ export * from './useTelemetry'; export * from './useTitleChange'; export * from './useToast'; export * from './useNodeSpecificationValues'; +export * from './useExecutionDebugging'; diff --git a/packages/editor-ui/src/composables/useExecutionDebugging.ts b/packages/editor-ui/src/composables/useExecutionDebugging.ts new file mode 100644 index 0000000000000..589b2ea17ce34 --- /dev/null +++ b/packages/editor-ui/src/composables/useExecutionDebugging.ts @@ -0,0 +1,63 @@ +import { useI18n, useMessage } from '@/composables'; +import type { IExecutionResponse, IWorkflowDb } from '@/Interface'; +import { MODAL_CONFIRM } from '@/constants'; + +export const useExecutionDebugging = () => { + const i18n = useI18n(); + const message = useMessage(); + + const pinExecutionData = async ( + workflow: IWorkflowDb, + execution: IExecutionResponse | undefined, + ): Promise => { + // If no execution data is available, return the workflow as is + if (!execution?.data?.resultData) { + return workflow; + } + + const { runData, pinData } = execution.data.resultData; + + // Get nodes from execution data and apply their pinned data or the first execution data + const executionNodesData = Object.entries(runData).map(([name, data]) => ({ + name, + data: pinData?.[name] ?? data?.[0].data?.main[0], + })); + const workflowPinnedNodeNames = Object.keys(workflow.pinData ?? {}); + + // Check if any of the workflow nodes have pinned data already + if (executionNodesData.some((eNode) => workflowPinnedNodeNames.includes(eNode.name))) { + const overWritePinnedDataConfirm = await message.confirm( + i18n.baseText('nodeView.confirmMessage.debug.message'), + i18n.baseText('nodeView.confirmMessage.debug.headline'), + { + type: 'warning', + confirmButtonText: i18n.baseText('nodeView.confirmMessage.debug.confirmButtonText'), + cancelButtonText: i18n.baseText('nodeView.confirmMessage.debug.cancelButtonText'), + dangerouslyUseHTMLString: true, + }, + ); + + if (overWritePinnedDataConfirm !== MODAL_CONFIRM) { + return workflow; + } + } + + // Overwrite the workflow pinned data with the execution data + workflow.pinData = executionNodesData.reduce( + (acc, { name, data }) => { + // Only add data if it exists and the node is in the workflow + if (acc && data && workflow.nodes.some((node) => node.name === name)) { + acc[name] = data; + } + return acc; + }, + {} as IWorkflowDb['pinData'], + ); + + return workflow; + }; + + return { + pinExecutionData, + }; +}; diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 6d41919427940..dc1b5118d0ff6 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -218,6 +218,7 @@ import { useMessage, useToast, useTitleChange, + useExecutionDebugging, } from '@/composables'; import { useUniqueNodeName } from '@/composables/useUniqueNodeName'; import { useI18n } from '@/composables/useI18n'; @@ -355,6 +356,7 @@ export default defineComponent({ ...useToast(), ...useMessage(), ...useUniqueNodeName(), + ...useExecutionDebugging(), // eslint-disable-next-line @typescript-eslint/no-misused-promises ...workflowRun.setup?.(props), }; @@ -753,60 +755,6 @@ export default defineComponent({ this.nodeCreatorStore.setShowScrim(true); this.onToggleNodeCreator({ source, createNodeActive: true }); }, - async pinExecutionData( - workflow: IWorkflowDb, - execution: IExecutionResponse | undefined, - ): Promise { - // If no execution data is available, return the workflow as is - if (!execution?.data?.resultData) { - return workflow; - } - - const { runData, pinData } = execution.data.resultData; - - // Get nodes from execution data and apply their pinned data or the first execution data - const executionNodesData = Object.entries(runData).map(([name, data]) => ({ - name, - data: pinData?.[name] ?? data?.[0].data?.main[0], - })); - const workflowPinnedNodeNames = Object.keys(workflow.pinData ?? {}); - - // Check if any of the workflow nodes have pinned data already - if (executionNodesData.some((eNode) => workflowPinnedNodeNames.includes(eNode.name))) { - const overWritePinnedDataConfirm = await this.confirm( - this.$locale.baseText('nodeView.confirmMessage.debug.message'), - this.$locale.baseText('nodeView.confirmMessage.debug.headline'), - { - type: 'warning', - confirmButtonText: this.$locale.baseText( - 'nodeView.confirmMessage.debug.confirmButtonText', - ), - cancelButtonText: this.$locale.baseText( - 'nodeView.confirmMessage.debug.cancelButtonText', - ), - dangerouslyUseHTMLString: true, - }, - ); - - if (overWritePinnedDataConfirm !== MODAL_CONFIRM) { - return workflow; - } - } - - // Overwrite the workflow pinned data with the execution data - workflow.pinData = executionNodesData.reduce( - (acc, { name, data }) => { - // Only add data if it exists and the node is in the workflow - if (acc && data && workflow.nodes.some((node) => node.name === name)) { - acc[name] = data; - } - return acc; - }, - {} as IWorkflowDb['pinData'], - ); - - return workflow; - }, async openExecution(executionId: string) { this.startLoading(); this.resetWorkspace(); From d1f932666406b25fc86f0a86c3b85b024c0af26c Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 3 Aug 2023 14:28:46 +0200 Subject: [PATCH 07/51] fix(editor): remove lint changes to reduce PR cognitive load --- .../editor-ui/src/stores/workflows.store.ts | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index 43bf2da3845b2..7c7b971084b31 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -32,7 +32,6 @@ import type { IWorkflowDb, IWorkflowsMap, WorkflowsState, - NodeMetadataMap, } from '@/Interface'; import { defineStore } from 'pinia'; import type { @@ -85,6 +84,7 @@ import { useNDVStore } from './ndv.store'; import { useNodeTypesStore } from './nodeTypes.store'; import { useUsersStore } from '@/stores/users.store'; import { useSettingsStore } from '@/stores/settings.store'; +import type { NodeMetadataMap } from '@/Interface'; const defaults: Omit & { settings: NonNullable } = { name: '', @@ -157,15 +157,25 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { workflowTriggerNodes(): INodeUi[] { return this.workflow.nodes.filter((node: INodeUi) => { const nodeTypesStore = useNodeTypesStore(); - const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion); - return nodeType?.group.includes('trigger'); + const nodeType = nodeTypesStore.getNodeType( + node.type as string, + node.typeVersion as number, + ); + return nodeType && nodeType.group.includes('trigger'); }); }, currentWorkflowHasWebhookNode(): boolean { return !!this.workflow.nodes.find((node: INodeUi) => !!node.webhookId); }, getWorkflowRunData(): IRunData | null { - return this.workflowExecutionData?.data?.resultData.runData || null; + if ( + !this.workflowExecutionData || + !this.workflowExecutionData.data || + !this.workflowExecutionData.data.resultData + ) { + return null; + } + return this.workflowExecutionData.data.resultData.runData; }, getWorkflowResultDataByNodeName() { return (nodeName: string): ITaskData[] | null => { @@ -250,12 +260,13 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { return this.workflowExecutionData ? this.workflowExecutionData.executedNode : undefined; }, getParametersLastUpdate(): (name: string) => number | undefined { - return (nodeName: string) => this.nodeMetadata[nodeName]?.parametersLastUpdatedAt; + return (nodeName: string) => + this.nodeMetadata[nodeName] && this.nodeMetadata[nodeName].parametersLastUpdatedAt; }, isNodePristine(): (name: string) => boolean { return (nodeName: string) => - this.nodeMetadata[nodeName] === undefined || this.nodeMetadata[nodeName].pristine; + this.nodeMetadata[nodeName] === undefined || this.nodeMetadata[nodeName].pristine === true; }, // Executions getters getExecutionDataById(): (id: string) => IExecutionsSummary | undefined { @@ -441,7 +452,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { this.workflow.usedCredentials = data; this.usedCredentials = data.reduce<{ [name: string]: IUsedCredential }>( (accu, credential) => { - accu[credential.id] = credential; + accu[credential.id!] = credential; return accu; }, {}, @@ -477,7 +488,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { const nodeCredentials: INodeCredentials | undefined = (node as unknown as INode) .credentials; - if (!nodeCredentials?.[data.type]) { + if (!nodeCredentials || !nodeCredentials[data.type]) { return; } @@ -739,7 +750,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { let connectionExists = false; connectionLoop: for (const existingConnection of this.workflow.connections[sourceData.node][ sourceData.type - ][sourceData.index]) { + ][sourceData.index]) { for (propertyName of checkProperties) { if ( (existingConnection as any)[propertyName] !== (destinationData as any)[propertyName] @@ -751,7 +762,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { break; } // Add the new connection if it does not exist already - if (!connectionExists) { + if (connectionExists === false) { this.workflow.connections[sourceData.node][sourceData.type][sourceData.index].push( destinationData, ); @@ -824,7 +835,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { connectionData = this.workflow.connections[sourceNode][type][parseInt(sourceIndex, 10)][ parseInt(connectionIndex, 10) - ]; + ]; if (connectionData.node === node.name) { indexesToRemove.push(connectionIndex); } @@ -847,7 +858,8 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { // If node has any WorkflowResultData rename also that one that the data // does still get displayed also after node got renamed if ( - this.workflowExecutionData?.data && + this.workflowExecutionData !== null && + this.workflowExecutionData.data && this.workflowExecutionData.data.resultData.runData.hasOwnProperty(nameData.old) ) { this.workflowExecutionData.data.resultData.runData[nameData.new] = @@ -907,11 +919,11 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { return false; } - const node = this.workflow.nodes[nodeIndex]; + const node = this.workflow.nodes[nodeIndex!]; if (nodeIssueData.value === null) { // Remove the value if one exists - if (node.issues?.[nodeIssueData.type] === undefined) { + if (node.issues === undefined || node.issues[nodeIssueData.type] === undefined) { // No values for type exist so nothing has to get removed return true; } @@ -1085,7 +1097,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { }, addNodeExecutionData(pushData: IPushDataNodeExecuteAfter): void { - if (!this.workflowExecutionData?.data) { + if (this.workflowExecutionData === null || !this.workflowExecutionData.data) { throw new Error('The "workflowExecutionData" is not initialized!'); } if (this.workflowExecutionData.data.resultData.runData[pushData.nodeName] === undefined) { @@ -1107,7 +1119,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { this.workflowExecutionPairedItemMappings = getPairedItemsMapping(this.workflowExecutionData); }, clearNodeExecutionData(nodeName: string): void { - if (!this.workflowExecutionData?.data) { + if (this.workflowExecutionData === null || !this.workflowExecutionData.data) { return; } @@ -1126,7 +1138,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { }, pinDataByNodeName(nodeName: string): INodeExecutionData[] | undefined { - if (!this.workflow.pinData?.[nodeName]) return undefined; + if (!this.workflow.pinData || !this.workflow.pinData[nodeName]) return undefined; return this.workflow.pinData[nodeName].map((item) => item.json) as INodeExecutionData[]; }, From cf1a444fdee4a9d938c5d99f3f838cb98ef6efb0 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 3 Aug 2023 14:31:25 +0200 Subject: [PATCH 08/51] fix(editor): remove lint changes to reduce PR cognitive load --- packages/editor-ui/src/stores/workflows.store.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index 7c7b971084b31..d2d6149e13b97 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -750,7 +750,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { let connectionExists = false; connectionLoop: for (const existingConnection of this.workflow.connections[sourceData.node][ sourceData.type - ][sourceData.index]) { + ][sourceData.index]) { for (propertyName of checkProperties) { if ( (existingConnection as any)[propertyName] !== (destinationData as any)[propertyName] @@ -835,7 +835,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { connectionData = this.workflow.connections[sourceNode][type][parseInt(sourceIndex, 10)][ parseInt(connectionIndex, 10) - ]; + ]; if (connectionData.node === node.name) { indexesToRemove.push(connectionIndex); } From bc74f1a49d21b035c6154b5109ed23a251a61d03 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 3 Aug 2023 14:39:09 +0200 Subject: [PATCH 09/51] fix(editor): remove lint changes to reduce PR cognitive load --- packages/editor-ui/src/views/NodeView.vue | 84 +++++++++++++---------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index dc1b5118d0ff6..cfd64db8131c8 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -213,12 +213,12 @@ import { externalHooks } from '@/mixins/externalHooks'; import { genericHelpers } from '@/mixins/genericHelpers'; import { moveNodeWorkflow } from '@/mixins/moveNodeWorkflow'; import { - useGlobalLinkActions, - useCanvasMouseSelect, - useMessage, - useToast, - useTitleChange, - useExecutionDebugging, + useGlobalLinkActions, + useCanvasMouseSelect, + useMessage, + useToast, + useTitleChange, + useExecutionDebugging, } from '@/composables'; import { useUniqueNodeName } from '@/composables/useUniqueNodeName'; import { useI18n } from '@/composables/useI18n'; @@ -356,7 +356,7 @@ export default defineComponent({ ...useToast(), ...useMessage(), ...useUniqueNodeName(), - ...useExecutionDebugging(), + ...useExecutionDebugging(), // eslint-disable-next-line @typescript-eslint/no-misused-promises ...workflowRun.setup?.(props), }; @@ -563,7 +563,7 @@ export default defineComponent({ workflowClasses() { const returnClasses = []; if (this.ctrlKeyPressed || this.moveCanvasKeyPressed) { - if (this.uiStore.nodeViewMoveInProgress) { + if (this.uiStore.nodeViewMoveInProgress === true) { returnClasses.push('move-in-process'); } else { returnClasses.push('move-active'); @@ -1068,7 +1068,7 @@ export default defineComponent({ lastSelectedNode.name, ); } - } else if (e.key === 'a' && this.isCtrlKeyPressed(e)) { + } else if (e.key === 'a' && this.isCtrlKeyPressed(e) === true) { // Select all nodes e.stopPropagation(); e.preventDefault(); @@ -1488,7 +1488,7 @@ export default defineComponent({ const currentTab = getNodeViewTab(this.$route); if (currentTab === MAIN_HEADER_TABS.WORKFLOW) { let workflowData: IWorkflowDataUpdate | undefined; - if (!this.editAllowedCheck()) { + if (this.editAllowedCheck() === false) { return; } // Check if it is an URL which could contain workflow data @@ -1763,9 +1763,11 @@ export default defineComponent({ parameters: {}, }; - const credentialPerType = nodeTypeData.credentials - ?.map((type) => this.credentialsStore.getUsableCredentialByType(type.name)) - .flat(); + const credentialPerType = + nodeTypeData.credentials && + nodeTypeData.credentials + .map((type) => this.credentialsStore.getUsableCredentialByType(type.name)) + .flat(); if (credentialPerType && credentialPerType.length === 1) { const defaultCredential = credentialPerType[0]; @@ -1802,7 +1804,10 @@ export default defineComponent({ return newNodeData; } - if (Object.keys(authDisplayOptions).length === 1 && authDisplayOptions.authentication) { + if ( + Object.keys(authDisplayOptions).length === 1 && + authDisplayOptions['authentication'] + ) { // ignore complex case when there's multiple dependencies newNodeData.credentials = credentials; @@ -1936,7 +1941,7 @@ export default defineComponent({ newNodeData.name = this.uniqueNodeName(localizedName); - if (nodeTypeData.webhooks?.length) { + if (nodeTypeData.webhooks && nodeTypeData.webhooks.length) { newNodeData.webhookId = uuid(); } @@ -1986,8 +1991,9 @@ export default defineComponent({ targetNodeName: string, targetNodeOuputIndex: number, ): IConnection | undefined { - const nodeConnections = - this.workflowsStore.outgoingConnectionsByNodeName(sourceNodeName).main; + const nodeConnections = ( + this.workflowsStore.outgoingConnectionsByNodeName(sourceNodeName) as INodeConnections + ).main; if (nodeConnections) { const connections: IConnection[] | null = nodeConnections[sourceNodeOutputIndex]; @@ -2069,7 +2075,7 @@ export default defineComponent({ if (lastSelectedNode) { await this.$nextTick(); - if (lastSelectedConnection?.__meta) { + if (lastSelectedConnection && lastSelectedConnection.__meta) { this.__deleteJSPlumbConnection(lastSelectedConnection, trackHistory); const targetNodeName = lastSelectedConnection.__meta.targetNodeName; @@ -2410,8 +2416,8 @@ export default defineComponent({ const { top, left, right, bottom } = element.getBoundingClientRect(); const [x, y] = NodeViewUtils.getMousePosition(e); if (top <= y && bottom >= y && left - inputMargin <= x && right >= x) { - const nodeName = (element as HTMLElement).dataset.name as string; - const node = this.workflowsStore.getNodeByName(nodeName); + const nodeName = (element as HTMLElement).dataset['name'] as string; + const node = this.workflowsStore.getNodeByName(nodeName) as INodeUi | null; if (node) { const nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion); if (nodeType && nodeType.inputs && nodeType.inputs.length === 1) { @@ -2460,7 +2466,7 @@ export default defineComponent({ .forEach((endpoint) => setTimeout(() => endpoint.instance.revalidate(endpoint.element), 0)); }, onPlusEndpointClick(endpoint: Endpoint) { - if (endpoint?.__meta) { + if (endpoint && endpoint.__meta) { this.insertNodeAfterSelected({ sourceId: endpoint.__meta.nodeId, index: endpoint.__meta.index, @@ -2554,6 +2560,7 @@ export default defineComponent({ this.stopLoading(); }, async tryToAddWelcomeSticky(): Promise { + const newWorkflow = this.workflowData; this.canvasStore.zoomToFit(); }, async initView(): Promise { @@ -2608,19 +2615,19 @@ export default defineComponent({ }); } - if (workflow) { - this.titleSet(workflow.name, 'IDLE'); + if (workflow) { + this.titleSet(workflow.name, 'IDLE'); - if (this.$route.name === VIEWS.EXECUTION_DEBUG) { - this.titleSet(workflow.name, 'DEBUG'); - const execution = await this.workflowsStore.getExecution( - this.$route.params.executionId as string, - ); - workflow = await this.pinExecutionData(workflow, execution); - } + if (this.$route.name === VIEWS.EXECUTION_DEBUG) { + this.titleSet(workflow.name, 'DEBUG'); + const execution = await this.workflowsStore.getExecution( + this.$route.params.executionId as string, + ); + workflow = await this.pinExecutionData(workflow, execution); + } - await this.openWorkflow(workflow); - } + await this.openWorkflow(workflow); + } } else if (this.$route.meta?.nodeView === true) { // Create new workflow await this.newWorkflow(); @@ -2743,7 +2750,8 @@ export default defineComponent({ const nodeTypeData = this.nodeTypesStore.getNodeType(node.type, node.typeVersion); if ( - nodeTypeData?.maxNodes !== undefined && + nodeTypeData && + nodeTypeData.maxNodes !== undefined && this.getNodeTypeCount(node.type) >= nodeTypeData.maxNodes ) { this.showMaxNodeTypeError(nodeTypeData); @@ -2898,7 +2906,7 @@ export default defineComponent({ }) { const pinData = this.workflowsStore.getPinData; - if (pinData?.[name]) return; + if (pinData && pinData[name]) return; const sourceNodeName = name; const sourceNode = this.workflowsStore.getNodeByName(sourceNodeName); @@ -2943,7 +2951,7 @@ export default defineComponent({ if (output.isArtificialRecoveredEventItem) { NodeViewUtils.recoveredConnection(connection); - } else if (!output?.total && !output.isArtificialRecoveredEventItem) { + } else if ((!output || !output.total) && !output.isArtificialRecoveredEventItem) { NodeViewUtils.resetConnection(connection); } else { NodeViewUtils.addConnectionOutputSuccess(connection, output); @@ -2955,7 +2963,7 @@ export default defineComponent({ sourceNodeName, parseInt(sourceOutputIndex, 10), ); - if (endpoint?.endpoint) { + if (endpoint && endpoint.endpoint) { const output = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0]; if (output && output.total > 0) { @@ -3245,7 +3253,7 @@ export default defineComponent({ ); }, async addNodes(nodes: INodeUi[], connections?: IConnections, trackHistory = false) { - if (!nodes?.length) { + if (!nodes || !nodes.length) { return; } @@ -3735,7 +3743,7 @@ export default defineComponent({ const mode = this.nodeCreatorStore.selectedView === TRIGGER_NODE_CREATOR_VIEW ? 'trigger' : 'regular'; - if (createNodeActive) this.nodeCreatorStore.setOpenSource(source); + if (createNodeActive === true) this.nodeCreatorStore.setOpenSource(source); void this.$externalHooks().run('nodeView.createNodeActiveChanged', { source, mode, From 71bdc772281e8902caf37b2ab8a13dfafe87edf4 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 3 Aug 2023 14:41:05 +0200 Subject: [PATCH 10/51] fix(editor): remove lint changes to reduce PR cognitive load --- packages/editor-ui/src/router.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index b5f0c69e0b292..ae52a1b02ba53 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -488,7 +488,7 @@ export const routes = [ shouldDeny: () => { const settingsStore = useSettingsStore(); return ( - settingsStore.settings.hideUsagePage || + settingsStore.settings.hideUsagePage === true || settingsStore.settings.deployment?.type === 'cloud' ); }, @@ -565,7 +565,7 @@ export const routes = [ deny: { shouldDeny: () => { const settingsStore = useSettingsStore(); - return !settingsStore.isPublicApiEnabled; + return settingsStore.isPublicApiEnabled === false; }, }, }, @@ -658,7 +658,7 @@ export const routes = [ deny: { shouldDeny: () => { const settingsStore = useSettingsStore(); - return !settingsStore.isCommunityNodesFeatureEnabled; + return settingsStore.isCommunityNodesFeatureEnabled === false; }, }, }, @@ -675,7 +675,7 @@ export const routes = [ pageCategory: 'settings', getProperties(route: RouteLocation) { return { - feature: route.params.featureId, + feature: route.params['featureId'], }; }, }, From 977fc06a5d74728b9798c5ac6015754eee46d670 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 3 Aug 2023 15:23:31 +0200 Subject: [PATCH 11/51] fix(editor): extend comment --- packages/editor-ui/src/composables/useExecutionDebugging.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/src/composables/useExecutionDebugging.ts b/packages/editor-ui/src/composables/useExecutionDebugging.ts index 589b2ea17ce34..cbeb5cf722b68 100644 --- a/packages/editor-ui/src/composables/useExecutionDebugging.ts +++ b/packages/editor-ui/src/composables/useExecutionDebugging.ts @@ -24,7 +24,7 @@ export const useExecutionDebugging = () => { })); const workflowPinnedNodeNames = Object.keys(workflow.pinData ?? {}); - // Check if any of the workflow nodes have pinned data already + // Check if any of the workflow nodes have pinned data already and ask for confirmation if (executionNodesData.some((eNode) => workflowPinnedNodeNames.includes(eNode.name))) { const overWritePinnedDataConfirm = await message.confirm( i18n.baseText('nodeView.confirmMessage.debug.message'), From cfc6a14ed9ca81e5665d6998340884ee6e66e9be Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Fri, 4 Aug 2023 06:21:34 +0200 Subject: [PATCH 12/51] fix(editor): replaces spaces to tab!? --- packages/editor-ui/src/views/NodeView.vue | 36 +++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index cfd64db8131c8..c14eac5eb0ebc 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -213,12 +213,12 @@ import { externalHooks } from '@/mixins/externalHooks'; import { genericHelpers } from '@/mixins/genericHelpers'; import { moveNodeWorkflow } from '@/mixins/moveNodeWorkflow'; import { - useGlobalLinkActions, - useCanvasMouseSelect, - useMessage, - useToast, - useTitleChange, - useExecutionDebugging, + useGlobalLinkActions, + useCanvasMouseSelect, + useMessage, + useToast, + useTitleChange, + useExecutionDebugging, } from '@/composables'; import { useUniqueNodeName } from '@/composables/useUniqueNodeName'; import { useI18n } from '@/composables/useI18n'; @@ -356,7 +356,7 @@ export default defineComponent({ ...useToast(), ...useMessage(), ...useUniqueNodeName(), - ...useExecutionDebugging(), + ...useExecutionDebugging(), // eslint-disable-next-line @typescript-eslint/no-misused-promises ...workflowRun.setup?.(props), }; @@ -2615,19 +2615,19 @@ export default defineComponent({ }); } - if (workflow) { - this.titleSet(workflow.name, 'IDLE'); + if (workflow) { + this.titleSet(workflow.name, 'IDLE'); - if (this.$route.name === VIEWS.EXECUTION_DEBUG) { - this.titleSet(workflow.name, 'DEBUG'); - const execution = await this.workflowsStore.getExecution( - this.$route.params.executionId as string, - ); - workflow = await this.pinExecutionData(workflow, execution); - } + if (this.$route.name === VIEWS.EXECUTION_DEBUG) { + this.titleSet(workflow.name, 'DEBUG'); + const execution = await this.workflowsStore.getExecution( + this.$route.params.executionId as string, + ); + workflow = await this.pinExecutionData(workflow, execution); + } - await this.openWorkflow(workflow); - } + await this.openWorkflow(workflow); + } } else if (this.$route.meta?.nodeView === true) { // Create new workflow await this.newWorkflow(); From 31a7ba5363adb6228aa9962ba2e50c79bb44d3ea Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Tue, 8 Aug 2023 17:26:14 +0200 Subject: [PATCH 13/51] fix(editor): adding Debug button --- .../ExecutionsView/ExecutionPreview.vue | 22 ++++++++++++++++++- .../src/components/MainHeader/MainHeader.vue | 6 ++++- .../src/plugins/i18n/locales/en.json | 1 + 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue index 50ba270e6837c..8a9289120baf8 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue @@ -123,6 +123,9 @@ :executionId="executionId" :executionMode="executionMode" /> + + {{ $locale.baseText('executionsList.openDebugMode') }} +
@@ -200,13 +203,23 @@ export default defineComponent({ retryDropdownRef.hide(); } }, + async openDebugMode() { + await this.$router.push({ + name: VIEWS.EXECUTION_DEBUG, + params: { + name: this.activeExecution?.workflowId, + executionId: this.activeExecution?.id, + }, + }); + }, }, }); diff --git a/packages/editor-ui/src/components/MainHeader/MainHeader.vue b/packages/editor-ui/src/components/MainHeader/MainHeader.vue index fbd49bf83ef91..f66dc9344ba24 100644 --- a/packages/editor-ui/src/components/MainHeader/MainHeader.vue +++ b/packages/editor-ui/src/components/MainHeader/MainHeader.vue @@ -109,7 +109,11 @@ export default defineComponent({ route.name === VIEWS.EXECUTION_PREVIEW ) { this.activeHeaderTab = MAIN_HEADER_TABS.EXECUTIONS; - } else if (route.name === VIEWS.WORKFLOW || route.name === VIEWS.NEW_WORKFLOW) { + } else if ( + route.name === VIEWS.WORKFLOW || + route.name === VIEWS.NEW_WORKFLOW || + route.name === VIEWS.EXECUTION_DEBUG + ) { this.activeHeaderTab = MAIN_HEADER_TABS.WORKFLOW; } const workflowName = route.params.name; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 48aca5e431936..e9db2c7b28a74 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -533,6 +533,7 @@ "executionsList.view": "View", "executionsList.stop": "Stop", "executionsList.statusTooltipText.theWorkflowIsWaitingIndefinitely": "The workflow is waiting indefinitely for an incoming webhook call.", + "executionsList.openDebugMode": "Debug execution", "executionSidebar.executionName": "Execution {id}", "executionSidebar.searchPlaceholder": "Search executions...", "executionView.onPaste.title": "Cannot paste here", From c16e21143a9b7fa8a629d8af8a79e91664de8a87 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Tue, 8 Aug 2023 17:32:25 +0200 Subject: [PATCH 14/51] fix(editor): adding Debug button --- .../ExecutionsView/ExecutionPreview.vue | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue index 8a9289120baf8..1904f6a1f53ec 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue @@ -203,14 +203,20 @@ export default defineComponent({ retryDropdownRef.hide(); } }, - async openDebugMode() { - await this.$router.push({ - name: VIEWS.EXECUTION_DEBUG, - params: { - name: this.activeExecution?.workflowId, - executionId: this.activeExecution?.id, - }, - }); + async openDebugMode(event: KeyboardEvent) { + if (event.ctrlKey || event.metaKey) { + const url = + window.location.origin + this.$router.resolve({ name: VIEWS.EXECUTION_DEBUG }).href; + window.open(url, '_blank'); + } else { + await this.$router.push({ + name: VIEWS.EXECUTION_DEBUG, + params: { + name: this.activeExecution?.workflowId, + executionId: this.activeExecution?.id, + }, + }); + } }, }, }); @@ -218,7 +224,7 @@ export default defineComponent({ From f41ea04bf46bee0bf9226efc12e1e9ded5ab3e24 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Wed, 9 Aug 2023 10:59:48 +0200 Subject: [PATCH 15/51] fix(editor): update Debug button --- packages/editor-ui/src/App.vue | 13 +++++-- .../ExecutionsView/ExecutionPreview.vue | 39 ++++++++++++++----- .../src/plugins/i18n/locales/en.json | 3 +- packages/editor-ui/src/router.ts | 1 + packages/editor-ui/src/views/NodeView.vue | 1 - 5 files changed, 42 insertions(+), 15 deletions(-) diff --git a/packages/editor-ui/src/App.vue b/packages/editor-ui/src/App.vue index e8b4ea949fbd0..55c76d70252d0 100644 --- a/packages/editor-ui/src/App.vue +++ b/packages/editor-ui/src/App.vue @@ -20,9 +20,10 @@
- + +
@@ -43,7 +44,7 @@ import { CLOUD_TRIAL_CHECK_INTERVAL, HIRING_BANNER, LOCAL_STORAGE_THEME, VIEWS } import { userHelpers } from '@/mixins/userHelpers'; import { loadLanguage } from '@/plugins/i18n'; -import { useGlobalLinkActions, useToast } from '@/composables'; +import { useGlobalLinkActions, useToast, useExternalHooks } from '@/composables'; import { useUIStore, useSettingsStore, @@ -58,7 +59,6 @@ import { import { useHistoryHelper } from '@/composables/useHistoryHelper'; import { newVersions } from '@/mixins/newVersions'; import { useRoute } from 'vue-router'; -import { useExternalHooks } from '@/composables'; export default defineComponent({ name: 'App', @@ -97,6 +97,11 @@ export default defineComponent({ isDemoMode(): boolean { return this.$route.name === VIEWS.DEMO; }, + keepAlive(): boolean { + // Using this condition to avoid having to add keepAlive to all the routes meta + // so if it's not explicitly set to false, it will be true + return this.$route.meta.keepAlive !== false; + }, }, data() { return { @@ -149,7 +154,7 @@ export default defineComponent({ }, trackPage(): void { this.uiStore.currentView = this.$route.name || ''; - if (this.$route && this.$route.meta && this.$route.meta.templatesEnabled) { + if (this.$route?.meta?.templatesEnabled) { this.templatesStore.setSessionId(); } else { this.templatesStore.resetSessionId(); // reset telemetry session id when user leaves template pages diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue index 1904f6a1f53ec..f9dff1fd14562 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue @@ -79,6 +79,19 @@
+ + + {{ debugButtonText }} + + - - {{ $locale.baseText('executionsList.openDebugMode') }} -
@@ -171,6 +181,11 @@ export default defineComponent({ executionMode(): string { return this.activeExecution?.mode || ''; }, + debugButtonText(): string { + return this.activeExecution.status === 'success' + ? this.$locale.baseText('executionsList.debug.button.copyToEditor') + : this.$locale.baseText('executionsList.debug.button.debugInEditor'); + }, }, methods: { async onDeleteExecution(): Promise { @@ -233,12 +248,18 @@ export default defineComponent({ position: absolute; padding: var(--spacing-m); padding-right: var(--spacing-xl); - width: calc(100% - 510px); + width: 100%; display: flex; justify-content: space-between; + align-items: center; transition: all 150ms ease-in-out; pointer-events: none; + > div:last-child { + display: flex; + align-items: center; + } + & * { pointer-events: all; } @@ -283,10 +304,10 @@ export default defineComponent({ text-align: center; } -.debugExecBtn { - position: absolute; - left: 50%; - bottom: 20%; - transform: translate(-50%, 0); +.debugLink { + margin-right: var(--spacing-xs); + a { + color: var(--color-text-xlight); + } } diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index e9db2c7b28a74..3b1b7b5730172 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -533,7 +533,8 @@ "executionsList.view": "View", "executionsList.stop": "Stop", "executionsList.statusTooltipText.theWorkflowIsWaitingIndefinitely": "The workflow is waiting indefinitely for an incoming webhook call.", - "executionsList.openDebugMode": "Debug execution", + "executionsList.debug.button.copyToEditor": "Copy to editor", + "executionsList.debug.button.debugInEditor": "Debug in editor", "executionSidebar.executionName": "Execution {id}", "executionSidebar.searchPlaceholder": "Search executions...", "executionView.onPaste.title": "Cannot paste here", diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index ae52a1b02ba53..34048c0acacd5 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -232,6 +232,7 @@ export const routes = [ }, meta: { nodeView: true, + keepAlive: false, permissions: { allow: { loginStatus: [LOGIN_STATUS.LoggedIn], diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index c14eac5eb0ebc..d0e133852ff4f 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -2560,7 +2560,6 @@ export default defineComponent({ this.stopLoading(); }, async tryToAddWelcomeSticky(): Promise { - const newWorkflow = this.workflowData; this.canvasStore.zoomToFit(); }, async initView(): Promise { From b52717e045708ee9c2ac10bf3c85423ebfeecc8e Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Wed, 9 Aug 2023 11:00:59 +0200 Subject: [PATCH 16/51] fix(editor): remove unused code --- .../ExecutionsView/ExecutionPreview.vue | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue index f9dff1fd14562..d929f52c00b8b 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue @@ -218,21 +218,6 @@ export default defineComponent({ retryDropdownRef.hide(); } }, - async openDebugMode(event: KeyboardEvent) { - if (event.ctrlKey || event.metaKey) { - const url = - window.location.origin + this.$router.resolve({ name: VIEWS.EXECUTION_DEBUG }).href; - window.open(url, '_blank'); - } else { - await this.$router.push({ - name: VIEWS.EXECUTION_DEBUG, - params: { - name: this.activeExecution?.workflowId, - executionId: this.activeExecution?.id, - }, - }); - } - }, }, }); From 37cdf4604841983b21188245c8b9504556e3b11a Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Wed, 9 Aug 2023 15:20:06 +0200 Subject: [PATCH 17/51] fix(editor): update debug button --- .../ExecutionsView/ExecutionPreview.vue | 52 +++++++++++++------ packages/editor-ui/src/constants.ts | 1 + .../src/plugins/i18n/locales/en.json | 2 + 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue index 0a0740ae3d322..1c9369183a203 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue @@ -79,19 +79,33 @@
- - - {{ debugButtonText }} - - + +
+ + + {{ debugButtonText }} + + {{ debugButtonText }} + +
+ +
import { defineComponent } from 'vue'; - +import { mapStores } from 'pinia'; +import { ElDropdown } from 'element-plus'; import { useMessage } from '@/composables'; import WorkflowPreview from '@/components/WorkflowPreview.vue'; import type { IExecutionUIData } from '@/mixins/executionsHelpers'; import { executionHelpers } from '@/mixins/executionsHelpers'; -import { MODAL_CONFIRM, VIEWS } from '@/constants'; -import { ElDropdown } from 'element-plus'; +import { EnterpriseEditionFeature, MODAL_CONFIRM, VIEWS } from '@/constants'; +import { useSettingsStore } from '@/stores'; type RetryDropdownRef = InstanceType & { hide: () => void }; @@ -169,6 +184,7 @@ export default defineComponent({ }; }, computed: { + ...mapStores(useSettingsStore), executionUIDetails(): IExecutionUIData | null { return this.activeExecution ? this.getExecutionUIDetails(this.activeExecution) : null; }, @@ -180,6 +196,10 @@ export default defineComponent({ ? this.$locale.baseText('executionsList.debug.button.copyToEditor') : this.$locale.baseText('executionsList.debug.button.debugInEditor'); }, + isDebugEnabled(): boolean { + // TODO: Remove the "|| true" once the feature key is ready to use + return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.DebugInEditor) || true; + }, }, methods: { async onDeleteExecution(): Promise { diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 7b0ed401b60a0..fdcfa8b2f52da 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -447,6 +447,7 @@ export const enum EnterpriseEditionFeature { Saml = 'saml', SourceControl = 'sourceControl', AuditLogs = 'auditLogs', + DebugInEditor = 'debugInEditor', } export const MAIN_NODE_PANEL_WIDTH = 360; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 3b1b7b5730172..c2117419135a1 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -535,6 +535,8 @@ "executionsList.statusTooltipText.theWorkflowIsWaitingIndefinitely": "The workflow is waiting indefinitely for an incoming webhook call.", "executionsList.debug.button.copyToEditor": "Copy to editor", "executionsList.debug.button.debugInEditor": "Debug in editor", + "executionsList.debug.tooltip.featureEnabled": "Copy execution data into the editor, so you can make changes and re-run it", + "executionsList.debug.tooltip.featureDisabled": "Copy execution data into the editor, so you can make changes and re-run it. Available on Pro and Enterprise plans.", "executionSidebar.executionName": "Execution {id}", "executionSidebar.searchPlaceholder": "Search executions...", "executionView.onPaste.title": "Cannot paste here", From 824c57bd190300bc0a1d0f41cfb054d6bd653503 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Wed, 9 Aug 2023 15:34:09 +0200 Subject: [PATCH 18/51] fix(editor): update router to deny debug route if feature is not enabled --- packages/editor-ui/src/router.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index 34048c0acacd5..9d22c7a2c6dbb 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -237,6 +237,11 @@ export const routes = [ allow: { loginStatus: [LOGIN_STATUS.LoggedIn], }, + deny: { + shouldDeny: () => false, + // TODO: Uncomment when feature is ready + //!useSettingsStore().isEnterpriseFeatureEnabled(EnterpriseEditionFeature.DebugInEditor), + }, }, }, }, @@ -489,7 +494,7 @@ export const routes = [ shouldDeny: () => { const settingsStore = useSettingsStore(); return ( - settingsStore.settings.hideUsagePage === true || + settingsStore.settings.hideUsagePage || settingsStore.settings.deployment?.type === 'cloud' ); }, @@ -566,7 +571,7 @@ export const routes = [ deny: { shouldDeny: () => { const settingsStore = useSettingsStore(); - return settingsStore.isPublicApiEnabled === false; + return !settingsStore.isPublicApiEnabled; }, }, }, @@ -659,7 +664,7 @@ export const routes = [ deny: { shouldDeny: () => { const settingsStore = useSettingsStore(); - return settingsStore.isCommunityNodesFeatureEnabled === false; + return !settingsStore.isCommunityNodesFeatureEnabled; }, }, }, @@ -676,7 +681,7 @@ export const routes = [ pageCategory: 'settings', getProperties(route: RouteLocation) { return { - feature: route.params['featureId'], + feature: route.params.featureId, }; }, }, From 0bb1aa60c8bbb5f65be1aef1efd137cd52be254b Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Wed, 9 Aug 2023 17:54:05 +0200 Subject: [PATCH 19/51] fix(editor): apply execution data first to debug --- .../src/composables/useExecutionDebugging.ts | 28 ++++++++++++------- packages/editor-ui/src/views/NodeView.vue | 8 ++---- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/editor-ui/src/composables/useExecutionDebugging.ts b/packages/editor-ui/src/composables/useExecutionDebugging.ts index cbeb5cf722b68..b7a142116b299 100644 --- a/packages/editor-ui/src/composables/useExecutionDebugging.ts +++ b/packages/editor-ui/src/composables/useExecutionDebugging.ts @@ -1,18 +1,24 @@ import { useI18n, useMessage } from '@/composables'; -import type { IExecutionResponse, IWorkflowDb } from '@/Interface'; import { MODAL_CONFIRM } from '@/constants'; +import { useWorkflowsStore } from '@/stores'; export const useExecutionDebugging = () => { const i18n = useI18n(); const message = useMessage(); + const workflowsStore = useWorkflowsStore(); + + const applyExecutionData = async (executionId: string): Promise => { + const { workflow } = workflowsStore; + const execution = await workflowsStore.getExecution(executionId); + + console.log('applyExecutionData'); + console.log(workflow); + console.log('exec'); + console.log(execution); - const pinExecutionData = async ( - workflow: IWorkflowDb, - execution: IExecutionResponse | undefined, - ): Promise => { // If no execution data is available, return the workflow as is if (!execution?.data?.resultData) { - return workflow; + return; } const { runData, pinData } = execution.data.resultData; @@ -38,12 +44,14 @@ export const useExecutionDebugging = () => { ); if (overWritePinnedDataConfirm !== MODAL_CONFIRM) { - return workflow; + return; } } + workflowsStore.setWorkflowExecutionData(execution); + // Overwrite the workflow pinned data with the execution data - workflow.pinData = executionNodesData.reduce( + /*workflow.pinData = executionNodesData.reduce( (acc, { name, data }) => { // Only add data if it exists and the node is in the workflow if (acc && data && workflow.nodes.some((node) => node.name === name)) { @@ -54,10 +62,10 @@ export const useExecutionDebugging = () => { {} as IWorkflowDb['pinData'], ); - return workflow; + return workflow;*/ }; return { - pinExecutionData, + applyExecutionData, }; }; diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 73d1c2c1fe281..e0632d9c62313 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -2634,16 +2634,12 @@ export default defineComponent({ if (workflow) { this.titleSet(workflow.name, 'IDLE'); + await this.openWorkflow(workflow); if (this.$route.name === VIEWS.EXECUTION_DEBUG) { this.titleSet(workflow.name, 'DEBUG'); - const execution = await this.workflowsStore.getExecution( - this.$route.params.executionId as string, - ); - workflow = await this.pinExecutionData(workflow, execution); + await this.applyExecutionData(this.$route.params.executionId as string); } - - await this.openWorkflow(workflow); } } else if (this.$route.meta?.nodeView === true) { // Create new workflow From e1e1889d092cc5a211d7669f82d99a27a5b2cc1e Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Wed, 9 Aug 2023 17:58:24 +0200 Subject: [PATCH 20/51] fix(editor): checking feature --- .../src/components/ExecutionsView/ExecutionPreview.vue | 3 +-- packages/editor-ui/src/router.ts | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue index 1c9369183a203..eddd1bfcd7804 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue @@ -197,8 +197,7 @@ export default defineComponent({ : this.$locale.baseText('executionsList.debug.button.debugInEditor'); }, isDebugEnabled(): boolean { - // TODO: Remove the "|| true" once the feature key is ready to use - return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.DebugInEditor) || true; + return this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.DebugInEditor); }, }, methods: { diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index 9d22c7a2c6dbb..ba9a476497747 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -39,7 +39,7 @@ import SignoutView from '@/views/SignoutView.vue'; import SamlOnboarding from '@/views/SamlOnboarding.vue'; import SettingsSourceControl from './views/SettingsSourceControl.vue'; import SettingsAuditLogs from './views/SettingsAuditLogs.vue'; -import { VIEWS } from '@/constants'; +import { EnterpriseEditionFeature, VIEWS } from '@/constants'; interface IRouteConfig { meta: { @@ -238,9 +238,8 @@ export const routes = [ loginStatus: [LOGIN_STATUS.LoggedIn], }, deny: { - shouldDeny: () => false, - // TODO: Uncomment when feature is ready - //!useSettingsStore().isEnterpriseFeatureEnabled(EnterpriseEditionFeature.DebugInEditor), + shouldDeny: () => + !useSettingsStore().isEnterpriseFeatureEnabled(EnterpriseEditionFeature.DebugInEditor), }, }, }, From 13ace63d4855247ff4aeda43308c7c5142f5e14c Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 10 Aug 2023 07:26:12 +0200 Subject: [PATCH 21/51] fix(editor): confirm unpinning --- .../src/composables/useExecutionDebugging.ts | 50 ++++++++----------- .../src/plugins/i18n/locales/en.json | 6 +-- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/packages/editor-ui/src/composables/useExecutionDebugging.ts b/packages/editor-ui/src/composables/useExecutionDebugging.ts index b7a142116b299..85c2e34f106b2 100644 --- a/packages/editor-ui/src/composables/useExecutionDebugging.ts +++ b/packages/editor-ui/src/composables/useExecutionDebugging.ts @@ -16,24 +16,29 @@ export const useExecutionDebugging = () => { console.log('exec'); console.log(execution); - // If no execution data is available, return the workflow as is if (!execution?.data?.resultData) { return; } - const { runData, pinData } = execution.data.resultData; + workflowsStore.setWorkflowExecutionData(execution); + + const { runData } = execution.data.resultData; - // Get nodes from execution data and apply their pinned data or the first execution data - const executionNodesData = Object.entries(runData).map(([name, data]) => ({ - name, - data: pinData?.[name] ?? data?.[0].data?.main[0], - })); + const executionNodeNames = Object.keys(runData); const workflowPinnedNodeNames = Object.keys(workflow.pinData ?? {}); - // Check if any of the workflow nodes have pinned data already and ask for confirmation - if (executionNodesData.some((eNode) => workflowPinnedNodeNames.includes(eNode.name))) { + const matchingPinnedNodeNames = executionNodeNames.filter((name) => + workflowPinnedNodeNames.includes(name), + ); + const matchingPinnedNodeNamesToHtmlList = `
    ${matchingPinnedNodeNames + .map((name) => `
  • ${name}
  • `) + .join('')}
`; + + if (matchingPinnedNodeNames.length > 0) { const overWritePinnedDataConfirm = await message.confirm( - i18n.baseText('nodeView.confirmMessage.debug.message'), + i18n.baseText('nodeView.confirmMessage.debug.message', { + interpolate: { nodeNames: matchingPinnedNodeNamesToHtmlList }, + }), i18n.baseText('nodeView.confirmMessage.debug.headline'), { type: 'warning', @@ -43,26 +48,15 @@ export const useExecutionDebugging = () => { }, ); - if (overWritePinnedDataConfirm !== MODAL_CONFIRM) { - return; + if (overWritePinnedDataConfirm === MODAL_CONFIRM) { + matchingPinnedNodeNames.forEach((name) => { + const node = workflowsStore.getNodeByName(name); + if (node) { + workflowsStore.unpinData({ node }); + } + }); } } - - workflowsStore.setWorkflowExecutionData(execution); - - // Overwrite the workflow pinned data with the execution data - /*workflow.pinData = executionNodesData.reduce( - (acc, { name, data }) => { - // Only add data if it exists and the node is in the workflow - if (acc && data && workflow.nodes.some((node) => node.name === name)) { - acc[name] = data; - } - return acc; - }, - {} as IWorkflowDb['pinData'], - ); - - return workflow;*/ }; return { diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index c2117419135a1..2f2648803c2f9 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -858,9 +858,9 @@ "nodeView.confirmMessage.receivedCopyPasteData.headline": "Import Workflow?", "nodeView.confirmMessage.receivedCopyPasteData.message": "Workflow will be imported from
{plainTextData}", "nodeView.confirmMessage.debug.cancelButtonText": "Cancel", - "nodeView.confirmMessage.debug.confirmButtonText": "Proceed", - "nodeView.confirmMessage.debug.headline": "Debug workflow with execution data", - "nodeView.confirmMessage.debug.message": "Some nodes in this workflow have pinned data. This will be overwritten with the data from the execution. Do you want to proceed?", + "nodeView.confirmMessage.debug.confirmButtonText": "Unpin", + "nodeView.confirmMessage.debug.headline": "Unpin workflow data?", + "nodeView.confirmMessage.debug.message": "To see the execution data in the editor, some nodes need to be unpinned:

{nodeNames}", "nodeView.couldntImportWorkflow": "Could not import workflow", "nodeView.deletesTheCurrentExecutionData": "Deletes the current execution data", "nodeView.executesTheWorkflowFromATriggerNode": "Runs the workflow, starting from a Trigger node", From 08800e6f818c47d987dad10e65494f4b48a044b2 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 10 Aug 2023 15:21:26 +0200 Subject: [PATCH 22/51] fix(editor): pin root nodes --- .../src/composables/useExecutionDebugging.ts | 22 ++++++--- packages/editor-ui/src/views/NodeView.vue | 48 ++++++++----------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/packages/editor-ui/src/composables/useExecutionDebugging.ts b/packages/editor-ui/src/composables/useExecutionDebugging.ts index 85c2e34f106b2..c8a4471afc359 100644 --- a/packages/editor-ui/src/composables/useExecutionDebugging.ts +++ b/packages/editor-ui/src/composables/useExecutionDebugging.ts @@ -1,3 +1,4 @@ +import type { INode } from 'n8n-workflow'; import { useI18n, useMessage } from '@/composables'; import { MODAL_CONFIRM } from '@/constants'; import { useWorkflowsStore } from '@/stores'; @@ -8,14 +9,9 @@ export const useExecutionDebugging = () => { const workflowsStore = useWorkflowsStore(); const applyExecutionData = async (executionId: string): Promise => { - const { workflow } = workflowsStore; + const workflow = workflowsStore.getCurrentWorkflow(); const execution = await workflowsStore.getExecution(executionId); - console.log('applyExecutionData'); - console.log(workflow); - console.log('exec'); - console.log(execution); - if (!execution?.data?.resultData) { return; } @@ -57,6 +53,20 @@ export const useExecutionDebugging = () => { }); } } + + const rootNodes = workflowsStore + .getNodes() + .filter((node: INode) => !workflow.getParentNodes(node.name).length); + + rootNodes.forEach((node: INode) => { + const nodeData = runData[node.name]?.[0].data?.main[0]; + if (nodeData) { + workflowsStore.pinData({ + node, + data: nodeData, + }); + } + }); }; return { diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index e0632d9c62313..93506d9b1db18 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -563,7 +563,7 @@ export default defineComponent({ workflowClasses() { const returnClasses = []; if (this.ctrlKeyPressed || this.moveCanvasKeyPressed) { - if (this.uiStore.nodeViewMoveInProgress === true) { + if (this.uiStore.nodeViewMoveInProgress) { returnClasses.push('move-in-process'); } else { returnClasses.push('move-active'); @@ -1086,7 +1086,7 @@ export default defineComponent({ lastSelectedNode.name, ); } - } else if (e.key === 'a' && this.isCtrlKeyPressed(e) === true) { + } else if (e.key === 'a' && this.isCtrlKeyPressed(e)) { // Select all nodes e.stopPropagation(); e.preventDefault(); @@ -1506,7 +1506,7 @@ export default defineComponent({ const currentTab = getNodeViewTab(this.$route); if (currentTab === MAIN_HEADER_TABS.WORKFLOW) { let workflowData: IWorkflowDataUpdate | undefined; - if (this.editAllowedCheck() === false) { + if (!this.editAllowedCheck()) { return; } // Check if it is an URL which could contain workflow data @@ -1781,11 +1781,9 @@ export default defineComponent({ parameters: {}, }; - const credentialPerType = - nodeTypeData.credentials && - nodeTypeData.credentials - .map((type) => this.credentialsStore.getUsableCredentialByType(type.name)) - .flat(); + const credentialPerType = nodeTypeData.credentials + ?.map((type) => this.credentialsStore.getUsableCredentialByType(type.name)) + .flat(); if (credentialPerType && credentialPerType.length === 1) { const defaultCredential = credentialPerType[0]; @@ -1822,10 +1820,7 @@ export default defineComponent({ return newNodeData; } - if ( - Object.keys(authDisplayOptions).length === 1 && - authDisplayOptions['authentication'] - ) { + if (Object.keys(authDisplayOptions).length === 1 && authDisplayOptions.authentication) { // ignore complex case when there's multiple dependencies newNodeData.credentials = credentials; @@ -1959,7 +1954,7 @@ export default defineComponent({ newNodeData.name = this.uniqueNodeName(localizedName); - if (nodeTypeData.webhooks && nodeTypeData.webhooks.length) { + if (nodeTypeData.webhooks?.length) { newNodeData.webhookId = uuid(); } @@ -2009,9 +2004,8 @@ export default defineComponent({ targetNodeName: string, targetNodeOuputIndex: number, ): IConnection | undefined { - const nodeConnections = ( - this.workflowsStore.outgoingConnectionsByNodeName(sourceNodeName) as INodeConnections - ).main; + const nodeConnections = + this.workflowsStore.outgoingConnectionsByNodeName(sourceNodeName).main; if (nodeConnections) { const connections: IConnection[] | null = nodeConnections[sourceNodeOutputIndex]; @@ -2093,7 +2087,7 @@ export default defineComponent({ if (lastSelectedNode) { await this.$nextTick(); - if (lastSelectedConnection && lastSelectedConnection.__meta) { + if (lastSelectedConnection?.__meta) { this.__deleteJSPlumbConnection(lastSelectedConnection, trackHistory); const targetNodeName = lastSelectedConnection.__meta.targetNodeName; @@ -2434,8 +2428,8 @@ export default defineComponent({ const { top, left, right, bottom } = element.getBoundingClientRect(); const [x, y] = NodeViewUtils.getMousePosition(e); if (top <= y && bottom >= y && left - inputMargin <= x && right >= x) { - const nodeName = (element as HTMLElement).dataset['name'] as string; - const node = this.workflowsStore.getNodeByName(nodeName) as INodeUi | null; + const nodeName = (element as HTMLElement).dataset.name as string; + const node = this.workflowsStore.getNodeByName(nodeName); if (node) { const nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion); if (nodeType && nodeType.inputs && nodeType.inputs.length === 1) { @@ -2484,7 +2478,7 @@ export default defineComponent({ .forEach((endpoint) => setTimeout(() => endpoint.instance.revalidate(endpoint.element), 0)); }, onPlusEndpointClick(endpoint: Endpoint) { - if (endpoint && endpoint.__meta) { + if (endpoint?.__meta) { this.insertNodeAfterSelected({ sourceId: endpoint.__meta.nodeId, index: endpoint.__meta.index, @@ -2638,6 +2632,7 @@ export default defineComponent({ if (this.$route.name === VIEWS.EXECUTION_DEBUG) { this.titleSet(workflow.name, 'DEBUG'); + await this.$nextTick(); await this.applyExecutionData(this.$route.params.executionId as string); } } @@ -2763,8 +2758,7 @@ export default defineComponent({ const nodeTypeData = this.nodeTypesStore.getNodeType(node.type, node.typeVersion); if ( - nodeTypeData && - nodeTypeData.maxNodes !== undefined && + nodeTypeData?.maxNodes !== undefined && this.getNodeTypeCount(node.type) >= nodeTypeData.maxNodes ) { this.showMaxNodeTypeError(nodeTypeData); @@ -2919,7 +2913,7 @@ export default defineComponent({ }) { const pinData = this.workflowsStore.getPinData; - if (pinData && pinData[name]) return; + if (pinData?.[name]) return; const sourceNodeName = name; const sourceNode = this.workflowsStore.getNodeByName(sourceNodeName); @@ -2964,7 +2958,7 @@ export default defineComponent({ if (output.isArtificialRecoveredEventItem) { NodeViewUtils.recoveredConnection(connection); - } else if ((!output || !output.total) && !output.isArtificialRecoveredEventItem) { + } else if (!output?.total && !output.isArtificialRecoveredEventItem) { NodeViewUtils.resetConnection(connection); } else { NodeViewUtils.addConnectionOutputSuccess(connection, output); @@ -2976,7 +2970,7 @@ export default defineComponent({ sourceNodeName, parseInt(sourceOutputIndex, 10), ); - if (endpoint && endpoint.endpoint) { + if (endpoint?.endpoint) { const output = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0]; if (output && output.total > 0) { @@ -3266,7 +3260,7 @@ export default defineComponent({ ); }, async addNodes(nodes: INodeUi[], connections?: IConnections, trackHistory = false) { - if (!nodes || !nodes.length) { + if (!nodes?.length) { return; } @@ -3756,7 +3750,7 @@ export default defineComponent({ const mode = this.nodeCreatorStore.selectedView === TRIGGER_NODE_CREATOR_VIEW ? 'trigger' : 'regular'; - if (createNodeActive === true) this.nodeCreatorStore.setOpenSource(source); + if (createNodeActive) this.nodeCreatorStore.setOpenSource(source); void this.$externalHooks().run('nodeView.createNodeActiveChanged', { source, mode, From 498a872b90cdeea48a956eb6c5f58fd44f9b105a Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 10 Aug 2023 15:27:28 +0200 Subject: [PATCH 23/51] fix(editor): remove unnecessary code --- packages/editor-ui/src/views/NodeView.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 93506d9b1db18..6aec237e26cf6 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -2632,7 +2632,6 @@ export default defineComponent({ if (this.$route.name === VIEWS.EXECUTION_DEBUG) { this.titleSet(workflow.name, 'DEBUG'); - await this.$nextTick(); await this.applyExecutionData(this.$route.params.executionId as string); } } From cce377a044f53b8c600092197460c0d5b2b5b654 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 10 Aug 2023 16:51:57 +0200 Subject: [PATCH 24/51] fix(editor): add toast messages --- .../src/composables/useExecutionDebugging.ts | 54 +++++++++++++------ .../src/plugins/i18n/locales/en.json | 4 ++ 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/packages/editor-ui/src/composables/useExecutionDebugging.ts b/packages/editor-ui/src/composables/useExecutionDebugging.ts index c8a4471afc359..47663e51c9077 100644 --- a/packages/editor-ui/src/composables/useExecutionDebugging.ts +++ b/packages/editor-ui/src/composables/useExecutionDebugging.ts @@ -1,28 +1,30 @@ -import type { INode } from 'n8n-workflow'; -import { useI18n, useMessage } from '@/composables'; +import { useI18n, useMessage, useToast } from '@/composables'; import { MODAL_CONFIRM } from '@/constants'; import { useWorkflowsStore } from '@/stores'; +import type { INodeUi } from '@/Interface'; export const useExecutionDebugging = () => { const i18n = useI18n(); const message = useMessage(); + const toast = useToast(); const workflowsStore = useWorkflowsStore(); const applyExecutionData = async (executionId: string): Promise => { - const workflow = workflowsStore.getCurrentWorkflow(); const execution = await workflowsStore.getExecution(executionId); + const workflow = workflowsStore.getCurrentWorkflow(); + const workflowNodes = workflowsStore.getNodes(); if (!execution?.data?.resultData) { return; } - workflowsStore.setWorkflowExecutionData(execution); - const { runData } = execution.data.resultData; const executionNodeNames = Object.keys(runData); + const missingNodeNames = executionNodeNames.filter( + (name) => !workflowNodes.some((node) => node.name === name), + ); const workflowPinnedNodeNames = Object.keys(workflow.pinData ?? {}); - const matchingPinnedNodeNames = executionNodeNames.filter((name) => workflowPinnedNodeNames.includes(name), ); @@ -54,19 +56,37 @@ export const useExecutionDebugging = () => { } } - const rootNodes = workflowsStore - .getNodes() - .filter((node: INode) => !workflow.getParentNodes(node.name).length); + // Set execution data + workflowsStore.setWorkflowExecutionData(execution); - rootNodes.forEach((node: INode) => { - const nodeData = runData[node.name]?.[0].data?.main[0]; - if (nodeData) { - workflowsStore.pinData({ - node, - data: nodeData, - }); - } + // Pin data of all nodes which do not have a parent node + workflowNodes + .filter((node: INodeUi) => !workflow.getParentNodes(node.name).length) + .forEach((node: INodeUi) => { + const nodeData = runData[node.name]?.[0].data?.main[0]; + if (nodeData) { + workflowsStore.pinData({ + node, + data: nodeData, + }); + } + }); + + toast.showToast({ + title: i18n.baseText('nodeView.showMessage.debug.title'), + message: i18n.baseText('nodeView.showMessage.debug.content'), + type: 'info', }); + + if (missingNodeNames.length) { + toast.showToast({ + title: i18n.baseText('nodeView.showMessage.debug.missingNodes.title'), + message: i18n.baseText('nodeView.showMessage.debug.missingNodes.content', { + interpolate: { nodeNames: missingNodeNames.join(', ') }, + }), + type: 'warning', + }); + } }; return { diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 2f2648803c2f9..74efa1c453350 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -900,6 +900,10 @@ "nodeView.showMessage.stopExecutionCatch.message": "It completed before it could be stopped", "nodeView.showMessage.stopExecutionCatch.title": "Workflow finished executing", "nodeView.showMessage.stopExecutionTry.title": "Execution stopped", + "nodeView.showMessage.debug.title": "Execution data imported", + "nodeView.showMessage.debug.content": "You can make edits and re-execute. Once you're done, unpin the the first node.", + "nodeView.showMessage.debug.missingNodes.title": "Some execution data wasn't imported", + "nodeView.showMessage.debug.missingNodes.content": "Some nodes have been deleted or renamed or added to the workflow since the execution ran.", "nodeView.stopCurrentExecution": "Stop current execution", "nodeView.stopWaitingForWebhookCall": "Stop waiting for webhook call", "nodeView.stoppingCurrentExecution": "Stopping current execution", From 7d369d318a03adf551774eb1ed0677d1db1e010c Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Mon, 14 Aug 2023 11:48:17 +0200 Subject: [PATCH 25/51] fix(editor): enable moving nodes in debug mode --- packages/editor-ui/src/stores/ui.store.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index 5cba16cc1e445..0c5949dbb3228 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -282,7 +282,9 @@ export const useUIStore = defineStore(STORES.UI, { this.fakeDoorFeatures.find((fakeDoor) => fakeDoor.id.toString() === id); }, isReadOnlyView(): boolean { - return ![VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW].includes(this.currentView as VIEWS); + return ![VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW, VIEWS.EXECUTION_DEBUG].includes( + this.currentView as VIEWS, + ); }, isNodeView(): boolean { return [ From ffb6063276abe803a651bee53c1a36bc1472e90f Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Mon, 14 Aug 2023 16:35:46 +0200 Subject: [PATCH 26/51] fix(editor): update debug button and unpin tooltip --- .../ExecutionsView/ExecutionPreview.vue | 25 +++++++++++++------ .../src/plugins/i18n/locales/en.json | 4 +-- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue index eddd1bfcd7804..2ca01989dba1b 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue @@ -81,7 +81,12 @@
- + - {{ debugButtonText }} + {{ debugButtonData.text }} - {{ debugButtonText }} + {{ debugButtonData.text }}
+ + + +
@@ -148,6 +154,7 @@ import { ASK_AI_MODAL_KEY, SOURCE_CONTROL_PUSH_MODAL_KEY, SOURCE_CONTROL_PULL_MODAL_KEY, + DEBUG_PAYWALL_MODAL_KEY, } from '@/constants'; import AboutModal from './AboutModal.vue'; @@ -174,6 +181,7 @@ import WorkflowShareModal from './WorkflowShareModal.ee.vue'; import EventDestinationSettingsModal from '@/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue'; import SourceControlPushModal from '@/components/SourceControlPushModal.ee.vue'; import SourceControlPullModal from '@/components/SourceControlPullModal.ee.vue'; +import DebugPaywallModal from '@/components/DebugPaywallModal.vue'; export default defineComponent({ name: 'Modals', @@ -202,6 +210,7 @@ export default defineComponent({ EventDestinationSettingsModal, SourceControlPushModal, SourceControlPullModal, + DebugPaywallModal, }, data: () => ({ COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, @@ -227,6 +236,7 @@ export default defineComponent({ LOG_STREAM_MODAL_KEY, SOURCE_CONTROL_PUSH_MODAL_KEY, SOURCE_CONTROL_PULL_MODAL_KEY, + DEBUG_PAYWALL_MODAL_KEY, }), }); diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 2b6c41da6539d..17d867212fc7d 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -46,9 +46,9 @@ export const COMMUNITY_PACKAGE_INSTALL_MODAL_KEY = 'communityPackageInstall'; export const COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY = 'communityPackageManageConfirm'; export const IMPORT_CURL_MODAL_KEY = 'importCurl'; export const LOG_STREAM_MODAL_KEY = 'settingsLogStream'; - export const SOURCE_CONTROL_PUSH_MODAL_KEY = 'sourceControlPush'; export const SOURCE_CONTROL_PULL_MODAL_KEY = 'sourceControlPull'; +export const DEBUG_PAYWALL_MODAL_KEY = 'debugPaywall'; export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = { UNINSTALL: 'uninstall', diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 67aa9f10e7cc7..a22326048c903 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -61,6 +61,7 @@ "generic.workflow": "Workflow", "generic.workflowSaved": "Workflow changes saved", "generic.editor": "Editor", + "generic.seePlans": "See plans", "about.aboutN8n": "About n8n", "about.close": "Close", "about.license": "License", @@ -535,8 +536,9 @@ "executionsList.statusTooltipText.theWorkflowIsWaitingIndefinitely": "The workflow is waiting indefinitely for an incoming webhook call.", "executionsList.debug.button.copyToEditor": "Copy to editor", "executionsList.debug.button.debugInEditor": "Debug in editor", - "executionsList.debug.tooltip.featureEnabled": "Copy execution data into the editor, so you can make changes and re-run it", - "executionsList.debug.tooltip.featureDisabled": "Copy execution data into the editor, so you can make changes and re-run it. Available on Pro and Enterprise plans.", + "executionsList.debug.paywall.content": "Debug in Editor allows you to debug a previous execution with the actual data pinned, right in your editor.", + "executionsList.debug.paywall.link.text": "Read more in the docs", + "executionsList.debug.paywall.link.url": "#", "executionSidebar.executionName": "Execution {id}", "executionSidebar.searchPlaceholder": "Search executions...", "executionView.onPaste.title": "Cannot paste here", @@ -1836,6 +1838,10 @@ "contextual.upgradeLinkUrl.cloud": "https://app.n8n.cloud/account/change-plan", "contextual.upgradeLinkUrl.desktop": "https://n8n.io/pricing/?utm_source=n8n-internal&utm_medium=desktop", + "contextual.feature.unavailable.title": "Available on the Enterprise Plan", + "contextual.feature.unavailable.title.cloud": "Available on the Pro Plan", + "contextual.feature.unavailable.title.desktop": "Available on cloud hosting", + "settings.ldap": "LDAP", "settings.ldap.note": "LDAP allows users to authenticate with their centralized account. It's compatible with services that provide an LDAP interface like Active Directory, Okta and Jumpcloud.", "settings.ldap.infoTip": "Learn more about LDAP in the Docs", diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index 0c5949dbb3228..35deba74dd71f 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -31,6 +31,7 @@ import { WORKFLOW_SHARE_MODAL_KEY, SOURCE_CONTROL_PUSH_MODAL_KEY, SOURCE_CONTROL_PULL_MODAL_KEY, + DEBUG_PAYWALL_MODAL_KEY, } from '@/constants'; import type { CloudUpdateLinkSourceType, @@ -141,6 +142,9 @@ export const useUIStore = defineStore(STORES.UI, { [SOURCE_CONTROL_PULL_MODAL_KEY]: { open: false, }, + [DEBUG_PAYWALL_MODAL_KEY]: { + open: false, + }, }, modalStack: [], sidebarMenuCollapsed: true, @@ -196,6 +200,11 @@ export const useUIStore = defineStore(STORES.UI, { return { upgradeLinkUrl: `contextual.upgradeLinkUrl${contextKey}`, + feature: { + unavailable: { + title: `contextual.feature.unavailable.title${contextKey}`, + }, + }, credentials: { sharing: { unavailable: { From 2e5ef26367ae224b0854939617cd6af65789c8fb Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Wed, 16 Aug 2023 15:46:12 +0200 Subject: [PATCH 41/51] fix(editor): move debug button click handler into composable --- .../ExecutionsView/ExecutionPreview.vue | 32 ++--------------- .../src/composables/useExecutionDebugging.ts | 36 +++++++++++++++++-- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue index 7a418a93d2349..441f5b45f8c32 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue @@ -82,7 +82,7 @@ import { defineComponent } from 'vue'; -import { mapStores } from 'pinia'; import { ElDropdown } from 'element-plus'; import { useExecutionDebugging, useMessage } from '@/composables'; import WorkflowPreview from '@/components/WorkflowPreview.vue'; import type { IExecutionUIData } from '@/mixins/executionsHelpers'; import { executionHelpers } from '@/mixins/executionsHelpers'; import { MODAL_CONFIRM, VIEWS } from '@/constants'; -import { useWorkflowsStore } from '@/stores'; type RetryDropdownRef = InstanceType & { hide: () => void }; @@ -180,7 +178,6 @@ export default defineComponent({ }; }, computed: { - ...mapStores(useWorkflowsStore), executionUIDetails(): IExecutionUIData | null { return this.activeExecution ? this.getExecutionUIDetails(this.activeExecution) : null; }, From fa4376aec1273bba0dfa9f8d2ec103a9744404f3 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Fri, 18 Aug 2023 08:22:43 +0200 Subject: [PATCH 44/51] test: E2E debug button --- cypress/e2e/28-debug.cy.ts | 66 +++++++++++++++++++ cypress/pages/index.ts | 1 + cypress/pages/workflow-executions-tab.ts | 1 + .../ExecutionsView/ExecutionPreview.vue | 1 + 4 files changed, 69 insertions(+) create mode 100644 cypress/e2e/28-debug.cy.ts diff --git a/cypress/e2e/28-debug.cy.ts b/cypress/e2e/28-debug.cy.ts new file mode 100644 index 0000000000000..e8e5ce22a7b84 --- /dev/null +++ b/cypress/e2e/28-debug.cy.ts @@ -0,0 +1,66 @@ +import { + HTTP_REQUEST_NODE_NAME, + MANUAL_TRIGGER_NODE_NAME, + SET_NODE_NAME, +} from '../constants'; +import { WorkflowPage, NDV, WorkflowExecutionsTab } from '../pages'; +import {getVisibleModalOverlay} from "../utils/modal"; + +const workflowPage = new WorkflowPage(); +const ndv = new NDV(); +const executionsTab = new WorkflowExecutionsTab(); + +describe('Debug', () => { + beforeEach(() => { + workflowPage.actions.visit(); + }); + + it('Should be able to debug executions', () => { + cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); + cy.intercept('GET', '/rest/executions/*').as('getExecution'); + cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions'); + cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun'); + + workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); + workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME); + workflowPage.actions.openNode(HTTP_REQUEST_NODE_NAME); + ndv.actions.typeIntoParameterInput('url', 'https://foo.bar'); + ndv.actions.close(); + + workflowPage.actions.addNodeToCanvas(SET_NODE_NAME, true); + + workflowPage.actions.saveWorkflowUsingKeyboardShortcut(); + workflowPage.actions.executeWorkflow(); + + cy.wait(['@postWorkflowRun']); + + executionsTab.actions.switchToExecutionsTab(); + + cy.wait(['@getExecutions', '@getCurrentExecutions']); + + executionsTab.getters.executionDebugButton().should('have.text', 'Debug in editor').click(); + getVisibleModalOverlay().click(1, 1); + + executionsTab.actions.switchToEditorTab(); + + workflowPage.actions.openNode(HTTP_REQUEST_NODE_NAME); + ndv.actions.clearParameterInput('url'); + ndv.actions.typeIntoParameterInput('url', 'https://postman-echo.com/get?foo1=bar1&foo2=bar2'); + ndv.actions.close(); + + workflowPage.actions.saveWorkflowUsingKeyboardShortcut(); + workflowPage.actions.executeWorkflow(); + + cy.wait(['@postWorkflowRun']); + + executionsTab.actions.switchToExecutionsTab(); + + cy.wait(['@getExecutions', '@getCurrentExecutions']); + + executionsTab.getters.executionListItems().should('have.length', 2).first().click(); + cy.wait(['@getExecution']); + + executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor'); + + }); +}); diff --git a/cypress/pages/index.ts b/cypress/pages/index.ts index 4d95611a12129..ed631fd928423 100644 --- a/cypress/pages/index.ts +++ b/cypress/pages/index.ts @@ -8,3 +8,4 @@ export * from './settings-log-streaming'; export * from './sidebar'; export * from './ndv'; export * from './bannerStack'; +export * from './workflow-executions-tab'; diff --git a/cypress/pages/workflow-executions-tab.ts b/cypress/pages/workflow-executions-tab.ts index 674ff6d5f3911..eff3fedd3095b 100644 --- a/cypress/pages/workflow-executions-tab.ts +++ b/cypress/pages/workflow-executions-tab.ts @@ -22,6 +22,7 @@ export class WorkflowExecutionsTab extends BasePage { this.getters.executionPreviewDetails().find('[data-test-id="execution-preview-label"]'), executionPreviewId: () => this.getters.executionPreviewDetails().find('[data-test-id="execution-preview-id"]'), + executionDebugButton: () => cy.getByTestId('execution-debug-button'), }; actions = { toggleNodeEnabled: (nodeName: string) => { diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue index d1c9d01f4a202..f16e8d4c6654e 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue @@ -87,6 +87,7 @@ [$style.debugLink]: true, [$style.secondary]: debugButtonData.type === 'secondary', }" + data-test-id="execution-debug-button" > { - beforeEach(() => { - workflowPage.actions.visit(); - }); - - it('Should be able to debug executions', () => { - cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); - cy.intercept('GET', '/rest/executions/*').as('getExecution'); - cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions'); - cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun'); + before(() => { + cy.viewport(1300, 640) + }); + it('should be able to debug executions', () => { + cy.intercept('GET', '/rest/settings', (req) => { + req.on('response', (res) => { + res.send({ + data: { ...res.body.data, enterprise: { debugInEditor: true } }, + }); + }); + }).as('loadSettings'); + cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); + cy.intercept('GET', '/rest/executions/*').as('getExecution'); + cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions'); + cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun'); + + cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + + workflowPage.actions.visit(); workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME); @@ -39,9 +49,8 @@ describe('Debug', () => { cy.wait(['@getExecutions', '@getCurrentExecutions']); executionsTab.getters.executionDebugButton().should('have.text', 'Debug in editor').click(); - getVisibleModalOverlay().click(1, 1); + cy.get('.matching-pinned-nodes-confirmation').should('not.exist') - executionsTab.actions.switchToEditorTab(); workflowPage.actions.openNode(HTTP_REQUEST_NODE_NAME); ndv.actions.clearParameterInput('url'); @@ -53,6 +62,10 @@ describe('Debug', () => { cy.wait(['@postWorkflowRun']); + workflowPage.actions.openNode(HTTP_REQUEST_NODE_NAME); + ndv.actions.pinData(); + ndv.actions.close(); + executionsTab.actions.switchToExecutionsTab(); cy.wait(['@getExecutions', '@getCurrentExecutions']); @@ -60,7 +73,24 @@ describe('Debug', () => { executionsTab.getters.executionListItems().should('have.length', 2).first().click(); cy.wait(['@getExecution']); - executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor'); + executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click(); + + let confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible') + confirmDialog.find('li').should('have.length', 2) + confirmDialog.get('.btn--cancel').click(); + + cy.wait(['@getExecutions', '@getCurrentExecutions']); + + executionsTab.getters.executionListItems().should('have.length', 2).first().click(); + cy.wait(['@getExecution']); + + executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click(); + + confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible') + confirmDialog.find('li').should('have.length', 2) + confirmDialog.get('.btn--confirm').click(); + + workflowPage.getters.canvasNodes().filter(':has(.node-pin-data-icon)').should('have.length', 1) }); }); diff --git a/packages/editor-ui/src/components/Modals.vue b/packages/editor-ui/src/components/Modals.vue index 3ddcaeee031b1..1690006d40956 100644 --- a/packages/editor-ui/src/components/Modals.vue +++ b/packages/editor-ui/src/components/Modals.vue @@ -118,7 +118,7 @@
diff --git a/packages/editor-ui/src/composables/useExecutionDebugging.ts b/packages/editor-ui/src/composables/useExecutionDebugging.ts index 41490cbfdfdeb..64834bf1b0b80 100644 --- a/packages/editor-ui/src/composables/useExecutionDebugging.ts +++ b/packages/editor-ui/src/composables/useExecutionDebugging.ts @@ -60,6 +60,7 @@ export const useExecutionDebugging = () => { confirmButtonText: i18n.baseText('nodeView.confirmMessage.debug.confirmButtonText'), cancelButtonText: i18n.baseText('nodeView.confirmMessage.debug.cancelButtonText'), dangerouslyUseHTMLString: true, + customClass: 'matching-pinned-nodes-confirmation', }, ); From 0c94d46316fd5b4e94e8e9b1dc71a774b2c4a40a Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Fri, 18 Aug 2023 20:44:13 +0200 Subject: [PATCH 46/51] test: E2E debugging --- cypress/e2e/28-debug.cy.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cypress/e2e/28-debug.cy.ts b/cypress/e2e/28-debug.cy.ts index 1d02156e44a38..f52b6144ad976 100644 --- a/cypress/e2e/28-debug.cy.ts +++ b/cypress/e2e/28-debug.cy.ts @@ -11,9 +11,6 @@ const ndv = new NDV(); const executionsTab = new WorkflowExecutionsTab(); describe('Debug', () => { - before(() => { - cy.viewport(1300, 640) - }); it('should be able to debug executions', () => { cy.intercept('GET', '/rest/settings', (req) => { req.on('response', (res) => { @@ -90,7 +87,8 @@ describe('Debug', () => { confirmDialog.find('li').should('have.length', 2) confirmDialog.get('.btn--confirm').click(); - workflowPage.getters.canvasNodes().filter(':has(.node-pin-data-icon)').should('have.length', 1) + workflowPage.getters.canvasNodes().first().should('have.descendants', '.node-pin-data-icon') + workflowPage.getters.canvasNodes().not(':first').should('not.have.descendants', '.node-pin-data-icon') }); }); From d416c057515e94ba1fe5401b5cc40fcf87d2b2b8 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Sun, 20 Aug 2023 18:06:31 +0200 Subject: [PATCH 47/51] test: extend E2E debugging --- cypress/e2e/28-debug.cy.ts | 53 +++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/cypress/e2e/28-debug.cy.ts b/cypress/e2e/28-debug.cy.ts index f52b6144ad976..ede3b500cb707 100644 --- a/cypress/e2e/28-debug.cy.ts +++ b/cypress/e2e/28-debug.cy.ts @@ -1,5 +1,5 @@ import { - HTTP_REQUEST_NODE_NAME, + HTTP_REQUEST_NODE_NAME, IF_NODE_NAME, INSTANCE_OWNER, MANUAL_TRIGGER_NODE_NAME, SET_NODE_NAME, @@ -46,7 +46,8 @@ describe('Debug', () => { cy.wait(['@getExecutions', '@getCurrentExecutions']); executionsTab.getters.executionDebugButton().should('have.text', 'Debug in editor').click(); - cy.get('.matching-pinned-nodes-confirmation').should('not.exist') + cy.get('.el-notification').contains('Execution data imported').should('be.visible'); + cy.get('.matching-pinned-nodes-confirmation').should('not.exist'); workflowPage.actions.openNode(HTTP_REQUEST_NODE_NAME); @@ -72,8 +73,8 @@ describe('Debug', () => { executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click(); - let confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible') - confirmDialog.find('li').should('have.length', 2) + let confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible'); + confirmDialog.find('li').should('have.length', 2); confirmDialog.get('.btn--cancel').click(); cy.wait(['@getExecutions', '@getCurrentExecutions']); @@ -83,12 +84,46 @@ describe('Debug', () => { executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click(); - confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible') - confirmDialog.find('li').should('have.length', 2) + confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible'); + confirmDialog.find('li').should('have.length', 2); confirmDialog.get('.btn--confirm').click(); - workflowPage.getters.canvasNodes().first().should('have.descendants', '.node-pin-data-icon') - workflowPage.getters.canvasNodes().not(':first').should('not.have.descendants', '.node-pin-data-icon') + workflowPage.getters.canvasNodes().first().should('have.descendants', '.node-pin-data-icon'); + workflowPage.getters.canvasNodes().not(':first').should('not.have.descendants', '.node-pin-data-icon'); - }); + cy.reload(true); + cy.wait(['@getExecution']); + + confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible'); + confirmDialog.find('li').should('have.length', 1); + confirmDialog.get('.btn--confirm').click(); + + workflowPage.getters.canvasNodePlusEndpointByName(SET_NODE_NAME).click(); + workflowPage.actions.addNodeToCanvas(IF_NODE_NAME, false); + workflowPage.actions.saveWorkflowUsingKeyboardShortcut(); + + executionsTab.actions.switchToExecutionsTab(); + cy.wait(['@getExecutions', '@getCurrentExecutions']); + executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click(); + + confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible'); + confirmDialog.find('li').should('have.length', 1); + confirmDialog.get('.btn--confirm').click(); + workflowPage.getters.canvasNodes().last().find('.node-info-icon').should('be.empty'); + + workflowPage.getters.canvasNodes().first().dblclick(); + ndv.getters.pinDataButton().click(); + ndv.actions.close(); + + workflowPage.actions.saveWorkflowUsingKeyboardShortcut(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.deleteNode(IF_NODE_NAME); + + executionsTab.actions.switchToExecutionsTab(); + cy.wait(['@getExecutions', '@getCurrentExecutions']); + executionsTab.getters.executionListItems().should('have.length', 3).first().click(); + cy.wait(['@getExecution']); + executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click(); + cy.get('.el-notification').contains('Some execution data wasn\'t imported').should('be.visible'); + }); }); From 8bd0443ae11d5563972f6890bcadc8c840d03d64 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Mon, 21 Aug 2023 00:05:10 +0200 Subject: [PATCH 48/51] fix(editor): update debug link and add unit test --- .../ExecutionsView/ExecutionPreview.vue | 13 +- .../__tests__/ExecutionPreview.test.ts | 113 ++++++++++++++++++ 2 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 packages/editor-ui/src/components/ExecutionsView/__tests__/ExecutionPreview.test.ts diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue index f16e8d4c6654e..5468f6831fded 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue @@ -82,23 +82,23 @@ - {{ debugButtonData.text }} + {{ + debugButtonData.text + }} @@ -300,12 +300,13 @@ export default defineComponent({ margin-right: var(--spacing-xs); &.secondary { - a { + a span { color: var(--color-primary-shade-1); } } - a { + a span { + display: block; padding: var(--spacing-xs) var(--spacing-m); color: var(--color-text-xlight); } diff --git a/packages/editor-ui/src/components/ExecutionsView/__tests__/ExecutionPreview.test.ts b/packages/editor-ui/src/components/ExecutionsView/__tests__/ExecutionPreview.test.ts new file mode 100644 index 0000000000000..267e4604b8a8e --- /dev/null +++ b/packages/editor-ui/src/components/ExecutionsView/__tests__/ExecutionPreview.test.ts @@ -0,0 +1,113 @@ +import { vi, describe, expect } from 'vitest'; +import { render } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; +import { faker } from '@faker-js/faker'; +import { createRouter, createWebHistory } from 'vue-router'; +import { createPinia, PiniaVuePlugin, setActivePinia } from 'pinia'; +import type { IExecutionsSummary } from 'n8n-workflow'; +import { useSettingsStore, useWorkflowsStore } from '@/stores'; +import ExecutionPreview from '@/components/ExecutionsView/ExecutionPreview.vue'; +import { VIEWS } from '@/constants'; +import { i18nInstance, I18nPlugin } from '@/plugins/i18n'; +import { FontAwesomePlugin } from '@/plugins/icons'; +import { GlobalComponentsPlugin } from '@/plugins/components'; + +let pinia: ReturnType; + +const routes = [ + { path: '/', name: 'home', component: { template: '
' } }, + { + path: '/workflow/:name/debug/:executionId', + name: VIEWS.EXECUTION_DEBUG, + component: { template: '
' }, + }, +]; + +const router = createRouter({ + history: createWebHistory(), + routes, +}); + +const $route = { + params: {}, +}; + +const generateUndefinedNullOrString = () => { + switch (Math.floor(Math.random() * 4)) { + case 0: + return undefined; + case 1: + return null; + case 2: + return faker.string.uuid(); + case 3: + return ''; + default: + return undefined; + } +}; + +const executionDataFactory = (): IExecutionsSummary => ({ + id: faker.string.uuid(), + finished: faker.datatype.boolean(), + mode: faker.helpers.arrayElement(['manual', 'trigger']), + startedAt: faker.date.past(), + stoppedAt: faker.date.past(), + workflowId: faker.number.int().toString(), + workflowName: faker.string.sample(), + status: faker.helpers.arrayElement(['failed', 'success']), + nodeExecutionStatus: {}, + retryOf: generateUndefinedNullOrString(), + retrySuccessId: generateUndefinedNullOrString(), +}); + +describe('ExecutionPreview.vue', () => { + let workflowsStore: ReturnType; + let settingsStore: ReturnType; + const executionData: IExecutionsSummary = executionDataFactory(); + + beforeEach(() => { + pinia = createPinia(); + setActivePinia(pinia); + + workflowsStore = useWorkflowsStore(); + settingsStore = useSettingsStore(); + + vi.spyOn(workflowsStore, 'activeWorkflowExecution', 'get').mockReturnValue(executionData); + }); + + test.each([ + [false, '/'], + [true, `/workflow/${executionData.workflowId}/debug/${executionData.id}`], + ])( + 'when debug enterprise feature is %s it should handle debug link click accordingly', + async (availability, path) => { + vi.spyOn(settingsStore, 'isEnterpriseFeatureEnabled', 'get').mockReturnValue( + () => availability, + ); + + // Not using createComponentRenderer helper here because this component should not mock `router-link` + const { getByTestId } = render(ExecutionPreview, { + global: { + plugins: [ + I18nPlugin, + i18nInstance, + PiniaVuePlugin, + FontAwesomePlugin, + GlobalComponentsPlugin, + pinia, + router, + ], + stubs: {}, + mocks: { + $route, + }, + }, + }); + + await userEvent.click(getByTestId('execution-debug-button')); + + expect(router.currentRoute.value.path).toBe(path); + }, + ); +}); From 98fd63a57c2f2230c0e49edb8ede5deba7d9df4c Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Mon, 21 Aug 2023 00:07:01 +0200 Subject: [PATCH 49/51] test: update debug link unit test --- .../ExecutionsView/__tests__/ExecutionPreview.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/editor-ui/src/components/ExecutionsView/__tests__/ExecutionPreview.test.ts b/packages/editor-ui/src/components/ExecutionsView/__tests__/ExecutionPreview.test.ts index 267e4604b8a8e..fb13c06a34958 100644 --- a/packages/editor-ui/src/components/ExecutionsView/__tests__/ExecutionPreview.test.ts +++ b/packages/editor-ui/src/components/ExecutionsView/__tests__/ExecutionPreview.test.ts @@ -86,7 +86,7 @@ describe('ExecutionPreview.vue', () => { () => availability, ); - // Not using createComponentRenderer helper here because this component should not mock `router-link` + // Not using createComponentRenderer helper here because this component should not stub `router-link` const { getByTestId } = render(ExecutionPreview, { global: { plugins: [ @@ -98,7 +98,6 @@ describe('ExecutionPreview.vue', () => { pinia, router, ], - stubs: {}, mocks: { $route, }, From cfb7ac6263e811ad15c958e763364eb450f251c8 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Mon, 21 Aug 2023 21:29:25 +0200 Subject: [PATCH 50/51] fix(editor): use Vue render function to produce confirm message --- .../src/composables/useExecutionDebugging.ts | 18 +++++++++++------- .../editor-ui/src/composables/useMessage.ts | 7 ++++--- .../editor-ui/src/plugins/i18n/locales/en.json | 2 +- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/editor-ui/src/composables/useExecutionDebugging.ts b/packages/editor-ui/src/composables/useExecutionDebugging.ts index 64834bf1b0b80..b0ca84a6bc0fd 100644 --- a/packages/editor-ui/src/composables/useExecutionDebugging.ts +++ b/packages/editor-ui/src/composables/useExecutionDebugging.ts @@ -1,4 +1,4 @@ -import { computed } from 'vue'; +import { h, computed } from 'vue'; import { useRouter } from 'vue-router'; import { useI18n, useMessage, useToast } from '@/composables'; import { @@ -45,15 +45,19 @@ export const useExecutionDebugging = () => { const matchingPinnedNodeNames = executionNodeNames.filter((name) => workflowPinnedNodeNames.includes(name), ); - const matchingPinnedNodeNamesToHtmlList = `
    ${matchingPinnedNodeNames - .map((name) => `
  • ${name}
  • `) - .join('')}
`; if (matchingPinnedNodeNames.length > 0) { + const confirmMessage = h('p', [ + i18n.baseText('nodeView.confirmMessage.debug.message'), + h( + 'ul', + { class: 'mt-l ml-l' }, + matchingPinnedNodeNames.map((name) => h('li', name)), + ), + ]); + const overWritePinnedDataConfirm = await message.confirm( - i18n.baseText('nodeView.confirmMessage.debug.message', { - interpolate: { nodeNames: matchingPinnedNodeNamesToHtmlList }, - }), + confirmMessage, i18n.baseText('nodeView.confirmMessage.debug.headline'), { type: 'warning', diff --git a/packages/editor-ui/src/composables/useMessage.ts b/packages/editor-ui/src/composables/useMessage.ts index 20847dffd92c4..7b6ddeaf48338 100644 --- a/packages/editor-ui/src/composables/useMessage.ts +++ b/packages/editor-ui/src/composables/useMessage.ts @@ -1,3 +1,4 @@ +import type { VNode } from "vue"; import type { ElMessageBoxOptions } from 'element-plus'; import { ElMessageBox as MessageBox } from 'element-plus'; @@ -10,7 +11,7 @@ export function useMessage() { }; async function alert( - message: string, + message: ElMessageBoxOptions['message'], configOrTitle?: string | ElMessageBoxOptions, config?: ElMessageBoxOptions, ) { @@ -27,7 +28,7 @@ export function useMessage() { } async function confirm( - message: string, + message: ElMessageBoxOptions['message'], configOrTitle?: string | ElMessageBoxOptions, config?: ElMessageBoxOptions, ): Promise { @@ -51,7 +52,7 @@ export function useMessage() { } async function prompt( - message: string, + message: ElMessageBoxOptions['message'], configOrTitle?: string | ElMessageBoxOptions, config?: ElMessageBoxOptions, ) { diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index fed48d9192c3d..451eabacf8295 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -893,7 +893,7 @@ "nodeView.confirmMessage.debug.cancelButtonText": "Cancel", "nodeView.confirmMessage.debug.confirmButtonText": "Unpin", "nodeView.confirmMessage.debug.headline": "Unpin workflow data", - "nodeView.confirmMessage.debug.message": "Loading this execution will unpin the data currently pinned in these nodes

{nodeNames}", + "nodeView.confirmMessage.debug.message": "Loading this execution will unpin the data currently pinned in these nodes", "nodeView.couldntImportWorkflow": "Could not import workflow", "nodeView.deletesTheCurrentExecutionData": "Deletes the current execution data", "nodeView.executesTheWorkflowFromATriggerNode": "Runs the workflow, starting from a Trigger node", From 234f1076eb9b4bcf97b6cf1508fc60eaade99b3b Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Mon, 21 Aug 2023 21:49:37 +0200 Subject: [PATCH 51/51] fix(editor): remove unused import --- packages/editor-ui/src/composables/useMessage.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/editor-ui/src/composables/useMessage.ts b/packages/editor-ui/src/composables/useMessage.ts index 7b6ddeaf48338..066c627da7ddd 100644 --- a/packages/editor-ui/src/composables/useMessage.ts +++ b/packages/editor-ui/src/composables/useMessage.ts @@ -1,4 +1,3 @@ -import type { VNode } from "vue"; import type { ElMessageBoxOptions } from 'element-plus'; import { ElMessageBox as MessageBox } from 'element-plus';