fix(editor): Fix the last updated label in the migration report (no-changelog) (#40889)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Grozav 2026-10-12 13:38:00 +00:00 committed by GitHub
parent 2a51f38d55
commit 7ceb84f06c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 69 additions and 23 deletions

View File

@ -156,7 +156,8 @@ const breakingChangeReportDataSchema = {
const breakingChangeReportSchema = z.object(breakingChangeReportDataSchema).strict();
const breakingChangeLightReportDataSchema = {
generatedAt: z.date(),
// Absent until the first sync of the target version completes.
generatedAt: z.date().optional(),
targetVersion: z.string(),
currentVersion: z.string(),
instanceResults: z.array(instanceRuleResultsSchema),

View File

@ -33,8 +33,9 @@ export const MIGRATION_REPORT_TELEMETRY = defineTelemetryEvents({
.describe('One entry per workflow rule with at least one open finding'),
synced_at: z
.string()
.optional()
.describe(
'ISO time the finding table was last filled, which the overview shows as the report date',
'ISO time the finding table was last filled, which the overview shows as the report date. Absent before the first sync',
),
}),
},

View File

@ -4672,6 +4672,30 @@ describe('TelemetryEventRelay', () => {
},
);
});
it('omits synced_at when the report has no sync time', () => {
eventService.emit('migration-report-viewed', {
user: { id: 'user-1' },
targetVersion: 'v3',
refreshed: false,
report: {
report: {
targetVersion: 'v3',
currentVersion: '2.0.0',
instanceResults: [],
workflowResults: [],
},
totalWorkflows: 0,
totalAffectedWorkflows: 0,
shouldCache: false,
},
});
expect(telemetry.track).toHaveBeenCalledWith(
TELEMETRY_EVENT.MIGRATION_REPORT.USER_VIEWED_MIGRATION_REPORT,
expect.objectContaining({ synced_at: undefined }),
);
});
});
describe('workflow history compaction events', () => {

View File

@ -2408,7 +2408,9 @@ export class TelemetryEventRelay extends EventRelay {
impact: rule.ruleImpact,
affected_workflows: rule.nbAffectedWorkflows,
})),
synced_at: new Date(report.report.generatedAt).toISOString(),
synced_at: report.report.generatedAt
? new Date(report.report.generatedAt).toISOString()
: undefined,
});
}

View File

@ -325,16 +325,10 @@ describe('MigrationFindingQueryService', () => {
expect(workflowRepository.count).not.toHaveBeenCalled();
});
it('falls back to the current time as generatedAt when no sync ran yet', async () => {
const now = new Date('2026-06-01T12:00:00.000Z');
vi.useFakeTimers({ now });
try {
const result = await service.getLightReport(TARGET_VERSION, INSTANCE);
it('reports no generatedAt when no sync ran yet', async () => {
const result = await service.getLightReport(TARGET_VERSION, INSTANCE);
expect(result.report.generatedAt).toEqual(now);
} finally {
vi.useRealTimers();
}
expect(result.report.generatedAt).toBeUndefined();
});
});

View File

@ -134,8 +134,7 @@ export class MigrationFindingQueryService {
return {
report: {
// Before the first sync there is nothing to date, so the read time stands in.
generatedAt: sync?.syncedAt ?? new Date(),
generatedAt: sync?.syncedAt,
targetVersion,
currentVersion: N8N_VERSION,
instanceResults,

View File

@ -4613,7 +4613,8 @@
"settings.migrationReport.progress.label": "{compatibleCount} of {totalCount} compatible",
"settings.migrationReport.refreshButton": "Refresh",
"settings.migrationReport.scopeNote": "This report only includes the workflows you can edit.",
"settings.migrationReport.lastSynced": "Last synced {time}",
"settings.migrationReport.lastUpdated": "Last updated {time}",
"settings.migrationReport.neverUpdated": "Never updated",
"settings.migrationReport.tabs.workflowIssues": "Workflow issues",
"settings.migrationReport.tabs.instanceIssues": "Instance issues",
"settings.migrationReport.impact.upgradeBlocked": "Upgrade blocked",

View File

@ -545,7 +545,7 @@ describe('MigrationRules', () => {
},
);
it('should show when the report was last synced, from generatedAt', async () => {
it('should show when the report was last updated, from generatedAt', async () => {
// One year in the past, so the relative label is stable whatever the test date.
const generatedAt = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000);
vi.mocked(breakingChangesApi.getReport).mockResolvedValue(
@ -563,12 +563,26 @@ describe('MigrationRules', () => {
renderComponent();
await waitFor(() => {
const lastSynced = screen.getByTestId('migration-report-last-synced');
expect(lastSynced).toHaveTextContent(/Last synced\s+1 year ago/);
const lastUpdated = screen.getByTestId('migration-report-last-updated');
expect(lastUpdated).toHaveTextContent(/Last updated\s+1 year ago/);
});
});
it('should update the last synced time after Refresh', async () => {
it('should show "Never updated" when the report has no generatedAt', async () => {
vi.mocked(breakingChangesApi.getReport).mockResolvedValue(
createMockReport({ report: { ...mockReport.report, generatedAt: undefined } }),
);
renderComponent();
await waitFor(() => {
expect(screen.getByTestId('migration-report-last-updated')).toHaveTextContent(
'Never updated',
);
});
});
it('should update the last updated time after Refresh', async () => {
const oldDate = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000);
const newDate = new Date(Date.now() - 60 * 1000);
vi.mocked(breakingChangesApi.getReport).mockResolvedValue(
@ -581,13 +595,13 @@ describe('MigrationRules', () => {
renderComponent();
await waitFor(() => {
expect(screen.getByTestId('migration-report-last-synced')).toHaveTextContent(/1 year ago/);
expect(screen.getByTestId('migration-report-last-updated')).toHaveTextContent(/1 year ago/);
});
await userEvent.click(screen.getByText('Refresh'));
await waitFor(() => {
expect(screen.getByTestId('migration-report-last-synced')).toHaveTextContent(
expect(screen.getByTestId('migration-report-last-updated')).toHaveTextContent(
/1 minute ago/,
);
});

View File

@ -218,14 +218,24 @@ const sortedInstanceResults = computed(() => {
v-if="state?.report.generatedAt"
size="small"
color="text-light"
data-test-id="migration-report-last-synced"
:class="$style.NoLineBreak"
data-test-id="migration-report-last-updated"
>
<I18nT keypath="settings.migrationReport.lastSynced" tag="span" scope="global">
<I18nT keypath="settings.migrationReport.lastUpdated" tag="span" scope="global">
<template #time>
<TimeAgo :date="state.report.generatedAt.toString()" />
</template>
</I18nT>
</N8nText>
<N8nText
v-else-if="state"
size="small"
color="text-light"
:class="$style.NoLineBreak"
data-test-id="migration-report-last-updated"
>
{{ i18n.baseText('settings.migrationReport.neverUpdated') }}
</N8nText>
<N8nButton
v-if="canManageReport"
variant="subtle"