mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-14 08:34: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 {
|
return {
|
||||||
|
userAgent: userAgent.value,
|
||||||
isTouchDevice: isTouchDevice.value,
|
isTouchDevice: isTouchDevice.value,
|
||||||
isMacOs: isMacOs.value,
|
isMacOs: isMacOs.value,
|
||||||
controlKeyCode: controlKeyCode.value,
|
controlKeyCode: controlKeyCode.value,
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
"format:check": "biome ci . && prettier --check . --ignore-path ../../.prettierignore",
|
"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",
|
"serve": "cross-env VUE_APP_URL_BASE_API=http://localhost:5678/ vite --host 0.0.0.0 --port 8080 dev",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:dev": "vitest"
|
"test:dev": "vitest --silent=false"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.16.0",
|
"@codemirror/autocomplete": "^6.16.0",
|
||||||
|
|
|
@ -4,10 +4,10 @@ import type { Placement } from 'element-plus';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
label: string;
|
label: string;
|
||||||
shortcut: KeyboardShortcut;
|
shortcut?: KeyboardShortcut;
|
||||||
placement?: Placement;
|
placement?: Placement;
|
||||||
}
|
}
|
||||||
withDefaults(defineProps<Props>(), { placement: 'top' });
|
withDefaults(defineProps<Props>(), { placement: 'top', shortcut: undefined });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -15,7 +15,7 @@ withDefaults(defineProps<Props>(), { placement: 'top' });
|
||||||
<template #content>
|
<template #content>
|
||||||
<div :class="$style.shortcut">
|
<div :class="$style.shortcut">
|
||||||
<div :class="$style.label">{{ label }}</div>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useUserHelpers } from '@/composables/useUserHelpers';
|
import { useUserHelpers } from '@/composables/useUserHelpers';
|
||||||
|
|
||||||
import { ABOUT_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS } from '@/constants';
|
import { ABOUT_MODAL_KEY, VERSIONS_MODAL_KEY, VIEWS } from '@/constants';
|
||||||
|
import { useBugReporting } from '@/composables/useBugReporting';
|
||||||
|
|
||||||
const becomeTemplateCreatorStore = useBecomeTemplateCreatorStore();
|
const becomeTemplateCreatorStore = useBecomeTemplateCreatorStore();
|
||||||
const cloudPlanStore = useCloudPlanStore();
|
const cloudPlanStore = useCloudPlanStore();
|
||||||
|
@ -37,6 +38,7 @@ const locale = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
const { getReportingURL } = useBugReporting();
|
||||||
|
|
||||||
useUserHelpers(router, route);
|
useUserHelpers(router, route);
|
||||||
|
|
||||||
|
@ -143,6 +145,15 @@ const mainMenuItems = ref([
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'report-bug',
|
||||||
|
icon: 'bug',
|
||||||
|
label: locale.baseText('mainSidebar.helpMenuItems.reportBug'),
|
||||||
|
link: {
|
||||||
|
href: getReportingURL(),
|
||||||
|
target: '_blank',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'about',
|
id: 'about',
|
||||||
icon: 'info',
|
icon: 'info',
|
||||||
|
|
|
@ -82,6 +82,7 @@ const props = withDefaults(
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
executing?: boolean;
|
executing?: boolean;
|
||||||
keyBindings?: boolean;
|
keyBindings?: boolean;
|
||||||
|
showBugReportingButton?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
id: 'canvas',
|
id: 'canvas',
|
||||||
|
@ -592,6 +593,7 @@ provide(CanvasKey, {
|
||||||
:class="$style.canvasControls"
|
:class="$style.canvasControls"
|
||||||
:position="controlsPosition"
|
:position="controlsPosition"
|
||||||
:show-interactive="false"
|
:show-interactive="false"
|
||||||
|
:show-bug-reporting-button="showBugReportingButton"
|
||||||
:zoom="zoom"
|
:zoom="zoom"
|
||||||
@zoom-to-fit="onFitView"
|
@zoom-to-fit="onFitView"
|
||||||
@zoom-in="onZoomIn"
|
@zoom-in="onZoomIn"
|
||||||
|
|
|
@ -22,6 +22,7 @@ const props = withDefaults(
|
||||||
eventBus?: EventBus<CanvasEventBusEvents>;
|
eventBus?: EventBus<CanvasEventBusEvents>;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
executing?: boolean;
|
executing?: boolean;
|
||||||
|
showBugReportingButton?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
id: 'canvas',
|
id: 'canvas',
|
||||||
|
@ -57,6 +58,7 @@ const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping(
|
||||||
v-if="workflow"
|
v-if="workflow"
|
||||||
:nodes="mappedNodes"
|
:nodes="mappedNodes"
|
||||||
:connections="mappedConnections"
|
:connections="mappedConnections"
|
||||||
|
:show-bug-reporting-button="showBugReportingButton"
|
||||||
:event-bus="eventBus"
|
:event-bus="eventBus"
|
||||||
:read-only="readOnly"
|
:read-only="readOnly"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
|
|
|
@ -1,15 +1,43 @@
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import CanvasControlButtons from './CanvasControlButtons.vue';
|
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);
|
const renderComponent = createComponentRenderer(CanvasControlButtons);
|
||||||
|
|
||||||
describe('CanvasControlButtons', () => {
|
describe('CanvasControlButtons', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
setActivePinia(createTestingPinia());
|
||||||
|
});
|
||||||
|
|
||||||
it('should render correctly', () => {
|
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();
|
const wrapper = renderComponent();
|
||||||
|
|
||||||
expect(wrapper.getByTestId('zoom-in-button')).toBeVisible();
|
expect(wrapper.getByTestId('zoom-in-button')).toBeVisible();
|
||||||
expect(wrapper.getByTestId('zoom-out-button')).toBeVisible();
|
expect(wrapper.getByTestId('zoom-out-button')).toBeVisible();
|
||||||
expect(wrapper.getByTestId('zoom-to-fit')).toBeVisible();
|
expect(wrapper.getByTestId('zoom-to-fit')).toBeVisible();
|
||||||
|
expect(wrapper.queryByTestId('report-bug')).not.toBeInTheDocument();
|
||||||
|
|
||||||
expect(wrapper.html()).toMatchSnapshot();
|
expect(wrapper.html()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,13 +2,17 @@
|
||||||
import { Controls } from '@vue-flow/controls';
|
import { Controls } from '@vue-flow/controls';
|
||||||
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { useBugReporting } from '@/composables/useBugReporting';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
zoom?: number;
|
zoom?: number;
|
||||||
|
showBugReportingButton?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
|
showBugReportingButton: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -19,6 +23,9 @@ const emit = defineEmits<{
|
||||||
'zoom-to-fit': [];
|
'zoom-to-fit': [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const { getReportingURL } = useBugReporting();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
const isResetZoomVisible = computed(() => props.zoom !== 1);
|
const isResetZoomVisible = computed(() => props.zoom !== 1);
|
||||||
|
|
||||||
function onResetZoom() {
|
function onResetZoom() {
|
||||||
|
@ -36,6 +43,10 @@ function onZoomOut() {
|
||||||
function onZoomToFit() {
|
function onZoomToFit() {
|
||||||
emit('zoom-to-fit');
|
emit('zoom-to-fit');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trackBugReport() {
|
||||||
|
telemetry.track('User clicked bug report button in canvas', {}, { withPostHog: true });
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Controls :show-zoom="false" :show-fit-view="false">
|
<Controls :show-zoom="false" :show-fit-view="false">
|
||||||
|
@ -88,6 +99,14 @@ function onZoomToFit() {
|
||||||
@click="onResetZoom"
|
@click="onResetZoom"
|
||||||
/>
|
/>
|
||||||
</KeyboardShortcutTooltip>
|
</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>
|
</Controls>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,35 @@ exports[`CanvasControlButtons > should render correctly 1`] = `
|
||||||
</button>
|
</button>
|
||||||
<!--teleport start-->
|
<!--teleport start-->
|
||||||
<!--teleport end-->
|
<!--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-->
|
<!--v-if-->
|
||||||
</div>"
|
</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 { useRootStore } from '@/stores/root.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import { useDeviceSupport } from 'n8n-design-system';
|
||||||
import type { WorkflowSettings } from 'n8n-workflow';
|
import type { WorkflowSettings } from 'n8n-workflow';
|
||||||
|
|
||||||
type DebugInfo = {
|
type DebugInfo = {
|
||||||
|
@ -10,7 +11,7 @@ type DebugInfo = {
|
||||||
database: 'sqlite' | 'mysql' | 'mariadb' | 'postgres';
|
database: 'sqlite' | 'mysql' | 'mariadb' | 'postgres';
|
||||||
executionMode: 'regular' | 'scaling';
|
executionMode: 'regular' | 'scaling';
|
||||||
license: 'community' | 'enterprise (production)' | 'enterprise (sandbox)';
|
license: 'community' | 'enterprise (production)' | 'enterprise (sandbox)';
|
||||||
consumerId: string;
|
consumerId?: string;
|
||||||
concurrency: number;
|
concurrency: number;
|
||||||
};
|
};
|
||||||
storage: {
|
storage: {
|
||||||
|
@ -36,14 +37,19 @@ type DebugInfo = {
|
||||||
secureCookie?: boolean;
|
secureCookie?: boolean;
|
||||||
blockFileAccessToN8nFiles?: boolean;
|
blockFileAccessToN8nFiles?: boolean;
|
||||||
};
|
};
|
||||||
|
client: {
|
||||||
|
userAgent: string;
|
||||||
|
isTouchDevice: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useDebugInfo() {
|
export function useDebugInfo() {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
|
const { isTouchDevice, userAgent } = useDeviceSupport();
|
||||||
|
|
||||||
const coreInfo = () => {
|
const coreInfo = (skipSensitive?: boolean) => {
|
||||||
return {
|
const info = {
|
||||||
n8nVersion: rootStore.versionCli,
|
n8nVersion: rootStore.versionCli,
|
||||||
platform:
|
platform:
|
||||||
settingsStore.isDocker && settingsStore.deploymentType === 'cloud'
|
settingsStore.isDocker && settingsStore.deploymentType === 'cloud'
|
||||||
|
@ -60,13 +66,22 @@ export function useDebugInfo() {
|
||||||
: settingsStore.databaseType,
|
: settingsStore.databaseType,
|
||||||
executionMode: settingsStore.isQueueModeEnabled ? 'scaling' : 'regular',
|
executionMode: settingsStore.isQueueModeEnabled ? 'scaling' : 'regular',
|
||||||
concurrency: settingsStore.settings.concurrency,
|
concurrency: settingsStore.settings.concurrency,
|
||||||
license: settingsStore.isCommunityPlan
|
license:
|
||||||
|
settingsStore.isCommunityPlan || !settingsStore.settings.license
|
||||||
? 'community'
|
? 'community'
|
||||||
: settingsStore.settings.license.environment === 'production'
|
: settingsStore.settings.license.environment === 'production'
|
||||||
? 'enterprise (production)'
|
? 'enterprise (production)'
|
||||||
: 'enterprise (sandbox)',
|
: 'enterprise (sandbox)',
|
||||||
consumerId: settingsStore.consumerId,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
if (!skipSensitive) {
|
||||||
|
return {
|
||||||
|
...info,
|
||||||
|
consumerId: !skipSensitive ? settingsStore.consumerId : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
};
|
};
|
||||||
|
|
||||||
const storageInfo = (): DebugInfo['storage'] => {
|
const storageInfo = (): DebugInfo['storage'] => {
|
||||||
|
@ -101,11 +116,19 @@ export function useDebugInfo() {
|
||||||
return info;
|
return info;
|
||||||
};
|
};
|
||||||
|
|
||||||
const gatherDebugInfo = () => {
|
const client = (): DebugInfo['client'] => {
|
||||||
|
return {
|
||||||
|
userAgent,
|
||||||
|
isTouchDevice,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const gatherDebugInfo = (skipSensitive?: boolean) => {
|
||||||
const debugInfo: DebugInfo = {
|
const debugInfo: DebugInfo = {
|
||||||
core: coreInfo(),
|
core: coreInfo(skipSensitive),
|
||||||
storage: storageInfo(),
|
storage: storageInfo(),
|
||||||
pruning: pruningInfo(),
|
pruning: pruningInfo(),
|
||||||
|
client: client(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const security = securityInfo();
|
const security = securityInfo();
|
||||||
|
@ -115,11 +138,15 @@ export function useDebugInfo() {
|
||||||
return debugInfo;
|
return debugInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
const toMarkdown = (debugInfo: DebugInfo): string => {
|
const toMarkdown = (
|
||||||
let markdown = '# Debug info\n\n';
|
debugInfo: DebugInfo,
|
||||||
|
{ secondaryHeader }: { secondaryHeader?: boolean },
|
||||||
|
): string => {
|
||||||
|
const extraLevel = secondaryHeader ? '#' : '';
|
||||||
|
let markdown = `${extraLevel}# Debug info\n\n`;
|
||||||
|
|
||||||
for (const sectionKey in debugInfo) {
|
for (const sectionKey in debugInfo) {
|
||||||
markdown += `## ${sectionKey}\n\n`;
|
markdown += `${extraLevel}## ${sectionKey}\n\n`;
|
||||||
|
|
||||||
const section = debugInfo[sectionKey as keyof DebugInfo];
|
const section = debugInfo[sectionKey as keyof DebugInfo];
|
||||||
|
|
||||||
|
@ -140,8 +167,11 @@ export function useDebugInfo() {
|
||||||
return `${markdown}Generated at: ${new Date().toISOString()}`;
|
return `${markdown}Generated at: ${new Date().toISOString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateDebugInfo = () => {
|
const generateDebugInfo = ({
|
||||||
return appendTimestamp(toMarkdown(gatherDebugInfo()));
|
skipSensitive,
|
||||||
|
secondaryHeader,
|
||||||
|
}: { skipSensitive?: boolean; secondaryHeader?: boolean } = {}) => {
|
||||||
|
return appendTimestamp(toMarkdown(gatherDebugInfo(skipSensitive), { secondaryHeader }));
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -876,6 +876,7 @@
|
||||||
"mainSidebar.helpMenuItems.documentation": "Documentation",
|
"mainSidebar.helpMenuItems.documentation": "Documentation",
|
||||||
"mainSidebar.helpMenuItems.forum": "Forum",
|
"mainSidebar.helpMenuItems.forum": "Forum",
|
||||||
"mainSidebar.helpMenuItems.quickstart": "Quickstart",
|
"mainSidebar.helpMenuItems.quickstart": "Quickstart",
|
||||||
|
"mainSidebar.helpMenuItems.reportBug": "Report a bug",
|
||||||
"mainSidebar.new": "New",
|
"mainSidebar.new": "New",
|
||||||
"mainSidebar.newTemplate": "New from template",
|
"mainSidebar.newTemplate": "New from template",
|
||||||
"mainSidebar.open": "Open",
|
"mainSidebar.open": "Open",
|
||||||
|
@ -1274,6 +1275,7 @@
|
||||||
"nodeView.redirecting": "Redirecting",
|
"nodeView.redirecting": "Redirecting",
|
||||||
"nodeView.refresh": "Refresh",
|
"nodeView.refresh": "Refresh",
|
||||||
"nodeView.resetZoom": "Reset Zoom",
|
"nodeView.resetZoom": "Reset Zoom",
|
||||||
|
"nodeView.reportBug": "Report a bug",
|
||||||
"nodeView.runButtonText.executeWorkflow": "Test workflow",
|
"nodeView.runButtonText.executeWorkflow": "Test workflow",
|
||||||
"nodeView.runButtonText.executingWorkflow": "Executing workflow",
|
"nodeView.runButtonText.executingWorkflow": "Executing workflow",
|
||||||
"nodeView.runButtonText.waitingForTriggerEvent": "Waiting for trigger event",
|
"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"
|
:event-bus="canvasEventBus"
|
||||||
:read-only="isCanvasReadOnly"
|
:read-only="isCanvasReadOnly"
|
||||||
:executing="isWorkflowRunning"
|
:executing="isWorkflowRunning"
|
||||||
|
:show-bug-reporting-button="!isDemoRoute || !!executionsStore.activeExecution"
|
||||||
:key-bindings="keyBindingsEnabled"
|
:key-bindings="keyBindingsEnabled"
|
||||||
@update:nodes:position="onUpdateNodesPosition"
|
@update:nodes:position="onUpdateNodesPosition"
|
||||||
@update:node:position="onUpdateNodePosition"
|
@update:node:position="onUpdateNodePosition"
|
||||||
|
|
Loading…
Reference in a new issue