feat(editor): Debug executions in the editor (#6834)
This commit is contained in:
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,
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user