mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-13 16:14:07 -08:00
feat: Add report bug buttons (#11304)
Co-authored-by: Cornelius Suermann <cornelius@n8n.io>
This commit is contained in:
parent
ba2827e7bb
commit
296f68f041
|
@ -28,6 +28,7 @@ export function useDeviceSupport() {
|
|||
}
|
||||
|
||||
return {
|
||||
userAgent: userAgent.value,
|
||||
isTouchDevice: isTouchDevice.value,
|
||||
isMacOs: isMacOs.value,
|
||||
controlKeyCode: controlKeyCode.value,
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
"format:check": "biome ci . && prettier --check . --ignore-path ../../.prettierignore",
|
||||
"serve": "cross-env VUE_APP_URL_BASE_API=http://localhost:5678/ vite --host 0.0.0.0 --port 8080 dev",
|
||||
"test": "vitest run",
|
||||
"test:dev": "vitest"
|
||||
"test:dev": "vitest --silent=false"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.16.0",
|
||||
|
|
|
@ -4,10 +4,10 @@ import type { Placement } from 'element-plus';
|
|||
|
||||
interface Props {
|
||||
label: string;
|
||||
shortcut: KeyboardShortcut;
|
||||
shortcut?: KeyboardShortcut;
|
||||
placement?: Placement;
|
||||
}
|
||||
withDefaults(defineProps<Props>(), { placement: 'top' });
|
||||
withDefaults(defineProps<Props>(), { placement: 'top', shortcut: undefined });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -15,7 +15,7 @@ withDefaults(defineProps<Props>(), { placement: 'top' });
|
|||
<template #content>
|
||||
<div :class="$style.shortcut">
|
||||
<div :class="$style.label">{{ label }}</div>
|
||||
<n8n-keyboard-shortcut v-bind="shortcut"></n8n-keyboard-shortcut>
|
||||
<n8n-keyboard-shortcut v-if="shortcut" v-bind="shortcut" />
|
||||
</div>
|
||||
</template>
|
||||
<slot />
|
||||
|
|
|
@ -20,6 +20,7 @@ import { useTelemetry } from '@/composables/useTelemetry';
|
|||
import { useUserHelpers } from '@/composables/useUserHelpers';
|
||||
|
||||
import { ABOUT_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS } from '@/constants';
|
||||
import { useBugReporting } from '@/composables/useBugReporting';
|
||||
|
||||
const becomeTemplateCreatorStore = useBecomeTemplateCreatorStore();
|
||||
const cloudPlanStore = useCloudPlanStore();
|
||||
|
@ -37,6 +38,7 @@ const locale = useI18n();
|
|||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const telemetry = useTelemetry();
|
||||
const { getReportingURL } = useBugReporting();
|
||||
|
||||
useUserHelpers(router, route);
|
||||
|
||||
|
@ -143,6 +145,15 @@ const mainMenuItems = ref([
|
|||
target: '_blank',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'report-bug',
|
||||
icon: 'bug',
|
||||
label: locale.baseText('mainSidebar.helpMenuItems.reportBug'),
|
||||
link: {
|
||||
href: getReportingURL(),
|
||||
target: '_blank',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
icon: 'info',
|
||||
|
|
|
@ -82,6 +82,7 @@ const props = withDefaults(
|
|||
readOnly?: boolean;
|
||||
executing?: boolean;
|
||||
keyBindings?: boolean;
|
||||
showBugReportingButton?: boolean;
|
||||
}>(),
|
||||
{
|
||||
id: 'canvas',
|
||||
|
@ -592,6 +593,7 @@ provide(CanvasKey, {
|
|||
:class="$style.canvasControls"
|
||||
:position="controlsPosition"
|
||||
:show-interactive="false"
|
||||
:show-bug-reporting-button="showBugReportingButton"
|
||||
:zoom="zoom"
|
||||
@zoom-to-fit="onFitView"
|
||||
@zoom-in="onZoomIn"
|
||||
|
|
|
@ -22,6 +22,7 @@ const props = withDefaults(
|
|||
eventBus?: EventBus<CanvasEventBusEvents>;
|
||||
readOnly?: boolean;
|
||||
executing?: boolean;
|
||||
showBugReportingButton?: boolean;
|
||||
}>(),
|
||||
{
|
||||
id: 'canvas',
|
||||
|
@ -57,6 +58,7 @@ const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping(
|
|||
v-if="workflow"
|
||||
:nodes="mappedNodes"
|
||||
:connections="mappedConnections"
|
||||
:show-bug-reporting-button="showBugReportingButton"
|
||||
:event-bus="eventBus"
|
||||
:read-only="readOnly"
|
||||
v-bind="$attrs"
|
||||
|
|
|
@ -1,15 +1,43 @@
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import CanvasControlButtons from './CanvasControlButtons.vue';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
|
||||
const MOCK_URL = 'mock-url';
|
||||
|
||||
vi.mock('@/composables/useBugReporting', () => ({
|
||||
useBugReporting: () => ({ getReportingURL: () => MOCK_URL }),
|
||||
}));
|
||||
|
||||
const renderComponent = createComponentRenderer(CanvasControlButtons);
|
||||
|
||||
describe('CanvasControlButtons', () => {
|
||||
beforeAll(() => {
|
||||
setActivePinia(createTestingPinia());
|
||||
});
|
||||
|
||||
it('should render correctly', () => {
|
||||
const wrapper = renderComponent({
|
||||
props: {
|
||||
showBugReportingButton: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.getByTestId('zoom-in-button')).toBeVisible();
|
||||
expect(wrapper.getByTestId('zoom-out-button')).toBeVisible();
|
||||
expect(wrapper.getByTestId('zoom-to-fit')).toBeVisible();
|
||||
expect(wrapper.getByTestId('report-bug')).toBeVisible();
|
||||
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render correctly without bug reporting button', () => {
|
||||
const wrapper = renderComponent();
|
||||
|
||||
expect(wrapper.getByTestId('zoom-in-button')).toBeVisible();
|
||||
expect(wrapper.getByTestId('zoom-out-button')).toBeVisible();
|
||||
expect(wrapper.getByTestId('zoom-to-fit')).toBeVisible();
|
||||
expect(wrapper.queryByTestId('report-bug')).not.toBeInTheDocument();
|
||||
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
|
|
@ -2,13 +2,17 @@
|
|||
import { Controls } from '@vue-flow/controls';
|
||||
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
||||
import { computed } from 'vue';
|
||||
import { useBugReporting } from '@/composables/useBugReporting';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
zoom?: number;
|
||||
showBugReportingButton?: boolean;
|
||||
}>(),
|
||||
{
|
||||
zoom: 1,
|
||||
showBugReportingButton: false,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -19,6 +23,9 @@ const emit = defineEmits<{
|
|||
'zoom-to-fit': [];
|
||||
}>();
|
||||
|
||||
const { getReportingURL } = useBugReporting();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const isResetZoomVisible = computed(() => props.zoom !== 1);
|
||||
|
||||
function onResetZoom() {
|
||||
|
@ -36,6 +43,10 @@ function onZoomOut() {
|
|||
function onZoomToFit() {
|
||||
emit('zoom-to-fit');
|
||||
}
|
||||
|
||||
function trackBugReport() {
|
||||
telemetry.track('User clicked bug report button in canvas', {}, { withPostHog: true });
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Controls :show-zoom="false" :show-fit-view="false">
|
||||
|
@ -88,6 +99,14 @@ function onZoomToFit() {
|
|||
@click="onResetZoom"
|
||||
/>
|
||||
</KeyboardShortcutTooltip>
|
||||
<KeyboardShortcutTooltip
|
||||
v-if="props.showBugReportingButton"
|
||||
:label="$locale.baseText('nodeView.reportBug')"
|
||||
>
|
||||
<a :href="getReportingURL()" target="_blank" @click="trackBugReport">
|
||||
<N8nIconButton type="tertiary" size="large" icon="bug" data-test-id="report-bug" />
|
||||
</a>
|
||||
</KeyboardShortcutTooltip>
|
||||
</Controls>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -20,6 +20,35 @@ exports[`CanvasControlButtons > should render correctly 1`] = `
|
|||
</button>
|
||||
<!--teleport start-->
|
||||
<!--teleport end-->
|
||||
<!--v-if--><a href="mock-url" target="_blank" class="el-tooltip__trigger"><button class="button button tertiary large withIcon square" aria-live="polite" data-test-id="report-bug"><span class="icon"><span class="n8n-text compact size-large regular n8n-icon n8n-icon"><svg class="svg-inline--fa fa-bug fa-w-16 large" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="bug" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path class="" fill="currentColor" d="M511.988 288.9c-.478 17.43-15.217 31.1-32.653 31.1H424v16c0 21.864-4.882 42.584-13.6 61.145l60.228 60.228c12.496 12.497 12.496 32.758 0 45.255-12.498 12.497-32.759 12.496-45.256 0l-54.736-54.736C345.886 467.965 314.351 480 280 480V236c0-6.627-5.373-12-12-12h-24c-6.627 0-12 5.373-12 12v244c-34.351 0-65.886-12.035-90.636-32.108l-54.736 54.736c-12.498 12.497-32.759 12.496-45.256 0-12.496-12.497-12.496-32.758 0-45.255l60.228-60.228C92.882 378.584 88 357.864 88 336v-16H32.666C15.23 320 .491 306.33.013 288.9-.484 270.816 14.028 256 32 256h56v-58.745l-46.628-46.628c-12.496-12.497-12.496-32.758 0-45.255 12.498-12.497 32.758-12.497 45.256 0L141.255 160h229.489l54.627-54.627c12.498-12.497 32.758-12.497 45.256 0 12.496 12.497 12.496 32.758 0 45.255L424 197.255V256h56c17.972 0 32.484 14.816 31.988 32.9zM257 0c-61.856 0-112 50.144-112 112h224C369 50.144 318.856 0 257 0z"></path></svg></span></span>
|
||||
<!--v-if-->
|
||||
</button></a>
|
||||
<!--teleport start-->
|
||||
<!--teleport end-->
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`CanvasControlButtons > should render correctly without bug reporting button 1`] = `
|
||||
"<div class="vue-flow__panel bottom left vue-flow__controls" style="pointer-events: all;">
|
||||
<!---->
|
||||
<!----><button class="vue-flow__controls-button vue-flow__controls-interactive"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32">
|
||||
<path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0c-4.114 1.828-1.37 2.133.305 2.438 1.676.305 4.42 2.59 4.42 5.181v3.048H3.047A3.056 3.056 0 0 0 0 13.714v15.238A3.056 3.056 0 0 0 3.048 32h18.285a3.056 3.056 0 0 0 3.048-3.048V13.714a3.056 3.056 0 0 0-3.048-3.047zM12.19 24.533a3.056 3.056 0 0 1-3.047-3.047 3.056 3.056 0 0 1 3.047-3.048 3.056 3.056 0 0 1 3.048 3.048 3.056 3.056 0 0 1-3.048 3.047z"></path>
|
||||
</svg>
|
||||
<!---->
|
||||
</button><button class="button button tertiary large withIcon square el-tooltip__trigger el-tooltip__trigger" aria-live="polite" data-test-id="zoom-to-fit"><span class="icon"><span class="n8n-text compact size-large regular n8n-icon n8n-icon"><svg class="svg-inline--fa fa-expand fa-w-14 large" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="expand" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path class="" fill="currentColor" d="M0 180V56c0-13.3 10.7-24 24-24h124c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12H64v84c0 6.6-5.4 12-12 12H12c-6.6 0-12-5.4-12-12zM288 44v40c0 6.6 5.4 12 12 12h84v84c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12V56c0-13.3-10.7-24-24-24H300c-6.6 0-12 5.4-12 12zm148 276h-40c-6.6 0-12 5.4-12 12v84h-84c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h124c13.3 0 24-10.7 24-24V332c0-6.6-5.4-12-12-12zM160 468v-40c0-6.6-5.4-12-12-12H64v-84c0-6.6-5.4-12-12-12H12c-6.6 0-12 5.4-12 12v124c0 13.3 10.7 24 24 24h124c6.6 0 12-5.4 12-12z"></path></svg></span></span>
|
||||
<!--v-if-->
|
||||
</button>
|
||||
<!--teleport start-->
|
||||
<!--teleport end--><button class="button button tertiary large withIcon square el-tooltip__trigger el-tooltip__trigger" aria-live="polite" data-test-id="zoom-in-button"><span class="icon"><span class="n8n-text compact size-large regular n8n-icon n8n-icon"><svg class="svg-inline--fa fa-search-plus fa-w-16 large" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="search-plus" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path class="" fill="currentColor" d="M304 192v32c0 6.6-5.4 12-12 12h-56v56c0 6.6-5.4 12-12 12h-32c-6.6 0-12-5.4-12-12v-56h-56c-6.6 0-12-5.4-12-12v-32c0-6.6 5.4-12 12-12h56v-56c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v56h56c6.6 0 12 5.4 12 12zm201 284.7L476.7 505c-9.4 9.4-24.6 9.4-33.9 0L343 405.3c-4.5-4.5-7-10.6-7-17V372c-35.3 27.6-79.7 44-128 44C93.1 416 0 322.9 0 208S93.1 0 208 0s208 93.1 208 208c0 48.3-16.4 92.7-44 128h16.3c6.4 0 12.5 2.5 17 7l99.7 99.7c9.3 9.4 9.3 24.6 0 34zM344 208c0-75.2-60.8-136-136-136S72 132.8 72 208s60.8 136 136 136 136-60.8 136-136z"></path></svg></span></span>
|
||||
<!--v-if-->
|
||||
</button>
|
||||
<!--teleport start-->
|
||||
<!--teleport end--><button class="button button tertiary large withIcon square el-tooltip__trigger el-tooltip__trigger" aria-live="polite" data-test-id="zoom-out-button"><span class="icon"><span class="n8n-text compact size-large regular n8n-icon n8n-icon"><svg class="svg-inline--fa fa-search-minus fa-w-16 large" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="search-minus" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path class="" fill="currentColor" d="M304 192v32c0 6.6-5.4 12-12 12H124c-6.6 0-12-5.4-12-12v-32c0-6.6 5.4-12 12-12h168c6.6 0 12 5.4 12 12zm201 284.7L476.7 505c-9.4 9.4-24.6 9.4-33.9 0L343 405.3c-4.5-4.5-7-10.6-7-17V372c-35.3 27.6-79.7 44-128 44C93.1 416 0 322.9 0 208S93.1 0 208 0s208 93.1 208 208c0 48.3-16.4 92.7-44 128h16.3c6.4 0 12.5 2.5 17 7l99.7 99.7c9.3 9.4 9.3 24.6 0 34zM344 208c0-75.2-60.8-136-136-136S72 132.8 72 208s60.8 136 136 136 136-60.8 136-136z"></path></svg></span></span>
|
||||
<!--v-if-->
|
||||
</button>
|
||||
<!--teleport start-->
|
||||
<!--teleport end-->
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
</div>"
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`useBugReporting > should generate the correct reporting URL 1`] = `"https://github.com/n8n-io/n8n/issues/new?labels=bug-report&body=%0A%3C%21--+Please+follow+the+template+below.+Skip+the+questions+that+are+not+relevant+to+you.+--%3E%0A%0A%23%23+Describe+the+problem%2Ferror%2Fquestion%0A%0A%0A%23%23+What+is+the+error+message+%28if+any%29%3F%0A%0A%0A%23%23+Please+share+your+workflow%2Fscreenshots%2Frecording%0A%0A%60%60%60%0A%28Select+the+nodes+on+your+canvas+and+use+the+keyboard+shortcuts+CMD%2BC%2FCTRL%2BC+and+CMD%2BV%2FCTRL%2BV+to+copy+and+paste+the+workflow.%29%0A%60%60%60%0A%0A%0A%23%23+Share+the+output+returned+by+the+last+node%0A%3C%21--+If+you+need+help+with+data+transformations%2C+please+also+share+your+expected+output.+--%3E%0A%0A%0Amocked+debug+info%7D"`;
|
|
@ -0,0 +1,42 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`useDebugInfo > should generate debug info 1`] = `
|
||||
"# Debug info
|
||||
|
||||
## core
|
||||
|
||||
- n8nVersion: 0.123.0
|
||||
- platform: docker (cloud)
|
||||
- nodeJsVersion: 14.17.0
|
||||
- database: postgres
|
||||
- executionMode: regular
|
||||
- concurrency: 10
|
||||
- license: community
|
||||
- consumerId: consumer-123
|
||||
|
||||
## storage
|
||||
|
||||
- success: all
|
||||
- error: none
|
||||
- progress: true
|
||||
- manual: true
|
||||
- binaryMode: memory
|
||||
|
||||
## pruning
|
||||
|
||||
- enabled: true
|
||||
- maxAge: 24 hours
|
||||
- maxCount: 100 executions
|
||||
|
||||
## client
|
||||
|
||||
- userAgent: Mozilla/5.0
|
||||
- isTouchDevice: false
|
||||
|
||||
## security
|
||||
|
||||
- blockFileAccessToN8nFiles: false
|
||||
- secureCookie: false
|
||||
|
||||
Generated at: 2024-06-05T15:40:04.819Z"
|
||||
`;
|
18
packages/editor-ui/src/composables/useBugReporting.spec.ts
Normal file
18
packages/editor-ui/src/composables/useBugReporting.spec.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { useBugReporting } from './useBugReporting';
|
||||
|
||||
vi.mock('@/composables/useDebugInfo', () => ({
|
||||
useDebugInfo: () => ({
|
||||
generateDebugInfo: () => 'mocked debug info',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useBugReporting', () => {
|
||||
it('should generate the correct reporting URL', () => {
|
||||
const { getReportingURL } = useBugReporting();
|
||||
const url = getReportingURL();
|
||||
|
||||
expect(url).toContain('mocked+debug+info');
|
||||
expect(url).toMatchSnapshot();
|
||||
});
|
||||
});
|
41
packages/editor-ui/src/composables/useBugReporting.ts
Normal file
41
packages/editor-ui/src/composables/useBugReporting.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { useDebugInfo } from '@/composables/useDebugInfo';
|
||||
|
||||
const BASE_FORUM_URL = 'https://github.com/n8n-io/n8n/issues/new?labels=bug-report';
|
||||
|
||||
const REPORT_TEMPLATE = `
|
||||
<!-- Please follow the template below. Skip the questions that are not relevant to you. -->
|
||||
|
||||
## Describe the problem/error/question
|
||||
|
||||
|
||||
## What is the error message (if any)?
|
||||
|
||||
|
||||
## Please share your workflow/screenshots/recording
|
||||
|
||||
\`\`\`
|
||||
(Select the nodes on your canvas and use the keyboard shortcuts CMD+C/CTRL+C and CMD+V/CTRL+V to copy and paste the workflow.)
|
||||
\`\`\`
|
||||
|
||||
|
||||
## Share the output returned by the last node
|
||||
<!-- If you need help with data transformations, please also share your expected output. -->
|
||||
|
||||
`;
|
||||
|
||||
export function useBugReporting() {
|
||||
const debugInfo = useDebugInfo();
|
||||
|
||||
const getReportingURL = () => {
|
||||
const url = new URL(BASE_FORUM_URL);
|
||||
|
||||
const report = `${REPORT_TEMPLATE}\n${debugInfo.generateDebugInfo({ skipSensitive: true, secondaryHeader: true })}}`;
|
||||
url.searchParams.append('body', report);
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
return {
|
||||
getReportingURL,
|
||||
};
|
||||
}
|
117
packages/editor-ui/src/composables/useDebugInfo.spec.ts
Normal file
117
packages/editor-ui/src/composables/useDebugInfo.spec.ts
Normal file
|
@ -0,0 +1,117 @@
|
|||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { useDebugInfo } from './useDebugInfo';
|
||||
import type { RootState } from '@/Interface';
|
||||
import type { useSettingsStore as useSettingsStoreType } from '@/stores/settings.store';
|
||||
import type { RecursivePartial } from '@/type-utils';
|
||||
|
||||
vi.mock('@/stores/root.store', () => ({
|
||||
useRootStore: (): Partial<RootState> => ({
|
||||
versionCli: '0.123.0',
|
||||
}),
|
||||
}));
|
||||
|
||||
const MOCK_BASE_SETTINGS: RecursivePartial<ReturnType<typeof useSettingsStoreType>> = {
|
||||
isDocker: true,
|
||||
deploymentType: 'cloud',
|
||||
nodeJsVersion: '14.17.0',
|
||||
databaseType: 'postgresdb',
|
||||
isQueueModeEnabled: false,
|
||||
settings: {
|
||||
concurrency: 10,
|
||||
license: {
|
||||
consumerId: 'consumer-id',
|
||||
environment: 'production',
|
||||
},
|
||||
},
|
||||
isCommunityPlan: true,
|
||||
consumerId: 'consumer-123',
|
||||
saveDataSuccessExecution: 'all',
|
||||
saveDataErrorExecution: 'none',
|
||||
saveDataProgressExecution: true,
|
||||
saveManualExecutions: true,
|
||||
binaryDataMode: 'default',
|
||||
pruning: {
|
||||
isEnabled: true,
|
||||
maxAge: 24,
|
||||
maxCount: 100,
|
||||
},
|
||||
security: {
|
||||
blockFileAccessToN8nFiles: false,
|
||||
secureCookie: false,
|
||||
},
|
||||
};
|
||||
|
||||
const { useSettingsStore } = vi.hoisted(() => ({
|
||||
useSettingsStore: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/stores/settings.store', () => ({
|
||||
useSettingsStore,
|
||||
}));
|
||||
|
||||
vi.mock('n8n-design-system', () => ({
|
||||
useDeviceSupport: () => ({
|
||||
isTouchDevice: false,
|
||||
userAgent: 'Mozilla/5.0',
|
||||
}),
|
||||
}));
|
||||
|
||||
const NOW = 1717602004819;
|
||||
|
||||
vi.useFakeTimers({
|
||||
now: NOW,
|
||||
});
|
||||
|
||||
describe('useDebugInfo', () => {
|
||||
beforeEach(() => {
|
||||
useSettingsStore.mockReturnValue(MOCK_BASE_SETTINGS);
|
||||
});
|
||||
|
||||
it('should generate debug info', () => {
|
||||
const { generateDebugInfo } = useDebugInfo();
|
||||
const debugInfo = generateDebugInfo();
|
||||
|
||||
expect(debugInfo).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate debug info without sensitive data', () => {
|
||||
const { generateDebugInfo } = useDebugInfo();
|
||||
const debugInfo = generateDebugInfo({ skipSensitive: true });
|
||||
|
||||
expect(debugInfo).not.toContain('consumerId');
|
||||
expect(debugInfo).toContain('Generated at:');
|
||||
});
|
||||
|
||||
it('should include security info if insecure settings are found', () => {
|
||||
const { generateDebugInfo } = useDebugInfo();
|
||||
const debugInfo = generateDebugInfo();
|
||||
|
||||
expect(debugInfo).toContain('blockFileAccessToN8nFiles: false');
|
||||
expect(debugInfo).toContain('secureCookie: false');
|
||||
});
|
||||
|
||||
it('should not include security info if all settings are secure', () => {
|
||||
useSettingsStore.mockReturnValue({
|
||||
...MOCK_BASE_SETTINGS,
|
||||
security: {
|
||||
...MOCK_BASE_SETTINGS.security,
|
||||
blockFileAccessToN8nFiles: true,
|
||||
secureCookie: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { generateDebugInfo } = useDebugInfo();
|
||||
const debugInfo = generateDebugInfo();
|
||||
|
||||
expect(debugInfo).not.toContain('blockFileAccessToN8nFiles');
|
||||
expect(debugInfo).not.toContain('secureCookie');
|
||||
});
|
||||
|
||||
it('should generate markdown with secondary headers', () => {
|
||||
const { generateDebugInfo } = useDebugInfo();
|
||||
const debugInfo = generateDebugInfo({ secondaryHeader: true });
|
||||
|
||||
expect(debugInfo).toContain('### core');
|
||||
expect(debugInfo).toContain('## Debug info');
|
||||
});
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
import { useRootStore } from '@/stores/root.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useDeviceSupport } from 'n8n-design-system';
|
||||
import type { WorkflowSettings } from 'n8n-workflow';
|
||||
|
||||
type DebugInfo = {
|
||||
|
@ -10,7 +11,7 @@ type DebugInfo = {
|
|||
database: 'sqlite' | 'mysql' | 'mariadb' | 'postgres';
|
||||
executionMode: 'regular' | 'scaling';
|
||||
license: 'community' | 'enterprise (production)' | 'enterprise (sandbox)';
|
||||
consumerId: string;
|
||||
consumerId?: string;
|
||||
concurrency: number;
|
||||
};
|
||||
storage: {
|
||||
|
@ -36,14 +37,19 @@ type DebugInfo = {
|
|||
secureCookie?: boolean;
|
||||
blockFileAccessToN8nFiles?: boolean;
|
||||
};
|
||||
client: {
|
||||
userAgent: string;
|
||||
isTouchDevice: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export function useDebugInfo() {
|
||||
const settingsStore = useSettingsStore();
|
||||
const rootStore = useRootStore();
|
||||
const { isTouchDevice, userAgent } = useDeviceSupport();
|
||||
|
||||
const coreInfo = () => {
|
||||
return {
|
||||
const coreInfo = (skipSensitive?: boolean) => {
|
||||
const info = {
|
||||
n8nVersion: rootStore.versionCli,
|
||||
platform:
|
||||
settingsStore.isDocker && settingsStore.deploymentType === 'cloud'
|
||||
|
@ -60,13 +66,22 @@ export function useDebugInfo() {
|
|||
: settingsStore.databaseType,
|
||||
executionMode: settingsStore.isQueueModeEnabled ? 'scaling' : 'regular',
|
||||
concurrency: settingsStore.settings.concurrency,
|
||||
license: settingsStore.isCommunityPlan
|
||||
license:
|
||||
settingsStore.isCommunityPlan || !settingsStore.settings.license
|
||||
? 'community'
|
||||
: settingsStore.settings.license.environment === 'production'
|
||||
? 'enterprise (production)'
|
||||
: 'enterprise (sandbox)',
|
||||
consumerId: settingsStore.consumerId,
|
||||
} as const;
|
||||
|
||||
if (!skipSensitive) {
|
||||
return {
|
||||
...info,
|
||||
consumerId: !skipSensitive ? settingsStore.consumerId : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
const storageInfo = (): DebugInfo['storage'] => {
|
||||
|
@ -101,11 +116,19 @@ export function useDebugInfo() {
|
|||
return info;
|
||||
};
|
||||
|
||||
const gatherDebugInfo = () => {
|
||||
const client = (): DebugInfo['client'] => {
|
||||
return {
|
||||
userAgent,
|
||||
isTouchDevice,
|
||||
};
|
||||
};
|
||||
|
||||
const gatherDebugInfo = (skipSensitive?: boolean) => {
|
||||
const debugInfo: DebugInfo = {
|
||||
core: coreInfo(),
|
||||
core: coreInfo(skipSensitive),
|
||||
storage: storageInfo(),
|
||||
pruning: pruningInfo(),
|
||||
client: client(),
|
||||
};
|
||||
|
||||
const security = securityInfo();
|
||||
|
@ -115,11 +138,15 @@ export function useDebugInfo() {
|
|||
return debugInfo;
|
||||
};
|
||||
|
||||
const toMarkdown = (debugInfo: DebugInfo): string => {
|
||||
let markdown = '# Debug info\n\n';
|
||||
const toMarkdown = (
|
||||
debugInfo: DebugInfo,
|
||||
{ secondaryHeader }: { secondaryHeader?: boolean },
|
||||
): string => {
|
||||
const extraLevel = secondaryHeader ? '#' : '';
|
||||
let markdown = `${extraLevel}# Debug info\n\n`;
|
||||
|
||||
for (const sectionKey in debugInfo) {
|
||||
markdown += `## ${sectionKey}\n\n`;
|
||||
markdown += `${extraLevel}## ${sectionKey}\n\n`;
|
||||
|
||||
const section = debugInfo[sectionKey as keyof DebugInfo];
|
||||
|
||||
|
@ -140,8 +167,11 @@ export function useDebugInfo() {
|
|||
return `${markdown}Generated at: ${new Date().toISOString()}`;
|
||||
};
|
||||
|
||||
const generateDebugInfo = () => {
|
||||
return appendTimestamp(toMarkdown(gatherDebugInfo()));
|
||||
const generateDebugInfo = ({
|
||||
skipSensitive,
|
||||
secondaryHeader,
|
||||
}: { skipSensitive?: boolean; secondaryHeader?: boolean } = {}) => {
|
||||
return appendTimestamp(toMarkdown(gatherDebugInfo(skipSensitive), { secondaryHeader }));
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
@ -876,6 +876,7 @@
|
|||
"mainSidebar.helpMenuItems.documentation": "Documentation",
|
||||
"mainSidebar.helpMenuItems.forum": "Forum",
|
||||
"mainSidebar.helpMenuItems.quickstart": "Quickstart",
|
||||
"mainSidebar.helpMenuItems.reportBug": "Report a bug",
|
||||
"mainSidebar.new": "New",
|
||||
"mainSidebar.newTemplate": "New from template",
|
||||
"mainSidebar.open": "Open",
|
||||
|
@ -1274,6 +1275,7 @@
|
|||
"nodeView.redirecting": "Redirecting",
|
||||
"nodeView.refresh": "Refresh",
|
||||
"nodeView.resetZoom": "Reset Zoom",
|
||||
"nodeView.reportBug": "Report a bug",
|
||||
"nodeView.runButtonText.executeWorkflow": "Test workflow",
|
||||
"nodeView.runButtonText.executingWorkflow": "Executing workflow",
|
||||
"nodeView.runButtonText.waitingForTriggerEvent": "Waiting for trigger event",
|
||||
|
|
7
packages/editor-ui/src/type-utils.d.ts
vendored
Normal file
7
packages/editor-ui/src/type-utils.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
export type RecursivePartial<T> = {
|
||||
[P in keyof T]?: T[P] extends Array<infer U>
|
||||
? Array<RecursivePartial<U>>
|
||||
: T[P] extends object | undefined
|
||||
? RecursivePartial<T[P]>
|
||||
: T[P];
|
||||
};
|
|
@ -1572,6 +1572,7 @@ onBeforeUnmount(() => {
|
|||
:event-bus="canvasEventBus"
|
||||
:read-only="isCanvasReadOnly"
|
||||
:executing="isWorkflowRunning"
|
||||
:show-bug-reporting-button="!isDemoRoute || !!executionsStore.activeExecution"
|
||||
:key-bindings="keyBindingsEnabled"
|
||||
@update:nodes:position="onUpdateNodesPosition"
|
||||
@update:node:position="onUpdateNodePosition"
|
||||
|
|
Loading…
Reference in a new issue