diff --git a/cypress/composables/logs.ts b/cypress/composables/logs.ts index 27bfa3d015..f022c92279 100644 --- a/cypress/composables/logs.ts +++ b/cypress/composables/logs.ts @@ -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(); } diff --git a/packages/frontend/editor-ui/src/features/logs/components/LogsPanel.test.ts b/packages/frontend/editor-ui/src/features/logs/components/LogsPanel.test.ts index 88a6eb16ed..25c229cbda 100644 --- a/packages/frontend/editor-ui/src/features/logs/components/LogsPanel.test.ts +++ b/packages/frontend/editor-ui/src/features/logs/components/LogsPanel.test.ts @@ -63,7 +63,7 @@ describe('LogsPanel', () => { let uiStore: ReturnType>; 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'; diff --git a/packages/frontend/editor-ui/src/features/logs/composables/useLogsExecutionData.test.ts b/packages/frontend/editor-ui/src/features/logs/composables/useLogsExecutionData.test.ts index 4a4b21dee3..d2fcd5024a 100644 --- a/packages/frontend/editor-ui/src/features/logs/composables/useLogsExecutionData.test.ts +++ b/packages/frontend/editor-ui/src/features/logs/composables/useLogsExecutionData.test.ts @@ -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()); }); }); diff --git a/packages/frontend/editor-ui/src/features/logs/composables/useLogsExecutionData.ts b/packages/frontend/editor-ui/src/features/logs/composables/useLogsExecutionData.ts index 99c8304c9b..4c09e86645 100644 --- a/packages/frontend/editor-ui/src/features/logs/composables/useLogsExecutionData.ts +++ b/packages/frontend/editor-ui/src/features/logs/composables/useLogsExecutionData.ts @@ -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(() => { 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, diff --git a/packages/frontend/editor-ui/src/features/logs/composables/useLogsSelection.ts b/packages/frontend/editor-ui/src/features/logs/composables/useLogsSelection.ts index b11cc183a0..7edb0f2e04 100644 --- a/packages/frontend/editor-ui/src/features/logs/composables/useLogsSelection.ts +++ b/packages/frontend/editor-ui/src/features/logs/composables/useLogsSelection.ts @@ -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, - tree: ComputedRef, + tree: Ref, flatLogEntries: ComputedRef, toggleExpand: (entry: LogEntry, expand?: boolean) => void, ) { diff --git a/packages/frontend/editor-ui/src/features/logs/composables/useLogsTreeExpand.ts b/packages/frontend/editor-ui/src/features/logs/composables/useLogsTreeExpand.ts index 6be49d870f..f5609695c0 100644 --- a/packages/frontend/editor-ui/src/features/logs/composables/useLogsTreeExpand.ts +++ b/packages/frontend/editor-ui/src/features/logs/composables/useLogsTreeExpand.ts @@ -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, + entries: Ref, loadSubExecution: (logEntry: LogEntry) => Promise, ) { const collapsedEntries = shallowRef>({}); - const flatLogEntries = computed(() => flattenLogEntries(entries.value, collapsedEntries.value)); + const flatLogEntries = computed(() => + flattenLogEntries(entries.value, collapsedEntries.value), + ); function toggleExpanded(treeNode: LogEntry, expand?: boolean) { if (hasSubExecution(treeNode) && treeNode.children.length === 0) { diff --git a/packages/frontend/editor-ui/src/features/logs/logs.types.ts b/packages/frontend/editor-ui/src/features/logs/logs.types.ts index 072bbc4401..e43a197386 100644 --- a/packages/frontend/editor-ui/src/features/logs/logs.types.ts +++ b/packages/frontend/editor-ui/src/features/logs/logs.types.ts @@ -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; diff --git a/packages/frontend/editor-ui/src/features/logs/logs.utils.ts b/packages/frontend/editor-ui/src/features/logs/logs.utils.ts index cfe38e3248..2d64069872 100644 --- a/packages/frontend/editor-ui/src/features/logs/logs.utils.ts +++ b/packages/frontend/editor-ui/src/features/logs/logs.utils.ts @@ -192,24 +192,7 @@ function findLogEntryToAutoSelect(subTree: LogEntry[]): LogEntry | undefined { return subTree[subTree.length - 1]; } -export function createLogTree( - workflow: Workflow, - response: IExecutionResponse, - workflows: Record = {}, - subWorkflowData: Record = {}, -) { - 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 = {}, + subWorkflowData: Record = {}, +): 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[],