mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
perf(editor): Optimize log entries calculation with throttled watcher (no-changelog) (#18486)
This commit is contained in:
@@ -71,7 +71,7 @@ export function toggleInputPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clickOpenNdvAtRow(rowIndex: number) {
|
export function clickOpenNdvAtRow(rowIndex: number) {
|
||||||
getLogEntries().eq(rowIndex).realHover();
|
getLogEntries().eq(rowIndex).trigger('focus').realHover();
|
||||||
getLogEntries().eq(rowIndex).find('[aria-label="Open..."]').click();
|
getLogEntries().eq(rowIndex).find('[aria-label="Open..."]').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ describe('LogsPanel', () => {
|
|||||||
let uiStore: ReturnType<typeof mockedStore<typeof useUIStore>>;
|
let uiStore: ReturnType<typeof mockedStore<typeof useUIStore>>;
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
return renderComponent(LogsPanel, {
|
const wrapper = renderComponent(LogsPanel, {
|
||||||
global: {
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
[ChatSymbol as symbol]: {},
|
[ChatSymbol as symbol]: {},
|
||||||
@@ -78,9 +78,15 @@ describe('LogsPanel', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||||
|
|
||||||
pinia = createTestingPinia({ stubActions: false, fakeApp: true });
|
pinia = createTestingPinia({ stubActions: false, fakeApp: true });
|
||||||
|
|
||||||
setActivePinia(pinia);
|
setActivePinia(pinia);
|
||||||
@@ -148,7 +154,9 @@ describe('LogsPanel', () => {
|
|||||||
|
|
||||||
const rendered = render();
|
const rendered = render();
|
||||||
|
|
||||||
expect(rendered.queryByTestId('log-details-header')).toHaveTextContent('AI Agent');
|
await waitFor(() =>
|
||||||
|
expect(rendered.queryByTestId('log-details-header')).toHaveTextContent('AI Agent'),
|
||||||
|
);
|
||||||
expect(rendered.queryByTestId('log-details-input')).not.toBeInTheDocument();
|
expect(rendered.queryByTestId('log-details-input')).not.toBeInTheDocument();
|
||||||
expect(rendered.queryByTestId('log-details-output')).toBeInTheDocument();
|
expect(rendered.queryByTestId('log-details-output')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -160,7 +168,9 @@ describe('LogsPanel', () => {
|
|||||||
|
|
||||||
const rendered = render();
|
const rendered = render();
|
||||||
|
|
||||||
expect(rendered.queryByTestId('log-details-header')).toHaveTextContent('AI Model');
|
await waitFor(() =>
|
||||||
|
expect(rendered.queryByTestId('log-details-header')).toHaveTextContent('AI Model'),
|
||||||
|
);
|
||||||
expect(rendered.queryByTestId('log-details-input')).toBeInTheDocument();
|
expect(rendered.queryByTestId('log-details-input')).toBeInTheDocument();
|
||||||
expect(rendered.queryByTestId('log-details-output')).toBeInTheDocument();
|
expect(rendered.queryByTestId('log-details-output')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -289,6 +299,7 @@ describe('LogsPanel', () => {
|
|||||||
|
|
||||||
const rendered = render();
|
const rendered = render();
|
||||||
|
|
||||||
|
await waitFor(() => expect(rendered.getByText('Overview')).toBeInTheDocument());
|
||||||
await fireEvent.click(rendered.getByText('Overview'));
|
await fireEvent.click(rendered.getByText('Overview'));
|
||||||
|
|
||||||
expect(rendered.getByText(/Running/)).toBeInTheDocument();
|
expect(rendered.getByText(/Running/)).toBeInTheDocument();
|
||||||
@@ -300,6 +311,8 @@ describe('LogsPanel', () => {
|
|||||||
data: { executionIndex: 0, startTime: Date.parse('2025-04-20T12:34:51.000Z'), source: [] },
|
data: { executionIndex: 0, startTime: Date.parse('2025-04-20T12:34:51.000Z'), source: [] },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(2000);
|
||||||
|
|
||||||
const lastTreeItem = await waitFor(() => {
|
const lastTreeItem = await waitFor(() => {
|
||||||
const items = rendered.getAllByRole('treeitem');
|
const items = rendered.getAllByRole('treeitem');
|
||||||
|
|
||||||
@@ -321,6 +334,9 @@ describe('LogsPanel', () => {
|
|||||||
executionStatus: 'success',
|
executionStatus: 'success',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
|
||||||
expect(await lastTreeItem.findByText('AI Agent')).toBeInTheDocument();
|
expect(await lastTreeItem.findByText('AI Agent')).toBeInTheDocument();
|
||||||
expect(await lastTreeItem.findByText('Success')).toBeInTheDocument();
|
expect(await lastTreeItem.findByText('Success')).toBeInTheDocument();
|
||||||
expect(lastTreeItem.getByText('in 33ms')).toBeInTheDocument();
|
expect(lastTreeItem.getByText('in 33ms')).toBeInTheDocument();
|
||||||
@@ -334,6 +350,8 @@ describe('LogsPanel', () => {
|
|||||||
stoppedAt: new Date('2025-04-20T12:34:56.000Z'),
|
stoppedAt: new Date('2025-04-20T12:34:56.000Z'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
|
||||||
expect(await rendered.findByText('Success in 6s')).toBeInTheDocument();
|
expect(await rendered.findByText('Success in 6s')).toBeInTheDocument();
|
||||||
expect(rendered.queryByText('AI Agent')).toBeInTheDocument();
|
expect(rendered.queryByText('AI Agent')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -417,6 +435,7 @@ describe('LogsPanel', () => {
|
|||||||
|
|
||||||
const rendered = render();
|
const rendered = render();
|
||||||
|
|
||||||
|
await waitFor(() => expect(rendered.getByTestId('log-details-header')).toBeInTheDocument());
|
||||||
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')).toBeInTheDocument();
|
||||||
@@ -472,7 +491,9 @@ describe('LogsPanel', () => {
|
|||||||
const { getByTestId, findByRole } = render();
|
const { getByTestId, findByRole } = render();
|
||||||
const overview = getByTestId('logs-overview');
|
const overview = getByTestId('logs-overview');
|
||||||
|
|
||||||
expect(await findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Model/);
|
await waitFor(async () =>
|
||||||
|
expect(await findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Model/),
|
||||||
|
);
|
||||||
await fireEvent.keyDown(overview, { key: 'K' });
|
await fireEvent.keyDown(overview, { key: 'K' });
|
||||||
expect(await findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Agent/);
|
expect(await findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Agent/);
|
||||||
await fireEvent.keyDown(overview, { key: 'J' });
|
await fireEvent.keyDown(overview, { key: 'J' });
|
||||||
@@ -526,7 +547,9 @@ describe('LogsPanel', () => {
|
|||||||
|
|
||||||
const { rerender, findByRole } = render();
|
const { rerender, findByRole } = render();
|
||||||
|
|
||||||
expect(await findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Model/);
|
await waitFor(async () =>
|
||||||
|
expect(await findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Model/),
|
||||||
|
);
|
||||||
|
|
||||||
await canvasOperations.renameNode('AI Agent', 'Renamed Agent');
|
await canvasOperations.renameNode('AI Agent', 'Renamed Agent');
|
||||||
uiStore.lastSelectedNode = 'Renamed Agent';
|
uiStore.lastSelectedNode = 'Renamed Agent';
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ describe(useLogsExecutionData, () => {
|
|||||||
|
|
||||||
describe('loadSubExecution', () => {
|
describe('loadSubExecution', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||||
|
|
||||||
workflowsStore.setWorkflowExecutionData(
|
workflowsStore.setWorkflowExecutionData(
|
||||||
createTestWorkflowExecutionResponse({
|
createTestWorkflowExecutionResponse({
|
||||||
id: 'e0',
|
id: 'e0',
|
||||||
@@ -59,6 +61,8 @@ describe(useLogsExecutionData, () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add runs from sub execution to the entries', async () => {
|
it('should add runs from sub execution to the entries', async () => {
|
||||||
@@ -79,6 +83,8 @@ describe(useLogsExecutionData, () => {
|
|||||||
|
|
||||||
await loadSubExecution(entries.value[1]);
|
await loadSubExecution(entries.value[1]);
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(entries.value).toHaveLength(2);
|
expect(entries.value).toHaveLength(2);
|
||||||
expect(entries.value[1].children).toHaveLength(1);
|
expect(entries.value[1].children).toHaveLength(1);
|
||||||
@@ -104,6 +110,9 @@ describe(useLogsExecutionData, () => {
|
|||||||
const { loadSubExecution, entries } = useLogsExecutionData();
|
const { loadSubExecution, entries } = useLogsExecutionData();
|
||||||
|
|
||||||
await loadSubExecution(entries.value[1]);
|
await loadSubExecution(entries.value[1]);
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
|
||||||
await waitFor(() => expect(showErrorSpy).toHaveBeenCalled());
|
await waitFor(() => expect(showErrorSpy).toHaveBeenCalled());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { parse } from 'flatted';
|
|||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import type { LatestNodeInfo, LogEntry } from '../logs.types';
|
import type { LatestNodeInfo, LogEntry } from '../logs.types';
|
||||||
import { isChatNode } from '@/utils/aiUtils';
|
import { isChatNode } from '@/utils/aiUtils';
|
||||||
|
import { PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
|
||||||
|
|
||||||
export function useLogsExecutionData() {
|
export function useLogsExecutionData() {
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
@@ -47,6 +48,7 @@ export function useLogsExecutionData() {
|
|||||||
nodes.some(isChatNode),
|
nodes.some(isChatNode),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const entries = computed<LogEntry[]>(() => {
|
const entries = computed<LogEntry[]>(() => {
|
||||||
if (!execData.value?.data || !workflow.value) {
|
if (!execData.value?.data || !workflow.value) {
|
||||||
return [];
|
return [];
|
||||||
@@ -59,7 +61,8 @@ export function useLogsExecutionData() {
|
|||||||
subWorkflowExecData.value,
|
subWorkflowExecData.value,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const updateInterval = computed(() => ((entries.value?.length ?? 0) > 10 ? 300 : 0));
|
|
||||||
|
const updateInterval = computed(() => ((entries.value?.length ?? 0) > 1 ? 1000 : 0));
|
||||||
|
|
||||||
function resetExecutionData() {
|
function resetExecutionData() {
|
||||||
execData.value = undefined;
|
execData.value = undefined;
|
||||||
@@ -126,6 +129,15 @@ export function useLogsExecutionData() {
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => workflowsStore.workflowId,
|
||||||
|
(newId) => {
|
||||||
|
if (newId === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
|
||||||
|
resetExecutionData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
execution: computed(() => execData.value),
|
execution: computed(() => execData.value),
|
||||||
entries,
|
entries,
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ import { useCanvasStore } from '@/stores/canvas.store';
|
|||||||
import { useLogsStore } from '@/stores/logs.store';
|
import { useLogsStore } from '@/stores/logs.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { shallowRef, watch } from 'vue';
|
import { shallowRef, watch } from 'vue';
|
||||||
import { computed, type ComputedRef } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import type { Ref, ComputedRef } from 'vue';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
|
||||||
export function useLogsSelection(
|
export function useLogsSelection(
|
||||||
execution: ComputedRef<IExecutionResponse | undefined>,
|
execution: ComputedRef<IExecutionResponse | undefined>,
|
||||||
tree: ComputedRef<LogEntry[]>,
|
tree: Ref<LogEntry[]>,
|
||||||
flatLogEntries: ComputedRef<LogEntry[]>,
|
flatLogEntries: ComputedRef<LogEntry[]>,
|
||||||
toggleExpand: (entry: LogEntry, expand?: boolean) => void,
|
toggleExpand: (entry: LogEntry, expand?: boolean) => void,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { flattenLogEntries, hasSubExecution } from '@/features/logs/logs.utils';
|
import { flattenLogEntries, hasSubExecution } from '@/features/logs/logs.utils';
|
||||||
import { computed, shallowRef, type ComputedRef } from 'vue';
|
import { computed, shallowRef, type Ref } from 'vue';
|
||||||
import type { LogEntry } from '../logs.types';
|
import type { LogEntry } from '../logs.types';
|
||||||
|
|
||||||
export function useLogsTreeExpand(
|
export function useLogsTreeExpand(
|
||||||
entries: ComputedRef<LogEntry[]>,
|
entries: Ref<LogEntry[]>,
|
||||||
loadSubExecution: (logEntry: LogEntry) => Promise<void>,
|
loadSubExecution: (logEntry: LogEntry) => Promise<void>,
|
||||||
) {
|
) {
|
||||||
const collapsedEntries = shallowRef<Record<string, boolean>>({});
|
const collapsedEntries = shallowRef<Record<string, boolean>>({});
|
||||||
const flatLogEntries = computed(() => flattenLogEntries(entries.value, collapsedEntries.value));
|
const flatLogEntries = computed<LogEntry[]>(() =>
|
||||||
|
flattenLogEntries(entries.value, collapsedEntries.value),
|
||||||
|
);
|
||||||
|
|
||||||
function toggleExpanded(treeNode: LogEntry, expand?: boolean) {
|
function toggleExpanded(treeNode: LogEntry, expand?: boolean) {
|
||||||
if (hasSubExecution(treeNode) && treeNode.children.length === 0) {
|
if (hasSubExecution(treeNode) && treeNode.children.length === 0) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { LOG_DETAILS_PANEL_STATE, LOGS_PANEL_STATE } from '@/features/logs/
|
|||||||
import type { INodeUi, LlmTokenUsageData } from '@/Interface';
|
import type { INodeUi, LlmTokenUsageData } from '@/Interface';
|
||||||
import type { IRunExecutionData, ITaskData, Workflow } from 'n8n-workflow';
|
import type { IRunExecutionData, ITaskData, Workflow } from 'n8n-workflow';
|
||||||
|
|
||||||
export interface LogEntry {
|
export type LogEntry = {
|
||||||
parent?: LogEntry;
|
parent?: LogEntry;
|
||||||
node: INodeUi;
|
node: INodeUi;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -13,7 +13,7 @@ export interface LogEntry {
|
|||||||
workflow: Workflow;
|
workflow: Workflow;
|
||||||
executionId: string;
|
executionId: string;
|
||||||
execution: IRunExecutionData;
|
execution: IRunExecutionData;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface LogTreeCreationContext {
|
export interface LogTreeCreationContext {
|
||||||
parent: LogEntry | undefined;
|
parent: LogEntry | undefined;
|
||||||
|
|||||||
@@ -192,24 +192,7 @@ function findLogEntryToAutoSelect(subTree: LogEntry[]): LogEntry | undefined {
|
|||||||
return subTree[subTree.length - 1];
|
return subTree[subTree.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createLogTree(
|
function createLogTreeRec(context: LogTreeCreationContext): LogEntry[] {
|
||||||
workflow: Workflow,
|
|
||||||
response: IExecutionResponse,
|
|
||||||
workflows: Record<string, Workflow> = {},
|
|
||||||
subWorkflowData: Record<string, IRunExecutionData> = {},
|
|
||||||
) {
|
|
||||||
return createLogTreeRec({
|
|
||||||
parent: undefined,
|
|
||||||
ancestorRunIndexes: [],
|
|
||||||
executionId: response.id,
|
|
||||||
workflow,
|
|
||||||
workflows,
|
|
||||||
data: response.data ?? { resultData: { runData: {} } },
|
|
||||||
subWorkflowData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createLogTreeRec(context: LogTreeCreationContext) {
|
|
||||||
const runData = context.data.resultData.runData;
|
const runData = context.data.resultData.runData;
|
||||||
|
|
||||||
return Object.entries(runData)
|
return Object.entries(runData)
|
||||||
@@ -258,6 +241,23 @@ function createLogTreeRec(context: LogTreeCreationContext) {
|
|||||||
.sort(sortLogEntries);
|
.sort(sortLogEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createLogTree(
|
||||||
|
workflow: Workflow,
|
||||||
|
response: IExecutionResponse,
|
||||||
|
workflows: Record<string, Workflow> = {},
|
||||||
|
subWorkflowData: Record<string, IRunExecutionData> = {},
|
||||||
|
): LogEntry[] {
|
||||||
|
return createLogTreeRec({
|
||||||
|
parent: undefined,
|
||||||
|
ancestorRunIndexes: [],
|
||||||
|
executionId: response.id,
|
||||||
|
workflow,
|
||||||
|
workflows,
|
||||||
|
data: response.data ?? { resultData: { runData: {} } },
|
||||||
|
subWorkflowData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function findLogEntryRec(
|
export function findLogEntryRec(
|
||||||
isMatched: (entry: LogEntry) => boolean,
|
isMatched: (entry: LogEntry) => boolean,
|
||||||
entries: LogEntry[],
|
entries: LogEntry[],
|
||||||
|
|||||||
Reference in New Issue
Block a user