fix(editor): Don't render now when startedAt is null (#15283)

This commit is contained in:
Danny Martini
2025-05-14 10:31:52 +02:00
committed by GitHub
parent 0cddc9576f
commit 44ecad5883
12 changed files with 150 additions and 13 deletions

View File

@@ -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');
});
});

View File

@@ -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>

View File

@@ -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');
});
});

View File

@@ -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"

View File

@@ -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()', () => {

View File

@@ -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,

View File

@@ -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()),
);
});

View File

@@ -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);
}
});