Skip to content

Commit 44ecad5

Browse files
authored
fix(editor): Don't render now when startedAt is null (#15283)
1 parent 0cddc95 commit 44ecad5

File tree

12 files changed

+150
-13
lines changed

12 files changed

+150
-13
lines changed

packages/@n8n/db/src/repositories/execution.repository.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
868868
private toSummary(execution: {
869869
id: number | string;
870870
createdAt?: Date | string;
871-
startedAt?: Date | string;
871+
startedAt: Date | string | null;
872872
stoppedAt?: Date | string;
873873
waitTill?: Date | string | null;
874874
}): ExecutionSummary {
@@ -967,7 +967,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
967967
if (lastId) qb.andWhere('execution.id < :lastId', { lastId });
968968

969969
if (query.order?.startedAt === 'DESC') {
970-
qb.orderBy({ 'execution.startedAt': 'DESC' });
970+
qb.orderBy({ 'COALESCE(execution.startedAt, execution.createdAt)': 'DESC' });
971971
} else if (query.order?.top) {
972972
qb.orderBy(`(CASE WHEN execution.status = '${query.order.top}' THEN 0 ELSE 1 END)`);
973973
} else {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ExecutionRepository } from '@n8n/db';
2+
import { Container } from '@n8n/di';
3+
4+
import { createExecution } from '@test-integration/db/executions';
5+
import { createWorkflow } from '@test-integration/db/workflows';
6+
7+
import * as testDb from './shared/test-db';
8+
9+
describe('UserRepository', () => {
10+
let executionRepository: ExecutionRepository;
11+
12+
beforeAll(async () => {
13+
await testDb.init();
14+
executionRepository = Container.get(ExecutionRepository);
15+
});
16+
17+
beforeEach(async () => {
18+
await testDb.truncate(['ExecutionEntity']);
19+
});
20+
21+
afterAll(async () => {
22+
await testDb.terminate();
23+
});
24+
25+
describe('findManyByRangeQuery', () => {
26+
test('sort by `createdAt` if `startedAt` is null', async () => {
27+
const workflow = await createWorkflow();
28+
const execution1 = await createExecution({}, workflow);
29+
const execution2 = await createExecution({ startedAt: null }, workflow);
30+
const execution3 = await createExecution({}, workflow);
31+
32+
const executions = await executionRepository.findManyByRangeQuery({
33+
workflowId: workflow.id,
34+
accessibleWorkflowIds: [workflow.id],
35+
kind: 'range',
36+
range: { limit: 10 },
37+
order: { startedAt: 'DESC' },
38+
});
39+
40+
// Executions are returned in reverse order, and if `startedAt` is not
41+
// defined `createdAt` is used.
42+
expect(executions.map((e) => e.id)).toStrictEqual([
43+
execution3.id,
44+
execution2.id,
45+
execution1.id,
46+
]);
47+
});
48+
});
49+
});

packages/cli/test/integration/shared/db/executions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export async function createExecution(
3939
finished: finished ?? true,
4040
mode: mode ?? 'manual',
4141
createdAt: new Date(),
42-
startedAt: startedAt ?? new Date(),
42+
startedAt: startedAt === undefined ? new Date() : startedAt,
4343
...(workflow !== undefined && { workflowId: workflow.id }),
4444
stoppedAt: stoppedAt ?? new Date(),
4545
waitTill: waitTill ?? null,

packages/frontend/editor-ui/src/components/executions/global/GlobalExecutionsListItem.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,48 @@ describe('GlobalExecutionsListItem', () => {
157157

158158
expect(globalExecutionsListItemQueuedTooltipRenderSpy).toHaveBeenCalled();
159159
});
160+
161+
afterEach(() => {
162+
vitest.useRealTimers();
163+
});
164+
165+
it('uses `createdAt` to calculate running time if `startedAt` is undefined', async () => {
166+
const createdAt = new Date('2024-09-27T12:00:00Z');
167+
const now = new Date('2024-09-27T12:30:00Z');
168+
vitest.useFakeTimers({ now });
169+
const { getByTestId } = renderComponent({
170+
props: {
171+
execution: { status: 'running', id: 123, workflowName: 'Test Workflow', createdAt },
172+
workflowPermissions: {},
173+
concurrencyCap: 5,
174+
},
175+
});
176+
177+
const executionTimeElement = getByTestId('execution-time');
178+
expect(executionTimeElement).toBeVisible();
179+
expect(executionTimeElement.textContent).toBe('-1727438401s');
180+
});
181+
182+
it('uses `createdAt` to calculate running time if `startedAt` is undefined and `stoppedAt` is defined', async () => {
183+
const createdAt = new Date('2024-09-27T12:00:00Z');
184+
const now = new Date('2024-09-27T12:30:00Z');
185+
vitest.useFakeTimers({ now });
186+
const { getByTestId } = renderComponent({
187+
props: {
188+
execution: {
189+
status: 'running',
190+
id: 123,
191+
workflowName: 'Test Workflow',
192+
createdAt,
193+
stoppedAt: now,
194+
},
195+
workflowPermissions: {},
196+
concurrencyCap: 5,
197+
},
198+
});
199+
200+
const executionTimeElement = getByTestId('execution-time');
201+
expect(executionTimeElement).toBeVisible();
202+
expect(executionTimeElement.textContent).toBe('30:00m');
203+
});
160204
});

packages/frontend/editor-ui/src/components/executions/global/GlobalExecutionsListItem.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ const formattedStoppedAtDate = computed(() => {
130130
return props.execution.stoppedAt
131131
? locale.displayTimer(
132132
new Date(props.execution.stoppedAt).getTime() -
133-
new Date(props.execution.startedAt).getTime(),
133+
new Date(props.execution.startedAt ?? props.execution.createdAt).getTime(),
134134
true,
135135
)
136136
: '';
@@ -233,11 +233,11 @@ async function handleActionItemClick(commandData: Command) {
233233
<td>
234234
{{ formattedStartedAtDate }}
235235
</td>
236-
<td>
236+
<td data-test-id="execution-time">
237237
<template v-if="formattedStoppedAtDate">
238238
{{ formattedStoppedAtDate }}
239239
</template>
240-
<ExecutionsTime v-else :start-time="execution.startedAt" />
240+
<ExecutionsTime v-else :start-time="execution.startedAt ?? execution.createdAt" />
241241
</td>
242242
<td>
243243
<span v-if="execution.id">{{ execution.id }}</span>

packages/frontend/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,29 @@ describe('WorkflowExecutionsCard', () => {
172172
expect(executionTimeElement).toBeVisible();
173173
expect(executionTimeElement.textContent).toBe('27 Sep - Starting soon');
174174
});
175+
176+
afterEach(() => {
177+
vitest.useRealTimers();
178+
});
179+
180+
test('uses `createdAt` to calculate running time if `startedAt` is undefined', () => {
181+
const createdAt = new Date('2024-09-27T12:00:00Z');
182+
const now = new Date('2024-09-27T12:30:00Z');
183+
vitest.useFakeTimers({ now });
184+
const props = {
185+
execution: {
186+
id: '1',
187+
mode: 'webhook',
188+
status: 'running',
189+
createdAt: createdAt.toISOString(),
190+
},
191+
workflowPermissions: { execute: true },
192+
};
193+
194+
const { getByTestId } = renderComponent({ props });
195+
196+
const executionTimeElement = getByTestId('execution-time-in-status');
197+
expect(executionTimeElement).toBeVisible();
198+
expect(executionTimeElement.textContent).toBe('for -1727438401s');
199+
});
175200
});

packages/frontend/editor-ui/src/components/executions/workflow/WorkflowExecutionsCard.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,11 @@ function onRetryMenuItemSelect(action: string): void {
108108
v-if="executionUIDetails.name === 'running'"
109109
:color="isActive ? 'text-dark' : 'text-base'"
110110
size="small"
111+
data-test-id="execution-time-in-status"
111112
>
112113
{{ locale.baseText('executionDetails.runningTimeRunning') }}
113-
<ExecutionsTime :start-time="execution.startedAt" />
114+
<!-- Just here to make typescript happy, since `startedAt` will always be defined for running executions -->
115+
<ExecutionsTime :start-time="execution.startedAt ?? execution.createdAt" />
114116
</N8nText>
115117
<N8nText
116118
v-if="executionUIDetails.name === 'new' && execution.createdAt"

packages/frontend/editor-ui/src/composables/useExecutionHelpers.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useExecutionHelpers } from '@/composables/useExecutionHelpers';
22
import type { ExecutionSummary } from 'n8n-workflow';
33
import { i18n } from '@/plugins/i18n';
44
import { convertToDisplayDate } from '@/utils/formatters/dateFormatter';
5+
import { mock } from 'vitest-mock-extended';
56

67
const { resolve, track } = vi.hoisted(() => ({
78
resolve: vi.fn(),
@@ -48,6 +49,21 @@ describe('useExecutionHelpers()', () => {
4849
expect(uiDetails.runningTime).toEqual('0s');
4950
},
5051
);
52+
53+
it('use `createdAt` if `startedAt` is null', async () => {
54+
const date = new Date('2025-01-01T00:00:00.000Z');
55+
const execution = mock<ExecutionSummary>({
56+
id: '1',
57+
startedAt: null,
58+
createdAt: date,
59+
stoppedAt: date,
60+
status: 'error',
61+
});
62+
const { getUIDetails } = useExecutionHelpers();
63+
const uiDetails = getUIDetails(execution);
64+
65+
expect(uiDetails.startTime).toEqual('Jan 1, 00:00:00');
66+
});
5167
});
5268

5369
describe('formatDate()', () => {

packages/frontend/editor-ui/src/composables/useExecutionHelpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function useExecutionHelpers() {
2525
const status = {
2626
name: 'unknown',
2727
createdAt: execution.createdAt?.toString() ?? '',
28-
startTime: formatDate(execution.startedAt),
28+
startTime: formatDate(execution.startedAt ?? execution.createdAt),
2929
label: 'Status unknown',
3030
runningTime: '',
3131
showTimestamp: true,

packages/frontend/editor-ui/src/stores/executions.store.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,13 @@ describe('executions.store', () => {
5858
});
5959

6060
it('should delete executions started before given date', async () => {
61-
const deleteBefore = mockExecutions[1].startedAt;
61+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
62+
const deleteBefore = mockExecutions[1].startedAt!;
6263
await executionsStore.deleteExecutions({ deleteBefore });
6364

6465
expect(executionsStore.executions.length).toBe(2);
6566
executionsStore.executions.forEach(({ startedAt }) =>
66-
expect(startedAt.getTime()).toBeGreaterThanOrEqual(deleteBefore.getTime()),
67+
expect(startedAt?.getTime()).toBeGreaterThanOrEqual(deleteBefore.getTime()),
6768
);
6869
});
6970

packages/frontend/editor-ui/src/stores/executions.store.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const useExecutionsStore = defineStore('executions', () => {
7070

7171
const currentExecutionsById = ref<Record<string, ExecutionSummaryWithScopes>>({});
7272
const startedAtSortFn = (a: ExecutionSummary, b: ExecutionSummary) =>
73-
new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime();
73+
new Date(b.startedAt ?? b.createdAt).getTime() - new Date(a.startedAt ?? a.createdAt).getTime();
7474

7575
/**
7676
* Prioritize `running` over `new` executions, then sort by start timestamp.
@@ -268,7 +268,7 @@ export const useExecutionsStore = defineStore('executions', () => {
268268
if (sendData.deleteBefore) {
269269
const deleteBefore = new Date(sendData.deleteBefore);
270270
allExecutions.value.forEach((execution) => {
271-
if (new Date(execution.startedAt) < deleteBefore) {
271+
if (new Date(execution.startedAt ?? execution.createdAt) < deleteBefore) {
272272
removeExecution(execution.id);
273273
}
274274
});

packages/workflow/src/Interfaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2645,7 +2645,7 @@ export interface ExecutionSummary {
26452645
retrySuccessId?: string | null;
26462646
waitTill?: Date;
26472647
createdAt: Date;
2648-
startedAt: Date;
2648+
startedAt: Date | null;
26492649
stoppedAt?: Date;
26502650
workflowId: string;
26512651
workflowName?: string;

0 commit comments

Comments
 (0)