mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 20:54:07 -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 './ndv';
|
||||
export * from './bannerStack';
|
||||
export * from './workflow-executions-tab';
|
||||
export * from './signin';
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -725,7 +725,7 @@ export interface ITimeoutHMS {
|
|||
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;
|
||||
|
||||
|
@ -897,6 +897,7 @@ export interface WorkflowsState {
|
|||
workflowExecutionData: IExecutionResponse | null;
|
||||
workflowExecutionPairedItemMappings: { [itemId: string]: Set<string> };
|
||||
workflowsById: IWorkflowsMap;
|
||||
isInDebugMode?: boolean;
|
||||
}
|
||||
|
||||
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>
|
||||
</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
|
||||
v-if="executionUIDetails?.name === 'error'"
|
||||
trigger="click"
|
||||
|
@ -128,13 +151,12 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { useMessage } from '@/composables';
|
||||
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 { ElDropdown } from 'element-plus';
|
||||
|
||||
type RetryDropdownRef = InstanceType<typeof ElDropdown> & { hide: () => void };
|
||||
|
||||
|
@ -153,6 +175,7 @@ export default defineComponent({
|
|||
setup() {
|
||||
return {
|
||||
...useMessage(),
|
||||
...useExecutionDebugging(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -162,6 +185,17 @@ export default defineComponent({
|
|||
executionMode(): string {
|
||||
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: {
|
||||
async onDeleteExecution(): Promise<void> {
|
||||
|
@ -212,9 +246,15 @@ export default defineComponent({
|
|||
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;
|
||||
}
|
||||
|
@ -254,4 +294,21 @@ export default defineComponent({
|
|||
margin-top: var(--spacing-l);
|
||||
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>
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
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;
|
||||
|
|
|
@ -119,6 +119,12 @@
|
|||
<SourceControlPullModal :modalName="modalName" :data="data" />
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
@ -147,6 +153,7 @@ import {
|
|||
LOG_STREAM_MODAL_KEY,
|
||||
SOURCE_CONTROL_PUSH_MODAL_KEY,
|
||||
SOURCE_CONTROL_PULL_MODAL_KEY,
|
||||
DEBUG_PAYWALL_MODAL_KEY,
|
||||
MFA_SETUP_MODAL_KEY,
|
||||
} from '@/constants';
|
||||
|
||||
|
@ -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',
|
||||
|
@ -201,6 +209,7 @@ export default defineComponent({
|
|||
EventDestinationSettingsModal,
|
||||
SourceControlPushModal,
|
||||
SourceControlPullModal,
|
||||
DebugPaywallModal,
|
||||
MfaSetupModal,
|
||||
},
|
||||
data: () => ({
|
||||
|
@ -226,6 +235,7 @@ export default defineComponent({
|
|||
LOG_STREAM_MODAL_KEY,
|
||||
SOURCE_CONTROL_PUSH_MODAL_KEY,
|
||||
SOURCE_CONTROL_PULL_MODAL_KEY,
|
||||
DEBUG_PAYWALL_MODAL_KEY,
|
||||
MFA_SETUP_MODAL_KEY,
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -13,3 +13,4 @@ export * from './useTitleChange';
|
|||
export * from './useToast';
|
||||
export * from './useNodeSpecificationValues';
|
||||
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(
|
||||
message: string,
|
||||
message: ElMessageBoxOptions['message'],
|
||||
configOrTitle?: string | ElMessageBoxOptions,
|
||||
config?: ElMessageBoxOptions,
|
||||
) {
|
||||
|
@ -27,7 +27,7 @@ export function useMessage() {
|
|||
}
|
||||
|
||||
async function confirm(
|
||||
message: string,
|
||||
message: ElMessageBoxOptions['message'],
|
||||
configOrTitle?: string | ElMessageBoxOptions,
|
||||
config?: ElMessageBoxOptions,
|
||||
): Promise<MessageBoxConfirmResult> {
|
||||
|
@ -51,7 +51,7 @@ export function useMessage() {
|
|||
}
|
||||
|
||||
async function prompt(
|
||||
message: string,
|
||||
message: ElMessageBoxOptions['message'],
|
||||
configOrTitle?: string | ElMessageBoxOptions,
|
||||
config?: ElMessageBoxOptions,
|
||||
) {
|
||||
|
|
|
@ -47,6 +47,7 @@ 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 MFA_SETUP_MODAL_KEY = 'mfaSetup';
|
||||
|
||||
export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = {
|
||||
|
@ -346,6 +347,7 @@ export const enum VIEWS {
|
|||
COLLECTION = 'TemplatesCollectionView',
|
||||
EXECUTIONS = 'Executions',
|
||||
EXECUTION_PREVIEW = 'ExecutionPreview',
|
||||
EXECUTION_DEBUG = 'ExecutionDebug',
|
||||
EXECUTION_HOME = 'ExecutionsLandingPage',
|
||||
TEMPLATE = 'TemplatesWorkflowView',
|
||||
TEMPLATES = 'TemplatesSearchView',
|
||||
|
@ -446,6 +448,7 @@ export const enum EnterpriseEditionFeature {
|
|||
Saml = 'saml',
|
||||
SourceControl = 'sourceControl',
|
||||
AuditLogs = 'auditLogs',
|
||||
DebugInEditor = 'debugInEditor',
|
||||
}
|
||||
export const MAIN_NODE_PANEL_WIDTH = 360;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
@ -564,6 +565,11 @@
|
|||
"executionsList.view": "View",
|
||||
"executionsList.stop": "Stop",
|
||||
"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.searchPlaceholder": "Search executions...",
|
||||
"executionView.onPaste.title": "Cannot paste here",
|
||||
|
@ -884,6 +890,10 @@
|
|||
"nodeView.confirmMessage.receivedCopyPasteData.confirmButtonText": "Yes, import",
|
||||
"nodeView.confirmMessage.receivedCopyPasteData.headline": "Import Workflow?",
|
||||
"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.deletesTheCurrentExecutionData": "Deletes the current execution data",
|
||||
"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.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",
|
||||
|
@ -1854,6 +1868,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 <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 SettingsSourceControl from './views/SettingsSourceControl.vue';
|
||||
import SettingsAuditLogs from './views/SettingsAuditLogs.vue';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
|
||||
|
||||
interface IRouteConfig {
|
||||
meta: {
|
||||
|
@ -207,10 +207,6 @@ export const routes = [
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/workflow',
|
||||
redirect: '/workflow/new',
|
||||
},
|
||||
{
|
||||
path: '/workflows',
|
||||
name: VIEWS.WORKFLOWS,
|
||||
|
@ -227,8 +223,8 @@ export const routes = [
|
|||
},
|
||||
},
|
||||
{
|
||||
path: '/workflow/new',
|
||||
name: VIEWS.NEW_WORKFLOW,
|
||||
path: '/workflow/:name/debug/:executionId',
|
||||
name: VIEWS.EXECUTION_DEBUG,
|
||||
components: {
|
||||
default: NodeView,
|
||||
header: MainHeader,
|
||||
|
@ -240,22 +236,9 @@ export const routes = [
|
|||
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],
|
||||
deny: {
|
||||
shouldDeny: () =>
|
||||
!useSettingsStore().isEnterpriseFeatureEnabled(EnterpriseEditionFeature.DebugInEditor),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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',
|
||||
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',
|
||||
name: VIEWS.SIGNIN,
|
||||
|
|
|
@ -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,
|
||||
|
@ -142,6 +143,9 @@ export const useUIStore = defineStore(STORES.UI, {
|
|||
[SOURCE_CONTROL_PULL_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[DEBUG_PAYWALL_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
},
|
||||
modalStack: [],
|
||||
sidebarMenuCollapsed: true,
|
||||
|
@ -198,6 +202,11 @@ export const useUIStore = defineStore(STORES.UI, {
|
|||
|
||||
return {
|
||||
upgradeLinkUrl: `contextual.upgradeLinkUrl${contextKey}`,
|
||||
feature: {
|
||||
unavailable: {
|
||||
title: `contextual.feature.unavailable.title${contextKey}`,
|
||||
},
|
||||
},
|
||||
credentials: {
|
||||
sharing: {
|
||||
unavailable: {
|
||||
|
@ -284,7 +293,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 [
|
||||
|
|
|
@ -128,6 +128,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
|||
executingNode: null,
|
||||
executionWaitingForWebhook: false,
|
||||
nodeMetadata: {},
|
||||
isInDebugMode: false,
|
||||
}),
|
||||
getters: {
|
||||
// Workflow getters
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
@ -365,7 +367,7 @@ export default defineComponent({
|
|||
},
|
||||
watch: {
|
||||
// Listen to route changes and load the workflow accordingly
|
||||
$route(to: Route, from: Route) {
|
||||
async $route(to: Route, from: Route) {
|
||||
this.readOnlyEnvRouteCheck();
|
||||
|
||||
const currentTab = getNodeViewTab(to);
|
||||
|
@ -388,14 +390,13 @@ export default defineComponent({
|
|||
this.resetWorkspace();
|
||||
this.uiStore.stateIsDirty = previousDirtyState;
|
||||
}
|
||||
void this.loadCredentials();
|
||||
void this.initView().then(() => {
|
||||
this.stopLoading();
|
||||
if (this.blankRedirect) {
|
||||
this.blankRedirect = false;
|
||||
}
|
||||
});
|
||||
await Promise.all([this.loadCredentials(), this.initView()]);
|
||||
this.stopLoading();
|
||||
if (this.blankRedirect) {
|
||||
this.blankRedirect = false;
|
||||
}
|
||||
}
|
||||
await this.checkAndInitDebugMode();
|
||||
}
|
||||
// Also, when landing on executions tab, check if workflow data is changed
|
||||
if (currentTab === MAIN_HEADER_TABS.EXECUTIONS) {
|
||||
|
@ -561,7 +562,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');
|
||||
|
@ -1084,7 +1085,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();
|
||||
|
@ -1504,7 +1505,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
|
||||
|
@ -1779,11 +1780,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];
|
||||
|
@ -1820,10 +1819,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;
|
||||
|
||||
|
@ -1957,7 +1953,7 @@ export default defineComponent({
|
|||
|
||||
newNodeData.name = this.uniqueNodeName(localizedName);
|
||||
|
||||
if (nodeTypeData.webhooks && nodeTypeData.webhooks.length) {
|
||||
if (nodeTypeData.webhooks?.length) {
|
||||
newNodeData.webhookId = uuid();
|
||||
}
|
||||
|
||||
|
@ -2007,9 +2003,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];
|
||||
|
||||
|
@ -2091,7 +2086,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;
|
||||
|
@ -2432,8 +2427,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) {
|
||||
|
@ -2482,7 +2477,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,
|
||||
|
@ -2576,7 +2571,6 @@ export default defineComponent({
|
|||
this.stopLoading();
|
||||
},
|
||||
async tryToAddWelcomeSticky(): Promise<void> {
|
||||
const newWorkflow = this.workflowData;
|
||||
this.canvasStore.zoomToFit();
|
||||
},
|
||||
async initView(): Promise<void> {
|
||||
|
@ -2633,8 +2627,8 @@ export default defineComponent({
|
|||
|
||||
if (workflow) {
|
||||
this.titleSet(workflow.name, 'IDLE');
|
||||
// Open existing workflow
|
||||
await this.openWorkflow(workflow);
|
||||
await this.checkAndInitDebugMode();
|
||||
}
|
||||
} else if (this.$route.meta?.nodeView === true) {
|
||||
// Create new workflow
|
||||
|
@ -2758,8 +2752,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);
|
||||
|
@ -2914,7 +2907,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);
|
||||
|
@ -2959,7 +2952,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);
|
||||
|
@ -2971,7 +2964,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) {
|
||||
|
@ -3261,7 +3254,7 @@ export default defineComponent({
|
|||
);
|
||||
},
|
||||
async addNodes(nodes: INodeUi[], connections?: IConnections, trackHistory = false) {
|
||||
if (!nodes || !nodes.length) {
|
||||
if (!nodes?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -3751,7 +3744,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,
|
||||
|
@ -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() {
|
||||
let workflowId = null as string | null;
|
||||
|
|
Loading…
Reference in a new issue