mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Debug executions in the editor (#6834)
This commit is contained in:
parent
72f65dcdd6
commit
c833078c87
129
cypress/e2e/28-debug.cy.ts
Normal file
129
cypress/e2e/28-debug.cy.ts
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import {
|
||||||
|
HTTP_REQUEST_NODE_NAME, IF_NODE_NAME,
|
||||||
|
INSTANCE_OWNER,
|
||||||
|
MANUAL_TRIGGER_NODE_NAME,
|
||||||
|
SET_NODE_NAME,
|
||||||
|
} from '../constants';
|
||||||
|
import { WorkflowPage, NDV, WorkflowExecutionsTab } from '../pages';
|
||||||
|
|
||||||
|
const workflowPage = new WorkflowPage();
|
||||||
|
const ndv = new NDV();
|
||||||
|
const executionsTab = new WorkflowExecutionsTab();
|
||||||
|
|
||||||
|
describe('Debug', () => {
|
||||||
|
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);
|
||||||
|
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();
|
||||||
|
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);
|
||||||
|
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']);
|
||||||
|
|
||||||
|
workflowPage.actions.openNode(HTTP_REQUEST_NODE_NAME);
|
||||||
|
ndv.actions.pinData();
|
||||||
|
ndv.actions.close();
|
||||||
|
|
||||||
|
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').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().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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -8,4 +8,5 @@ export * from './settings-log-streaming';
|
||||||
export * from './sidebar';
|
export * from './sidebar';
|
||||||
export * from './ndv';
|
export * from './ndv';
|
||||||
export * from './bannerStack';
|
export * from './bannerStack';
|
||||||
|
export * from './workflow-executions-tab';
|
||||||
export * from './signin';
|
export * from './signin';
|
||||||
|
|
|
@ -22,6 +22,7 @@ export class WorkflowExecutionsTab extends BasePage {
|
||||||
this.getters.executionPreviewDetails().find('[data-test-id="execution-preview-label"]'),
|
this.getters.executionPreviewDetails().find('[data-test-id="execution-preview-label"]'),
|
||||||
executionPreviewId: () =>
|
executionPreviewId: () =>
|
||||||
this.getters.executionPreviewDetails().find('[data-test-id="execution-preview-id"]'),
|
this.getters.executionPreviewDetails().find('[data-test-id="execution-preview-id"]'),
|
||||||
|
executionDebugButton: () => cy.getByTestId('execution-debug-button'),
|
||||||
};
|
};
|
||||||
actions = {
|
actions = {
|
||||||
toggleNodeEnabled: (nodeName: string) => {
|
toggleNodeEnabled: (nodeName: string) => {
|
||||||
|
|
|
@ -725,7 +725,7 @@ export interface ITimeoutHMS {
|
||||||
seconds: number;
|
seconds: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkflowTitleStatus = 'EXECUTING' | 'IDLE' | 'ERROR';
|
export type WorkflowTitleStatus = 'EXECUTING' | 'IDLE' | 'ERROR' | 'DEBUG';
|
||||||
|
|
||||||
export type ExtractActionKeys<T> = T extends SimplifiedNodeType ? T['name'] : never;
|
export type ExtractActionKeys<T> = T extends SimplifiedNodeType ? T['name'] : never;
|
||||||
|
|
||||||
|
@ -897,6 +897,7 @@ export interface WorkflowsState {
|
||||||
workflowExecutionData: IExecutionResponse | null;
|
workflowExecutionData: IExecutionResponse | null;
|
||||||
workflowExecutionPairedItemMappings: { [itemId: string]: Set<string> };
|
workflowExecutionPairedItemMappings: { [itemId: string]: Set<string> };
|
||||||
workflowsById: IWorkflowsMap;
|
workflowsById: IWorkflowsMap;
|
||||||
|
isInDebugMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RootState {
|
export interface RootState {
|
||||||
|
|
31
packages/editor-ui/src/__tests__/router.test.ts
Normal file
31
packages/editor-ui/src/__tests__/router.test.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import router from '@/router';
|
||||||
|
import { VIEWS } from '@/constants';
|
||||||
|
|
||||||
|
const App = {
|
||||||
|
template: '<div />',
|
||||||
|
};
|
||||||
|
const renderComponent = createComponentRenderer(App);
|
||||||
|
|
||||||
|
describe('router', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
const pinia = createPinia();
|
||||||
|
setActivePinia(pinia);
|
||||||
|
renderComponent({ pinia });
|
||||||
|
});
|
||||||
|
|
||||||
|
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],
|
||||||
|
['/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(name);
|
||||||
|
});
|
||||||
|
});
|
40
packages/editor-ui/src/components/DebugPaywallModal.vue
Normal file
40
packages/editor-ui/src/components/DebugPaywallModal.vue
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useI18n } from '@/composables';
|
||||||
|
import Modal from '@/components/Modal.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modalName: string;
|
||||||
|
data: { title: string; footerButtonAction: () => void };
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal width="500px" :title="props.data.title" :name="props.modalName">
|
||||||
|
<template #content>
|
||||||
|
<n8n-text>
|
||||||
|
{{ i18n.baseText('executionsList.debug.paywall.content') }}
|
||||||
|
<br />
|
||||||
|
<n8n-link :to="i18n.baseText('executionsList.debug.paywall.link.url')">
|
||||||
|
{{ i18n.baseText('executionsList.debug.paywall.link.text') }}
|
||||||
|
</n8n-link>
|
||||||
|
</n8n-text>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div :class="$style.footer">
|
||||||
|
<n8n-button @click="props.data.footerButtonAction">
|
||||||
|
{{ i18n.baseText('generic.seePlans') }}
|
||||||
|
</n8n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -79,6 +79,29 @@
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<n8n-button
|
||||||
|
size="large"
|
||||||
|
:type="debugButtonData.type"
|
||||||
|
:class="{
|
||||||
|
[$style.debugLink]: true,
|
||||||
|
[$style.secondary]: debugButtonData.type === 'secondary',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: VIEWS.EXECUTION_DEBUG,
|
||||||
|
params: {
|
||||||
|
name: activeExecution.workflowId,
|
||||||
|
executionId: activeExecution.id,
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span @click="handleDebugLinkClick" data-test-id="execution-debug-button">{{
|
||||||
|
debugButtonData.text
|
||||||
|
}}</span>
|
||||||
|
</router-link>
|
||||||
|
</n8n-button>
|
||||||
|
|
||||||
<el-dropdown
|
<el-dropdown
|
||||||
v-if="executionUIDetails?.name === 'error'"
|
v-if="executionUIDetails?.name === 'error'"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
|
@ -128,13 +151,12 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
import { ElDropdown } from 'element-plus';
|
||||||
import { useMessage } from '@/composables';
|
import { useExecutionDebugging, useMessage } from '@/composables';
|
||||||
import WorkflowPreview from '@/components/WorkflowPreview.vue';
|
import WorkflowPreview from '@/components/WorkflowPreview.vue';
|
||||||
import type { IExecutionUIData } from '@/mixins/executionsHelpers';
|
import type { IExecutionUIData } from '@/mixins/executionsHelpers';
|
||||||
import { executionHelpers } from '@/mixins/executionsHelpers';
|
import { executionHelpers } from '@/mixins/executionsHelpers';
|
||||||
import { MODAL_CONFIRM, VIEWS } from '@/constants';
|
import { MODAL_CONFIRM, VIEWS } from '@/constants';
|
||||||
import { ElDropdown } from 'element-plus';
|
|
||||||
|
|
||||||
type RetryDropdownRef = InstanceType<typeof ElDropdown> & { hide: () => void };
|
type RetryDropdownRef = InstanceType<typeof ElDropdown> & { hide: () => void };
|
||||||
|
|
||||||
|
@ -153,6 +175,7 @@ export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
...useMessage(),
|
...useMessage(),
|
||||||
|
...useExecutionDebugging(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -162,6 +185,17 @@ export default defineComponent({
|
||||||
executionMode(): string {
|
executionMode(): string {
|
||||||
return this.activeExecution?.mode || '';
|
return this.activeExecution?.mode || '';
|
||||||
},
|
},
|
||||||
|
debugButtonData(): Record<string, string> {
|
||||||
|
return this.activeExecution?.status === 'success'
|
||||||
|
? {
|
||||||
|
text: this.$locale.baseText('executionsList.debug.button.copyToEditor'),
|
||||||
|
type: 'secondary',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
text: this.$locale.baseText('executionsList.debug.button.debugInEditor'),
|
||||||
|
type: 'primary',
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async onDeleteExecution(): Promise<void> {
|
async onDeleteExecution(): Promise<void> {
|
||||||
|
@ -212,9 +246,15 @@ export default defineComponent({
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
transition: all 150ms ease-in-out;
|
transition: all 150ms ease-in-out;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
|
> div:last-child {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
& * {
|
& * {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
@ -254,4 +294,21 @@ export default defineComponent({
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-l);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.debugLink {
|
||||||
|
padding: 0;
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
a span {
|
||||||
|
color: var(--color-primary-shade-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a span {
|
||||||
|
display: block;
|
||||||
|
padding: var(--spacing-xs) var(--spacing-m);
|
||||||
|
color: var(--color-text-xlight);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
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<typeof createPinia>;
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{ path: '/', name: 'home', component: { template: '<div></div>' } },
|
||||||
|
{
|
||||||
|
path: '/workflow/:name/debug/:executionId',
|
||||||
|
name: VIEWS.EXECUTION_DEBUG,
|
||||||
|
component: { template: '<div></div>' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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<typeof useWorkflowsStore>;
|
||||||
|
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||||
|
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 stub `router-link`
|
||||||
|
const { getByTestId } = render(ExecutionPreview, {
|
||||||
|
global: {
|
||||||
|
plugins: [
|
||||||
|
I18nPlugin,
|
||||||
|
i18nInstance,
|
||||||
|
PiniaVuePlugin,
|
||||||
|
FontAwesomePlugin,
|
||||||
|
GlobalComponentsPlugin,
|
||||||
|
pinia,
|
||||||
|
router,
|
||||||
|
],
|
||||||
|
mocks: {
|
||||||
|
$route,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.click(getByTestId('execution-debug-button'));
|
||||||
|
|
||||||
|
expect(router.currentRoute.value.path).toBe(path);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
|
@ -109,7 +109,11 @@ export default defineComponent({
|
||||||
route.name === VIEWS.EXECUTION_PREVIEW
|
route.name === VIEWS.EXECUTION_PREVIEW
|
||||||
) {
|
) {
|
||||||
this.activeHeaderTab = MAIN_HEADER_TABS.EXECUTIONS;
|
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;
|
this.activeHeaderTab = MAIN_HEADER_TABS.WORKFLOW;
|
||||||
}
|
}
|
||||||
const workflowName = route.params.name;
|
const workflowName = route.params.name;
|
||||||
|
|
|
@ -119,6 +119,12 @@
|
||||||
<SourceControlPullModal :modalName="modalName" :data="data" />
|
<SourceControlPullModal :modalName="modalName" :data="data" />
|
||||||
</template>
|
</template>
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
|
||||||
|
<ModalRoot :name="DEBUG_PAYWALL_MODAL_KEY">
|
||||||
|
<template #default="{ modalName, data }">
|
||||||
|
<DebugPaywallModal data-test-id="debug-paywall-modal" :modalName="modalName" :data="data" />
|
||||||
|
</template>
|
||||||
|
</ModalRoot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -147,6 +153,7 @@ import {
|
||||||
LOG_STREAM_MODAL_KEY,
|
LOG_STREAM_MODAL_KEY,
|
||||||
SOURCE_CONTROL_PUSH_MODAL_KEY,
|
SOURCE_CONTROL_PUSH_MODAL_KEY,
|
||||||
SOURCE_CONTROL_PULL_MODAL_KEY,
|
SOURCE_CONTROL_PULL_MODAL_KEY,
|
||||||
|
DEBUG_PAYWALL_MODAL_KEY,
|
||||||
MFA_SETUP_MODAL_KEY,
|
MFA_SETUP_MODAL_KEY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
|
||||||
|
@ -174,6 +181,7 @@ import WorkflowShareModal from './WorkflowShareModal.ee.vue';
|
||||||
import EventDestinationSettingsModal from '@/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue';
|
import EventDestinationSettingsModal from '@/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue';
|
||||||
import SourceControlPushModal from '@/components/SourceControlPushModal.ee.vue';
|
import SourceControlPushModal from '@/components/SourceControlPushModal.ee.vue';
|
||||||
import SourceControlPullModal from '@/components/SourceControlPullModal.ee.vue';
|
import SourceControlPullModal from '@/components/SourceControlPullModal.ee.vue';
|
||||||
|
import DebugPaywallModal from '@/components/DebugPaywallModal.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Modals',
|
name: 'Modals',
|
||||||
|
@ -201,6 +209,7 @@ export default defineComponent({
|
||||||
EventDestinationSettingsModal,
|
EventDestinationSettingsModal,
|
||||||
SourceControlPushModal,
|
SourceControlPushModal,
|
||||||
SourceControlPullModal,
|
SourceControlPullModal,
|
||||||
|
DebugPaywallModal,
|
||||||
MfaSetupModal,
|
MfaSetupModal,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
@ -226,6 +235,7 @@ export default defineComponent({
|
||||||
LOG_STREAM_MODAL_KEY,
|
LOG_STREAM_MODAL_KEY,
|
||||||
SOURCE_CONTROL_PUSH_MODAL_KEY,
|
SOURCE_CONTROL_PUSH_MODAL_KEY,
|
||||||
SOURCE_CONTROL_PULL_MODAL_KEY,
|
SOURCE_CONTROL_PULL_MODAL_KEY,
|
||||||
|
DEBUG_PAYWALL_MODAL_KEY,
|
||||||
MFA_SETUP_MODAL_KEY,
|
MFA_SETUP_MODAL_KEY,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,3 +13,4 @@ export * from './useTitleChange';
|
||||||
export * from './useToast';
|
export * from './useToast';
|
||||||
export * from './useNodeSpecificationValues';
|
export * from './useNodeSpecificationValues';
|
||||||
export * from './useDataSchema';
|
export * from './useDataSchema';
|
||||||
|
export * from './useExecutionDebugging';
|
||||||
|
|
143
packages/editor-ui/src/composables/useExecutionDebugging.ts
Normal file
143
packages/editor-ui/src/composables/useExecutionDebugging.ts
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
import { h, computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useI18n, useMessage, useToast } from '@/composables';
|
||||||
|
import {
|
||||||
|
DEBUG_PAYWALL_MODAL_KEY,
|
||||||
|
EnterpriseEditionFeature,
|
||||||
|
MODAL_CONFIRM,
|
||||||
|
VIEWS,
|
||||||
|
} from '@/constants';
|
||||||
|
import { useSettingsStore, useUIStore, useWorkflowsStore } from '@/stores';
|
||||||
|
import type { INodeUi } from '@/Interface';
|
||||||
|
|
||||||
|
export const useExecutionDebugging = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const i18n = useI18n();
|
||||||
|
const message = useMessage();
|
||||||
|
const toast = useToast();
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const uiStore = useUIStore();
|
||||||
|
|
||||||
|
const isDebugEnabled = computed(() =>
|
||||||
|
settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.DebugInEditor),
|
||||||
|
);
|
||||||
|
|
||||||
|
const applyExecutionData = async (executionId: string): Promise<void> => {
|
||||||
|
const execution = await workflowsStore.getExecution(executionId);
|
||||||
|
const workflow = workflowsStore.getCurrentWorkflow();
|
||||||
|
const workflowNodes = workflowsStore.getNodes();
|
||||||
|
|
||||||
|
if (!execution?.data?.resultData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { runData } = execution.data.resultData;
|
||||||
|
|
||||||
|
const executionNodeNames = Object.keys(runData);
|
||||||
|
const missingNodeNames = executionNodeNames.filter(
|
||||||
|
(name) => !workflowNodes.some((node) => node.name === name),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Using the pinned data of the workflow to check if the node is pinned
|
||||||
|
// because workflowsStore.getCurrentWorkflow() returns a cached workflow without the updated pinned data
|
||||||
|
const workflowPinnedNodeNames = Object.keys(workflowsStore.workflow.pinData ?? {});
|
||||||
|
const matchingPinnedNodeNames = executionNodeNames.filter((name) =>
|
||||||
|
workflowPinnedNodeNames.includes(name),
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
confirmMessage,
|
||||||
|
i18n.baseText('nodeView.confirmMessage.debug.headline'),
|
||||||
|
{
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: i18n.baseText('nodeView.confirmMessage.debug.confirmButtonText'),
|
||||||
|
cancelButtonText: i18n.baseText('nodeView.confirmMessage.debug.cancelButtonText'),
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
customClass: 'matching-pinned-nodes-confirmation',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (overWritePinnedDataConfirm === MODAL_CONFIRM) {
|
||||||
|
matchingPinnedNodeNames.forEach((name) => {
|
||||||
|
const node = workflowsStore.getNodeByName(name);
|
||||||
|
if (node) {
|
||||||
|
workflowsStore.unpinData({ node });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await router.push({
|
||||||
|
name: VIEWS.EXECUTION_PREVIEW,
|
||||||
|
params: { name: workflow.id, executionId },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set execution data
|
||||||
|
workflowsStore.setWorkflowExecutionData(execution);
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDebugLinkClick = (event: Event): void => {
|
||||||
|
if (!isDebugEnabled.value) {
|
||||||
|
uiStore.openModalWithData({
|
||||||
|
name: DEBUG_PAYWALL_MODAL_KEY,
|
||||||
|
data: {
|
||||||
|
title: i18n.baseText(uiStore.contextBasedTranslationKeys.feature.unavailable.title),
|
||||||
|
footerButtonAction: () => {
|
||||||
|
uiStore.closeModal(DEBUG_PAYWALL_MODAL_KEY);
|
||||||
|
uiStore.goToUpgrade('debug', 'upgrade-debug');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
workflowsStore.isInDebugMode = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
applyExecutionData,
|
||||||
|
handleDebugLinkClick,
|
||||||
|
};
|
||||||
|
};
|
|
@ -10,7 +10,7 @@ export function useMessage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
async function alert(
|
async function alert(
|
||||||
message: string,
|
message: ElMessageBoxOptions['message'],
|
||||||
configOrTitle?: string | ElMessageBoxOptions,
|
configOrTitle?: string | ElMessageBoxOptions,
|
||||||
config?: ElMessageBoxOptions,
|
config?: ElMessageBoxOptions,
|
||||||
) {
|
) {
|
||||||
|
@ -27,7 +27,7 @@ export function useMessage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirm(
|
async function confirm(
|
||||||
message: string,
|
message: ElMessageBoxOptions['message'],
|
||||||
configOrTitle?: string | ElMessageBoxOptions,
|
configOrTitle?: string | ElMessageBoxOptions,
|
||||||
config?: ElMessageBoxOptions,
|
config?: ElMessageBoxOptions,
|
||||||
): Promise<MessageBoxConfirmResult> {
|
): Promise<MessageBoxConfirmResult> {
|
||||||
|
@ -51,7 +51,7 @@ export function useMessage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prompt(
|
async function prompt(
|
||||||
message: string,
|
message: ElMessageBoxOptions['message'],
|
||||||
configOrTitle?: string | ElMessageBoxOptions,
|
configOrTitle?: string | ElMessageBoxOptions,
|
||||||
config?: ElMessageBoxOptions,
|
config?: ElMessageBoxOptions,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -47,6 +47,7 @@ export const IMPORT_CURL_MODAL_KEY = 'importCurl';
|
||||||
export const LOG_STREAM_MODAL_KEY = 'settingsLogStream';
|
export const LOG_STREAM_MODAL_KEY = 'settingsLogStream';
|
||||||
export const SOURCE_CONTROL_PUSH_MODAL_KEY = 'sourceControlPush';
|
export const SOURCE_CONTROL_PUSH_MODAL_KEY = 'sourceControlPush';
|
||||||
export const SOURCE_CONTROL_PULL_MODAL_KEY = 'sourceControlPull';
|
export const SOURCE_CONTROL_PULL_MODAL_KEY = 'sourceControlPull';
|
||||||
|
export const DEBUG_PAYWALL_MODAL_KEY = 'debugPaywall';
|
||||||
export const MFA_SETUP_MODAL_KEY = 'mfaSetup';
|
export const MFA_SETUP_MODAL_KEY = 'mfaSetup';
|
||||||
|
|
||||||
export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = {
|
export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = {
|
||||||
|
@ -346,6 +347,7 @@ export const enum VIEWS {
|
||||||
COLLECTION = 'TemplatesCollectionView',
|
COLLECTION = 'TemplatesCollectionView',
|
||||||
EXECUTIONS = 'Executions',
|
EXECUTIONS = 'Executions',
|
||||||
EXECUTION_PREVIEW = 'ExecutionPreview',
|
EXECUTION_PREVIEW = 'ExecutionPreview',
|
||||||
|
EXECUTION_DEBUG = 'ExecutionDebug',
|
||||||
EXECUTION_HOME = 'ExecutionsLandingPage',
|
EXECUTION_HOME = 'ExecutionsLandingPage',
|
||||||
TEMPLATE = 'TemplatesWorkflowView',
|
TEMPLATE = 'TemplatesWorkflowView',
|
||||||
TEMPLATES = 'TemplatesSearchView',
|
TEMPLATES = 'TemplatesSearchView',
|
||||||
|
@ -446,6 +448,7 @@ export const enum EnterpriseEditionFeature {
|
||||||
Saml = 'saml',
|
Saml = 'saml',
|
||||||
SourceControl = 'sourceControl',
|
SourceControl = 'sourceControl',
|
||||||
AuditLogs = 'auditLogs',
|
AuditLogs = 'auditLogs',
|
||||||
|
DebugInEditor = 'debugInEditor',
|
||||||
}
|
}
|
||||||
export const MAIN_NODE_PANEL_WIDTH = 360;
|
export const MAIN_NODE_PANEL_WIDTH = 360;
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,12 @@ export const genericHelpers = defineComponent({
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useSourceControlStore),
|
...mapStores(useSourceControlStore),
|
||||||
isReadOnlyRoute(): boolean {
|
isReadOnlyRoute(): boolean {
|
||||||
return ![VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW, VIEWS.LOG_STREAMING_SETTINGS].includes(
|
return ![
|
||||||
this.$route.name as VIEWS,
|
VIEWS.WORKFLOW,
|
||||||
);
|
VIEWS.NEW_WORKFLOW,
|
||||||
|
VIEWS.LOG_STREAMING_SETTINGS,
|
||||||
|
VIEWS.EXECUTION_DEBUG,
|
||||||
|
].includes(this.$route.name as VIEWS);
|
||||||
},
|
},
|
||||||
readOnlyEnv(): boolean {
|
readOnlyEnv(): boolean {
|
||||||
return this.sourceControlStore.preferences.branchReadOnly;
|
return this.sourceControlStore.preferences.branchReadOnly;
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
"generic.workflow": "Workflow",
|
"generic.workflow": "Workflow",
|
||||||
"generic.workflowSaved": "Workflow changes saved",
|
"generic.workflowSaved": "Workflow changes saved",
|
||||||
"generic.editor": "Editor",
|
"generic.editor": "Editor",
|
||||||
|
"generic.seePlans": "See plans",
|
||||||
"about.aboutN8n": "About n8n",
|
"about.aboutN8n": "About n8n",
|
||||||
"about.close": "Close",
|
"about.close": "Close",
|
||||||
"about.license": "License",
|
"about.license": "License",
|
||||||
|
@ -564,6 +565,11 @@
|
||||||
"executionsList.view": "View",
|
"executionsList.view": "View",
|
||||||
"executionsList.stop": "Stop",
|
"executionsList.stop": "Stop",
|
||||||
"executionsList.statusTooltipText.theWorkflowIsWaitingIndefinitely": "The workflow is waiting indefinitely for an incoming webhook call.",
|
"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.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.executionName": "Execution {id}",
|
||||||
"executionSidebar.searchPlaceholder": "Search executions...",
|
"executionSidebar.searchPlaceholder": "Search executions...",
|
||||||
"executionView.onPaste.title": "Cannot paste here",
|
"executionView.onPaste.title": "Cannot paste here",
|
||||||
|
@ -884,6 +890,10 @@
|
||||||
"nodeView.confirmMessage.receivedCopyPasteData.confirmButtonText": "Yes, import",
|
"nodeView.confirmMessage.receivedCopyPasteData.confirmButtonText": "Yes, import",
|
||||||
"nodeView.confirmMessage.receivedCopyPasteData.headline": "Import Workflow?",
|
"nodeView.confirmMessage.receivedCopyPasteData.headline": "Import Workflow?",
|
||||||
"nodeView.confirmMessage.receivedCopyPasteData.message": "Workflow will be imported from<br /><i>{plainTextData}<i>",
|
"nodeView.confirmMessage.receivedCopyPasteData.message": "Workflow will be imported from<br /><i>{plainTextData}<i>",
|
||||||
|
"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",
|
||||||
"nodeView.couldntImportWorkflow": "Could not import workflow",
|
"nodeView.couldntImportWorkflow": "Could not import workflow",
|
||||||
"nodeView.deletesTheCurrentExecutionData": "Deletes the current execution data",
|
"nodeView.deletesTheCurrentExecutionData": "Deletes the current execution data",
|
||||||
"nodeView.executesTheWorkflowFromATriggerNode": "Runs the workflow, starting from a Trigger node",
|
"nodeView.executesTheWorkflowFromATriggerNode": "Runs the workflow, starting from a Trigger node",
|
||||||
|
@ -923,6 +933,10 @@
|
||||||
"nodeView.showMessage.stopExecutionCatch.message": "It completed before it could be stopped",
|
"nodeView.showMessage.stopExecutionCatch.message": "It completed before it could be stopped",
|
||||||
"nodeView.showMessage.stopExecutionCatch.title": "Workflow finished executing",
|
"nodeView.showMessage.stopExecutionCatch.title": "Workflow finished executing",
|
||||||
"nodeView.showMessage.stopExecutionTry.title": "Execution stopped",
|
"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.stopCurrentExecution": "Stop current execution",
|
||||||
"nodeView.stopWaitingForWebhookCall": "Stop waiting for webhook call",
|
"nodeView.stopWaitingForWebhookCall": "Stop waiting for webhook call",
|
||||||
"nodeView.stoppingCurrentExecution": "Stopping current execution",
|
"nodeView.stoppingCurrentExecution": "Stopping current execution",
|
||||||
|
@ -1854,6 +1868,10 @@
|
||||||
"contextual.upgradeLinkUrl.cloud": "https://app.n8n.cloud/account/change-plan",
|
"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.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": "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.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 <a href='https://docs.n8n.io/user-management/ldap/' target='_blank'>LDAP in the Docs</a>",
|
"settings.ldap.infoTip": "Learn more about <a href='https://docs.n8n.io/user-management/ldap/' target='_blank'>LDAP in the Docs</a>",
|
||||||
|
|
|
@ -39,7 +39,7 @@ import SignoutView from '@/views/SignoutView.vue';
|
||||||
import SamlOnboarding from '@/views/SamlOnboarding.vue';
|
import SamlOnboarding from '@/views/SamlOnboarding.vue';
|
||||||
import SettingsSourceControl from './views/SettingsSourceControl.vue';
|
import SettingsSourceControl from './views/SettingsSourceControl.vue';
|
||||||
import SettingsAuditLogs from './views/SettingsAuditLogs.vue';
|
import SettingsAuditLogs from './views/SettingsAuditLogs.vue';
|
||||||
import { VIEWS } from '@/constants';
|
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
|
||||||
|
|
||||||
interface IRouteConfig {
|
interface IRouteConfig {
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -207,10 +207,6 @@ export const routes = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/workflow',
|
|
||||||
redirect: '/workflow/new',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/workflows',
|
path: '/workflows',
|
||||||
name: VIEWS.WORKFLOWS,
|
name: VIEWS.WORKFLOWS,
|
||||||
|
@ -227,8 +223,8 @@ export const routes = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/workflow/new',
|
path: '/workflow/:name/debug/:executionId',
|
||||||
name: VIEWS.NEW_WORKFLOW,
|
name: VIEWS.EXECUTION_DEBUG,
|
||||||
components: {
|
components: {
|
||||||
default: NodeView,
|
default: NodeView,
|
||||||
header: MainHeader,
|
header: MainHeader,
|
||||||
|
@ -240,22 +236,9 @@ export const routes = [
|
||||||
allow: {
|
allow: {
|
||||||
loginStatus: [LOGIN_STATUS.LoggedIn],
|
loginStatus: [LOGIN_STATUS.LoggedIn],
|
||||||
},
|
},
|
||||||
},
|
deny: {
|
||||||
},
|
shouldDeny: () =>
|
||||||
},
|
!useSettingsStore().isEnterpriseFeatureEnabled(EnterpriseEditionFeature.DebugInEditor),
|
||||||
{
|
|
||||||
path: '/workflow/:name',
|
|
||||||
name: VIEWS.WORKFLOW,
|
|
||||||
components: {
|
|
||||||
default: NodeView,
|
|
||||||
header: MainHeader,
|
|
||||||
sidebar: MainSidebar,
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
nodeView: true,
|
|
||||||
permissions: {
|
|
||||||
allow: {
|
|
||||||
loginStatus: [LOGIN_STATUS.LoggedIn],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -309,20 +292,6 @@ export const routes = [
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/workflows/demo',
|
|
||||||
name: VIEWS.DEMO,
|
|
||||||
components: {
|
|
||||||
default: NodeView,
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
permissions: {
|
|
||||||
allow: {
|
|
||||||
loginStatus: [LOGIN_STATUS.LoggedIn],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/workflows/templates/:id',
|
path: '/workflows/templates/:id',
|
||||||
name: VIEWS.TEMPLATE_IMPORT,
|
name: VIEWS.TEMPLATE_IMPORT,
|
||||||
|
@ -341,6 +310,58 @@ 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: '/workflows/demo',
|
||||||
|
name: VIEWS.DEMO,
|
||||||
|
components: {
|
||||||
|
default: NodeView,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
permissions: {
|
||||||
|
allow: {
|
||||||
|
loginStatus: [LOGIN_STATUS.LoggedIn],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/workflow/:name',
|
||||||
|
name: VIEWS.WORKFLOW,
|
||||||
|
components: {
|
||||||
|
default: NodeView,
|
||||||
|
header: MainHeader,
|
||||||
|
sidebar: MainSidebar,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
nodeView: true,
|
||||||
|
permissions: {
|
||||||
|
allow: {
|
||||||
|
loginStatus: [LOGIN_STATUS.LoggedIn],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/workflow',
|
||||||
|
redirect: '/workflow/new',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/signin',
|
path: '/signin',
|
||||||
name: VIEWS.SIGNIN,
|
name: VIEWS.SIGNIN,
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
WORKFLOW_SHARE_MODAL_KEY,
|
WORKFLOW_SHARE_MODAL_KEY,
|
||||||
SOURCE_CONTROL_PUSH_MODAL_KEY,
|
SOURCE_CONTROL_PUSH_MODAL_KEY,
|
||||||
SOURCE_CONTROL_PULL_MODAL_KEY,
|
SOURCE_CONTROL_PULL_MODAL_KEY,
|
||||||
|
DEBUG_PAYWALL_MODAL_KEY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import type {
|
import type {
|
||||||
CloudUpdateLinkSourceType,
|
CloudUpdateLinkSourceType,
|
||||||
|
@ -142,6 +143,9 @@ export const useUIStore = defineStore(STORES.UI, {
|
||||||
[SOURCE_CONTROL_PULL_MODAL_KEY]: {
|
[SOURCE_CONTROL_PULL_MODAL_KEY]: {
|
||||||
open: false,
|
open: false,
|
||||||
},
|
},
|
||||||
|
[DEBUG_PAYWALL_MODAL_KEY]: {
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
modalStack: [],
|
modalStack: [],
|
||||||
sidebarMenuCollapsed: true,
|
sidebarMenuCollapsed: true,
|
||||||
|
@ -198,6 +202,11 @@ export const useUIStore = defineStore(STORES.UI, {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
upgradeLinkUrl: `contextual.upgradeLinkUrl${contextKey}`,
|
upgradeLinkUrl: `contextual.upgradeLinkUrl${contextKey}`,
|
||||||
|
feature: {
|
||||||
|
unavailable: {
|
||||||
|
title: `contextual.feature.unavailable.title${contextKey}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
credentials: {
|
credentials: {
|
||||||
sharing: {
|
sharing: {
|
||||||
unavailable: {
|
unavailable: {
|
||||||
|
@ -284,7 +293,9 @@ export const useUIStore = defineStore(STORES.UI, {
|
||||||
this.fakeDoorFeatures.find((fakeDoor) => fakeDoor.id.toString() === id);
|
this.fakeDoorFeatures.find((fakeDoor) => fakeDoor.id.toString() === id);
|
||||||
},
|
},
|
||||||
isReadOnlyView(): boolean {
|
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 {
|
isNodeView(): boolean {
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -128,6 +128,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||||
executingNode: null,
|
executingNode: null,
|
||||||
executionWaitingForWebhook: false,
|
executionWaitingForWebhook: false,
|
||||||
nodeMetadata: {},
|
nodeMetadata: {},
|
||||||
|
isInDebugMode: false,
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
// Workflow getters
|
// Workflow getters
|
||||||
|
|
|
@ -218,6 +218,7 @@ import {
|
||||||
useMessage,
|
useMessage,
|
||||||
useToast,
|
useToast,
|
||||||
useTitleChange,
|
useTitleChange,
|
||||||
|
useExecutionDebugging,
|
||||||
} from '@/composables';
|
} from '@/composables';
|
||||||
import { useUniqueNodeName } from '@/composables/useUniqueNodeName';
|
import { useUniqueNodeName } from '@/composables/useUniqueNodeName';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
@ -355,6 +356,7 @@ export default defineComponent({
|
||||||
...useToast(),
|
...useToast(),
|
||||||
...useMessage(),
|
...useMessage(),
|
||||||
...useUniqueNodeName(),
|
...useUniqueNodeName(),
|
||||||
|
...useExecutionDebugging(),
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
...workflowRun.setup?.(props),
|
...workflowRun.setup?.(props),
|
||||||
};
|
};
|
||||||
|
@ -365,7 +367,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
// Listen to route changes and load the workflow accordingly
|
// Listen to route changes and load the workflow accordingly
|
||||||
$route(to: Route, from: Route) {
|
async $route(to: Route, from: Route) {
|
||||||
this.readOnlyEnvRouteCheck();
|
this.readOnlyEnvRouteCheck();
|
||||||
|
|
||||||
const currentTab = getNodeViewTab(to);
|
const currentTab = getNodeViewTab(to);
|
||||||
|
@ -388,14 +390,13 @@ export default defineComponent({
|
||||||
this.resetWorkspace();
|
this.resetWorkspace();
|
||||||
this.uiStore.stateIsDirty = previousDirtyState;
|
this.uiStore.stateIsDirty = previousDirtyState;
|
||||||
}
|
}
|
||||||
void this.loadCredentials();
|
await Promise.all([this.loadCredentials(), this.initView()]);
|
||||||
void this.initView().then(() => {
|
this.stopLoading();
|
||||||
this.stopLoading();
|
if (this.blankRedirect) {
|
||||||
if (this.blankRedirect) {
|
this.blankRedirect = false;
|
||||||
this.blankRedirect = false;
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
await this.checkAndInitDebugMode();
|
||||||
}
|
}
|
||||||
// Also, when landing on executions tab, check if workflow data is changed
|
// Also, when landing on executions tab, check if workflow data is changed
|
||||||
if (currentTab === MAIN_HEADER_TABS.EXECUTIONS) {
|
if (currentTab === MAIN_HEADER_TABS.EXECUTIONS) {
|
||||||
|
@ -561,7 +562,7 @@ export default defineComponent({
|
||||||
workflowClasses() {
|
workflowClasses() {
|
||||||
const returnClasses = [];
|
const returnClasses = [];
|
||||||
if (this.ctrlKeyPressed || this.moveCanvasKeyPressed) {
|
if (this.ctrlKeyPressed || this.moveCanvasKeyPressed) {
|
||||||
if (this.uiStore.nodeViewMoveInProgress === true) {
|
if (this.uiStore.nodeViewMoveInProgress) {
|
||||||
returnClasses.push('move-in-process');
|
returnClasses.push('move-in-process');
|
||||||
} else {
|
} else {
|
||||||
returnClasses.push('move-active');
|
returnClasses.push('move-active');
|
||||||
|
@ -1084,7 +1085,7 @@ export default defineComponent({
|
||||||
lastSelectedNode.name,
|
lastSelectedNode.name,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (e.key === 'a' && this.isCtrlKeyPressed(e) === true) {
|
} else if (e.key === 'a' && this.isCtrlKeyPressed(e)) {
|
||||||
// Select all nodes
|
// Select all nodes
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -1504,7 +1505,7 @@ export default defineComponent({
|
||||||
const currentTab = getNodeViewTab(this.$route);
|
const currentTab = getNodeViewTab(this.$route);
|
||||||
if (currentTab === MAIN_HEADER_TABS.WORKFLOW) {
|
if (currentTab === MAIN_HEADER_TABS.WORKFLOW) {
|
||||||
let workflowData: IWorkflowDataUpdate | undefined;
|
let workflowData: IWorkflowDataUpdate | undefined;
|
||||||
if (this.editAllowedCheck() === false) {
|
if (!this.editAllowedCheck()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check if it is an URL which could contain workflow data
|
// Check if it is an URL which could contain workflow data
|
||||||
|
@ -1779,11 +1780,9 @@ export default defineComponent({
|
||||||
parameters: {},
|
parameters: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const credentialPerType =
|
const credentialPerType = nodeTypeData.credentials
|
||||||
nodeTypeData.credentials &&
|
?.map((type) => this.credentialsStore.getUsableCredentialByType(type.name))
|
||||||
nodeTypeData.credentials
|
.flat();
|
||||||
.map((type) => this.credentialsStore.getUsableCredentialByType(type.name))
|
|
||||||
.flat();
|
|
||||||
|
|
||||||
if (credentialPerType && credentialPerType.length === 1) {
|
if (credentialPerType && credentialPerType.length === 1) {
|
||||||
const defaultCredential = credentialPerType[0];
|
const defaultCredential = credentialPerType[0];
|
||||||
|
@ -1820,10 +1819,7 @@ export default defineComponent({
|
||||||
return newNodeData;
|
return newNodeData;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (Object.keys(authDisplayOptions).length === 1 && authDisplayOptions.authentication) {
|
||||||
Object.keys(authDisplayOptions).length === 1 &&
|
|
||||||
authDisplayOptions['authentication']
|
|
||||||
) {
|
|
||||||
// ignore complex case when there's multiple dependencies
|
// ignore complex case when there's multiple dependencies
|
||||||
newNodeData.credentials = credentials;
|
newNodeData.credentials = credentials;
|
||||||
|
|
||||||
|
@ -1957,7 +1953,7 @@ export default defineComponent({
|
||||||
|
|
||||||
newNodeData.name = this.uniqueNodeName(localizedName);
|
newNodeData.name = this.uniqueNodeName(localizedName);
|
||||||
|
|
||||||
if (nodeTypeData.webhooks && nodeTypeData.webhooks.length) {
|
if (nodeTypeData.webhooks?.length) {
|
||||||
newNodeData.webhookId = uuid();
|
newNodeData.webhookId = uuid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2007,9 +2003,8 @@ export default defineComponent({
|
||||||
targetNodeName: string,
|
targetNodeName: string,
|
||||||
targetNodeOuputIndex: number,
|
targetNodeOuputIndex: number,
|
||||||
): IConnection | undefined {
|
): IConnection | undefined {
|
||||||
const nodeConnections = (
|
const nodeConnections =
|
||||||
this.workflowsStore.outgoingConnectionsByNodeName(sourceNodeName) as INodeConnections
|
this.workflowsStore.outgoingConnectionsByNodeName(sourceNodeName).main;
|
||||||
).main;
|
|
||||||
if (nodeConnections) {
|
if (nodeConnections) {
|
||||||
const connections: IConnection[] | null = nodeConnections[sourceNodeOutputIndex];
|
const connections: IConnection[] | null = nodeConnections[sourceNodeOutputIndex];
|
||||||
|
|
||||||
|
@ -2091,7 +2086,7 @@ export default defineComponent({
|
||||||
if (lastSelectedNode) {
|
if (lastSelectedNode) {
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
|
|
||||||
if (lastSelectedConnection && lastSelectedConnection.__meta) {
|
if (lastSelectedConnection?.__meta) {
|
||||||
this.__deleteJSPlumbConnection(lastSelectedConnection, trackHistory);
|
this.__deleteJSPlumbConnection(lastSelectedConnection, trackHistory);
|
||||||
|
|
||||||
const targetNodeName = lastSelectedConnection.__meta.targetNodeName;
|
const targetNodeName = lastSelectedConnection.__meta.targetNodeName;
|
||||||
|
@ -2432,8 +2427,8 @@ export default defineComponent({
|
||||||
const { top, left, right, bottom } = element.getBoundingClientRect();
|
const { top, left, right, bottom } = element.getBoundingClientRect();
|
||||||
const [x, y] = NodeViewUtils.getMousePosition(e);
|
const [x, y] = NodeViewUtils.getMousePosition(e);
|
||||||
if (top <= y && bottom >= y && left - inputMargin <= x && right >= x) {
|
if (top <= y && bottom >= y && left - inputMargin <= x && right >= x) {
|
||||||
const nodeName = (element as HTMLElement).dataset['name'] as string;
|
const nodeName = (element as HTMLElement).dataset.name as string;
|
||||||
const node = this.workflowsStore.getNodeByName(nodeName) as INodeUi | null;
|
const node = this.workflowsStore.getNodeByName(nodeName);
|
||||||
if (node) {
|
if (node) {
|
||||||
const nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
const nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||||
if (nodeType && nodeType.inputs && nodeType.inputs.length === 1) {
|
if (nodeType && nodeType.inputs && nodeType.inputs.length === 1) {
|
||||||
|
@ -2482,7 +2477,7 @@ export default defineComponent({
|
||||||
.forEach((endpoint) => setTimeout(() => endpoint.instance.revalidate(endpoint.element), 0));
|
.forEach((endpoint) => setTimeout(() => endpoint.instance.revalidate(endpoint.element), 0));
|
||||||
},
|
},
|
||||||
onPlusEndpointClick(endpoint: Endpoint) {
|
onPlusEndpointClick(endpoint: Endpoint) {
|
||||||
if (endpoint && endpoint.__meta) {
|
if (endpoint?.__meta) {
|
||||||
this.insertNodeAfterSelected({
|
this.insertNodeAfterSelected({
|
||||||
sourceId: endpoint.__meta.nodeId,
|
sourceId: endpoint.__meta.nodeId,
|
||||||
index: endpoint.__meta.index,
|
index: endpoint.__meta.index,
|
||||||
|
@ -2576,7 +2571,6 @@ export default defineComponent({
|
||||||
this.stopLoading();
|
this.stopLoading();
|
||||||
},
|
},
|
||||||
async tryToAddWelcomeSticky(): Promise<void> {
|
async tryToAddWelcomeSticky(): Promise<void> {
|
||||||
const newWorkflow = this.workflowData;
|
|
||||||
this.canvasStore.zoomToFit();
|
this.canvasStore.zoomToFit();
|
||||||
},
|
},
|
||||||
async initView(): Promise<void> {
|
async initView(): Promise<void> {
|
||||||
|
@ -2633,8 +2627,8 @@ export default defineComponent({
|
||||||
|
|
||||||
if (workflow) {
|
if (workflow) {
|
||||||
this.titleSet(workflow.name, 'IDLE');
|
this.titleSet(workflow.name, 'IDLE');
|
||||||
// Open existing workflow
|
|
||||||
await this.openWorkflow(workflow);
|
await this.openWorkflow(workflow);
|
||||||
|
await this.checkAndInitDebugMode();
|
||||||
}
|
}
|
||||||
} else if (this.$route.meta?.nodeView === true) {
|
} else if (this.$route.meta?.nodeView === true) {
|
||||||
// Create new workflow
|
// Create new workflow
|
||||||
|
@ -2758,8 +2752,7 @@ export default defineComponent({
|
||||||
const nodeTypeData = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
const nodeTypeData = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
nodeTypeData &&
|
nodeTypeData?.maxNodes !== undefined &&
|
||||||
nodeTypeData.maxNodes !== undefined &&
|
|
||||||
this.getNodeTypeCount(node.type) >= nodeTypeData.maxNodes
|
this.getNodeTypeCount(node.type) >= nodeTypeData.maxNodes
|
||||||
) {
|
) {
|
||||||
this.showMaxNodeTypeError(nodeTypeData);
|
this.showMaxNodeTypeError(nodeTypeData);
|
||||||
|
@ -2914,7 +2907,7 @@ export default defineComponent({
|
||||||
}) {
|
}) {
|
||||||
const pinData = this.workflowsStore.getPinData;
|
const pinData = this.workflowsStore.getPinData;
|
||||||
|
|
||||||
if (pinData && pinData[name]) return;
|
if (pinData?.[name]) return;
|
||||||
|
|
||||||
const sourceNodeName = name;
|
const sourceNodeName = name;
|
||||||
const sourceNode = this.workflowsStore.getNodeByName(sourceNodeName);
|
const sourceNode = this.workflowsStore.getNodeByName(sourceNodeName);
|
||||||
|
@ -2959,7 +2952,7 @@ export default defineComponent({
|
||||||
|
|
||||||
if (output.isArtificialRecoveredEventItem) {
|
if (output.isArtificialRecoveredEventItem) {
|
||||||
NodeViewUtils.recoveredConnection(connection);
|
NodeViewUtils.recoveredConnection(connection);
|
||||||
} else if ((!output || !output.total) && !output.isArtificialRecoveredEventItem) {
|
} else if (!output?.total && !output.isArtificialRecoveredEventItem) {
|
||||||
NodeViewUtils.resetConnection(connection);
|
NodeViewUtils.resetConnection(connection);
|
||||||
} else {
|
} else {
|
||||||
NodeViewUtils.addConnectionOutputSuccess(connection, output);
|
NodeViewUtils.addConnectionOutputSuccess(connection, output);
|
||||||
|
@ -2971,7 +2964,7 @@ export default defineComponent({
|
||||||
sourceNodeName,
|
sourceNodeName,
|
||||||
parseInt(sourceOutputIndex, 10),
|
parseInt(sourceOutputIndex, 10),
|
||||||
);
|
);
|
||||||
if (endpoint && endpoint.endpoint) {
|
if (endpoint?.endpoint) {
|
||||||
const output = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0];
|
const output = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0];
|
||||||
|
|
||||||
if (output && output.total > 0) {
|
if (output && output.total > 0) {
|
||||||
|
@ -3261,7 +3254,7 @@ export default defineComponent({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
async addNodes(nodes: INodeUi[], connections?: IConnections, trackHistory = false) {
|
async addNodes(nodes: INodeUi[], connections?: IConnections, trackHistory = false) {
|
||||||
if (!nodes || !nodes.length) {
|
if (!nodes?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3751,7 +3744,7 @@ export default defineComponent({
|
||||||
const mode =
|
const mode =
|
||||||
this.nodeCreatorStore.selectedView === TRIGGER_NODE_CREATOR_VIEW ? 'trigger' : 'regular';
|
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', {
|
void this.$externalHooks().run('nodeView.createNodeActiveChanged', {
|
||||||
source,
|
source,
|
||||||
mode,
|
mode,
|
||||||
|
@ -3872,6 +3865,15 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async checkAndInitDebugMode() {
|
||||||
|
if (this.$route.name === VIEWS.EXECUTION_DEBUG) {
|
||||||
|
this.titleSet(this.workflowName, 'DEBUG');
|
||||||
|
if (!this.workflowsStore.isInDebugMode) {
|
||||||
|
await this.applyExecutionData(this.$route.params.executionId as string);
|
||||||
|
this.workflowsStore.isInDebugMode = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async onSourceControlPull() {
|
async onSourceControlPull() {
|
||||||
let workflowId = null as string | null;
|
let workflowId = null as string | null;
|
||||||
|
|
Loading…
Reference in a new issue