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