feat(ui): reconnect release investigation routes [SPRINT-022]

Mount deploy-diff, change-trace, and timeline under /releases/investigation
as bounded secondary routes. Timeline uses correlation-based model to avoid
collision with shipped run-workspace tab.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
master
2026-03-08 19:25:38 +02:00
parent 1b934ad47a
commit 38cbdb79dd
7 changed files with 274 additions and 2 deletions

View File

@@ -1,11 +1,20 @@
// -----------------------------------------------------------------------------
// change-trace.routes.ts
// Sprint: SPRINT_20260112_200_007_FE_ui_components
// Updated: Sprint 022 - Mounted under /releases/investigation/change-trace
// Description: Routes for change-trace feature.
// -----------------------------------------------------------------------------
import { Routes } from '@angular/router';
/**
* Change Trace Routes
*
* Mounted under /releases/investigation/change-trace by releases.routes.ts (Sprint 022).
* Canonical URLs:
* /releases/investigation/change-trace - Trace viewer
* /releases/investigation/change-trace/:traceId - Specific trace detail
*/
export const changeTraceRoutes: Routes = [
{
path: '',
@@ -13,6 +22,8 @@ export const changeTraceRoutes: Routes = [
import('./change-trace-viewer.component').then(
(m) => m.ChangeTraceViewerComponent
),
title: 'Change Trace',
data: { breadcrumb: 'Change Trace' },
},
{
path: ':traceId',
@@ -20,5 +31,7 @@ export const changeTraceRoutes: Routes = [
import('./change-trace-viewer.component').then(
(m) => m.ChangeTraceViewerComponent
),
title: 'Change Trace Detail',
data: { breadcrumb: 'Trace Detail' },
},
];

View File

@@ -1,6 +1,7 @@
/**
* @file deploy-diff.routes.ts
* @sprint SPRINT_20260125_006_FE_ab_deploy_diff_panel (DD-009)
* @updated Sprint 022 - Mounted under /releases/investigation/deploy-diff
* @description Routes for deploy diff feature module.
*/
@@ -9,7 +10,8 @@ import { Routes } from '@angular/router';
/**
* Deploy diff feature routes.
*
* Route: /deploy/diff?from={digest}&to={digest}
* Mounted under /releases/investigation/deploy-diff by releases.routes.ts (Sprint 022).
* Canonical URL: /releases/investigation/deploy-diff?from={digest}&to={digest}
*/
export const DEPLOY_DIFF_ROUTES: Routes = [
{
@@ -18,7 +20,7 @@ export const DEPLOY_DIFF_ROUTES: Routes = [
import('./pages/deploy-diff.page').then(m => m.DeployDiffPage),
title: 'Deployment Diff',
data: {
breadcrumb: 'Compare Versions',
breadcrumb: 'Deploy Diff',
},
},
];

View File

@@ -5,6 +5,17 @@
import { Routes } from '@angular/router';
/**
* Investigation Timeline Routes
*
* Mounted under /releases/investigation/timeline by releases.routes.ts (Sprint 022).
* This is a correlation-based investigation timeline, distinct from the run
* workspace tab at /releases/runs/:runId/timeline which shows run execution flow.
*
* Canonical URLs:
* /releases/investigation/timeline - Timeline overview
* /releases/investigation/timeline/:correlationId - Correlated event drill-in
*/
export const TIMELINE_ROUTES: Routes = [
{
path: '',
@@ -12,6 +23,8 @@ export const TIMELINE_ROUTES: Routes = [
import('./pages/timeline-page/timeline-page.component').then(
(m) => m.TimelinePageComponent
),
title: 'Investigation Timeline',
data: { breadcrumb: 'Timeline' },
},
{
path: ':correlationId',
@@ -19,6 +32,8 @@ export const TIMELINE_ROUTES: Routes = [
import('./pages/timeline-page/timeline-page.component').then(
(m) => m.TimelinePageComponent
),
title: 'Correlated Events',
data: { breadcrumb: 'Correlation' },
},
];

View File

@@ -0,0 +1,84 @@
/**
* Releases Routes - Route structure verification
* Sprint 022: FE-URI-004
*
* Validates that release investigation routes (timeline, deploy-diff,
* change-trace) are mounted under /releases/investigation/ without
* colliding with the shipped run workspace tabs.
*/
import { RELEASES_ROUTES } from './releases.routes';
describe('RELEASES_ROUTES', () => {
it('should export a non-empty route array', () => {
expect(RELEASES_ROUTES).toBeDefined();
expect(Array.isArray(RELEASES_ROUTES)).toBe(true);
expect(RELEASES_ROUTES.length).toBeGreaterThan(0);
});
it('should mount investigation timeline at "investigation/timeline"', () => {
const timelineRoute = RELEASES_ROUTES.find((r) => r.path === 'investigation/timeline');
expect(timelineRoute).toBeDefined();
expect(timelineRoute!.loadChildren).toBeDefined();
expect(timelineRoute!.title).toBe('Investigation Timeline');
expect(timelineRoute!.data?.['breadcrumb']).toBe('Investigation Timeline');
});
it('should mount deploy-diff at "investigation/deploy-diff"', () => {
const diffRoute = RELEASES_ROUTES.find((r) => r.path === 'investigation/deploy-diff');
expect(diffRoute).toBeDefined();
expect(diffRoute!.loadChildren).toBeDefined();
expect(diffRoute!.title).toBe('Deployment Diff');
expect(diffRoute!.data?.['breadcrumb']).toBe('Deploy Diff');
});
it('should mount change-trace at "investigation/change-trace"', () => {
const traceRoute = RELEASES_ROUTES.find((r) => r.path === 'investigation/change-trace');
expect(traceRoute).toBeDefined();
expect(traceRoute!.loadChildren).toBeDefined();
expect(traceRoute!.title).toBe('Change Trace');
expect(traceRoute!.data?.['breadcrumb']).toBe('Change Trace');
});
it('should not collide with the shipped run workspace timeline tab', () => {
// The run workspace tabs include 'timeline' at runs/:runId/timeline
// Investigation timeline must NOT be mounted at runs/:runId/timeline
const investigationTimeline = RELEASES_ROUTES.find(
(r) => r.path === 'investigation/timeline'
);
expect(investigationTimeline).toBeDefined();
// Verify the run workspace tab still exists (generated from RUN_WORKSPACE_TABS)
const runTimelineTab = RELEASES_ROUTES.find(
(r) => r.path === 'runs/:runId/timeline'
);
expect(runTimelineTab).toBeDefined();
// They are different routes
expect(investigationTimeline).not.toBe(runTimelineTab);
});
it('should preserve existing canonical release routes', () => {
const paths = RELEASES_ROUTES.map((r) => r.path);
expect(paths).toContain('overview');
expect(paths).toContain('versions');
expect(paths).toContain('runs');
expect(paths).toContain('deployments');
expect(paths).toContain('bundles');
expect(paths).toContain('approvals');
expect(paths).toContain('promotions');
});
it('should use loadChildren for lazy-loaded investigation routes', () => {
const investigationPaths = [
'investigation/timeline',
'investigation/deploy-diff',
'investigation/change-trace',
];
for (const path of investigationPaths) {
const route = RELEASES_ROUTES.find((r) => r.path === path);
expect(route).toBeDefined();
expect(typeof route!.loadChildren).toBe('function');
expect(route!.loadComponent).toBeUndefined();
}
});
});

View File

@@ -205,6 +205,32 @@ export const RELEASES_ROUTES: Routes = [
loadComponent: () =>
import('../features/deployments/deployments-list-page.component').then((m) => m.DeploymentsListPageComponent),
},
// --- Release investigation routes (Sprint 022) ---
// The investigation timeline is mounted as a bounded secondary route under
// /releases/investigation/timeline to avoid colliding with the shipped
// run workspace tab at /releases/runs/:runId/timeline.
// Decision: bounded-secondary-route (not absorb-into-run-workspace).
{
path: 'investigation/timeline',
title: 'Investigation Timeline',
data: { breadcrumb: 'Investigation Timeline' },
loadChildren: () =>
import('../features/timeline/timeline.routes').then((m) => m.TIMELINE_ROUTES),
},
{
path: 'investigation/deploy-diff',
title: 'Deployment Diff',
data: { breadcrumb: 'Deploy Diff' },
loadChildren: () =>
import('../features/deploy-diff/deploy-diff.routes').then((m) => m.DEPLOY_DIFF_ROUTES),
},
{
path: 'investigation/change-trace',
title: 'Change Trace',
data: { breadcrumb: 'Change Trace' },
loadChildren: () =>
import('../features/change-trace/change-trace.routes').then((m) => m.changeTraceRoutes),
},
{
path: 'bundles',
title: 'Bundles',