feat(ui): ship topology and trust admin cutover
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# Sprint 20260308_005_FE - Topology And Trust Administration Cutover
|
||||
|
||||
## Topic & Scope
|
||||
- Complete the `Setup` cutover for `Topology` and `Trust & Signing` so the canonical setup surfaces are fully usable and old settings or admin entry points no longer strand operators on placeholder pages or broken links.
|
||||
- Replace stale `/platform/setup/*`, `/settings/trust*`, `/administration/trust*`, and `/admin/*` trust or setup links with mounted canonical routes while preserving bookmark compatibility where practical.
|
||||
- Finish the missing workflow exposure for topology inventory and trust administration so preserved pages are actually reachable from the shell instead of hiding behind weak-route drift.
|
||||
- Working directory: `src/Web/StellaOps.Web/`.
|
||||
- Expected evidence: targeted Angular tests, Playwright setup/trust cutover coverage, shipped UI docs, and archived sprint notes.
|
||||
|
||||
## Dependencies & Concurrency
|
||||
- Depends on the shipped `Platform Ops Consolidation`, `Watchlist`, and `Execution Operations` cutovers already archived in `docs-archived/implplan/`.
|
||||
- Safe parallelism: backend APIs are out of scope; this sprint is limited to frontend code, frontend docs, and verification assets.
|
||||
|
||||
## Documentation Prerequisites
|
||||
- `AGENTS.md`
|
||||
- `docs/modules/ui/AGENTS.md`
|
||||
- `src/Web/StellaOps.Web/AGENTS.md`
|
||||
- `docs/modules/ui/README.md`
|
||||
- `docs/modules/ui/architecture.md`
|
||||
- `docs/modules/ui/implementation_plan.md`
|
||||
- `docs/modules/ui/component-preservation-map/RESTORATION_PRIORITIES.md`
|
||||
- `docs/modules/ui/component-preservation-map/components/weak-route/topology/README.md`
|
||||
- `docs/modules/ui/component-preservation-map/components/weak-route/trust-admin/README.md`
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
### FE-TTA-001 - Freeze canonical setup owner and alias contract
|
||||
Status: DONE
|
||||
Dependency: none
|
||||
Owners: Developer / Implementer
|
||||
Task description:
|
||||
- Make `Setup > Topology` and `Setup > Trust & Signing` the canonical owners for setup inventory and trust administration workflows.
|
||||
- Standardize redirect behavior for stale `platform/setup`, `settings/trust`, `administration/trust`, and old `admin/*` trust-related entry points so they land on mounted canonical pages without dropping context.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Canonical route helpers and alias policy are defined for topology and trust setup pages.
|
||||
- [x] Stale platform or trust entry points land on mounted canonical pages.
|
||||
- [x] Active navigation no longer points trust administration at nonexistent `/admin/*` roots.
|
||||
|
||||
### FE-TTA-002 - Complete topology shell exposure and platform setup handoffs
|
||||
Status: DONE
|
||||
Dependency: FE-TTA-001
|
||||
Owners: Developer / Implementer
|
||||
Task description:
|
||||
- Expose the preserved topology pages through the active setup shell so operators can reach regions, environments, promotion graph, workflows, gate profiles, and related detail flows without relying on typed URLs.
|
||||
- Repair `Platform Setup` quick links and topology drill-ins so they hand off into the canonical `Setup > Topology` subtree instead of stale or broken `platform/setup` routes.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Topology shell navigation exposes the preserved topology pages that are already mounted.
|
||||
- [x] Platform Setup handoffs point to canonical setup or topology routes.
|
||||
- [x] Topology overview and setup entry points use working route-backed drill-ins.
|
||||
|
||||
### FE-TTA-003 - Merge legacy trust settings and issuer entry points into usable trust administration
|
||||
Status: DONE
|
||||
Dependency: FE-TTA-001
|
||||
Owners: Developer / Implementer
|
||||
Task description:
|
||||
- Replace the placeholder `TrustSettingsPageComponent` routes with the real trust-administration shell and merge remaining legacy issuer or trust entry points into that shell.
|
||||
- Keep watchlist, keys, issuers, certificates, audit, air-gap, incidents, and analytics accessible from the canonical trust workspace while preserving operator context from old bookmarks where possible.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Live trust routes no longer render the placeholder trust settings page.
|
||||
- [x] Legacy issuer and trust entry points hand off into the canonical trust workspace.
|
||||
- [x] Trust navigation and summary state remain usable from both setup and legacy entry paths.
|
||||
|
||||
### FE-TTA-004 - Verify cutover, sync docs, and archive
|
||||
Status: DONE
|
||||
Dependency: FE-TTA-002, FE-TTA-003
|
||||
Owners: Developer / Implementer, QA
|
||||
Task description:
|
||||
- Add focused tests for the setup alias contract and repaired topology or trust workflows, then run targeted Angular and Playwright verification.
|
||||
- Record the shipped setup cutover in checked-feature docs and archive the sprint only after every delivery task is done.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Targeted Angular tests cover redirect contracts and repaired topology or trust workflows.
|
||||
- [x] Playwright verifies at least one end-to-end setup journey across topology and trust handoffs.
|
||||
- [x] UI docs and checked-feature notes reflect the shipped behavior.
|
||||
- [x] Sprint moved to `docs-archived/implplan/` only after all tasks are marked DONE.
|
||||
|
||||
## Execution Log
|
||||
| Date (UTC) | Update | Owner |
|
||||
| --- | --- | --- |
|
||||
| 2026-03-08 | Sprint created and moved to DOING for the topology and trust administration cutover. | Codex |
|
||||
| 2026-03-08 | Repaired canonical setup alias helpers, old admin and settings trust bookmarks, navigation targets, and platform-setup handoffs into Topology and Trust & Signing. | Developer |
|
||||
| 2026-03-08 | Expanded topology shell exposure and removed live placeholder trust ownership in favor of the mounted trust-admin workspace. | Developer |
|
||||
| 2026-03-08 | Verified targeted Angular coverage with `npm run test -- --watch=false --include src/tests/platform/platform-setup-routes.spec.ts --include src/tests/topology/topology-routes.spec.ts --include src/tests/topology/topology-shell.component.spec.ts --include src/tests/setup/setup-topology-trust-cutover.spec.ts --include src/tests/trust_admin/trust-scoring-dashboard-ui.behavior.spec.ts`: 20 tests passed across 5 files. | QA |
|
||||
| 2026-03-08 | Verified browser cutover flow with `npx playwright test --config playwright.config.ts tests/e2e/topology-trust-admin-cutover.spec.ts --workers=1`: 1 scenario passed. | QA |
|
||||
| 2026-03-08 | Production build passed via `npm run build`; existing bundle budget warnings remain unchanged from the baseline. | QA |
|
||||
| 2026-03-08 | Synced topology and trust administration docs, checked-feature evidence, and task-board status for archive. | Documentation author |
|
||||
|
||||
## Decisions & Risks
|
||||
- Risk: the current setup area has two overlapping trust surfaces, and one of them is only a placeholder shell.
|
||||
- Mitigation: make the trust-admin shell canonical and route old trust settings entry points into it in the same sprint.
|
||||
- Risk: `Platform Setup` still carries stale quick links and duplicate setup concepts that may drift from the canonical topology shell.
|
||||
- Mitigation: keep topology ownership in `Setup` and repair old handoffs instead of growing another setup product tree.
|
||||
- Risk: some old `/admin/*` links may have no top-level route owner anymore.
|
||||
- Mitigation: either retarget them to canonical setup destinations or add explicit alias redirects so bookmarks still resolve.
|
||||
- Delivery rule: this sprint is only complete when canonical setup routes are mounted, stale trust and topology entry points are repaired, and the core operator journeys are verified end to end.
|
||||
- Reference design note: `docs/modules/ui/topology-trust-administration/README.md`.
|
||||
- Docs synced:
|
||||
- `docs/modules/ui/topology-trust-administration/README.md`
|
||||
- `docs/features/checked/web/topology-trust-administration-ui.md`
|
||||
- `docs/modules/ui/README.md`
|
||||
- `docs/modules/ui/implementation_plan.md`
|
||||
- `docs/modules/ui/TASKS.md`
|
||||
|
||||
## Next Checkpoints
|
||||
- 2026-03-08: archived after implementation, verification, and docs sync completed.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Topology And Trust Administration UI
|
||||
|
||||
## Module
|
||||
Web
|
||||
|
||||
## Status
|
||||
VERIFIED
|
||||
|
||||
## Description
|
||||
Shipped the canonical `Setup > Topology` and `Setup > Trust & Signing` cutover so operators land on mounted setup shells instead of stale `settings`, `administration`, `admin`, or `platform/setup` routes. The topology shell now exposes the preserved setup pages directly, and the trust workspace replaces the live placeholder settings page.
|
||||
|
||||
## Implementation Details
|
||||
- **Feature directories**:
|
||||
- `src/Web/StellaOps.Web/src/app/features/topology/`
|
||||
- `src/Web/StellaOps.Web/src/app/features/trust-admin/`
|
||||
- `src/Web.StellaOps.Web/src/app/features/platform/setup/`
|
||||
- `src/Web.StellaOps.Web/src/app/features/settings/`
|
||||
- **Primary components**:
|
||||
- `topology-shell` (`src/Web.StellaOps.Web/src/app/features/topology/topology-shell.component.ts`)
|
||||
- `trust-admin` (`src/Web.StellaOps.Web/src/app/features/trust-admin/trust-admin.component.ts`)
|
||||
- `platform-setup-home` (`src/Web.StellaOps.Web/src/app/features/platform/setup/platform-setup-home.component.ts`)
|
||||
- **Canonical routes**:
|
||||
- `/setup/topology/overview`
|
||||
- `/setup/topology/regions`
|
||||
- `/setup/topology/promotion-graph`
|
||||
- `/setup/topology/workflows`
|
||||
- `/setup/topology/gate-profiles`
|
||||
- `/setup/trust-signing`
|
||||
- `/setup/trust-signing/issuers`
|
||||
- `/setup/trust-signing/watchlist`
|
||||
- `/setup/trust-signing/analytics`
|
||||
- **Legacy aliases**:
|
||||
- `/platform/setup/*`
|
||||
- `/settings/trust*`
|
||||
- `/administration/trust*`
|
||||
- `/admin/trust*`
|
||||
- `/admin/issuers`
|
||||
- **Secondary entry points**:
|
||||
- `Ops > Platform Setup`
|
||||
- `Setup > Topology`
|
||||
- `Setup > Trust & Signing`
|
||||
- admin navigation bookmarks for trust and notifications
|
||||
|
||||
## E2E Test Plan
|
||||
- **Setup**:
|
||||
- [x] Start the local Angular test server with `npm run serve:test`.
|
||||
- [x] Use a test session with setup and trust-read scopes.
|
||||
- **Core verification**:
|
||||
- [x] Open `/settings/trust` and verify redirect into the canonical trust shell.
|
||||
- [x] Open the `Trusted Issuers` tab and verify issuer data renders.
|
||||
- [x] Open `/admin/trust` and verify bookmark compatibility into the same trust shell.
|
||||
- [x] Open `/ops/platform-setup`, use the setup quick-link handoff, and verify navigation into canonical topology routes.
|
||||
|
||||
## Verification
|
||||
- Run:
|
||||
- `npm run test -- --watch=false --include src/tests/platform/platform-setup-routes.spec.ts --include src/tests/topology/topology-routes.spec.ts --include src/tests/topology/topology-shell.component.spec.ts --include src/tests/setup/setup-topology-trust-cutover.spec.ts --include src/tests/trust_admin/trust-scoring-dashboard-ui.behavior.spec.ts`
|
||||
- `npx playwright test --config playwright.config.ts tests/e2e/topology-trust-admin-cutover.spec.ts --workers=1`
|
||||
- `npm run build`
|
||||
- Tier 0 (source): pass
|
||||
- Tier 1 (build/tests): pass
|
||||
- Tier 2 (behavior): pass
|
||||
- Notes:
|
||||
- Angular targeted tests passed: `5` files, `20` tests.
|
||||
- Playwright passed: `1` topology/trust cutover scenario.
|
||||
- Production build passed; existing bundle-budget warnings remain unchanged from the baseline.
|
||||
- Verified on (UTC): 2026-03-08T08:06:30Z
|
||||
@@ -9,6 +9,8 @@
|
||||
The Console presents operator dashboards for scans, policies, VEX evidence, runtime posture, and admin workflows.
|
||||
|
||||
## Latest updates (2026-03-08)
|
||||
- Shipped the canonical `Setup > Topology` and `Setup > Trust & Signing` cutover, including repaired legacy trust bookmarks, fixed `Platform Setup` handoffs, and expanded topology shell exposure.
|
||||
- Added checked-feature verification for topology and trust administration at `../../features/checked/web/topology-trust-administration-ui.md`.
|
||||
- Shipped the execution-operations cutover for canonical JobEngine, Scheduler, Dead-Letter, and companion Scanner Ops workflows under `Ops > Operations`.
|
||||
- Added checked-feature verification for execution operations at `../../features/checked/web/execution-operations-ui.md`.
|
||||
|
||||
@@ -80,6 +82,7 @@ The Console presents operator dashboards for scans, policies, VEX evidence, runt
|
||||
- ./offline-operations/README.md
|
||||
- ./quota-health-aoc-operations/README.md
|
||||
- ./execution-operations/README.md
|
||||
- ./topology-trust-administration/README.md
|
||||
- ./triage-explainability-workspace/README.md
|
||||
- ./workflow-visualization-replay/README.md
|
||||
- ./contextual-actions-patterns/README.md
|
||||
|
||||
@@ -100,6 +100,10 @@
|
||||
- [DONE] FE-EXO-002 Complete JobEngine and scheduler operator workflows
|
||||
- [DONE] FE-EXO-003 Complete dead-letter and scanner-ops supporting workflows
|
||||
- [DONE] FE-EXO-004 Verify cutover, sync docs, and archive
|
||||
- [DONE] FE-TTA-001 Freeze canonical setup owner and alias contract
|
||||
- [DONE] FE-TTA-002 Complete topology shell exposure and platform setup handoffs
|
||||
- [DONE] FE-TTA-003 Merge legacy trust settings and issuer entry points into usable trust administration
|
||||
- [DONE] FE-TTA-004 Verify cutover, sync docs, and archive
|
||||
- [DONE] FE-PO-001 Freeze Operations overview taxonomy and submenu structure
|
||||
- [DONE] FE-PO-002 Overview page regrouping and blocking-card contract
|
||||
- [DONE] FE-PO-003 Legacy widget absorption matrix for Platform Ops
|
||||
|
||||
@@ -31,11 +31,13 @@ Provide a living plan for UI deliverables, dependencies, and evidence.
|
||||
- `docs/features/checked/web/offline-operations-ui.md` - shipped verification note for the canonical Offline Kit and Feeds & Airgap owner routes, repaired stale aliases, and completed offline shell actions.
|
||||
- `docs/features/checked/web/quota-health-aoc-operations-ui.md` - shipped verification note for canonical quota, health, and AOC owner routes, repaired deep links, route-backed filters, and completed operator actions.
|
||||
- `docs/features/checked/web/execution-operations-ui.md` - shipped verification note for canonical execution routes, repaired jobengine and scheduler aliases, completed dead-letter actions, and usable scanner-support workflows.
|
||||
- `docs/features/checked/web/topology-trust-administration-ui.md` - shipped verification note for canonical topology and trust setup shells, repaired settings/admin/platform aliases, and platform-setup handoffs.
|
||||
- `docs/modules/ui/reachability-witnessing/README.md` - detailed witness and proof UX dossier plus cross-shell deep-link contract.
|
||||
- `docs/modules/ui/platform-ops-consolidation/README.md` - detailed Operations overview taxonomy and legacy absorption plan.
|
||||
- `docs/modules/ui/offline-operations/README.md` - detailed owner-shell contract for Offline Kit, Feeds & Airgap, Evidence handoffs, and stale alias policy.
|
||||
- `docs/modules/ui/quota-health-aoc-operations/README.md` - canonical owner-shell contract for quota, health, and AOC operations cutover plus alias and action rules.
|
||||
- `docs/modules/ui/execution-operations/README.md` - canonical execution owner-shell contract for JobEngine, Scheduler, Dead-Letter, and companion Scanner Ops workflows.
|
||||
- `docs/modules/ui/topology-trust-administration/README.md` - canonical setup owner contract for topology inventory, trust administration, legacy trust redirects, and platform-setup handoffs.
|
||||
- `docs/modules/ui/triage-explainability-workspace/README.md` - detailed artifact workspace and audit-bundle UX dossier.
|
||||
- `docs/modules/ui/workflow-visualization-replay/README.md` - detailed run-detail graph, timeline, replay, and evidence UX dossier.
|
||||
- `docs/modules/ui/contextual-actions-patterns/README.md` - shared placement contract for stray actions, pages, drawers, and tabs.
|
||||
|
||||
85
docs/modules/ui/topology-trust-administration/README.md
Normal file
85
docs/modules/ui/topology-trust-administration/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Topology And Trust Administration
|
||||
|
||||
## Purpose
|
||||
- Make `Setup > Topology` and `Setup > Trust & Signing` the canonical owners for environment inventory and trust administration.
|
||||
- Keep legacy `settings`, `administration`, `admin`, and `platform/setup` entry points usable without preserving the old split-product shells.
|
||||
|
||||
## Canonical Owner
|
||||
- Owner shells:
|
||||
- `Setup > Topology`
|
||||
- `Setup > Trust & Signing`
|
||||
- Primary routes:
|
||||
- `/setup/topology/overview`
|
||||
- `/setup/topology/map`
|
||||
- `/setup/topology/regions`
|
||||
- `/setup/topology/targets`
|
||||
- `/setup/topology/hosts`
|
||||
- `/setup/topology/agents`
|
||||
- `/setup/topology/promotion-graph`
|
||||
- `/setup/topology/workflows`
|
||||
- `/setup/topology/gate-profiles`
|
||||
- `/setup/topology/connectivity`
|
||||
- `/setup/topology/runtime-drift`
|
||||
- `/setup/trust-signing`
|
||||
- `/setup/trust-signing/keys`
|
||||
- `/setup/trust-signing/issuers`
|
||||
- `/setup/trust-signing/certificates`
|
||||
- `/setup/trust-signing/watchlist`
|
||||
- `/setup/trust-signing/watchlist/entries`
|
||||
- `/setup/trust-signing/watchlist/alerts`
|
||||
- `/setup/trust-signing/watchlist/tuning`
|
||||
- `/setup/trust-signing/audit`
|
||||
- `/setup/trust-signing/airgap`
|
||||
- `/setup/trust-signing/incidents`
|
||||
- `/setup/trust-signing/analytics`
|
||||
- Secondary handoff route:
|
||||
- `/ops/platform-setup`
|
||||
|
||||
## Legacy Alias Policy
|
||||
- Preserve stale bookmarks and old links by redirecting:
|
||||
- `/platform/setup`
|
||||
- `/platform/setup/regions-environments`
|
||||
- `/platform/setup/promotion-paths`
|
||||
- `/platform/setup/workflows-gates`
|
||||
- `/platform/setup/gate-profiles`
|
||||
- `/platform/setup/trust-signing`
|
||||
- `/platform/setup/trust-signing/:page`
|
||||
- `/settings/trust`
|
||||
- `/settings/trust/issuers`
|
||||
- `/settings/trust/:page`
|
||||
- `/administration/trust`
|
||||
- `/administration/trust/issuers`
|
||||
- `/administration/trust/:page`
|
||||
- `/admin/trust`
|
||||
- `/admin/trust/:page`
|
||||
- `/admin/issuers`
|
||||
- Redirects must preserve query params and fragments so tenant, region, environment, and tab context survive the handoff.
|
||||
|
||||
## UX Rules
|
||||
- `Platform Setup` is a setup overview and handoff page, not the owner of topology or trust subtrees.
|
||||
- `Topology` owns region, environment, target, agent, promotion, workflow, gate-profile, connectivity, and runtime-drift navigation.
|
||||
- `Trust & Signing` owns keys, issuers, certificates, watchlist, audit, air-gap trust posture, incidents, and analytics.
|
||||
- Legacy settings or admin trust URLs should land directly on the live trust shell instead of placeholder pages.
|
||||
|
||||
## Preserved Value
|
||||
- Keep:
|
||||
- topology inventory and graph drill-ins
|
||||
- promotion, workflow, and gate-profile setup
|
||||
- trust summary, issuer management, certificate inventory, and watchlist
|
||||
- trust audit, incident, analytics, and air-gap administration
|
||||
- Why:
|
||||
- these are core release-setup capabilities, not experimental side branches
|
||||
- the product issue was weak wiring and stale route ownership, not missing product value
|
||||
|
||||
## Shipped In This Cut
|
||||
- Canonical setup alias helpers for trust and platform-setup handoffs.
|
||||
- Top-level `/admin/*` compatibility redirects for trust and notification bookmarks.
|
||||
- Expanded `Topology` shell tabs so preserved mounted pages are reachable from the live setup shell.
|
||||
- Fixed `Platform Setup` quick links so they hand off into canonical `Setup` routes.
|
||||
- Retired live trust-placeholder ownership in favor of the real `Trust Management` shell.
|
||||
|
||||
## Related Docs
|
||||
- `docs/features/checked/web/topology-trust-administration-ui.md`
|
||||
- `docs/modules/ui/watchlist-operations/README.md`
|
||||
- `docs/modules/ui/platform-ops-consolidation/README.md`
|
||||
- `docs/modules/ui/component-preservation-map/RESTORATION_PRIORITIES.md`
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { inject } from '@angular/core';
|
||||
import { Router, Routes } from '@angular/router';
|
||||
|
||||
import {
|
||||
requireAnyScopeGuard,
|
||||
@@ -78,6 +79,30 @@ const requireSetupGuard = requireAnyScopeGuard(
|
||||
'/console/profile',
|
||||
);
|
||||
|
||||
function preserveAppRedirect(template: string) {
|
||||
return ({
|
||||
params,
|
||||
queryParams,
|
||||
fragment,
|
||||
}: {
|
||||
params: Record<string, string>;
|
||||
queryParams: Record<string, string>;
|
||||
fragment?: string | null;
|
||||
}) => {
|
||||
const router = inject(Router);
|
||||
let targetPath = template;
|
||||
|
||||
for (const [name, value] of Object.entries(params ?? {})) {
|
||||
targetPath = targetPath.replaceAll(`:${name}`, encodeURIComponent(value));
|
||||
}
|
||||
|
||||
const target = router.parseUrl(targetPath);
|
||||
target.queryParams = { ...queryParams };
|
||||
target.fragment = fragment ?? null;
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
@@ -170,6 +195,29 @@ export const routes: Routes = [
|
||||
data: { breadcrumb: 'Console Admin' },
|
||||
loadChildren: () => import('./features/console-admin/console-admin.routes').then((m) => m.consoleAdminRoutes),
|
||||
},
|
||||
{
|
||||
path: 'admin',
|
||||
children: [
|
||||
{ path: '', redirectTo: '/administration', pathMatch: 'full' },
|
||||
{ path: 'notifications', redirectTo: preserveAppRedirect('/setup/notifications'), pathMatch: 'full' },
|
||||
{ path: 'notifications/:page', redirectTo: preserveAppRedirect('/setup/notifications/:page'), pathMatch: 'full' },
|
||||
{ path: 'trust', redirectTo: preserveAppRedirect('/setup/trust-signing'), pathMatch: 'full' },
|
||||
{ path: 'trust/:page', redirectTo: preserveAppRedirect('/setup/trust-signing/:page'), pathMatch: 'full' },
|
||||
{
|
||||
path: 'trust/:page/:child',
|
||||
redirectTo: preserveAppRedirect('/setup/trust-signing/:page/:child'),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{ path: 'issuers', redirectTo: preserveAppRedirect('/setup/trust-signing/issuers'), pathMatch: 'full' },
|
||||
{
|
||||
path: 'issuers/:page',
|
||||
redirectTo: preserveAppRedirect('/setup/trust-signing/issuers'),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{ path: 'registries', redirectTo: preserveAppRedirect('/setup/integrations'), pathMatch: 'full' },
|
||||
{ path: '**', redirectTo: '/administration' },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'platform-ops',
|
||||
loadChildren: () => import('./routes/platform-ops.routes').then((m) => m.PLATFORM_OPS_ROUTES),
|
||||
@@ -192,8 +240,43 @@ export const routes: Routes = [
|
||||
path: 'ops',
|
||||
loadChildren: () => import('./routes/platform-ops.routes').then((m) => m.PLATFORM_OPS_ROUTES),
|
||||
},
|
||||
{ path: 'setup', redirectTo: '/setup', pathMatch: 'full' },
|
||||
{ path: 'setup/:rest', redirectTo: '/setup/:rest' },
|
||||
{ path: 'setup', redirectTo: '/ops/platform-setup', pathMatch: 'full' },
|
||||
{
|
||||
path: 'setup/regions-environments',
|
||||
redirectTo: preserveAppRedirect('/setup/topology/regions'),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'setup/promotion-paths',
|
||||
redirectTo: preserveAppRedirect('/setup/topology/promotion-graph'),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'setup/workflows-gates',
|
||||
redirectTo: preserveAppRedirect('/setup/topology/workflows'),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'setup/gate-profiles',
|
||||
redirectTo: preserveAppRedirect('/setup/topology/gate-profiles'),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'setup/trust-signing',
|
||||
redirectTo: preserveAppRedirect('/setup/trust-signing'),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'setup/trust-signing/:page',
|
||||
redirectTo: preserveAppRedirect('/setup/trust-signing/:page'),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'setup/trust-signing/:page/:child',
|
||||
redirectTo: preserveAppRedirect('/setup/trust-signing/:page/:child'),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{ path: 'setup/:rest', redirectTo: preserveAppRedirect('/ops/platform-setup/:rest'), pathMatch: 'full' },
|
||||
{ path: '**', redirectTo: '/ops' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -588,14 +588,14 @@ export const NAVIGATION_GROUPS: NavGroup[] = [
|
||||
{
|
||||
id: 'admin-notifications',
|
||||
label: 'Notification Admin',
|
||||
route: '/admin/notifications',
|
||||
route: '/setup/notifications',
|
||||
icon: 'bell-config',
|
||||
tooltip: 'Configure notification rules, channels, and templates',
|
||||
},
|
||||
{
|
||||
id: 'admin-trust',
|
||||
label: 'Trust Management',
|
||||
route: '/admin/trust',
|
||||
route: '/setup/trust-signing',
|
||||
icon: 'certificate',
|
||||
tooltip: 'Manage signing keys, issuers, and certificates',
|
||||
},
|
||||
@@ -623,7 +623,7 @@ export const NAVIGATION_GROUPS: NavGroup[] = [
|
||||
{
|
||||
id: 'issuer-trust',
|
||||
label: 'Issuer Directory',
|
||||
route: '/admin/issuers',
|
||||
route: '/setup/trust-signing/issuers',
|
||||
icon: 'shield-check',
|
||||
tooltip: 'Manage issuer trust and key lifecycle',
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@ interface TopoLink extends d3.SimulationLinkDatum<TopoNode> {
|
||||
<section class="setup-home">
|
||||
<header class="setup-home__header">
|
||||
<h1 class="setup-home__title">Platform Setup</h1>
|
||||
<p class="setup-home__subtitle">Configure inventory, promotion, workflow, policy. Explore the topology graph below.</p>
|
||||
<p class="setup-home__subtitle">Configure canonical setup inventory, promotion, workflow, policy, and trust handoffs. Explore the topology graph below.</p>
|
||||
</header>
|
||||
|
||||
@if (error()) {
|
||||
@@ -370,13 +370,14 @@ export class PlatformSetupHomeComponent implements AfterViewInit, OnDestroy {
|
||||
private allLinks: TopoLink[] = [];
|
||||
|
||||
readonly quickLinks = [
|
||||
{ title: 'Regions & Environments', description: 'Region-first setup and risk tiers.', route: '/platform/setup/regions-environments' },
|
||||
{ title: 'Promotion Paths', description: 'Promotion flow graph and rules.', route: '/platform/setup/promotion-paths' },
|
||||
{ title: 'Workflows & Gates', description: 'Workflow and gate profile mapping.', route: '/platform/setup/workflows-gates' },
|
||||
{ title: 'Gate Profiles', description: 'Strict, risk-aware, and expedited lanes.', route: '/platform/setup/gate-profiles' },
|
||||
{ title: 'Release Templates', description: 'Release template and evidence defaults.', route: '/platform/setup/release-templates' },
|
||||
{ title: 'Feed Policy', description: 'Freshness thresholds and staleness.', route: '/platform/setup/feed-policy' },
|
||||
{ title: 'Defaults & Guardrails', description: 'Policy impact labels and degraded-mode.', route: '/platform/setup/defaults-guardrails' },
|
||||
{ title: 'Regions & Environments', description: 'Region-first setup and risk tiers.', route: '/setup/topology/regions' },
|
||||
{ title: 'Promotion Paths', description: 'Promotion flow graph and rules.', route: '/setup/topology/promotion-graph' },
|
||||
{ title: 'Workflows & Gates', description: 'Workflow inventory and gate bindings.', route: '/setup/topology/workflows' },
|
||||
{ title: 'Gate Profiles', description: 'Strict, risk-aware, and expedited lanes.', route: '/setup/topology/gate-profiles' },
|
||||
{ title: 'Release Templates', description: 'Release template and evidence defaults.', route: '/ops/platform-setup/release-templates' },
|
||||
{ title: 'Policy Bindings', description: 'Freshness thresholds and feed-policy bindings.', route: '/ops/platform-setup/policy-bindings' },
|
||||
{ title: 'Defaults & Guardrails', description: 'Policy impact labels and degraded-mode.', route: '/ops/platform-setup/defaults-guardrails' },
|
||||
{ title: 'Trust & Signing', description: 'Keys, issuers, watchlist, and trust audit workflows.', route: '/setup/trust-signing' },
|
||||
];
|
||||
|
||||
private readonly nodeColors: Record<TopoNodeKind, string> = {
|
||||
|
||||
@@ -12,28 +12,22 @@ export const PLATFORM_SETUP_ROUTES: Routes = [
|
||||
path: 'regions-environments',
|
||||
title: 'Setup Regions & Environments',
|
||||
data: { breadcrumb: 'Regions & Environments' },
|
||||
loadComponent: () =>
|
||||
import('./platform-setup-regions-environments-page.component').then(
|
||||
(m) => m.PlatformSetupRegionsEnvironmentsPageComponent,
|
||||
),
|
||||
redirectTo: '/setup/topology/regions',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'promotion-paths',
|
||||
title: 'Setup Promotion Paths',
|
||||
data: { breadcrumb: 'Promotion Paths' },
|
||||
loadComponent: () =>
|
||||
import('./platform-setup-promotion-paths-page.component').then(
|
||||
(m) => m.PlatformSetupPromotionPathsPageComponent,
|
||||
),
|
||||
redirectTo: '/setup/topology/promotion-graph',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'workflows-gates',
|
||||
title: 'Setup Workflows & Gates',
|
||||
data: { breadcrumb: 'Workflows & Gates' },
|
||||
loadComponent: () =>
|
||||
import('./platform-setup-workflows-gates-page.component').then(
|
||||
(m) => m.PlatformSetupWorkflowsGatesPageComponent,
|
||||
),
|
||||
redirectTo: '/setup/topology/workflows',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'release-templates',
|
||||
@@ -57,10 +51,8 @@ export const PLATFORM_SETUP_ROUTES: Routes = [
|
||||
path: 'gate-profiles',
|
||||
title: 'Gate Profiles',
|
||||
data: { breadcrumb: 'Gate Profiles' },
|
||||
loadComponent: () =>
|
||||
import('./platform-setup-gate-profiles-page.component').then(
|
||||
(m) => m.PlatformSetupGateProfilesPageComponent,
|
||||
),
|
||||
redirectTo: '/setup/topology/gate-profiles',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'defaults-guardrails',
|
||||
@@ -75,9 +67,7 @@ export const PLATFORM_SETUP_ROUTES: Routes = [
|
||||
path: 'trust-signing',
|
||||
title: 'Trust & Signing',
|
||||
data: { breadcrumb: 'Trust & Signing' },
|
||||
loadComponent: () =>
|
||||
import('../../settings/trust/trust-settings-page.component').then(
|
||||
(m) => m.TrustSettingsPageComponent,
|
||||
),
|
||||
redirectTo: '/setup/trust-signing',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,7 +3,32 @@
|
||||
* Sprint: SPRINT_20260118_002_FE_settings_consolidation
|
||||
*/
|
||||
|
||||
import { Routes } from '@angular/router';
|
||||
import { inject } from '@angular/core';
|
||||
import { Router, Routes } from '@angular/router';
|
||||
|
||||
function redirectToCanonicalSetup(path: string) {
|
||||
return ({
|
||||
params,
|
||||
queryParams,
|
||||
fragment,
|
||||
}: {
|
||||
params: Record<string, string>;
|
||||
queryParams: Record<string, string>;
|
||||
fragment?: string | null;
|
||||
}) => {
|
||||
const router = inject(Router);
|
||||
let targetPath = path;
|
||||
|
||||
for (const [name, value] of Object.entries(params ?? {})) {
|
||||
targetPath = targetPath.replaceAll(`:${name}`, encodeURIComponent(value));
|
||||
}
|
||||
|
||||
const target = router.parseUrl(targetPath);
|
||||
target.queryParams = { ...queryParams };
|
||||
target.fragment = fragment ?? null;
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
export const SETTINGS_ROUTES: Routes = [
|
||||
{
|
||||
@@ -51,16 +76,45 @@ export const SETTINGS_ROUTES: Routes = [
|
||||
{
|
||||
path: 'trust',
|
||||
title: 'Trust & Signing',
|
||||
loadComponent: () =>
|
||||
import('./trust/trust-settings-page.component').then(m => m.TrustSettingsPageComponent),
|
||||
data: { breadcrumb: 'Trust & Signing' },
|
||||
redirectTo: redirectToCanonicalSetup('/setup/trust-signing'),
|
||||
pathMatch: 'full' as const,
|
||||
},
|
||||
{
|
||||
path: 'trust/issuers',
|
||||
title: 'Trust & Signing',
|
||||
redirectTo: redirectToCanonicalSetup('/setup/trust-signing/issuers'),
|
||||
pathMatch: 'full' as const,
|
||||
},
|
||||
{
|
||||
path: 'trust/:page/:child',
|
||||
title: 'Trust & Signing',
|
||||
redirectTo: redirectToCanonicalSetup('/setup/trust-signing/:page/:child'),
|
||||
pathMatch: 'full' as const,
|
||||
},
|
||||
{
|
||||
path: 'trust/:page',
|
||||
title: 'Trust & Signing',
|
||||
loadComponent: () =>
|
||||
import('./trust/trust-settings-page.component').then(m => m.TrustSettingsPageComponent),
|
||||
redirectTo: redirectToCanonicalSetup('/setup/trust-signing/:page'),
|
||||
pathMatch: 'full' as const,
|
||||
},
|
||||
{
|
||||
path: 'trust-signing',
|
||||
title: 'Trust & Signing',
|
||||
redirectTo: redirectToCanonicalSetup('/setup/trust-signing'),
|
||||
pathMatch: 'full' as const,
|
||||
},
|
||||
{
|
||||
path: 'trust-signing/:page',
|
||||
title: 'Trust & Signing',
|
||||
redirectTo: redirectToCanonicalSetup('/setup/trust-signing/:page'),
|
||||
pathMatch: 'full' as const,
|
||||
},
|
||||
{
|
||||
path: 'trust-signing/:page/:child',
|
||||
title: 'Trust & Signing',
|
||||
redirectTo: redirectToCanonicalSetup('/setup/trust-signing/:page/:child'),
|
||||
data: { breadcrumb: 'Trust & Signing' },
|
||||
pathMatch: 'full' as const,
|
||||
},
|
||||
{
|
||||
path: 'security-data',
|
||||
|
||||
@@ -52,9 +52,13 @@ export class TopologyShellComponent {
|
||||
readonly tabs: TabItem[] = [
|
||||
{ id: 'overview', label: 'Overview', route: 'overview' },
|
||||
{ id: 'map', label: 'Map', route: 'map' },
|
||||
{ id: 'regions', label: 'Regions & Environments', route: 'regions' },
|
||||
{ id: 'targets', label: 'Targets', route: 'targets' },
|
||||
{ id: 'hosts', label: 'Hosts', route: 'hosts' },
|
||||
{ id: 'agents', label: 'Agents', route: 'agents' },
|
||||
{ id: 'promotion', label: 'Promotion Graph', route: 'promotion-graph' },
|
||||
{ id: 'workflows', label: 'Workflows', route: 'workflows' },
|
||||
{ id: 'gate-profiles', label: 'Gate Profiles', route: 'gate-profiles' },
|
||||
{ id: 'connectivity', label: 'Connectivity', route: 'connectivity' },
|
||||
{ id: 'drift', label: 'Runtime Drift', route: 'runtime-drift' },
|
||||
];
|
||||
|
||||
@@ -68,6 +68,30 @@ function redirectToEvidence(path: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function redirectToSetup(path: string) {
|
||||
return ({
|
||||
params,
|
||||
queryParams,
|
||||
fragment,
|
||||
}: {
|
||||
params: Record<string, string>;
|
||||
queryParams: Record<string, string>;
|
||||
fragment?: string | null;
|
||||
}) => {
|
||||
const router = inject(Router);
|
||||
let targetPath = path;
|
||||
|
||||
for (const [name, value] of Object.entries(params ?? {})) {
|
||||
targetPath = targetPath.replaceAll(`:${name}`, encodeURIComponent(value));
|
||||
}
|
||||
|
||||
const target = router.parseUrl(targetPath);
|
||||
target.queryParams = { ...queryParams };
|
||||
target.fragment = fragment ?? null;
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
export const ADMINISTRATION_ROUTES: Routes = [
|
||||
// A0 — Administration overview
|
||||
{
|
||||
@@ -294,38 +318,37 @@ export const ADMINISTRATION_ROUTES: Routes = [
|
||||
path: 'trust-signing',
|
||||
title: 'Trust & Signing',
|
||||
data: { breadcrumb: 'Trust & Signing' },
|
||||
loadChildren: () =>
|
||||
import('../features/trust-admin/trust-admin.routes').then(
|
||||
(m) => m.trustAdminRoutes
|
||||
),
|
||||
redirectTo: redirectToSetup('/setup/trust-signing'),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
// Legacy trust sub-paths (formerly /admin/trust/*)
|
||||
{
|
||||
path: 'trust',
|
||||
title: 'Trust & Signing',
|
||||
data: { breadcrumb: 'Trust & Signing' },
|
||||
loadComponent: () =>
|
||||
import('../features/settings/trust/trust-settings-page.component').then(
|
||||
(m) => m.TrustSettingsPageComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'trust/:page',
|
||||
title: 'Trust & Signing',
|
||||
data: { breadcrumb: 'Trust & Signing' },
|
||||
loadComponent: () =>
|
||||
import('../features/settings/trust/trust-settings-page.component').then(
|
||||
(m) => m.TrustSettingsPageComponent
|
||||
),
|
||||
redirectTo: redirectToSetup('/setup/trust-signing'),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'trust/issuers',
|
||||
title: 'Issuers',
|
||||
data: { breadcrumb: 'Issuers' },
|
||||
loadChildren: () =>
|
||||
import('../features/issuer-trust/issuer-trust.routes').then(
|
||||
(m) => m.issuerTrustRoutes
|
||||
),
|
||||
redirectTo: redirectToSetup('/setup/trust-signing/issuers'),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'trust/:page/:child',
|
||||
title: 'Trust & Signing',
|
||||
data: { breadcrumb: 'Trust & Signing' },
|
||||
redirectTo: redirectToSetup('/setup/trust-signing/:page/:child'),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'trust/:page',
|
||||
title: 'Trust & Signing',
|
||||
data: { breadcrumb: 'Trust & Signing' },
|
||||
redirectTo: redirectToSetup('/setup/trust-signing/:page'),
|
||||
pathMatch: 'full',
|
||||
},
|
||||
|
||||
// Legacy alias: /administration/identity-providers → /settings/identity-providers
|
||||
|
||||
@@ -47,6 +47,11 @@ export const TOPOLOGY_ROUTES: Routes = [
|
||||
(m) => m.TopologyRegionsEnvironmentsPageComponent,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'regions-environments',
|
||||
redirectTo: 'regions',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'environments',
|
||||
title: 'Environments',
|
||||
@@ -180,6 +185,11 @@ export const TOPOLOGY_ROUTES: Routes = [
|
||||
(m) => m.TopologyPromotionPathsPageComponent,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'promotion-paths',
|
||||
redirectTo: 'promotion-graph',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'workflows',
|
||||
title: 'Workflows',
|
||||
@@ -194,6 +204,11 @@ export const TOPOLOGY_ROUTES: Routes = [
|
||||
(m) => m.TopologyInventoryPageComponent,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'workflows-gates',
|
||||
redirectTo: 'workflows',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'gate-profiles',
|
||||
title: 'Gate Profiles',
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
import { PLATFORM_SETUP_ROUTES } from '../../app/features/platform/setup/platform-setup.routes';
|
||||
|
||||
describe('PLATFORM_SETUP_ROUTES (pre-alpha)', () => {
|
||||
it('uses policy-bindings as canonical policy setup page', () => {
|
||||
it('keeps policy-bindings as canonical policy setup page', () => {
|
||||
const route = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'policy-bindings');
|
||||
expect(route).toBeDefined();
|
||||
expect(route?.loadComponent).toBeDefined();
|
||||
});
|
||||
|
||||
it('includes gate profiles and defaults guardrails pages', () => {
|
||||
const gateProfiles = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'gate-profiles');
|
||||
it('keeps release templates and defaults guardrails as mounted pages', () => {
|
||||
const releaseTemplates = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'release-templates');
|
||||
const defaults = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'defaults-guardrails');
|
||||
|
||||
expect(gateProfiles?.loadComponent).toBeDefined();
|
||||
expect(releaseTemplates?.loadComponent).toBeDefined();
|
||||
expect(defaults?.loadComponent).toBeDefined();
|
||||
});
|
||||
|
||||
it('contains no redirect aliases', () => {
|
||||
for (const route of PLATFORM_SETUP_ROUTES) {
|
||||
expect(route.redirectTo).toBeUndefined();
|
||||
it('redirects absorbed topology and trust pages into canonical setup owners', () => {
|
||||
const regions = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'regions-environments');
|
||||
const promotion = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'promotion-paths');
|
||||
const workflows = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'workflows-gates');
|
||||
const gateProfiles = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'gate-profiles');
|
||||
const trustSigning = PLATFORM_SETUP_ROUTES.find((item) => item.path === 'trust-signing');
|
||||
|
||||
expect(regions?.redirectTo).toBe('/setup/topology/regions');
|
||||
expect(promotion?.redirectTo).toBe('/setup/topology/promotion-graph');
|
||||
expect(workflows?.redirectTo).toBe('/setup/topology/workflows');
|
||||
expect(gateProfiles?.redirectTo).toBe('/setup/topology/gate-profiles');
|
||||
expect(trustSigning?.redirectTo).toBe('/setup/trust-signing');
|
||||
});
|
||||
|
||||
it('retains mounted routes for the remaining platform setup surfaces', () => {
|
||||
const mountedPaths = ['policy-bindings', 'release-templates', 'defaults-guardrails'];
|
||||
|
||||
for (const path of mountedPaths) {
|
||||
const route = PLATFORM_SETUP_ROUTES.find((item) => item.path === path);
|
||||
expect(route?.loadComponent).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideRouter, Route, Router } from '@angular/router';
|
||||
|
||||
import { NAVIGATION_GROUPS } from '../../app/core/navigation/navigation.config';
|
||||
import { SETTINGS_ROUTES } from '../../app/features/settings/settings.routes';
|
||||
import { routes } from '../../app/app.routes';
|
||||
import { ADMINISTRATION_ROUTES } from '../../app/routes/administration.routes';
|
||||
|
||||
function resolveRedirect(route: Route | undefined, params: Record<string, string> = {}): string | undefined {
|
||||
const redirect = route?.redirectTo;
|
||||
if (typeof redirect === 'string') {
|
||||
return redirect;
|
||||
}
|
||||
|
||||
if (typeof redirect !== 'function') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return TestBed.runInInjectionContext(() => {
|
||||
const router = TestBed.inject(Router);
|
||||
const target = redirect({
|
||||
params,
|
||||
queryParams: {},
|
||||
fragment: null,
|
||||
} as never) as unknown;
|
||||
|
||||
return typeof target === 'string' ? target : router.serializeUrl(target as never);
|
||||
});
|
||||
}
|
||||
|
||||
describe('setup topology trust cutover contract', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([])],
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects legacy settings trust routes to canonical setup trust-signing pages', () => {
|
||||
const root = SETTINGS_ROUTES[0];
|
||||
const children = root.children ?? [];
|
||||
|
||||
expect(resolveRedirect(children.find((route) => route.path === 'trust'))).toBe('/setup/trust-signing');
|
||||
expect(resolveRedirect(children.find((route) => route.path === 'trust/issuers'))).toBe(
|
||||
'/setup/trust-signing/issuers',
|
||||
);
|
||||
expect(resolveRedirect(children.find((route) => route.path === 'trust/:page'), { page: 'analytics' })).toBe(
|
||||
'/setup/trust-signing/analytics',
|
||||
);
|
||||
expect(
|
||||
resolveRedirect(children.find((route) => route.path === 'trust/:page/:child'), {
|
||||
page: 'watchlist',
|
||||
child: 'alerts',
|
||||
}),
|
||||
).toBe('/setup/trust-signing/watchlist/alerts');
|
||||
expect(resolveRedirect(children.find((route) => route.path === 'trust-signing'))).toBe('/setup/trust-signing');
|
||||
});
|
||||
|
||||
it('redirects administration trust routes into canonical setup trust-signing pages', () => {
|
||||
expect(resolveRedirect(ADMINISTRATION_ROUTES.find((route) => route.path === 'trust-signing'))).toBe(
|
||||
'/setup/trust-signing',
|
||||
);
|
||||
expect(resolveRedirect(ADMINISTRATION_ROUTES.find((route) => route.path === 'trust'))).toBe('/setup/trust-signing');
|
||||
expect(resolveRedirect(ADMINISTRATION_ROUTES.find((route) => route.path === 'trust/issuers'))).toBe(
|
||||
'/setup/trust-signing/issuers',
|
||||
);
|
||||
expect(
|
||||
resolveRedirect(ADMINISTRATION_ROUTES.find((route) => route.path === 'trust/:page'), { page: 'analytics' }),
|
||||
).toBe('/setup/trust-signing/analytics');
|
||||
expect(
|
||||
resolveRedirect(ADMINISTRATION_ROUTES.find((route) => route.path === 'trust/:page/:child'), {
|
||||
page: 'watchlist',
|
||||
child: 'tuning',
|
||||
}),
|
||||
).toBe('/setup/trust-signing/watchlist/tuning');
|
||||
});
|
||||
|
||||
it('provides top-level admin aliases for notification and trust bookmarks', () => {
|
||||
const admin = routes.find((route) => route.path === 'admin');
|
||||
expect(admin).toBeDefined();
|
||||
|
||||
const childPaths = (admin?.children ?? []).map((route) => route.path);
|
||||
expect(childPaths).toEqual([
|
||||
'',
|
||||
'notifications',
|
||||
'notifications/:page',
|
||||
'trust',
|
||||
'trust/:page',
|
||||
'trust/:page/:child',
|
||||
'issuers',
|
||||
'issuers/:page',
|
||||
'registries',
|
||||
'**',
|
||||
]);
|
||||
});
|
||||
|
||||
it('retargets active admin navigation links to mounted setup destinations', () => {
|
||||
const adminGroup = NAVIGATION_GROUPS.find((group) => group.id === 'admin');
|
||||
expect(adminGroup).toBeDefined();
|
||||
|
||||
const itemById = new Map((adminGroup?.items ?? []).map((item) => [item.id, item.route]));
|
||||
|
||||
expect(itemById.get('admin-notifications')).toBe('/setup/notifications');
|
||||
expect(itemById.get('admin-trust')).toBe('/setup/trust-signing');
|
||||
expect(itemById.get('issuer-trust')).toBe('/setup/trust-signing/issuers');
|
||||
});
|
||||
|
||||
it('preserves top-level platform setup aliases for topology and trust destinations', () => {
|
||||
const platform = routes.find((route) => route.path === 'platform');
|
||||
expect(platform).toBeDefined();
|
||||
|
||||
const children = platform?.children ?? [];
|
||||
|
||||
expect(resolveRedirect(children.find((route) => route.path === 'setup/regions-environments'))).toBe(
|
||||
'/setup/topology/regions',
|
||||
);
|
||||
expect(resolveRedirect(children.find((route) => route.path === 'setup/promotion-paths'))).toBe(
|
||||
'/setup/topology/promotion-graph',
|
||||
);
|
||||
expect(resolveRedirect(children.find((route) => route.path === 'setup/workflows-gates'))).toBe(
|
||||
'/setup/topology/workflows',
|
||||
);
|
||||
expect(resolveRedirect(children.find((route) => route.path === 'setup/gate-profiles'))).toBe(
|
||||
'/setup/topology/gate-profiles',
|
||||
);
|
||||
expect(resolveRedirect(children.find((route) => route.path === 'setup/trust-signing'))).toBe('/setup/trust-signing');
|
||||
expect(
|
||||
resolveRedirect(children.find((route) => route.path === 'setup/trust-signing/:page'), { page: 'issuers' }),
|
||||
).toBe(
|
||||
'/setup/trust-signing/issuers',
|
||||
);
|
||||
expect(
|
||||
resolveRedirect(children.find((route) => route.path === 'setup/trust-signing/:page/:child'), {
|
||||
page: 'watchlist',
|
||||
child: 'alerts',
|
||||
}),
|
||||
).toBe(
|
||||
'/setup/trust-signing/watchlist/alerts',
|
||||
);
|
||||
expect(resolveRedirect(children.find((route) => route.path === 'setup/:rest'), { rest: 'policy-bindings' })).toBe(
|
||||
'/ops/platform-setup/policy-bindings',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,11 @@
|
||||
import { TOPOLOGY_ROUTES } from '../../app/routes/topology.routes';
|
||||
|
||||
describe('TOPOLOGY_ROUTES dedicated pages', () => {
|
||||
const topologyRoot = TOPOLOGY_ROUTES.find((item) => item.path === '');
|
||||
const childRoutes = topologyRoot?.children ?? [];
|
||||
|
||||
async function loadComponentName(path: string): Promise<string | null> {
|
||||
const route = TOPOLOGY_ROUTES.find((item) => item.path === path);
|
||||
const route = childRoutes.find((item) => item.path === path);
|
||||
expect(route).toBeDefined();
|
||||
expect(route?.loadComponent).toBeDefined();
|
||||
const component = await route!.loadComponent!();
|
||||
@@ -32,7 +35,12 @@ describe('TOPOLOGY_ROUTES dedicated pages', () => {
|
||||
});
|
||||
|
||||
it('keeps promotion-paths as an alias redirect', () => {
|
||||
const alias = TOPOLOGY_ROUTES.find((item) => item.path === 'promotion-paths');
|
||||
const alias = childRoutes.find((item) => item.path === 'promotion-paths');
|
||||
expect(alias?.redirectTo).toBe('promotion-graph');
|
||||
});
|
||||
|
||||
it('keeps regions-environments and workflows-gates as canonical aliases', () => {
|
||||
expect(childRoutes.find((item) => item.path === 'regions-environments')?.redirectTo).toBe('regions');
|
||||
expect(childRoutes.find((item) => item.path === 'workflows-gates')?.redirectTo).toBe('workflows');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { TopologyShellComponent } from '../../app/features/topology/topology-shell.component';
|
||||
|
||||
describe('TopologyShellComponent', () => {
|
||||
it('exposes canonical tabs for the preserved topology pages', () => {
|
||||
const component = new TopologyShellComponent();
|
||||
|
||||
expect(component.tabs).toEqual([
|
||||
{ id: 'overview', label: 'Overview', route: 'overview' },
|
||||
{ id: 'map', label: 'Map', route: 'map' },
|
||||
{ id: 'regions', label: 'Regions & Environments', route: 'regions' },
|
||||
{ id: 'targets', label: 'Targets', route: 'targets' },
|
||||
{ id: 'hosts', label: 'Hosts', route: 'hosts' },
|
||||
{ id: 'agents', label: 'Agents', route: 'agents' },
|
||||
{ id: 'promotion', label: 'Promotion Graph', route: 'promotion-graph' },
|
||||
{ id: 'workflows', label: 'Workflows', route: 'workflows' },
|
||||
{ id: 'gate-profiles', label: 'Gate Profiles', route: 'gate-profiles' },
|
||||
{ id: 'connectivity', label: 'Connectivity', route: 'connectivity' },
|
||||
{ id: 'drift', label: 'Runtime Drift', route: 'runtime-drift' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
|
||||
import type { StubAuthSession } from '../../src/app/testing/auth-fixtures';
|
||||
|
||||
const operatorSession: StubAuthSession = {
|
||||
subjectId: 'setup-e2e-user',
|
||||
tenant: 'tenant-default',
|
||||
scopes: [
|
||||
'admin',
|
||||
'ui.read',
|
||||
'ui.admin',
|
||||
'orch:read',
|
||||
'orch:operate',
|
||||
'release:read',
|
||||
'signer:read',
|
||||
],
|
||||
};
|
||||
|
||||
const mockConfig = {
|
||||
authority: {
|
||||
issuer: '/authority',
|
||||
clientId: 'stella-ops-ui',
|
||||
authorizeEndpoint: '/authority/connect/authorize',
|
||||
tokenEndpoint: '/authority/connect/token',
|
||||
logoutEndpoint: '/authority/connect/logout',
|
||||
redirectUri: 'https://127.0.0.1:4400/auth/callback',
|
||||
postLogoutRedirectUri: 'https://127.0.0.1:4400/',
|
||||
scope: 'openid profile email ui.read',
|
||||
audience: '/gateway',
|
||||
dpopAlgorithms: ['ES256'],
|
||||
refreshLeewaySeconds: 60,
|
||||
},
|
||||
apiBaseUrls: {
|
||||
authority: '/authority',
|
||||
scanner: '/scanner',
|
||||
policy: '/policy',
|
||||
concelier: '/concelier',
|
||||
attestor: '/attestor',
|
||||
gateway: '/gateway',
|
||||
},
|
||||
quickstartMode: true,
|
||||
setup: 'complete',
|
||||
};
|
||||
|
||||
const trustDashboardSummary = {
|
||||
keys: {
|
||||
total: 4,
|
||||
active: 3,
|
||||
expiringSoon: 1,
|
||||
expired: 0,
|
||||
revoked: 0,
|
||||
pendingRotation: 0,
|
||||
},
|
||||
issuers: {
|
||||
total: 2,
|
||||
fullTrust: 1,
|
||||
partialTrust: 1,
|
||||
minimalTrust: 0,
|
||||
untrusted: 0,
|
||||
blocked: 0,
|
||||
averageTrustScore: 89.5,
|
||||
},
|
||||
certificates: {
|
||||
total: 3,
|
||||
valid: 3,
|
||||
expiringSoon: 0,
|
||||
expired: 0,
|
||||
revoked: 0,
|
||||
invalidChains: 0,
|
||||
},
|
||||
recentEvents: [],
|
||||
expiryAlerts: [],
|
||||
};
|
||||
|
||||
const trustIssuers = {
|
||||
items: [
|
||||
{
|
||||
issuerId: 'issuer-001',
|
||||
tenantId: 'tenant-default',
|
||||
name: 'github-security-advisories',
|
||||
displayName: 'GitHub Security Advisories',
|
||||
description: 'Official GitHub advisory issuer',
|
||||
issuerType: 'csaf_publisher',
|
||||
trustLevel: 'full',
|
||||
trustScore: 95,
|
||||
publicKeyFingerprints: ['SHA256:issuer-001'],
|
||||
documentCount: 1200,
|
||||
verificationCount: 1188,
|
||||
weights: {
|
||||
baseWeight: 80,
|
||||
recencyFactor: 10,
|
||||
verificationBonus: 15,
|
||||
volumePenalty: 2,
|
||||
manualAdjustment: 0,
|
||||
},
|
||||
validFrom: '2025-01-01T00:00:00Z',
|
||||
lastVerifiedAt: '2026-03-08T06:00:00Z',
|
||||
isActive: true,
|
||||
createdAt: '2025-01-01T00:00:00Z',
|
||||
updatedAt: '2026-03-08T06:00:00Z',
|
||||
},
|
||||
],
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
totalCount: 1,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
const topologyRegions = [
|
||||
{ regionId: 'eu-west', displayName: 'EU West', environmentCount: 1, targetCount: 2 },
|
||||
];
|
||||
|
||||
const topologyEnvironments = [
|
||||
{
|
||||
environmentId: 'prod',
|
||||
regionId: 'eu-west',
|
||||
environmentType: 'prod',
|
||||
displayName: 'Production',
|
||||
targetCount: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const topologyTargets = [
|
||||
{
|
||||
targetId: 'target-001',
|
||||
environmentId: 'prod',
|
||||
regionId: 'eu-west',
|
||||
name: 'Gateway',
|
||||
targetType: 'vm',
|
||||
healthStatus: 'healthy',
|
||||
agentId: 'agent-001',
|
||||
},
|
||||
];
|
||||
|
||||
const topologyAgents = [
|
||||
{
|
||||
agentId: 'agent-001',
|
||||
agentName: 'Agent One',
|
||||
regionId: 'eu-west',
|
||||
environmentId: 'prod',
|
||||
status: 'active',
|
||||
assignedTargetCount: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const promotionPaths = [
|
||||
{
|
||||
pathId: 'path-001',
|
||||
regionId: 'eu-west',
|
||||
sourceEnvironmentId: 'stage',
|
||||
targetEnvironmentId: 'prod',
|
||||
status: 'running',
|
||||
requiredApprovals: 1,
|
||||
},
|
||||
];
|
||||
|
||||
async function fulfillJson(route: Route, body: unknown, status = 200): Promise<void> {
|
||||
await route.fulfill({
|
||||
status,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
async function setupHarness(page: Page): Promise<void> {
|
||||
await page.addInitScript((session) => {
|
||||
(window as { __stellaopsTestSession?: unknown }).__stellaopsTestSession = session;
|
||||
}, operatorSession);
|
||||
|
||||
await page.route('**/api/**', (route) => fulfillJson(route, {}));
|
||||
await page.route('**/platform/envsettings.json', (route) => fulfillJson(route, mockConfig));
|
||||
await page.route('**/platform/i18n/*.json', (route) => fulfillJson(route, {}));
|
||||
await page.route('**/config.json', (route) => fulfillJson(route, mockConfig));
|
||||
await page.route('**/.well-known/openid-configuration', (route) =>
|
||||
fulfillJson(route, {
|
||||
issuer: 'https://127.0.0.1:4400/authority',
|
||||
authorization_endpoint: 'https://127.0.0.1:4400/authority/connect/authorize',
|
||||
token_endpoint: 'https://127.0.0.1:4400/authority/connect/token',
|
||||
jwks_uri: 'https://127.0.0.1:4400/authority/.well-known/jwks.json',
|
||||
response_types_supported: ['code'],
|
||||
subject_types_supported: ['public'],
|
||||
id_token_signing_alg_values_supported: ['RS256'],
|
||||
}),
|
||||
);
|
||||
await page.route('**/authority/.well-known/jwks.json', (route) => fulfillJson(route, { keys: [] }));
|
||||
await page.route('**/console/branding**', (route) =>
|
||||
fulfillJson(route, {
|
||||
tenantId: operatorSession.tenant,
|
||||
appName: 'Stella Ops',
|
||||
logoUrl: null,
|
||||
cssVariables: {},
|
||||
}),
|
||||
);
|
||||
await page.route('**/console/profile**', (route) =>
|
||||
fulfillJson(route, {
|
||||
subjectId: operatorSession.subjectId,
|
||||
username: 'setup-e2e',
|
||||
displayName: 'Setup E2E',
|
||||
tenant: operatorSession.tenant,
|
||||
roles: ['platform-admin'],
|
||||
scopes: operatorSession.scopes,
|
||||
}),
|
||||
);
|
||||
await page.route('**/console/token/introspect**', (route) =>
|
||||
fulfillJson(route, {
|
||||
active: true,
|
||||
tenant: operatorSession.tenant,
|
||||
subject: operatorSession.subjectId,
|
||||
scopes: operatorSession.scopes,
|
||||
}),
|
||||
);
|
||||
await page.route('**/authority/console/tenants**', (route) =>
|
||||
fulfillJson(route, {
|
||||
tenants: [
|
||||
{
|
||||
tenantId: operatorSession.tenant,
|
||||
displayName: 'Default Tenant',
|
||||
isDefault: true,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
await page.route('**/api/v2/context/regions**', (route) =>
|
||||
fulfillJson(route, [{ regionId: 'eu-west', displayName: 'EU West', sortOrder: 1, enabled: true }]),
|
||||
);
|
||||
await page.route('**/api/v2/context/environments**', (route) =>
|
||||
fulfillJson(route, [
|
||||
{
|
||||
environmentId: 'prod',
|
||||
regionId: 'eu-west',
|
||||
environmentType: 'prod',
|
||||
displayName: 'Production',
|
||||
sortOrder: 1,
|
||||
enabled: true,
|
||||
},
|
||||
]),
|
||||
);
|
||||
await page.route('**/api/v2/context/preferences**', (route) =>
|
||||
fulfillJson(route, {
|
||||
tenantId: operatorSession.tenant,
|
||||
actorId: operatorSession.subjectId,
|
||||
regions: ['eu-west'],
|
||||
environments: ['prod'],
|
||||
timeWindow: '24h',
|
||||
stage: 'all',
|
||||
updatedAt: '2026-03-08T07:00:00Z',
|
||||
updatedBy: operatorSession.subjectId,
|
||||
}),
|
||||
);
|
||||
|
||||
await page.route(/\/api\/v1\/trust\/dashboard(?:\?.*)?$/, (route) => fulfillJson(route, trustDashboardSummary));
|
||||
await page.route(/\/api\/v1\/trust\/issuers(?:\?.*)?$/, (route) => fulfillJson(route, trustIssuers));
|
||||
await page.route(/\/api\/v2\/topology\/regions(?:\?.*)?$/, (route) => fulfillJson(route, topologyRegions));
|
||||
await page.route(/\/api\/v2\/topology\/environments(?:\?.*)?$/, (route) =>
|
||||
fulfillJson(route, topologyEnvironments),
|
||||
);
|
||||
await page.route(/\/api\/v2\/topology\/targets(?:\?.*)?$/, (route) => fulfillJson(route, topologyTargets));
|
||||
await page.route(/\/api\/v2\/topology\/agents(?:\?.*)?$/, (route) => fulfillJson(route, topologyAgents));
|
||||
await page.route(/\/api\/v2\/topology\/promotion-paths(?:\?.*)?$/, (route) =>
|
||||
fulfillJson(route, promotionPaths),
|
||||
);
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupHarness(page);
|
||||
});
|
||||
|
||||
test('topology and trust cutover keeps setup handoffs and legacy trust entry points usable', async ({ page }) => {
|
||||
await page.goto('/settings/trust', { waitUntil: 'networkidle' });
|
||||
await expect(page).toHaveURL(/\/setup\/trust-signing(?:\?.*)?$/);
|
||||
await expect(page.getByRole('heading', { name: 'Trust Management' })).toBeVisible();
|
||||
await page.getByRole('tab', { name: 'Trusted Issuers' }).click();
|
||||
await expect(page).toHaveURL(/\/setup\/trust-signing\/issuers(?:\?.*)?$/);
|
||||
await expect(page.getByText('GitHub Security Advisories')).toBeVisible();
|
||||
|
||||
await page.goto('/admin/trust', { waitUntil: 'networkidle' });
|
||||
await expect(page).toHaveURL(/\/setup\/trust-signing(?:\?.*)?$/);
|
||||
await expect(page.getByRole('tab', { name: 'Watchlist' })).toBeVisible();
|
||||
|
||||
await page.goto('/ops/platform-setup', { waitUntil: 'networkidle' });
|
||||
const regionsCard = page.locator('.setup-home__card').filter({ hasText: 'Regions & Environments' });
|
||||
await regionsCard.getByRole('link', { name: 'Open' }).click();
|
||||
await expect(page).toHaveURL(/\/setup\/topology\/regions(?:\?.*)?$/);
|
||||
await expect(page.getByRole('heading', { name: 'Topology' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Regions & Environments' })).toBeVisible();
|
||||
});
|
||||
Reference in New Issue
Block a user