diff --git a/src/Web/StellaOps.Web/src/app/features/policy-decisioning/policy-decisioning-shell.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-decisioning/policy-decisioning-shell.component.ts index f486254ec..b9559e65f 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-decisioning/policy-decisioning-shell.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-decisioning/policy-decisioning-shell.component.ts @@ -1,79 +1,18 @@ -import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, - DestroyRef, - computed, - inject, - signal, } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { - ActivatedRoute, - ActivatedRouteSnapshot, - NavigationEnd, - Router, - RouterLink, RouterOutlet, } from '@angular/router'; -import { filter, startWith } from 'rxjs'; - -import { - ContextHeaderComponent, -} from '../../shared/ui'; -import { - buildContextRouteParams, -} from '../../shared/ui/context-route-state/context-route-state'; - -type DecisioningContextKind = - | 'global' - | 'pack' - | 'release' - | 'approval' - | 'workflow' - | 'evidence'; - -interface DecisioningShellState { - readonly kind: DecisioningContextKind; - readonly packId: string | null; - readonly releaseId: string | null; - readonly approvalId: string | null; - readonly environment: string | null; - readonly artifact: string | null; - readonly returnTo: string | null; - readonly workflowId: string | null; - readonly evidenceId: string | null; -} @Component({ selector: 'app-policy-decisioning-shell', imports: [ - CommonModule, - RouterLink, RouterOutlet, - ContextHeaderComponent, ], template: `
- - - Reset view - - -
`, @@ -85,245 +24,9 @@ interface DecisioningShellState { .policy-decisioning-shell { display: grid; - gap: 1rem; - padding: 1.25rem; - } - - .shell-action { - display: inline-flex; - align-items: center; - border: 1px solid var(--color-border-primary); - border-radius: 0.75rem; - background: var(--color-surface-secondary); - color: var(--color-text-primary); - font-weight: 600; - padding: 0.6rem 0.9rem; - text-decoration: none; + gap: 0; } `], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class PolicyDecisioningShellComponent { - private readonly route = inject(ActivatedRoute); - private readonly router = inject(Router); - private readonly destroyRef = inject(DestroyRef); - - readonly shellState = signal(this.readShellState()); - - readonly headerTitle = computed(() => { - const state = this.shellState(); - - switch (state.kind) { - case 'pack': - return `Policy Pack ${state.packId}`; - case 'release': - return `Release ${state.releaseId} Decisioning`; - case 'approval': - return `Approval ${state.approvalId} Decisioning`; - case 'workflow': - return `Workflow ${state.workflowId} Decisioning`; - case 'evidence': - return `Evidence ${state.evidenceId} Decisioning`; - default: - return 'Policy Decisioning Studio'; - } - }); - - readonly headerSubtitle = computed(() => { - const state = this.shellState(); - - switch (state.kind) { - case 'pack': - return 'Author, validate, approve, and explain a pack from the canonical policy shell.'; - case 'release': - case 'approval': - return 'Review gates, simulation, VEX, and exceptions for a live release decision without leaving the shared shell.'; - case 'workflow': - return 'Keep workflow gate logic and release policy inspection in one decisioning workspace.'; - case 'evidence': - return 'Trace evidence, gate posture, and policy or VEX actions from a single canonical route.'; - default: - return 'Policy packs, governance, simulation, VEX, exceptions, and audit.'; - } - }); - - readonly headerNote = computed(() => { - const state = this.shellState(); - - switch (state.kind) { - case 'release': - return 'Release-context mode keeps gate review and operator actions inside the shared policy shell.'; - case 'approval': - return 'Approval-context mode preserves policy and VEX actions while allowing a direct return to the approval flow.'; - case 'pack': - return 'Pack mode exposes authoring, YAML, rules, approvals, simulation, and explainability for the selected pack.'; - case 'workflow': - return 'Workflow context is carried as a non-owning deep link for release pipeline editing.'; - case 'evidence': - return 'Evidence context is non-owning: Decisioning Studio stays focused on gates, policy, and VEX actions.'; - default: - return null; - } - }); - - readonly headerChips = computed(() => { - const state = this.shellState(); - const chips: string[] = []; - - if (state.packId) { - chips.push(`Pack ${state.packId}`); - } - if (state.releaseId) { - chips.push(`Release ${state.releaseId}`); - } - if (state.approvalId) { - chips.push(`Approval ${state.approvalId}`); - } - if (state.environment) { - chips.push(`Env ${state.environment}`); - } - if (state.artifact) { - chips.push(`Artifact ${truncateValue(state.artifact)}`); - } - if (state.workflowId) { - chips.push(`Workflow ${state.workflowId}`); - } - if (state.evidenceId) { - chips.push(`Evidence ${state.evidenceId}`); - } - - return chips; - }); - - constructor() { - this.router.events - .pipe( - filter((event): event is NavigationEnd => event instanceof NavigationEnd), - startWith(null), - takeUntilDestroyed(this.destroyRef), - ) - .subscribe(() => { - this.shellState.set(this.readShellState()); - }); - } - - overviewRoute(): readonly unknown[] { - return ['/ops/policy/overview']; - } - - gatesRoute(): readonly unknown[] { - const state = this.shellState(); - - if (state.approvalId) { - return ['/ops/policy/gates/approvals', state.approvalId]; - } - if (state.releaseId) { - return ['/ops/policy/gates/releases', state.releaseId]; - } - if (state.environment) { - return ['/ops/policy/gates/environments', state.environment]; - } - - return ['/ops/policy/gates']; - } - - contextQueryParams(): Record { - const state = this.shellState(); - - return buildContextRouteParams({ - releaseId: state.releaseId, - approvalId: state.approvalId, - environment: state.environment, - artifact: state.artifact, - returnTo: state.returnTo, - workflowId: state.workflowId, - evidenceId: state.evidenceId, - }); - } - - returnToSource(): void { - const returnTo = this.shellState().returnTo; - if (!returnTo) { - return; - } - - void this.router.navigateByUrl(returnTo); - } - - private readShellState(): DecisioningShellState { - const params = collectRouteParams(this.route.snapshot.root); - const queryParams = this.route.snapshot.root.queryParams ?? {}; - const releaseId = coerceString(params['releaseId']) ?? coerceString(queryParams['releaseId']); - const approvalId = coerceString(params['approvalId']) ?? coerceString(queryParams['approvalId']); - const packId = coerceString(params['packId']) ?? coerceString(queryParams['packId']); - const environment = coerceString(params['environment']) ?? coerceString(queryParams['environment']); - const artifact = - coerceString(queryParams['artifact']) - ?? coerceString(queryParams['artifactDigest']) - ?? coerceString(queryParams['bundleDigest']); - const workflowId = coerceString(queryParams['workflowId']); - const evidenceId = - coerceString(queryParams['evidenceId']) - ?? coerceString(queryParams['packetId']); - - let kind: DecisioningContextKind = 'global'; - - if (approvalId) { - kind = 'approval'; - } else if (releaseId) { - kind = 'release'; - } else if (packId) { - kind = 'pack'; - } else if (workflowId) { - kind = 'workflow'; - } else if (evidenceId) { - kind = 'evidence'; - } - - return { - kind, - packId, - releaseId, - approvalId, - environment, - artifact, - returnTo: coerceString(queryParams['returnTo']), - workflowId, - evidenceId, - }; - } -} - -function collectRouteParams(snapshot: ActivatedRouteSnapshot | null): Record { - const params: Record = {}; - - if (!snapshot) { - return params; - } - - const stack: ActivatedRouteSnapshot[] = [snapshot]; - while (stack.length > 0) { - const current = stack.pop()!; - for (const [key, value] of Object.entries(current.params ?? {})) { - if (typeof value === 'string' && value.length > 0) { - params[key] = value; - } - } - stack.push(...current.children); - } - - return params; -} - -function coerceString(value: unknown): string | null { - if (typeof value !== 'string') { - return null; - } - - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -function truncateValue(value: string): string { - return value.length > 20 ? `${value.slice(0, 12)}...${value.slice(-6)}` : value; -} +export class PolicyDecisioningShellComponent {}