mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(editor): Styling/UX improvements on the new logs view (#14789)
This commit is contained in:
@@ -207,6 +207,10 @@ export function useChatState(isReadOnly: boolean, onWindowResize?: () => void):
|
|||||||
nodeHelpers.updateNodesExecutionIssues();
|
nodeHelpers.updateNodesExecutionIssues();
|
||||||
messages.value = [];
|
messages.value = [];
|
||||||
currentSessionId.value = uuid().replace(/-/g, '');
|
currentSessionId.value = uuid().replace(/-/g, '');
|
||||||
|
|
||||||
|
if (logsPanelState.value !== LOGS_PANEL_STATE.CLOSED) {
|
||||||
|
chatEventBus.emit('focusInput');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayExecution(executionId: string) {
|
function displayExecution(executionId: string) {
|
||||||
|
|||||||
@@ -141,15 +141,17 @@ describe('LogsPanel', () => {
|
|||||||
await fireEvent.click(await rendered.findByTestId('logs-overview-header'));
|
await fireEvent.click(await rendered.findByTestId('logs-overview-header'));
|
||||||
await fireEvent.click(await rendered.findByText('AI Agent'));
|
await fireEvent.click(await rendered.findByText('AI Agent'));
|
||||||
|
|
||||||
const detailsPanel = rendered.getByTestId('log-details');
|
|
||||||
|
|
||||||
// Click the toggle button to close the panel
|
// Click the toggle button to close the panel
|
||||||
await fireEvent.click(within(detailsPanel).getByLabelText('Collapse panel'));
|
await fireEvent.click(
|
||||||
|
within(rendered.getByTestId('log-details')).getByLabelText('Collapse panel'),
|
||||||
|
);
|
||||||
expect(rendered.queryByTestId('chat-messages-empty')).not.toBeInTheDocument();
|
expect(rendered.queryByTestId('chat-messages-empty')).not.toBeInTheDocument();
|
||||||
expect(rendered.queryByTestId('logs-overview-body')).not.toBeInTheDocument();
|
expect(rendered.queryByTestId('logs-overview-body')).not.toBeInTheDocument();
|
||||||
|
|
||||||
// Click again to open the panel
|
// Click again to open the panel
|
||||||
await fireEvent.click(within(detailsPanel).getByLabelText('Open panel'));
|
await fireEvent.click(
|
||||||
|
within(rendered.getByTestId('logs-overview')).getByLabelText('Open panel'),
|
||||||
|
);
|
||||||
expect(await rendered.findByTestId('chat-messages-empty')).toBeInTheDocument();
|
expect(await rendered.findByTestId('chat-messages-empty')).toBeInTheDocument();
|
||||||
expect(await rendered.findByTestId('logs-overview-body')).toBeInTheDocument();
|
expect(await rendered.findByTestId('logs-overview-body')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -77,10 +77,10 @@ const selectedLogEntry = computed(() =>
|
|||||||
? undefined
|
? undefined
|
||||||
: manualLogEntrySelection.value.data,
|
: manualLogEntrySelection.value.data,
|
||||||
);
|
);
|
||||||
const isLogDetailsOpen = computed(
|
const isLogDetailsOpen = computed(() => isOpen.value && selectedLogEntry.value !== undefined);
|
||||||
() => selectedLogEntry.value !== undefined && !isCollapsingDetailsPanel.value,
|
const isLogDetailsVisuallyOpen = computed(
|
||||||
|
() => isLogDetailsOpen.value && !isCollapsingDetailsPanel.value,
|
||||||
);
|
);
|
||||||
const isLogDetailsOpenOrCollapsing = computed(() => selectedLogEntry.value !== undefined);
|
|
||||||
const logsPanelActionsProps = computed<InstanceType<typeof LogsPanelActions>['$props']>(() => ({
|
const logsPanelActionsProps = computed<InstanceType<typeof LogsPanelActions>['$props']>(() => ({
|
||||||
isOpen: isOpen.value,
|
isOpen: isOpen.value,
|
||||||
showToggleButton: !isPoppedOut.value,
|
showToggleButton: !isPoppedOut.value,
|
||||||
@@ -149,9 +149,9 @@ function handleResizeOverviewPanelEnd() {
|
|||||||
<N8nResizeWrapper
|
<N8nResizeWrapper
|
||||||
:class="$style.overviewResizer"
|
:class="$style.overviewResizer"
|
||||||
:width="overviewPanelWidth"
|
:width="overviewPanelWidth"
|
||||||
:style="{ width: isLogDetailsOpen ? `${overviewPanelWidth}px` : '' }"
|
:style="{ width: isLogDetailsVisuallyOpen ? `${overviewPanelWidth}px` : '' }"
|
||||||
:supported-directions="['right']"
|
:supported-directions="['right']"
|
||||||
:is-resizing-enabled="isLogDetailsOpenOrCollapsing"
|
:is-resizing-enabled="isLogDetailsOpen"
|
||||||
:window="pipWindow"
|
:window="pipWindow"
|
||||||
@resize="onOverviewPanelResize"
|
@resize="onOverviewPanelResize"
|
||||||
@resizeend="handleResizeOverviewPanelEnd"
|
@resizeend="handleResizeOverviewPanelEnd"
|
||||||
@@ -160,19 +160,22 @@ function handleResizeOverviewPanelEnd() {
|
|||||||
:class="$style.logsOverview"
|
:class="$style.logsOverview"
|
||||||
:is-open="isOpen"
|
:is-open="isOpen"
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
:is-compact="isLogDetailsOpen"
|
:is-compact="isLogDetailsVisuallyOpen"
|
||||||
:selected="selectedLogEntry"
|
:selected="selectedLogEntry"
|
||||||
:execution-tree="executionTree"
|
:execution-tree="executionTree"
|
||||||
@click-header="onToggleOpen(true)"
|
@click-header="onToggleOpen(true)"
|
||||||
@select="handleSelectLogEntry"
|
@select="handleSelectLogEntry"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<LogsPanelActions v-if="!isLogDetailsOpen" v-bind="logsPanelActionsProps" />
|
<LogsPanelActions
|
||||||
|
v-if="!isLogDetailsVisuallyOpen"
|
||||||
|
v-bind="logsPanelActionsProps"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</LogsOverviewPanel>
|
</LogsOverviewPanel>
|
||||||
</N8nResizeWrapper>
|
</N8nResizeWrapper>
|
||||||
<LogsDetailsPanel
|
<LogsDetailsPanel
|
||||||
v-if="isLogDetailsOpenOrCollapsing && selectedLogEntry"
|
v-if="isLogDetailsVisuallyOpen && selectedLogEntry"
|
||||||
:class="$style.logDetails"
|
:class="$style.logDetails"
|
||||||
:is-open="isOpen"
|
:is-open="isOpen"
|
||||||
:log-entry="selectedLogEntry"
|
:log-entry="selectedLogEntry"
|
||||||
@@ -180,7 +183,7 @@ function handleResizeOverviewPanelEnd() {
|
|||||||
@click-header="onToggleOpen(true)"
|
@click-header="onToggleOpen(true)"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<LogsPanelActions v-if="isLogDetailsOpen" v-bind="logsPanelActionsProps" />
|
<LogsPanelActions v-if="isLogDetailsVisuallyOpen" v-bind="logsPanelActionsProps" />
|
||||||
</template>
|
</template>
|
||||||
</LogsDetailsPanel>
|
</LogsDetailsPanel>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -92,9 +92,13 @@ describe('LogDetailsPanel', () => {
|
|||||||
createdAt: '2025-04-16T00:00:00.000Z',
|
createdAt: '2025-04-16T00:00:00.000Z',
|
||||||
startedAt: '2025-04-16T00:00:01.000Z',
|
startedAt: '2025-04-16T00:00:01.000Z',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
localStorage.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show name, run status, input, and output of the node', async () => {
|
it('should show name, run status, input, and output of the node', async () => {
|
||||||
|
localStorage.setItem('N8N_LOGS_DETAIL_PANEL_CONTENT', 'both');
|
||||||
|
|
||||||
const rendered = render({
|
const rendered = render({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
logEntry: createTestLogEntry({ node: 'AI Agent', runIndex: 0 }),
|
logEntry: createTestLogEntry({ node: 'AI Agent', runIndex: 0 }),
|
||||||
@@ -118,12 +122,12 @@ describe('LogDetailsPanel', () => {
|
|||||||
|
|
||||||
const header = within(rendered.getByTestId('log-details-header'));
|
const header = within(rendered.getByTestId('log-details-header'));
|
||||||
|
|
||||||
expect(rendered.queryByTestId('log-details-input')).toBeInTheDocument();
|
expect(rendered.queryByTestId('log-details-input')).not.toBeInTheDocument();
|
||||||
expect(rendered.queryByTestId('log-details-output')).toBeInTheDocument();
|
expect(rendered.queryByTestId('log-details-output')).toBeInTheDocument();
|
||||||
|
|
||||||
await fireEvent.click(header.getByText('Input'));
|
await fireEvent.click(header.getByText('Input'));
|
||||||
|
|
||||||
expect(rendered.queryByTestId('log-details-input')).not.toBeInTheDocument();
|
expect(rendered.queryByTestId('log-details-input')).toBeInTheDocument();
|
||||||
expect(rendered.queryByTestId('log-details-output')).toBeInTheDocument();
|
expect(rendered.queryByTestId('log-details-output')).toBeInTheDocument();
|
||||||
|
|
||||||
await fireEvent.click(header.getByText('Output'));
|
await fireEvent.click(header.getByText('Output'));
|
||||||
@@ -133,6 +137,8 @@ describe('LogDetailsPanel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should close input panel by dragging the divider to the left end', async () => {
|
it('should close input panel by dragging the divider to the left end', async () => {
|
||||||
|
localStorage.setItem('N8N_LOGS_DETAIL_PANEL_CONTENT', 'both');
|
||||||
|
|
||||||
const rendered = render({
|
const rendered = render({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
logEntry: createTestLogEntry({ node: 'AI Agent', runIndex: 0 }),
|
logEntry: createTestLogEntry({ node: 'AI Agent', runIndex: 0 }),
|
||||||
@@ -150,6 +156,8 @@ describe('LogDetailsPanel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should close output panel by dragging the divider to the right end', async () => {
|
it('should close output panel by dragging the divider to the right end', async () => {
|
||||||
|
localStorage.setItem('N8N_LOGS_DETAIL_PANEL_CONTENT', 'both');
|
||||||
|
|
||||||
const rendered = render({
|
const rendered = render({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
logEntry: createTestLogEntry({ node: 'AI Agent', runIndex: 0 }),
|
logEntry: createTestLogEntry({ node: 'AI Agent', runIndex: 0 }),
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ import { type INodeUi } from '@/Interface';
|
|||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { N8nButton, N8nResizeWrapper, N8nText } from '@n8n/design-system';
|
import { N8nButton, N8nResizeWrapper, N8nText } from '@n8n/design-system';
|
||||||
|
import { useLocalStorage } from '@vueuse/core';
|
||||||
import { type ITaskData } from 'n8n-workflow';
|
import { type ITaskData } from 'n8n-workflow';
|
||||||
import { computed, ref, useTemplateRef } from 'vue';
|
import { computed, useTemplateRef } from 'vue';
|
||||||
|
|
||||||
const MIN_IO_PANEL_WIDTH = 200;
|
const MIN_IO_PANEL_WIDTH = 200;
|
||||||
|
|
||||||
@@ -32,7 +33,11 @@ const telemetry = useTelemetry();
|
|||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const nodeTypeStore = useNodeTypesStore();
|
const nodeTypeStore = useNodeTypesStore();
|
||||||
|
|
||||||
const content = ref<LogDetailsContent>(LOG_DETAILS_CONTENT.BOTH);
|
const content = useLocalStorage<LogDetailsContent>(
|
||||||
|
'N8N_LOGS_DETAIL_PANEL_CONTENT',
|
||||||
|
LOG_DETAILS_CONTENT.OUTPUT,
|
||||||
|
{ writeDefaults: false },
|
||||||
|
);
|
||||||
|
|
||||||
const node = computed<INodeUi | undefined>(() => workflowsStore.nodesByName[logEntry.node]);
|
const node = computed<INodeUi | undefined>(() => workflowsStore.nodesByName[logEntry.node]);
|
||||||
const type = computed(() => (node.value ? nodeTypeStore.getNodeType(node.value.type) : undefined));
|
const type = computed(() => (node.value ? nodeTypeStore.getNodeType(node.value.type) : undefined));
|
||||||
@@ -100,7 +105,11 @@ function handleResizeEnd() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="container" :class="$style.container" data-test-id="log-details">
|
<div ref="container" :class="$style.container" data-test-id="log-details">
|
||||||
<PanelHeader data-test-id="log-details-header" @click="emit('clickHeader')">
|
<PanelHeader
|
||||||
|
data-test-id="log-details-header"
|
||||||
|
:class="$style.header"
|
||||||
|
@click="emit('clickHeader')"
|
||||||
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div :class="$style.title">
|
<div :class="$style.title">
|
||||||
<NodeIcon :node-type="type" :size="16" :class="$style.icon" />
|
<NodeIcon :node-type="type" :size="16" :class="$style.icon" />
|
||||||
@@ -183,6 +192,10 @@ function handleResizeEnd() {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ const isClearExecutionButtonVisible = useClearExecutionButtonVisible();
|
|||||||
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
|
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
|
||||||
const isEmpty = computed(() => workflowsStore.workflowExecutionData === null);
|
const isEmpty = computed(() => workflowsStore.workflowExecutionData === null);
|
||||||
const switchViewOptions = computed(() => [
|
const switchViewOptions = computed(() => [
|
||||||
{ label: locale.baseText('logs.overview.header.switch.details'), value: 'details' as const },
|
|
||||||
{ label: locale.baseText('logs.overview.header.switch.overview'), value: 'overview' as const },
|
{ label: locale.baseText('logs.overview.header.switch.overview'), value: 'overview' as const },
|
||||||
|
{ label: locale.baseText('logs.overview.header.switch.details'), value: 'details' as const },
|
||||||
]);
|
]);
|
||||||
const execution = computed(() => workflowsStore.workflowExecutionData);
|
const execution = computed(() => workflowsStore.workflowExecutionData);
|
||||||
const consumedTokens = computed(() =>
|
const consumedTokens = computed(() =>
|
||||||
@@ -166,7 +166,7 @@ async function handleTriggerPartialExecution(treeNode: TreeNode) {
|
|||||||
</template>
|
</template>
|
||||||
</ElTree>
|
</ElTree>
|
||||||
<N8nRadioButtons
|
<N8nRadioButtons
|
||||||
size="medium"
|
size="small"
|
||||||
:class="$style.switchViewButtons"
|
:class="$style.switchViewButtons"
|
||||||
:model-value="selected ? 'details' : 'overview'"
|
:model-value="selected ? 'details' : 'overview'"
|
||||||
:options="switchViewOptions"
|
:options="switchViewOptions"
|
||||||
@@ -193,6 +193,7 @@ async function handleTriggerPartialExecution(treeNode: TreeNode) {
|
|||||||
.clearButton {
|
.clearButton {
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
|
gap: var(--spacing-5xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -222,9 +223,7 @@ async function handleTriggerPartialExecution(treeNode: TreeNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
margin-bottom: var(--spacing-4xs);
|
padding: var(--spacing-2xs);
|
||||||
padding: var(--spacing-4xs) var(--spacing-2xs) 0 var(--spacing-2xs);
|
|
||||||
min-height: calc(30px + var(--spacing-s));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree {
|
.tree {
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ watch(
|
|||||||
:color="isError ? 'danger' : undefined"
|
:color="isError ? 'danger' : undefined"
|
||||||
>{{ node.name }}
|
>{{ node.name }}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
<N8nText tag="div" color="text-light" size="small" :class="$style.timeTook">
|
<N8nText v-if="!isCompact" tag="div" color="text-light" size="small" :class="$style.timeTook">
|
||||||
<I18nT v-if="isSettled && runData" keypath="logs.overview.body.summaryText">
|
<I18nT v-if="isSettled && runData" keypath="logs.overview.body.summaryText">
|
||||||
<template #status>
|
<template #status>
|
||||||
<N8nText v-if="isError" color="danger" :bold="true" size="small">
|
<N8nText v-if="isError" color="danger" :bold="true" size="small">
|
||||||
@@ -140,7 +140,7 @@ watch(
|
|||||||
startedAtText
|
startedAtText
|
||||||
}}</N8nText>
|
}}</N8nText>
|
||||||
<N8nText
|
<N8nText
|
||||||
v-if="subtreeConsumedTokens !== undefined"
|
v-if="!isCompact && subtreeConsumedTokens !== undefined"
|
||||||
tag="div"
|
tag="div"
|
||||||
color="text-light"
|
color="text-light"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -207,31 +207,31 @@ watch(
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
--row-gap-thickness: 1px;
|
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: var(--spacing-2xs);
|
padding: var(--spacing-2xs);
|
||||||
margin-bottom: var(--row-gap-thickness);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.background {
|
.background {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: calc(var(--row-gap-thickness) + var(--indent-depth) * 32px);
|
left: calc(var(--indent-depth) * 32px);
|
||||||
top: 0;
|
top: 0;
|
||||||
width: calc(100% - var(--indent-depth) * 32px - var(--row-gap-thickness));
|
width: calc(100% - var(--indent-depth) * 32px);
|
||||||
height: calc(100% - var(--row-gap-thickness));
|
height: 100%;
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
|
|
||||||
.selected &,
|
.selected & {
|
||||||
.container:hover & {
|
|
||||||
background-color: var(--color-foreground-base);
|
background-color: var(--color-foreground-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container:hover:not(.selected) & {
|
||||||
|
background-color: var(--color-background-light-base);
|
||||||
|
}
|
||||||
|
|
||||||
.selected:not(:hover).error & {
|
.selected:not(:hover).error & {
|
||||||
background-color: var(--color-danger-tint-2);
|
background-color: var(--color-danger-tint-2);
|
||||||
}
|
}
|
||||||
@@ -288,24 +288,12 @@ watch(
|
|||||||
margin-right: var(--spacing-4xs);
|
margin-right: var(--spacing-4xs);
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact & {
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact:hover & {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact:not(:hover) & {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.startedAt {
|
.startedAt {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 30%;
|
width: 25%;
|
||||||
|
|
||||||
.compact & {
|
.compact & {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -315,21 +303,8 @@ watch(
|
|||||||
.consumedTokens {
|
.consumedTokens {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 10%;
|
width: 15%;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
.compact & {
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact:hover & {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact &:empty,
|
|
||||||
.compact:not(:hover) & {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.compactErrorIcon {
|
.compactErrorIcon {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const emit = defineEmits<{ click: [] }>();
|
|||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding-inline-start: var(--spacing-s);
|
padding-inline-start: var(--spacing-s);
|
||||||
padding-inline-end: var(--spacing-xs);
|
padding-inline-end: var(--spacing-2xs);
|
||||||
padding-block: var(--spacing-2xs);
|
padding-block: var(--spacing-2xs);
|
||||||
background-color: var(--color-foreground-xlight);
|
background-color: var(--color-foreground-xlight);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ function handleClickOpenNdv() {
|
|||||||
:compact="true"
|
:compact="true"
|
||||||
:disable-pin="true"
|
:disable-pin="true"
|
||||||
:disable-edit="true"
|
:disable-edit="true"
|
||||||
|
:disable-hover-highlight="true"
|
||||||
table-header-bg-color="light"
|
table-header-bg-color="light"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
|
|||||||
@@ -35,14 +35,14 @@ export function useLayout(
|
|||||||
|
|
||||||
const chatPanelResizer = useResizablePanel(LOCAL_STORAGE_PANEL_WIDTH, {
|
const chatPanelResizer = useResizablePanel(LOCAL_STORAGE_PANEL_WIDTH, {
|
||||||
container,
|
container,
|
||||||
defaultSize: (size) => size * 0.3,
|
defaultSize: (size) => Math.min(800, size * 0.3),
|
||||||
minSize: 300,
|
minSize: 240,
|
||||||
maxSize: (size) => size * 0.8,
|
maxSize: (size) => size * 0.8,
|
||||||
});
|
});
|
||||||
|
|
||||||
const overviewPanelResizer = useResizablePanel(LOCAL_STORAGE_OVERVIEW_PANEL_WIDTH, {
|
const overviewPanelResizer = useResizablePanel(LOCAL_STORAGE_OVERVIEW_PANEL_WIDTH, {
|
||||||
container: logsContainer,
|
container: logsContainer,
|
||||||
defaultSize: (size) => size * 0.3,
|
defaultSize: (size) => Math.min(240, size * 0.2),
|
||||||
minSize: 80,
|
minSize: 80,
|
||||||
maxSize: 500,
|
maxSize: 500,
|
||||||
allowFullSize: true,
|
allowFullSize: true,
|
||||||
@@ -103,15 +103,19 @@ export function useLayout(
|
|||||||
resizer.onResizeEnd();
|
resizer.onResizeEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([panelState, resizer.size], ([state, height]) => {
|
watch(
|
||||||
canvasStore.setPanelHeight(
|
[panelState, resizer.size],
|
||||||
state === LOGS_PANEL_STATE.FLOATING
|
([state, height]) => {
|
||||||
? 0
|
canvasStore.setPanelHeight(
|
||||||
: state === LOGS_PANEL_STATE.ATTACHED
|
state === LOGS_PANEL_STATE.FLOATING
|
||||||
? height
|
? 0
|
||||||
: 32 /* collapsed panel height */,
|
: state === LOGS_PANEL_STATE.ATTACHED
|
||||||
);
|
? height
|
||||||
});
|
: 32 /* collapsed panel height */,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
height: resizer.size,
|
height: resizer.size,
|
||||||
|
|||||||
@@ -56,13 +56,21 @@ describe(useResizablePanel, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should restore value from local storage if valid number is stored', () => {
|
it('should restore value from local storage if valid number is stored', () => {
|
||||||
window.localStorage.setItem(localStorageKey, '333');
|
window.localStorage.setItem(localStorageKey, '0.333');
|
||||||
|
|
||||||
const { size } = useResizablePanel(localStorageKey, { container, defaultSize: 444 });
|
const { size } = useResizablePanel(localStorageKey, { container, defaultSize: 444 });
|
||||||
|
|
||||||
expect(size.value).toBe(333);
|
expect(size.value).toBe(333);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return defaultSize if invalid value is stored in local storage', () => {
|
||||||
|
window.localStorage.setItem(localStorageKey, '333');
|
||||||
|
|
||||||
|
const { size } = useResizablePanel(localStorageKey, { container, defaultSize: 444 });
|
||||||
|
|
||||||
|
expect(size.value).toBe(444);
|
||||||
|
});
|
||||||
|
|
||||||
it('should update size when onResize is called', () => {
|
it('should update size when onResize is called', () => {
|
||||||
const { size, onResize } = useResizablePanel(localStorageKey, { container, defaultSize: 444 });
|
const { size, onResize } = useResizablePanel(localStorageKey, { container, defaultSize: 444 });
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,17 @@ interface UseResizerV2Options {
|
|||||||
* Container element, to which relative size is calculated (doesn't necessarily have to be DOM parent node)
|
* Container element, to which relative size is calculated (doesn't necessarily have to be DOM parent node)
|
||||||
*/
|
*/
|
||||||
container: MaybeRef<HTMLElement | null>;
|
container: MaybeRef<HTMLElement | null>;
|
||||||
|
/**
|
||||||
|
* Default size in pixels
|
||||||
|
*/
|
||||||
defaultSize: GetSize;
|
defaultSize: GetSize;
|
||||||
|
/**
|
||||||
|
* Minimum size in pixels
|
||||||
|
*/
|
||||||
minSize?: GetSize;
|
minSize?: GetSize;
|
||||||
|
/**
|
||||||
|
* Maximum size in pixels
|
||||||
|
*/
|
||||||
maxSize?: GetSize;
|
maxSize?: GetSize;
|
||||||
/**
|
/**
|
||||||
* Which end of the container the resizable element itself is located
|
* Which end of the container the resizable element itself is located
|
||||||
@@ -46,31 +55,36 @@ export function useResizablePanel(
|
|||||||
}: UseResizerV2Options,
|
}: UseResizerV2Options,
|
||||||
) {
|
) {
|
||||||
const containerSize = ref(0);
|
const containerSize = ref(0);
|
||||||
const size = useLocalStorage(localStorageKey, -1, { writeDefaults: false });
|
const persistedSize = useLocalStorage(localStorageKey, -1, { writeDefaults: false });
|
||||||
const isResizing = ref(false);
|
const isResizing = ref(false);
|
||||||
|
const sizeOnResizeStart = ref<number>();
|
||||||
|
const minSizeValue = computed(() => resolveSize(minSize, containerSize.value));
|
||||||
|
const maxSizeValue = computed(() => resolveSize(maxSize, containerSize.value));
|
||||||
const constrainedSize = computed(() => {
|
const constrainedSize = computed(() => {
|
||||||
if (isResizing.value && allowCollapse && size.value < 30) {
|
const sizeInPixels =
|
||||||
|
persistedSize.value >= 0 && persistedSize.value <= 1
|
||||||
|
? containerSize.value * persistedSize.value
|
||||||
|
: -1;
|
||||||
|
|
||||||
|
if (isResizing.value && allowCollapse && sizeInPixels < 30) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isResizing.value && allowFullSize && size.value > containerSize.value - 30) {
|
if (isResizing.value && allowFullSize && sizeInPixels > containerSize.value - 30) {
|
||||||
return containerSize.value;
|
return containerSize.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSizeValue = resolveSize(defaultSize, containerSize.value);
|
const defaultSizeValue = resolveSize(defaultSize, containerSize.value);
|
||||||
|
|
||||||
if (Number.isNaN(size.value) || size.value < 0) {
|
if (Number.isNaN(sizeInPixels) || !Number.isFinite(sizeInPixels) || sizeInPixels < 0) {
|
||||||
return defaultSizeValue;
|
return defaultSizeValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const minSizeValue = resolveSize(minSize, containerSize.value);
|
|
||||||
const maxSizeValue = resolveSize(maxSize, containerSize.value);
|
|
||||||
|
|
||||||
return Math.max(
|
return Math.max(
|
||||||
minSizeValue,
|
minSizeValue.value,
|
||||||
Math.min(
|
Math.min(
|
||||||
snap && Math.abs(defaultSizeValue - size.value) < 30 ? defaultSizeValue : size.value,
|
snap && Math.abs(defaultSizeValue - sizeInPixels) < 30 ? defaultSizeValue : sizeInPixels,
|
||||||
maxSizeValue,
|
maxSizeValue.value,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -93,17 +107,31 @@ export function useResizablePanel(
|
|||||||
|
|
||||||
function onResize(data: ResizeData) {
|
function onResize(data: ResizeData) {
|
||||||
const containerRect = unref(container)?.getBoundingClientRect();
|
const containerRect = unref(container)?.getBoundingClientRect();
|
||||||
|
const newSizeInPixels = Math.max(
|
||||||
isResizing.value = true;
|
|
||||||
size.value = Math.max(
|
|
||||||
0,
|
0,
|
||||||
position === 'bottom'
|
position === 'bottom'
|
||||||
? (containerRect ? getSize(containerRect) : 0) - getValue(data)
|
? (containerRect ? getSize(containerRect) : 0) - getValue(data)
|
||||||
: getValue(data) - (containerRect ? getValue(containerRect) : 0),
|
: getValue(data) - (containerRect ? getValue(containerRect) : 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
isResizing.value = true;
|
||||||
|
persistedSize.value = newSizeInPixels / containerSize.value;
|
||||||
|
|
||||||
|
if (sizeOnResizeStart.value === undefined) {
|
||||||
|
sizeOnResizeStart.value = persistedSize.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onResizeEnd() {
|
function onResizeEnd() {
|
||||||
|
// If resizing ends with either collapsing or maximizing the panel, restore size at the start of dragging
|
||||||
|
if (
|
||||||
|
(minSizeValue.value > 0 && constrainedSize.value <= 0) ||
|
||||||
|
(maxSizeValue.value < containerSize.value && constrainedSize.value >= containerSize.value)
|
||||||
|
) {
|
||||||
|
persistedSize.value = sizeOnResizeStart.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeOnResizeStart.value = undefined;
|
||||||
isResizing.value = false;
|
isResizing.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,15 +155,6 @@ export function useResizablePanel(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(containerSize, (newValue, oldValue) => {
|
|
||||||
if (size.value > 0 && oldValue > 0) {
|
|
||||||
// Update size to maintain proportion
|
|
||||||
const ratio = size.value / oldValue;
|
|
||||||
|
|
||||||
size.value = Math.round(newValue * ratio);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isResizing: computed(() => isResizing.value),
|
isResizing: computed(() => isResizing.value),
|
||||||
isCollapsed: computed(() => isResizing.value && constrainedSize.value <= 0),
|
isCollapsed: computed(() => isResizing.value && constrainedSize.value <= 0),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { type ContextMenuAction, useContextMenu } from '@/composables/useContextMenu';
|
import { type ContextMenuAction, useContextMenu } from '@/composables/useContextMenu';
|
||||||
|
import { useStyles } from '@/composables/useStyles';
|
||||||
import { N8nActionDropdown } from '@n8n/design-system';
|
import { N8nActionDropdown } from '@n8n/design-system';
|
||||||
import { watch, ref } from 'vue';
|
import { watch, ref } from 'vue';
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ const contextMenu = useContextMenu();
|
|||||||
const { position, isOpen, actions, target } = contextMenu;
|
const { position, isOpen, actions, target } = contextMenu;
|
||||||
const dropdown = ref<InstanceType<typeof N8nActionDropdown>>();
|
const dropdown = ref<InstanceType<typeof N8nActionDropdown>>();
|
||||||
const emit = defineEmits<{ action: [action: ContextMenuAction, nodeIds: string[]] }>();
|
const emit = defineEmits<{ action: [action: ContextMenuAction, nodeIds: string[]] }>();
|
||||||
|
const { APP_Z_INDEXES } = useStyles();
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
isOpen,
|
isOpen,
|
||||||
@@ -40,6 +42,7 @@ function onVisibleChange(open: boolean) {
|
|||||||
:style="{
|
:style="{
|
||||||
left: `${position[0]}px`,
|
left: `${position[0]}px`,
|
||||||
top: `${position[1]}px`,
|
top: `${position[1]}px`,
|
||||||
|
zIndex: APP_Z_INDEXES.CONTEXT_MENU,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<N8nActionDropdown
|
<N8nActionDropdown
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ import { usePostHog } from '@/stores/posthog.store';
|
|||||||
import ViewSubExecution from './ViewSubExecution.vue';
|
import ViewSubExecution from './ViewSubExecution.vue';
|
||||||
import RunDataItemCount from '@/components/RunDataItemCount.vue';
|
import RunDataItemCount from '@/components/RunDataItemCount.vue';
|
||||||
import RunDataDisplayModeSelect from '@/components/RunDataDisplayModeSelect.vue';
|
import RunDataDisplayModeSelect from '@/components/RunDataDisplayModeSelect.vue';
|
||||||
|
import RunDataPaginationBar from '@/components/RunDataPaginationBar.vue';
|
||||||
|
|
||||||
const LazyRunDataTable = defineAsyncComponent(
|
const LazyRunDataTable = defineAsyncComponent(
|
||||||
async () => await import('@/components/RunDataTable.vue'),
|
async () => await import('@/components/RunDataTable.vue'),
|
||||||
@@ -141,6 +142,7 @@ type Props = {
|
|||||||
disablePin?: boolean;
|
disablePin?: boolean;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
tableHeaderBgColor?: 'base' | 'light';
|
tableHeaderBgColor?: 'base' | 'light';
|
||||||
|
disableHoverHighlight?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -158,6 +160,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
disableRunIndexSelection: false,
|
disableRunIndexSelection: false,
|
||||||
disableEdit: false,
|
disableEdit: false,
|
||||||
disablePin: false,
|
disablePin: false,
|
||||||
|
disableHoverHighlight: false,
|
||||||
compact: false,
|
compact: false,
|
||||||
tableHeaderBgColor: 'base',
|
tableHeaderBgColor: 'base',
|
||||||
});
|
});
|
||||||
@@ -203,7 +206,6 @@ const binaryDataDisplayVisible = ref(false);
|
|||||||
const binaryDataDisplayData = ref<IBinaryData | null>(null);
|
const binaryDataDisplayData = ref<IBinaryData | null>(null);
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const pageSize = ref(10);
|
const pageSize = ref(10);
|
||||||
const pageSizes = [1, 10, 25, 50, 100];
|
|
||||||
|
|
||||||
const pinDataDiscoveryTooltipVisible = ref(false);
|
const pinDataDiscoveryTooltipVisible = ref(false);
|
||||||
const isControlledPinDataTooltip = ref(false);
|
const isControlledPinDataTooltip = ref(false);
|
||||||
@@ -605,7 +607,6 @@ const itemsCountProps = computed<InstanceType<typeof RunDataItemCount>['$props']
|
|||||||
dataCount: dataCount.value,
|
dataCount: dataCount.value,
|
||||||
unfilteredDataCount: unfilteredDataCount.value,
|
unfilteredDataCount: unfilteredDataCount.value,
|
||||||
subExecutionsCount: activeTaskMetadata.value?.subExecutionsCount,
|
subExecutionsCount: activeTaskMetadata.value?.subExecutionsCount,
|
||||||
muted: props.compact || (props.paneType === 'input' && maxRunIndex.value === 0),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
watch(node, (newNode, prevNode) => {
|
watch(node, (newNode, prevNode) => {
|
||||||
@@ -1369,7 +1370,9 @@ defineExpose({ enterEditMode });
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<slot name="header"></slot>
|
<div :class="$style.title">
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-show="!hasRunError && !isTrimmedManualExecutionDataItem"
|
v-show="!hasRunError && !isTrimmedManualExecutionDataItem"
|
||||||
@@ -1776,6 +1779,8 @@ defineExpose({ enterEditMode });
|
|||||||
:has-default-hover-state="paneType === 'input' && !search"
|
:has-default-hover-state="paneType === 'input' && !search"
|
||||||
:search="search"
|
:search="search"
|
||||||
:header-bg-color="tableHeaderBgColor"
|
:header-bg-color="tableHeaderBgColor"
|
||||||
|
:compact="props.compact"
|
||||||
|
:disable-hover-highlight="props.disableHoverHighlight"
|
||||||
@mounted="emit('tableMounted', $event)"
|
@mounted="emit('tableMounted', $event)"
|
||||||
@active-row-changed="onItemHover"
|
@active-row-changed="onItemHover"
|
||||||
@display-mode-change="onDisplayModeChange"
|
@display-mode-change="onDisplayModeChange"
|
||||||
@@ -1795,6 +1800,7 @@ defineExpose({ enterEditMode });
|
|||||||
:output-index="currentOutputIndex"
|
:output-index="currentOutputIndex"
|
||||||
:total-runs="maxRunIndex"
|
:total-runs="maxRunIndex"
|
||||||
:search="search"
|
:search="search"
|
||||||
|
:compact="props.compact"
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
@@ -1815,6 +1821,7 @@ defineExpose({ enterEditMode });
|
|||||||
:total-runs="maxRunIndex"
|
:total-runs="maxRunIndex"
|
||||||
:search="search"
|
:search="search"
|
||||||
:class="$style.schema"
|
:class="$style.schema"
|
||||||
|
:compact="props.compact"
|
||||||
@clear:search="onSearchClear"
|
@clear:search="onSearchClear"
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
@@ -1908,7 +1915,7 @@ defineExpose({ enterEditMode });
|
|||||||
<slot name="node-not-run"></slot>
|
<slot name="node-not-run"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<RunDataPaginationBar
|
||||||
v-if="
|
v-if="
|
||||||
hidePagination === false &&
|
hidePagination === false &&
|
||||||
hasNodeRun &&
|
hasNodeRun &&
|
||||||
@@ -1919,41 +1926,17 @@ defineExpose({ enterEditMode });
|
|||||||
!isArtificialRecoveredEventItem
|
!isArtificialRecoveredEventItem
|
||||||
"
|
"
|
||||||
v-show="!editMode.enabled"
|
v-show="!editMode.enabled"
|
||||||
:class="$style.pagination"
|
:current-page="currentPage"
|
||||||
data-test-id="ndv-data-pagination"
|
:page-size="pageSize"
|
||||||
>
|
:total="dataCount"
|
||||||
<el-pagination
|
@update:current-page="onCurrentPageChange"
|
||||||
background
|
@update:page-size="onPageSizeChange"
|
||||||
:hide-on-single-page="true"
|
/>
|
||||||
:current-page="currentPage"
|
|
||||||
:pager-count="5"
|
|
||||||
:page-size="pageSize"
|
|
||||||
layout="prev, pager, next"
|
|
||||||
:total="dataCount"
|
|
||||||
@update:current-page="onCurrentPageChange"
|
|
||||||
>
|
|
||||||
</el-pagination>
|
|
||||||
|
|
||||||
<div :class="$style.pageSizeSelector">
|
|
||||||
<N8nSelect
|
|
||||||
size="mini"
|
|
||||||
:model-value="pageSize"
|
|
||||||
teleported
|
|
||||||
@update:model-value="onPageSizeChange"
|
|
||||||
>
|
|
||||||
<template #prepend>{{ i18n.baseText('ndv.output.pageSize') }}</template>
|
|
||||||
<N8nOption v-for="size in pageSizes" :key="size" :label="size" :value="size"> </N8nOption>
|
|
||||||
<N8nOption :label="i18n.baseText('ndv.output.all')" :value="dataCount"> </N8nOption>
|
|
||||||
</N8nSelect>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<N8nBlockUi :show="blockUI" :class="$style.uiBlocker" />
|
<N8nBlockUi :show="blockUI" :class="$style.uiBlocker" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
@import '@/styles/variables';
|
|
||||||
|
|
||||||
.infoIcon {
|
.infoIcon {
|
||||||
color: var(--color-foreground-dark);
|
color: var(--color-foreground-dark);
|
||||||
}
|
}
|
||||||
@@ -1999,10 +1982,15 @@ defineExpose({ enterEditMode });
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
min-height: calc(30px + var(--spacing-s));
|
min-height: calc(30px + var(--spacing-s));
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
|
container-type: inline-size;
|
||||||
|
|
||||||
.compact & {
|
.compact & {
|
||||||
margin-bottom: var(--spacing-4xs);
|
margin-bottom: var(--spacing-4xs);
|
||||||
padding: var(--spacing-4xs) var(--spacing-s) 0 var(--spacing-s);
|
padding: var(--spacing-2xs);
|
||||||
|
margin-bottom: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
min-height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
> *:first-child {
|
> *:first-child {
|
||||||
@@ -2014,12 +2002,6 @@ defineExpose({ enterEditMode });
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.actions-group {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dataDisplay {
|
.dataDisplay {
|
||||||
@@ -2032,6 +2014,10 @@ defineExpose({ enterEditMode });
|
|||||||
line-height: var(--font-line-height-xloose);
|
line-height: var(--font-line-height-xloose);
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
.compact & {
|
||||||
|
padding: 0 var(--spacing-2xs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inlineError {
|
.inlineError {
|
||||||
@@ -2102,22 +2088,6 @@ defineExpose({ enterEditMode });
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
bottom: 0;
|
|
||||||
padding: 5px;
|
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pageSizeSelector {
|
|
||||||
text-transform: capitalize;
|
|
||||||
max-width: 150px;
|
|
||||||
flex: 0 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.binaryIndex {
|
.binaryIndex {
|
||||||
display: block;
|
display: block;
|
||||||
padding: var(--spacing-2xs);
|
padding: var(--spacing-2xs);
|
||||||
@@ -2185,6 +2155,11 @@ defineExpose({ enterEditMode });
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
gap: var(--spacing-2xs);
|
gap: var(--spacing-2xs);
|
||||||
|
|
||||||
|
.compact & {
|
||||||
|
/* let title text alone decide the height */
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltipContain {
|
.tooltipContain {
|
||||||
@@ -2255,6 +2230,10 @@ defineExpose({ enterEditMode });
|
|||||||
margin-bottom: var(--spacing-xs);
|
margin-bottom: var(--spacing-xs);
|
||||||
margin-left: var(--spacing-s);
|
margin-left: var(--spacing-s);
|
||||||
margin-right: var(--spacing-s);
|
margin-right: var(--spacing-s);
|
||||||
|
|
||||||
|
.compact & {
|
||||||
|
margin: 0 var(--spacing-2xs) var(--spacing-2xs) var(--spacing-2xs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.schema {
|
.schema {
|
||||||
@@ -2263,15 +2242,21 @@ defineExpose({ enterEditMode });
|
|||||||
|
|
||||||
.search,
|
.search,
|
||||||
.displayModeSelect {
|
.displayModeSelect {
|
||||||
.compact & {
|
.compact:not(:hover) & {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
display: none;
|
||||||
transition: opacity 0.3s $ease-out-expo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.compact:hover & {
|
.compact:hover & {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 240px) {
|
||||||
|
/* Hide title when the panel is too narrow */
|
||||||
|
.compact:hover .title {
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const options = computed(() => {
|
|||||||
:model-value="value"
|
:model-value="value"
|
||||||
:options="options"
|
:options="options"
|
||||||
data-test-id="ndv-run-data-display-mode"
|
data-test-id="ndv-run-data-display-mode"
|
||||||
|
:size="compact ? 'small' : 'medium'"
|
||||||
@update:model-value="(selected) => emit('change', selected)"
|
@update:model-value="(selected) => emit('change', selected)"
|
||||||
>
|
>
|
||||||
<template v-if="compact" #option="option">
|
<template v-if="compact" #option="option">
|
||||||
@@ -64,6 +65,6 @@ const options = computed(() => {
|
|||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.icon {
|
.icon {
|
||||||
padding-inline: var(--spacing-2xs);
|
padding-inline: var(--spacing-4xs);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,20 +7,18 @@ const {
|
|||||||
unfilteredDataCount,
|
unfilteredDataCount,
|
||||||
subExecutionsCount = 0,
|
subExecutionsCount = 0,
|
||||||
search,
|
search,
|
||||||
muted,
|
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
dataCount: number;
|
dataCount: number;
|
||||||
unfilteredDataCount: number;
|
unfilteredDataCount: number;
|
||||||
subExecutionsCount?: number;
|
subExecutionsCount?: number;
|
||||||
search: string;
|
search: string;
|
||||||
muted: boolean;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<N8nText v-if="search" :class="[$style.itemsText, muted ? $style.muted : '']">
|
<N8nText v-if="search" :class="$style.itemsText">
|
||||||
{{
|
{{
|
||||||
i18n.baseText('ndv.search.items', {
|
i18n.baseText('ndv.search.items', {
|
||||||
adjustToNumber: unfilteredDataCount,
|
adjustToNumber: unfilteredDataCount,
|
||||||
@@ -28,7 +26,7 @@ const i18n = useI18n();
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
<N8nText v-else :class="[$style.itemsText, muted ? $style.muted : '']">
|
<N8nText v-else :class="$style.itemsText">
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
i18n.baseText('ndv.output.items', {
|
i18n.baseText('ndv.output.items', {
|
||||||
@@ -54,9 +52,6 @@ const i18n = useI18n();
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
|
||||||
|
|
||||||
.muted {
|
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const props = withDefaults(
|
|||||||
runIndex: number | undefined;
|
runIndex: number | undefined;
|
||||||
totalRuns: number | undefined;
|
totalRuns: number | undefined;
|
||||||
search: string | undefined;
|
search: string | undefined;
|
||||||
|
compact?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
editMode: () => ({}),
|
editMode: () => ({}),
|
||||||
@@ -123,7 +124,13 @@ const getListItemName = (path: string) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="jsonDataContainer" :class="[$style.jsonDisplay, { [$style.highlight]: highlight }]">
|
<div
|
||||||
|
ref="jsonDataContainer"
|
||||||
|
:class="[
|
||||||
|
$style.jsonDisplay,
|
||||||
|
{ [$style.highlight]: highlight, [$style.compact]: props.compact },
|
||||||
|
]"
|
||||||
|
>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<LazyRunDataJsonActions
|
<LazyRunDataJsonActions
|
||||||
v-if="!editMode.enabled"
|
v-if="!editMode.enabled"
|
||||||
@@ -238,6 +245,10 @@ const getListItemName = (path: string) => {
|
|||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
padding-left: var(--spacing-2xs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -245,6 +256,7 @@ const getListItemName = (path: string) => {
|
|||||||
.vjs-tree {
|
.vjs-tree {
|
||||||
color: var(--color-json-default);
|
color: var(--color-json-default);
|
||||||
--color-line-break: var(--color-code-line-break);
|
--color-line-break: var(--color-code-line-break);
|
||||||
|
font-size: var(--font-size-2xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vjs-tree-node {
|
.vjs-tree-node {
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
|
const { pageSize, total, currentPage } = defineProps<{
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
currentPage: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{ 'update:current-page': [number]; 'update:page-size': [number] }>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const pageSizes = [1, 10, 25, 50, 100];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="$style.pagination" data-test-id="ndv-data-pagination">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
:hide-on-single-page="true"
|
||||||
|
:current-page="currentPage"
|
||||||
|
:pager-count="5"
|
||||||
|
:page-size="pageSize"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
:total="total"
|
||||||
|
@update:current-page="(value: number) => emit('update:current-page', value)"
|
||||||
|
>
|
||||||
|
</el-pagination>
|
||||||
|
|
||||||
|
<div :class="$style.pageSizeSelector">
|
||||||
|
<N8nSelect
|
||||||
|
size="mini"
|
||||||
|
:model-value="pageSize"
|
||||||
|
teleported
|
||||||
|
@update:model-value="(value: number) => emit('update:page-size', value)"
|
||||||
|
>
|
||||||
|
<template #prepend>{{ i18n.baseText('ndv.output.pageSize') }}</template>
|
||||||
|
<N8nOption v-for="size in pageSizes" :key="size" :label="size" :value="size"> </N8nOption>
|
||||||
|
<N8nOption :label="i18n.baseText('ndv.output.all')" :value="total"> </N8nOption>
|
||||||
|
</N8nSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.pagination {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px;
|
||||||
|
overflow-y: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageSizeSelector {
|
||||||
|
text-transform: capitalize;
|
||||||
|
max-width: 150px;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -33,6 +33,8 @@ type Props = {
|
|||||||
hasDefaultHoverState?: boolean;
|
hasDefaultHoverState?: boolean;
|
||||||
search?: string;
|
search?: string;
|
||||||
headerBgColor?: 'base' | 'light';
|
headerBgColor?: 'base' | 'light';
|
||||||
|
compact?: boolean;
|
||||||
|
disableHoverHighlight?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -43,6 +45,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
hasDefaultHoverState: false,
|
hasDefaultHoverState: false,
|
||||||
search: '',
|
search: '',
|
||||||
headerBgColor: 'base',
|
headerBgColor: 'base',
|
||||||
|
disableHoverHighlight: false,
|
||||||
|
compact: false,
|
||||||
});
|
});
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
activeRowChanged: [row: number | null];
|
activeRowChanged: [row: number | null];
|
||||||
@@ -91,6 +95,10 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function isHoveringRow(row: number): boolean {
|
function isHoveringRow(row: number): boolean {
|
||||||
|
if (props.disableHoverHighlight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (row === activeRow.value) {
|
if (row === activeRow.value) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -427,7 +435,11 @@ watch(focusedMappableInput, (curr) => {
|
|||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
$style.dataDisplay,
|
$style.dataDisplay,
|
||||||
{ [$style.highlight]: highlight, [$style.lightHeader]: headerBgColor === 'light' },
|
{
|
||||||
|
[$style.highlight]: highlight,
|
||||||
|
[$style.lightHeader]: headerBgColor === 'light',
|
||||||
|
[$style.compact]: props.compact,
|
||||||
|
},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<table v-if="tableData.columns && tableData.columns.length === 0" :class="$style.table">
|
<table v-if="tableData.columns && tableData.columns.length === 0" :class="$style.table">
|
||||||
@@ -681,6 +693,10 @@ watch(focusedMappableInput, (curr) => {
|
|||||||
word-break: normal;
|
word-break: normal;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-bottom: var(--spacing-3xl);
|
padding-bottom: var(--spacing-3xl);
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
padding-left: var(--spacing-2xs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
@@ -848,6 +864,12 @@ watch(focusedMappableInput, (curr) => {
|
|||||||
border-right: none !important;
|
border-right: none !important;
|
||||||
border-top: none !important;
|
border-top: none !important;
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
|
|
||||||
|
.compact & {
|
||||||
|
padding: 0;
|
||||||
|
min-width: var(--spacing-2xs);
|
||||||
|
max-width: var(--spacing-2xs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hoveringRow {
|
.hoveringRow {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ type Props = {
|
|||||||
paneType: 'input' | 'output';
|
paneType: 'input' | 'output';
|
||||||
connectionType?: NodeConnectionType;
|
connectionType?: NodeConnectionType;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
compact?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -75,6 +76,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
connectionType: NodeConnectionTypes.Main,
|
connectionType: NodeConnectionTypes.Main,
|
||||||
search: '',
|
search: '',
|
||||||
mappingEnabled: false,
|
mappingEnabled: false,
|
||||||
|
compact: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
@@ -377,7 +379,7 @@ const onDragEnd = (el: HTMLElement) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="run-data-schema full-height">
|
<div :class="['run-data-schema', 'full-height', props.compact ? 'compact' : '']">
|
||||||
<div v-if="noSearchResults" class="no-results">
|
<div v-if="noSearchResults" class="no-results">
|
||||||
<N8nText tag="h3" size="large">{{ i18n.baseText('ndv.search.noNodeMatch.title') }}</N8nText>
|
<N8nText tag="h3" size="large">{{ i18n.baseText('ndv.search.noNodeMatch.title') }}</N8nText>
|
||||||
<N8nText>
|
<N8nText>
|
||||||
@@ -491,6 +493,10 @@ const onDragEnd = (el: HTMLElement) => {
|
|||||||
.scroller {
|
.scroller {
|
||||||
padding: 0 var(--spacing-s);
|
padding: 0 var(--spacing-s);
|
||||||
padding-bottom: var(--spacing-2xl);
|
padding-bottom: var(--spacing-2xl);
|
||||||
|
|
||||||
|
.compact & {
|
||||||
|
padding: 0 var(--spacing-2xs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-results {
|
.no-results {
|
||||||
|
|||||||
@@ -13,55 +13,60 @@ exports[`InputPanel > should render 1`] = `
|
|||||||
class="header"
|
class="header"
|
||||||
data-v-2e5cd75c=""
|
data-v-2e5cd75c=""
|
||||||
>
|
>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="titleSection"
|
class="title"
|
||||||
|
data-v-2e5cd75c=""
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
class="title"
|
|
||||||
>
|
|
||||||
Input
|
|
||||||
</span>
|
|
||||||
<div
|
<div
|
||||||
class="n8n-radio-buttons radioGroup"
|
class="titleSection"
|
||||||
data-test-id="input-panel-mode"
|
|
||||||
role="radiogroup"
|
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
<label
|
class="title"
|
||||||
aria-checked="true"
|
|
||||||
class="n8n-radio-button container hoverable"
|
|
||||||
role="radio"
|
|
||||||
tabindex="-1"
|
|
||||||
>
|
>
|
||||||
<div
|
Input
|
||||||
class="button active medium"
|
</span>
|
||||||
data-test-id="radio-button-mapping"
|
<div
|
||||||
>
|
class="n8n-radio-buttons radioGroup"
|
||||||
|
data-test-id="input-panel-mode"
|
||||||
Mapping
|
role="radiogroup"
|
||||||
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<label
|
|
||||||
aria-checked="false"
|
|
||||||
class="n8n-radio-button container hoverable"
|
|
||||||
role="radio"
|
|
||||||
tabindex="-1"
|
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
class="button medium"
|
<label
|
||||||
data-test-id="radio-button-debugging"
|
aria-checked="true"
|
||||||
|
class="n8n-radio-button container hoverable"
|
||||||
|
role="radio"
|
||||||
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
class="button active medium"
|
||||||
|
data-test-id="radio-button-mapping"
|
||||||
|
>
|
||||||
|
|
||||||
Debugging
|
Mapping
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
<label
|
||||||
|
aria-checked="false"
|
||||||
|
class="n8n-radio-button container hoverable"
|
||||||
|
role="radio"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="button medium"
|
||||||
|
data-test-id="radio-button-debugging"
|
||||||
|
>
|
||||||
|
|
||||||
|
Debugging
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="displayModes"
|
class="displayModes"
|
||||||
data-test-id="run-data-pane-header"
|
data-test-id="run-data-pane-header"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import CanvasBackground from './elements/background/CanvasBackground.vue';
|
|||||||
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
|
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
|
||||||
import Edge from './elements/edges/CanvasEdge.vue';
|
import Edge from './elements/edges/CanvasEdge.vue';
|
||||||
import Node from './elements/nodes/CanvasNode.vue';
|
import Node from './elements/nodes/CanvasNode.vue';
|
||||||
|
import { useViewportAutoAdjust } from '@/components/canvas/composables/useViewportAutoAdjust';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
@@ -141,6 +142,7 @@ const {
|
|||||||
findNode,
|
findNode,
|
||||||
viewport,
|
viewport,
|
||||||
nodesSelectionActive,
|
nodesSelectionActive,
|
||||||
|
setViewport,
|
||||||
onEdgeMouseLeave,
|
onEdgeMouseLeave,
|
||||||
onEdgeMouseEnter,
|
onEdgeMouseEnter,
|
||||||
onEdgeMouseMove,
|
onEdgeMouseMove,
|
||||||
@@ -536,6 +538,8 @@ function emitWithLastSelectedNode(emitFn: (id: string) => void) {
|
|||||||
const defaultZoom = 1;
|
const defaultZoom = 1;
|
||||||
const isPaneMoving = ref(false);
|
const isPaneMoving = ref(false);
|
||||||
|
|
||||||
|
useViewportAutoAdjust(viewportRef, viewport, setViewport);
|
||||||
|
|
||||||
function getProjectedPosition(event?: Pick<MouseEvent, 'clientX' | 'clientY'>) {
|
function getProjectedPosition(event?: Pick<MouseEvent, 'clientX' | 'clientY'>) {
|
||||||
const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
||||||
const offsetX = event?.clientX ?? 0;
|
const offsetX = event?.clientX ?? 0;
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
import { useViewportAutoAdjust } from './useViewportAutoAdjust';
|
||||||
|
import { waitFor } from '@testing-library/vue';
|
||||||
|
|
||||||
|
vi.mock('@/stores/settings.store', () => ({
|
||||||
|
useSettingsStore: vi.fn(() => ({ isNewLogsEnabled: true })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe(useViewportAutoAdjust, () => {
|
||||||
|
afterAll(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set viewport when canvas is resized', async () => {
|
||||||
|
let resizeHandler: ResizeObserverCallback = () => {};
|
||||||
|
|
||||||
|
vi.spyOn(window, 'ResizeObserver').mockImplementation((handler) => {
|
||||||
|
resizeHandler = handler;
|
||||||
|
|
||||||
|
return { observe() {}, disconnect() {}, unobserve() {} } as ResizeObserver;
|
||||||
|
});
|
||||||
|
const container = document.createElement('div');
|
||||||
|
|
||||||
|
Object.defineProperty(container, 'offsetWidth', {
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
return 1000;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(container, 'offsetHeight', {
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
return 800;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const viewportRef = ref(container);
|
||||||
|
const viewport = ref({ x: 30, y: 40, zoom: 0.5 });
|
||||||
|
const setViewport = vi.fn();
|
||||||
|
|
||||||
|
useViewportAutoAdjust(viewportRef, viewport, setViewport);
|
||||||
|
resizeHandler(
|
||||||
|
[{ contentRect: { x: 0, y: 0, width: 900, height: 1000 } } as ResizeObserverEntry],
|
||||||
|
{} as ResizeObserver,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(setViewport).toHaveBeenLastCalledWith({
|
||||||
|
x: -20, // 30 + (900 - 1000) / 2
|
||||||
|
y: 140, // 40 + (1000 - 800) / 2
|
||||||
|
zoom: 0.5, // unchanged
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
import type { Rect, SetViewport, ViewportTransform } from '@vue-flow/core';
|
||||||
|
import { type Ref, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When canvas is resized (via window resize or toggling logs panel), adjust viewport to maintain center
|
||||||
|
*/
|
||||||
|
export function useViewportAutoAdjust(
|
||||||
|
viewportRef: Ref<HTMLElement | null>,
|
||||||
|
viewport: Ref<ViewportTransform>,
|
||||||
|
setViewport: SetViewport,
|
||||||
|
) {
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
|
if (settingsStore.isNewLogsEnabled) {
|
||||||
|
const canvasRect = ref<Rect>();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
viewportRef,
|
||||||
|
(vp, _, onCleanUp) => {
|
||||||
|
if (!vp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
const entry = entries[0];
|
||||||
|
|
||||||
|
if (entry) {
|
||||||
|
canvasRect.value = entry.contentRect;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
canvasRect.value = {
|
||||||
|
x: vp.offsetLeft,
|
||||||
|
y: vp.offsetTop,
|
||||||
|
width: vp.offsetWidth,
|
||||||
|
height: vp.offsetHeight,
|
||||||
|
};
|
||||||
|
resizeObserver.observe(vp);
|
||||||
|
onCleanUp(() => resizeObserver.disconnect());
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(canvasRect, async (newRect, oldRect) => {
|
||||||
|
if (!newRect || !oldRect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await setViewport({
|
||||||
|
x: viewport.value.x + (newRect.width - oldRect.width) / 2,
|
||||||
|
y: viewport.value.y + (newRect.height - oldRect.height) / 2,
|
||||||
|
zoom: viewport.value.zoom,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ describe('useStyles', () => {
|
|||||||
setAppZIndexes();
|
setAppZIndexes();
|
||||||
|
|
||||||
expect(global.document.documentElement.style.setProperty).toHaveBeenNthCalledWith(
|
expect(global.document.documentElement.style.setProperty).toHaveBeenNthCalledWith(
|
||||||
1,
|
2,
|
||||||
'--z-index-app-header',
|
'--z-index-app-header',
|
||||||
'99',
|
'99',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const APP_Z_INDEXES = {
|
const APP_Z_INDEXES = {
|
||||||
|
CONTEXT_MENU: 10, // should be still in front of the logs panel
|
||||||
APP_HEADER: 99,
|
APP_HEADER: 99,
|
||||||
SELECT_BOX: 100,
|
SELECT_BOX: 100,
|
||||||
CANVAS_ADD_BUTTON: 101,
|
CANVAS_ADD_BUTTON: 101,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type { ApplicationError } from 'n8n-workflow';
|
|||||||
import { useStyles } from './useStyles';
|
import { useStyles } from './useStyles';
|
||||||
import { useCanvasStore } from '@/stores/canvas.store';
|
import { useCanvasStore } from '@/stores/canvas.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { LOGS_PANEL_STATE } from '@/components/CanvasChat/types/logs';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
|
||||||
export interface NotificationErrorWithNodeAndDescription extends ApplicationError {
|
export interface NotificationErrorWithNodeAndDescription extends ApplicationError {
|
||||||
node: {
|
node: {
|
||||||
@@ -32,26 +32,22 @@ export function useToast() {
|
|||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const { APP_Z_INDEXES } = useStyles();
|
const { APP_Z_INDEXES } = useStyles();
|
||||||
const canvasStore = useCanvasStore();
|
const canvasStore = useCanvasStore();
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
const messageDefaults: Partial<Omit<NotificationOptions, 'message'>> = {
|
|
||||||
dangerouslyUseHTMLString: true,
|
|
||||||
position: 'bottom-right',
|
|
||||||
zIndex: APP_Z_INDEXES.TOASTS, // above NDV and modal overlays
|
|
||||||
offset:
|
|
||||||
settingsStore.isAiAssistantEnabled ||
|
|
||||||
workflowsStore.logsPanelState === LOGS_PANEL_STATE.ATTACHED
|
|
||||||
? 64
|
|
||||||
: 0,
|
|
||||||
appendTo: '#app-grid',
|
|
||||||
customClass: 'content-toast',
|
|
||||||
};
|
|
||||||
|
|
||||||
function showMessage(messageData: Partial<NotificationOptions>, track = true) {
|
function showMessage(messageData: Partial<NotificationOptions>, track = true) {
|
||||||
|
const messageDefaults: Partial<Omit<NotificationOptions, 'message'>> = {
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
position: 'bottom-right',
|
||||||
|
zIndex: APP_Z_INDEXES.TOASTS, // above NDV and modal overlays
|
||||||
|
offset:
|
||||||
|
(settingsStore.isAiAssistantEnabled ? 64 : 0) +
|
||||||
|
(ndvStore.activeNode === null ? canvasStore.panelHeight : 0),
|
||||||
|
appendTo: '#app-grid',
|
||||||
|
customClass: 'content-toast',
|
||||||
|
};
|
||||||
const { message, title } = messageData;
|
const { message, title } = messageData;
|
||||||
const params = { ...messageDefaults, ...messageData };
|
const params = { ...messageDefaults, ...messageData };
|
||||||
|
|
||||||
params.offset = +canvasStore.panelHeight;
|
|
||||||
|
|
||||||
if (typeof message === 'string') {
|
if (typeof message === 'string') {
|
||||||
params.message = sanitizeHtml(message);
|
params.message = sanitizeHtml(message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1279,8 +1279,12 @@ const chatTriggerNodePinnedData = computed(() => {
|
|||||||
return workflowsStore.pinDataByNodeName(chatTriggerNode.value.name);
|
return workflowsStore.pinDataByNodeName(chatTriggerNode.value.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onOpenChat(isOpen?: boolean) {
|
async function onToggleChat() {
|
||||||
await toggleChatOpen('main', isOpen);
|
await toggleChatOpen('main');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onOpenChat() {
|
||||||
|
await toggleChatOpen('main', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1787,7 +1791,7 @@ onBeforeUnmount(() => {
|
|||||||
v-if="containsChatTriggerNodes"
|
v-if="containsChatTriggerNodes"
|
||||||
:type="isLogsPanelOpen ? 'tertiary' : 'primary'"
|
:type="isLogsPanelOpen ? 'tertiary' : 'primary'"
|
||||||
:label="isLogsPanelOpen ? i18n.baseText('chat.hide') : i18n.baseText('chat.open')"
|
:label="isLogsPanelOpen ? i18n.baseText('chat.hide') : i18n.baseText('chat.open')"
|
||||||
@click="onOpenChat(!isLogsPanelOpen)"
|
@click="onToggleChat"
|
||||||
/>
|
/>
|
||||||
<CanvasStopCurrentExecutionButton
|
<CanvasStopCurrentExecutionButton
|
||||||
v-if="isStopExecutionButtonVisible"
|
v-if="isStopExecutionButtonVisible"
|
||||||
|
|||||||
Reference in New Issue
Block a user