mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
fix(editor): Don't render now when startedAt is null (#15283)
This commit is contained in:
@@ -868,7 +868,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
||||
private toSummary(execution: {
|
||||
id: number | string;
|
||||
createdAt?: Date | string;
|
||||
startedAt?: Date | string;
|
||||
startedAt: Date | string | null;
|
||||
stoppedAt?: Date | string;
|
||||
waitTill?: Date | string | null;
|
||||
}): ExecutionSummary {
|
||||
@@ -967,7 +967,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
||||
if (lastId) qb.andWhere('execution.id < :lastId', { lastId });
|
||||
|
||||
if (query.order?.startedAt === 'DESC') {
|
||||
qb.orderBy({ 'execution.startedAt': 'DESC' });
|
||||
qb.orderBy({ 'COALESCE(execution.startedAt, execution.createdAt)': 'DESC' });
|
||||
} else if (query.order?.top) {
|
||||
qb.orderBy(`(CASE WHEN execution.status = '${query.order.top}' THEN 0 ELSE 1 END)`);
|
||||
} else {
|
||||
|
||||
49
packages/cli/test/integration/execution.repository.test.ts
Normal file
49
packages/cli/test/integration/execution.repository.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ExecutionRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { createExecution } from '@test-integration/db/executions';
|
||||
import { createWorkflow } from '@test-integration/db/workflows';
|
||||
|
||||
import * as testDb from './shared/test-db';
|
||||
|
||||
describe('UserRepository', () => {
|
||||
let executionRepository: ExecutionRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
await testDb.init();
|
||||
executionRepository = Container.get(ExecutionRepository);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['ExecutionEntity']);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testDb.terminate();
|
||||
});
|
||||
|
||||
describe('findManyByRangeQuery', () => {
|
||||
test('sort by `createdAt` if `startedAt` is null', async () => {
|
||||
const workflow = await createWorkflow();
|
||||
const execution1 = await createExecution({}, workflow);
|
||||
const execution2 = await createExecution({ startedAt: null }, workflow);
|
||||
const execution3 = await createExecution({}, workflow);
|
||||
|
||||
const executions = await executionRepository.findManyByRangeQuery({
|
||||
workflowId: workflow.id,
|
||||
accessibleWorkflowIds: [workflow.id],
|
||||
kind: 'range',
|
||||
range: { limit: 10 },
|
||||
order: { startedAt: 'DESC' },
|
||||
});
|
||||
|
||||
// Executions are returned in reverse order, and if `startedAt` is not
|
||||
// defined `createdAt` is used.
|
||||
expect(executions.map((e) => e.id)).toStrictEqual([
|
||||
execution3.id,
|
||||
execution2.id,
|
||||
execution1.id,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -39,7 +39,7 @@ export async function createExecution(
|
||||
finished: finished ?? true,
|
||||
mode: mode ?? 'manual',
|
||||
createdAt: new Date(),
|
||||
startedAt: startedAt ?? new Date(),
|
||||
startedAt: startedAt === undefined ? new Date() : startedAt,
|
||||
...(workflow !== undefined && { workflowId: workflow.id }),
|
||||
stoppedAt: stoppedAt ?? new Date(),
|
||||
waitTill: waitTill ?? null,
|
||||
|
||||
@@ -157,4 +157,48 @@ describe('GlobalExecutionsListItem', () => {
|
||||
|
||||
expect(globalExecutionsListItemQueuedTooltipRenderSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vitest.useRealTimers();
|
||||
});
|
||||
|
||||
it('uses `createdAt` to calculate running time if `startedAt` is undefined', async () => {
|
||||
const createdAt = new Date('2024-09-27T12:00:00Z');
|
||||
const now = new Date('2024-09-27T12:30:00Z');
|
||||
vitest.useFakeTimers({ now });
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
execution: { status: 'running', id: 123, workflowName: 'Test Workflow', createdAt },
|
||||
workflowPermissions: {},
|
||||
concurrencyCap: 5,
|
||||
},
|
||||
});
|
||||
|
||||
const executionTimeElement = getByTestId('execution-time');
|
||||
expect(executionTimeElement).toBeVisible();
|
||||
expect(executionTimeElement.textContent).toBe('-1727438401s');
|
||||
});
|
||||
|
||||
it('uses `createdAt` to calculate running time if `startedAt` is undefined and `stoppedAt` is defined', async () => {
|
||||
const createdAt = new Date('2024-09-27T12:00:00Z');
|
||||
const now = new Date('2024-09-27T12:30:00Z');
|
||||
vitest.useFakeTimers({ now });
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
execution: {
|
||||
status: 'running',
|
||||
id: 123,
|
||||
workflowName: 'Test Workflow',
|
||||
createdAt,
|
||||
stoppedAt: now,
|
||||
},
|
||||
workflowPermissions: {},
|
||||
concurrencyCap: 5,
|
||||
},
|
||||
});
|
||||
|
||||
const executionTimeElement = getByTestId('execution-time');
|
||||
expect(executionTimeElement).toBeVisible();
|
||||
expect(executionTimeElement.textContent).toBe('30:00m');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,7 +130,7 @@ const formattedStoppedAtDate = computed(() => {
|
||||
return props.execution.stoppedAt
|
||||
? locale.displayTimer(
|
||||
new Date(props.execution.stoppedAt).getTime() -
|
||||
new Date(props.execution.startedAt).getTime(),
|
||||
new Date(props.execution.startedAt ?? props.execution.createdAt).getTime(),
|
||||
true,
|
||||
)
|
||||
: '';
|
||||
@@ -233,11 +233,11 @@ async function handleActionItemClick(commandData: Command) {
|
||||
<td>
|
||||
{{ formattedStartedAtDate }}
|
||||
</td>
|
||||
<td>
|
||||
<td data-test-id="execution-time">
|
||||
<template v-if="formattedStoppedAtDate">
|
||||
{{ formattedStoppedAtDate }}
|
||||
</template>
|
||||
<ExecutionsTime v-else :start-time="execution.startedAt" />
|
||||
<ExecutionsTime v-else :start-time="execution.startedAt ?? execution.createdAt" />
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="execution.id">{{ execution.id }}</span>
|
||||
|
||||
@@ -172,4 +172,29 @@ describe('WorkflowExecutionsCard', () => {
|
||||
expect(executionTimeElement).toBeVisible();
|
||||
expect(executionTimeElement.textContent).toBe('27 Sep - Starting soon');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vitest.useRealTimers();
|
||||
});
|
||||
|
||||
test('uses `createdAt` to calculate running time if `startedAt` is undefined', () => {
|
||||
const createdAt = new Date('2024-09-27T12:00:00Z');
|
||||
const now = new Date('2024-09-27T12:30:00Z');
|
||||
vitest.useFakeTimers({ now });
|
||||
const props = {
|
||||
execution: {
|
||||
id: '1',
|
||||
mode: 'webhook',
|
||||
status: 'running',
|
||||
createdAt: createdAt.toISOString(),
|
||||
},
|
||||
workflowPermissions: { execute: true },
|
||||
};
|
||||
|
||||
const { getByTestId } = renderComponent({ props });
|
||||
|
||||
const executionTimeElement = getByTestId('execution-time-in-status');
|
||||
expect(executionTimeElement).toBeVisible();
|
||||
expect(executionTimeElement.textContent).toBe('for -1727438401s');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,9 +108,11 @@ function onRetryMenuItemSelect(action: string): void {
|
||||
v-if="executionUIDetails.name === 'running'"
|
||||
:color="isActive ? 'text-dark' : 'text-base'"
|
||||
size="small"
|
||||
data-test-id="execution-time-in-status"
|
||||
>
|
||||
{{ locale.baseText('executionDetails.runningTimeRunning') }}
|
||||
<ExecutionsTime :start-time="execution.startedAt" />
|
||||
<!-- Just here to make typescript happy, since `startedAt` will always be defined for running executions -->
|
||||
<ExecutionsTime :start-time="execution.startedAt ?? execution.createdAt" />
|
||||
</N8nText>
|
||||
<N8nText
|
||||
v-if="executionUIDetails.name === 'new' && execution.createdAt"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useExecutionHelpers } from '@/composables/useExecutionHelpers';
|
||||
import type { ExecutionSummary } from 'n8n-workflow';
|
||||
import { i18n } from '@/plugins/i18n';
|
||||
import { convertToDisplayDate } from '@/utils/formatters/dateFormatter';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
const { resolve, track } = vi.hoisted(() => ({
|
||||
resolve: vi.fn(),
|
||||
@@ -48,6 +49,21 @@ describe('useExecutionHelpers()', () => {
|
||||
expect(uiDetails.runningTime).toEqual('0s');
|
||||
},
|
||||
);
|
||||
|
||||
it('use `createdAt` if `startedAt` is null', async () => {
|
||||
const date = new Date('2025-01-01T00:00:00.000Z');
|
||||
const execution = mock<ExecutionSummary>({
|
||||
id: '1',
|
||||
startedAt: null,
|
||||
createdAt: date,
|
||||
stoppedAt: date,
|
||||
status: 'error',
|
||||
});
|
||||
const { getUIDetails } = useExecutionHelpers();
|
||||
const uiDetails = getUIDetails(execution);
|
||||
|
||||
expect(uiDetails.startTime).toEqual('Jan 1, 00:00:00');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDate()', () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export function useExecutionHelpers() {
|
||||
const status = {
|
||||
name: 'unknown',
|
||||
createdAt: execution.createdAt?.toString() ?? '',
|
||||
startTime: formatDate(execution.startedAt),
|
||||
startTime: formatDate(execution.startedAt ?? execution.createdAt),
|
||||
label: 'Status unknown',
|
||||
runningTime: '',
|
||||
showTimestamp: true,
|
||||
|
||||
@@ -58,12 +58,13 @@ describe('executions.store', () => {
|
||||
});
|
||||
|
||||
it('should delete executions started before given date', async () => {
|
||||
const deleteBefore = mockExecutions[1].startedAt;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const deleteBefore = mockExecutions[1].startedAt!;
|
||||
await executionsStore.deleteExecutions({ deleteBefore });
|
||||
|
||||
expect(executionsStore.executions.length).toBe(2);
|
||||
executionsStore.executions.forEach(({ startedAt }) =>
|
||||
expect(startedAt.getTime()).toBeGreaterThanOrEqual(deleteBefore.getTime()),
|
||||
expect(startedAt?.getTime()).toBeGreaterThanOrEqual(deleteBefore.getTime()),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ export const useExecutionsStore = defineStore('executions', () => {
|
||||
|
||||
const currentExecutionsById = ref<Record<string, ExecutionSummaryWithScopes>>({});
|
||||
const startedAtSortFn = (a: ExecutionSummary, b: ExecutionSummary) =>
|
||||
new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime();
|
||||
new Date(b.startedAt ?? b.createdAt).getTime() - new Date(a.startedAt ?? a.createdAt).getTime();
|
||||
|
||||
/**
|
||||
* Prioritize `running` over `new` executions, then sort by start timestamp.
|
||||
@@ -268,7 +268,7 @@ export const useExecutionsStore = defineStore('executions', () => {
|
||||
if (sendData.deleteBefore) {
|
||||
const deleteBefore = new Date(sendData.deleteBefore);
|
||||
allExecutions.value.forEach((execution) => {
|
||||
if (new Date(execution.startedAt) < deleteBefore) {
|
||||
if (new Date(execution.startedAt ?? execution.createdAt) < deleteBefore) {
|
||||
removeExecution(execution.id);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2645,7 +2645,7 @@ export interface ExecutionSummary {
|
||||
retrySuccessId?: string | null;
|
||||
waitTill?: Date;
|
||||
createdAt: Date;
|
||||
startedAt: Date;
|
||||
startedAt: Date | null;
|
||||
stoppedAt?: Date;
|
||||
workflowId: string;
|
||||
workflowName?: string;
|
||||
|
||||
Reference in New Issue
Block a user