feat(editor): Debug executions in the editor (#6834)

This commit is contained in:
Csaba Tuncsik
2023-08-25 09:39:14 +02:00
committed by GitHub
parent 72f65dcdd6
commit c833078c87
20 changed files with 675 additions and 86 deletions

View 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>

View File

@@ -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>

View File

@@ -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);
},
);
});

View File

@@ -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;

View File

@@ -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,
}),
});