Remove redundant Policy Decisioning Studio context header
The shell's context header (title, subtitle, Reset view link) duplicated context already provided by the sidebar navigation and each child page's own header. Simplified the shell to a pure router-outlet passthrough. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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: `
|
||||
<section class="policy-decisioning-shell" data-testid="policy-decisioning-shell">
|
||||
<app-context-header
|
||||
[eyebrow]="null"
|
||||
[title]="headerTitle()"
|
||||
[subtitle]="headerSubtitle()"
|
||||
[contextNote]="headerNote()"
|
||||
[chips]="headerChips()"
|
||||
[backLabel]="shellState().returnTo ? 'Return to source' : null"
|
||||
(backClick)="returnToSource()"
|
||||
>
|
||||
<a
|
||||
header-actions
|
||||
class="shell-action"
|
||||
[routerLink]="overviewRoute()"
|
||||
[queryParams]="contextQueryParams()"
|
||||
>
|
||||
Reset view
|
||||
</a>
|
||||
</app-context-header>
|
||||
|
||||
<router-outlet />
|
||||
</section>
|
||||
`,
|
||||
@@ -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<DecisioningShellState>(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<string, string | null | undefined> {
|
||||
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<string, string> {
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
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 {}
|
||||
|
||||
Reference in New Issue
Block a user