Add dynamic tab-aware subtitles to all policy shell pages
Follows the releases-unified-page pattern: static h1 title with a computed() subtitle signal that changes when switching tabs. Shell components updated: - Governance: static subtitle → computed per 10 tabs - Simulation: added h1 + computed subtitle for 9 tabs - VEX & Exceptions: added h1 + computed subtitle for 8 tabs - Policy Audit: added h1 + computed subtitle for 3 tabs - Packs: replaced card-style header with releases pattern Child h1 headers removed from 13 simulation sub-components to eliminate double titles (action buttons preserved as standalone divs). CSS aligned to releases pattern: title 1.5rem, subtitle 0.8125rem with --color-text-secondary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
DestroyRef,
|
||||
inject,
|
||||
signal,
|
||||
@@ -33,6 +34,13 @@ const PAGE_TABS: readonly StellaPageTab[] = [
|
||||
imports: [CommonModule, RouterOutlet, StellaPageTabsComponent],
|
||||
template: `
|
||||
<section class="policy-audit-shell" data-testid="policy-audit-shell">
|
||||
<header class="audit-shell__header">
|
||||
<div>
|
||||
<h1 class="audit-shell__title">Policy Audit</h1>
|
||||
<p class="audit-shell__subtitle">{{ activeSubtitle() }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<stella-page-tabs
|
||||
[tabs]="pageTabs"
|
||||
[activeTab]="activeSubview()"
|
||||
@@ -49,6 +57,9 @@ const PAGE_TABS: readonly StellaPageTab[] = [
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.audit-shell__header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 1rem; }
|
||||
.audit-shell__title { font-size: 1.5rem; font-weight: var(--font-weight-bold, 700); color: var(--color-text-heading); margin: 0 0 0.25rem; }
|
||||
.audit-shell__subtitle { font-size: 0.8125rem; color: var(--color-text-secondary); margin: 0; }
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
@@ -60,6 +71,15 @@ export class PolicyDecisioningAuditShellComponent {
|
||||
readonly pageTabs = PAGE_TABS;
|
||||
readonly activeSubview = signal<AuditSubview>(this.readSubview());
|
||||
|
||||
protected readonly activeSubtitle = computed(() => {
|
||||
switch (this.activeSubview()) {
|
||||
case 'policy': return 'Policy promotions, simulations, approvals, and lint events.';
|
||||
case 'vex': return 'VEX statement creation, revocation, and consensus events.';
|
||||
case 'log': return 'Unified audit log across all policy actions.';
|
||||
default: return 'Policy promotions, simulations, approvals, and lint events.';
|
||||
}
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.router.events
|
||||
.pipe(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
DestroyRef,
|
||||
inject,
|
||||
signal,
|
||||
@@ -46,6 +47,13 @@ const PAGE_TABS: readonly StellaPageTab[] = [
|
||||
imports: [CommonModule, RouterOutlet, StellaPageTabsComponent],
|
||||
template: `
|
||||
<section class="policy-vex-shell" data-testid="policy-vex-shell">
|
||||
<header class="vex-shell__header">
|
||||
<div>
|
||||
<h1 class="vex-shell__title">VEX & Exceptions</h1>
|
||||
<p class="vex-shell__subtitle">{{ activeSubtitle() }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<stella-page-tabs
|
||||
[tabs]="pageTabs"
|
||||
[activeTab]="activeSubview()"
|
||||
@@ -62,6 +70,9 @@ const PAGE_TABS: readonly StellaPageTab[] = [
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.vex-shell__header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 1rem; }
|
||||
.vex-shell__title { font-size: 1.5rem; font-weight: var(--font-weight-bold, 700); color: var(--color-text-heading); margin: 0 0 0.25rem; }
|
||||
.vex-shell__subtitle { font-size: 0.8125rem; color: var(--color-text-secondary); margin: 0; }
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
@@ -73,6 +84,20 @@ export class PolicyDecisioningVexShellComponent {
|
||||
readonly pageTabs = PAGE_TABS;
|
||||
readonly activeSubview = signal<VexSubview>(this.readSubview());
|
||||
|
||||
protected readonly activeSubtitle = computed(() => {
|
||||
switch (this.activeSubview()) {
|
||||
case 'dashboard': return 'VEX statement overview and activity feed.';
|
||||
case 'search': return 'Search VEX statements across all sources.';
|
||||
case 'create': return 'Create new VEX statements for findings.';
|
||||
case 'stats': return 'VEX coverage and statement metrics.';
|
||||
case 'consensus': return 'Multi-source consensus resolution.';
|
||||
case 'explorer': return 'Browse and inspect VEX data.';
|
||||
case 'conflicts': return 'VEX statement conflicts and resolution.';
|
||||
case 'exceptions': return 'Policy exception queue management.';
|
||||
default: return 'VEX statement overview and activity feed.';
|
||||
}
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.router.events
|
||||
.pipe(
|
||||
|
||||
@@ -47,17 +47,10 @@ const PACK_DETAIL_TABS: readonly StellaPageTab[] = [
|
||||
imports: [CommonModule, RouterOutlet, StellaPageTabsComponent],
|
||||
template: `
|
||||
<section class="policy-pack-shell" data-testid="policy-pack-shell">
|
||||
<header class="section-header">
|
||||
<header class="pack-shell__header">
|
||||
<div>
|
||||
<p class="section-header__eyebrow">Packs</p>
|
||||
<h2>{{ packId() ? 'Pack ' + packId() : 'Policy Pack Workspace' }}</h2>
|
||||
<p>
|
||||
{{
|
||||
packId()
|
||||
? 'Edit rules, YAML, approvals, and simulations for the selected pack.'
|
||||
: 'Browse deterministic pack inventory and open a pack into authoring mode.'
|
||||
}}
|
||||
</p>
|
||||
<h1 class="pack-shell__title">{{ packId() ? 'Pack ' + packId() : 'Policy Pack Workspace' }}</h1>
|
||||
<p class="pack-shell__subtitle">{{ activeSubtitle() }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -78,31 +71,9 @@ const PACK_DETAIL_TABS: readonly StellaPageTab[] = [
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface-primary);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.section-header__eyebrow {
|
||||
margin: 0 0 0.25rem;
|
||||
color: var(--color-status-success);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
color: var(--color-text-heading);
|
||||
}
|
||||
|
||||
.section-header p {
|
||||
margin: 0.35rem 0 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.pack-shell__header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 1rem; }
|
||||
.pack-shell__title { font-size: 1.5rem; font-weight: var(--font-weight-bold, 700); color: var(--color-text-heading); margin: 0 0 0.25rem; }
|
||||
.pack-shell__subtitle { font-size: 0.8125rem; color: var(--color-text-secondary); margin: 0; }
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
@@ -118,6 +89,23 @@ export class PolicyPackShellComponent {
|
||||
return this.packId() ? PACK_DETAIL_TABS : WORKSPACE_TABS;
|
||||
});
|
||||
|
||||
protected readonly activeSubtitle = computed(() => {
|
||||
if (!this.packId()) {
|
||||
return 'Browse deterministic pack inventory and open a pack into authoring mode.';
|
||||
}
|
||||
switch (this.activeSubview()) {
|
||||
case 'dashboard': return 'Overview of the selected policy pack.';
|
||||
case 'edit': return 'Edit rules and configuration for this pack.';
|
||||
case 'rules': return 'Manage individual rules within this pack.';
|
||||
case 'yaml': return 'View and edit the raw YAML definition.';
|
||||
case 'approvals': return 'Review and manage approval workflows.';
|
||||
case 'simulate': return 'Run simulations against this pack.';
|
||||
case 'explain': return 'Explain evaluation results for a simulation run.';
|
||||
case 'workspace': return 'Browse deterministic pack inventory and open a pack into authoring mode.';
|
||||
default: return 'Overview of the selected policy pack.';
|
||||
}
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.router.events
|
||||
.pipe(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { Component, ChangeDetectionStrategy, signal, inject, OnInit, DestroyRef } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, signal, computed, inject, OnInit, DestroyRef } from '@angular/core';
|
||||
import { Router, RouterOutlet, NavigationEnd, ActivatedRoute } from '@angular/router';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
@@ -33,7 +33,7 @@ const GOVERNANCE_TABS: readonly StellaPageTab[] = [
|
||||
<section class="governance">
|
||||
<div class="governance__header">
|
||||
<h1 class="governance__title">Policy Governance</h1>
|
||||
<p class="governance__subtitle">Configure risk budgets, trust weights, staleness rules, sealed mode, and risk profiles.</p>
|
||||
<p class="governance__subtitle">{{ activeSubtitle() }}</p>
|
||||
</div>
|
||||
|
||||
<stella-page-tabs
|
||||
@@ -61,20 +61,23 @@ const GOVERNANCE_TABS: readonly StellaPageTab[] = [
|
||||
}
|
||||
|
||||
.governance__header {
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.governance__title {
|
||||
margin: 0.25rem 0 0;
|
||||
font-size: 1.75rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: 1.5rem;
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
color: var(--color-text-heading);
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
|
||||
.governance__subtitle {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.95rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
`]
|
||||
@@ -116,6 +119,22 @@ export class PolicyGovernanceComponent implements OnInit {
|
||||
protected readonly activeTab = signal<string>('budget');
|
||||
protected readonly GOVERNANCE_TABS = GOVERNANCE_TABS;
|
||||
|
||||
protected readonly activeSubtitle = computed(() => {
|
||||
switch (this.activeTab()) {
|
||||
case 'budget': return 'Monitor budget consumption and manage risk thresholds.';
|
||||
case 'trust': return 'Configure trust weights for vulnerability sources and issuers.';
|
||||
case 'staleness': return 'Configure data freshness thresholds and enforcement rules.';
|
||||
case 'sealed': return 'Manage air-gapped operation mode and trusted source overrides.';
|
||||
case 'profiles': return 'Manage risk evaluation profiles and signal weights.';
|
||||
case 'validator': return 'Validate policy documents against the schema.';
|
||||
case 'audit': return 'Track all governance configuration changes.';
|
||||
case 'conflicts': return 'Identify and resolve rule overlaps and precedence issues.';
|
||||
case 'schema-playground': return 'Test and validate risk profile schemas interactively.';
|
||||
case 'schema-docs': return 'Reference documentation for risk profile configuration schemas.';
|
||||
default: return 'Monitor budget consumption and manage risk thresholds.';
|
||||
}
|
||||
});
|
||||
|
||||
ngOnInit(): void {
|
||||
this.syncTabFromRoute();
|
||||
this.router.events
|
||||
|
||||
@@ -23,15 +23,7 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="batch-evaluation">
|
||||
<header class="batch-evaluation__header">
|
||||
<div>
|
||||
<p class="batch-evaluation__eyebrow">Policy Simulation Studio</p>
|
||||
<h1>Batch Evaluation</h1>
|
||||
<p class="batch-evaluation__lede">
|
||||
Evaluate multiple artifacts against policy rules simultaneously.
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-tabs">
|
||||
<div class="batch-evaluation__actions">
|
||||
<button
|
||||
class="tab-btn"
|
||||
[class.tab-btn--active]="activeTab() === 'new'"
|
||||
@@ -47,7 +39,6 @@ import {
|
||||
History
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- New Evaluation Tab -->
|
||||
@if (activeTab() === 'new') {
|
||||
@@ -431,35 +422,11 @@ import {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.batch-evaluation__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.batch-evaluation__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--color-status-success);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.batch-evaluation__header h1 {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-heading);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.batch-evaluation__lede {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.header-tabs {
|
||||
.batch-evaluation__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
|
||||
@@ -24,15 +24,7 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="conflict-detection" [attr.aria-busy]="loading()">
|
||||
<header class="conflict-detection__header">
|
||||
<div>
|
||||
<p class="conflict-detection__eyebrow">Policy Simulation Studio</p>
|
||||
<h1>Conflict Detection</h1>
|
||||
<p class="conflict-detection__lede">
|
||||
Detect policy conflicts and get AI-assisted resolution suggestions.
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="conflict-detection__actions">
|
||||
<button class="btn btn--primary" (click)="analyzeConflicts()" [disabled]="selectedPolicies().length < 2 || loading()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
@@ -41,7 +33,6 @@ import {
|
||||
Analyze Conflicts
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Policy Selection -->
|
||||
<div class="policy-selection">
|
||||
@@ -344,35 +335,11 @@ import {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.conflict-detection__header {
|
||||
.conflict-detection__actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.conflict-detection__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--color-status-warning);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.conflict-detection__header h1 {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-heading);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.conflict-detection__lede {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
|
||||
@@ -29,23 +29,14 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="coverage" [attr.aria-busy]="loading()">
|
||||
<header class="coverage__header">
|
||||
<div>
|
||||
<p class="coverage__eyebrow">Policy Simulation Studio</p>
|
||||
<h1>Test Coverage</h1>
|
||||
<p class="coverage__lede">
|
||||
View coverage percentage per policy rule and identify missing test cases.
|
||||
</p>
|
||||
</div>
|
||||
<div class="coverage__actions">
|
||||
<button class="btn btn--secondary" (click)="loadCoverage()" [disabled]="loading()">
|
||||
Refresh
|
||||
</button>
|
||||
<button class="btn btn--primary" (click)="runTests()" [disabled]="loading()">
|
||||
{{ loading() ? 'Running...' : 'Run Tests' }}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="coverage__actions">
|
||||
<button class="btn btn--secondary" (click)="loadCoverage()" [disabled]="loading()">
|
||||
Refresh
|
||||
</button>
|
||||
<button class="btn btn--primary" (click)="runTests()" [disabled]="loading()">
|
||||
{{ loading() ? 'Running...' : 'Run Tests' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Coverage Summary -->
|
||||
@if (result()) {
|
||||
@@ -261,35 +252,11 @@ import {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.coverage__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.coverage__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--color-status-success);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.coverage__header h1 {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-heading);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.coverage__lede {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.coverage__actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
|
||||
@@ -25,18 +25,11 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="effective-policy" [attr.aria-busy]="loading()">
|
||||
<header class="effective-policy__header">
|
||||
<div>
|
||||
<p class="effective-policy__eyebrow">Policy Simulation Studio</p>
|
||||
<h1>Effective Policies</h1>
|
||||
<p class="effective-policy__lede">
|
||||
View which policies apply to each resource based on inheritance and overrides.
|
||||
</p>
|
||||
</div>
|
||||
<div class="effective-policy__actions">
|
||||
<button class="btn btn--primary" (click)="loadPolicies()" [disabled]="loading()">
|
||||
{{ loading() ? 'Loading...' : 'Refresh' }}
|
||||
</button>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="effective-policy__filters">
|
||||
@@ -167,31 +160,7 @@ import {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.effective-policy__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.effective-policy__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--color-status-excepted);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.effective-policy__header h1 {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-heading);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.effective-policy__lede {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.effective-policy__actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1.25rem;
|
||||
|
||||
@@ -25,18 +25,11 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="audit-log" [attr.aria-busy]="loading()">
|
||||
<header class="audit-log__header">
|
||||
<div>
|
||||
<p class="audit-log__eyebrow">Policy Simulation Studio</p>
|
||||
<h1>Audit Log</h1>
|
||||
<p class="audit-log__lede">
|
||||
View change history for policy packs with actor and timestamp details.
|
||||
</p>
|
||||
</div>
|
||||
<div class="audit-log__actions">
|
||||
<button class="btn btn--primary" (click)="loadAuditLog()" [disabled]="loading()">
|
||||
Refresh
|
||||
</button>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="audit-log__filters">
|
||||
@@ -193,31 +186,7 @@ import {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.audit-log__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.audit-log__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--color-status-excepted);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.audit-log__header h1 {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-heading);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.audit-log__lede {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.audit-log__actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1.25rem;
|
||||
|
||||
@@ -29,15 +29,7 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="diff-viewer" [attr.aria-busy]="loading()">
|
||||
<header class="diff-viewer__header">
|
||||
<div>
|
||||
<p class="diff-viewer__eyebrow">Policy Simulation Studio</p>
|
||||
<h1>Policy Diff</h1>
|
||||
<p class="diff-viewer__lede">
|
||||
Compare policy versions to see what changed.
|
||||
</p>
|
||||
</div>
|
||||
@if (result()) {
|
||||
@if (result()) {
|
||||
<div class="diff-viewer__versions">
|
||||
<span class="version-badge version-badge--from">v{{ result()?.fromVersion }}</span>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@@ -47,7 +39,6 @@ import {
|
||||
<span class="version-badge version-badge--to">v{{ result()?.toVersion }}</span>
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
|
||||
<!-- Stats Summary -->
|
||||
@if (result()) {
|
||||
@@ -175,32 +166,6 @@ import {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.diff-viewer__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.diff-viewer__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--color-status-info);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.diff-viewer__header h1 {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-heading);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.diff-viewer__lede {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.diff-viewer__versions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -24,18 +24,11 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="policy-exception" [attr.aria-busy]="loading()">
|
||||
<header class="policy-exception__header">
|
||||
<div>
|
||||
<p class="policy-exception__eyebrow">Policy Simulation Studio</p>
|
||||
<h1>Policy Exceptions</h1>
|
||||
<p class="policy-exception__lede">
|
||||
Manage policy exceptions for specific resources or vulnerabilities.
|
||||
</p>
|
||||
</div>
|
||||
<div class="policy-exception__actions">
|
||||
<button class="btn btn--primary" (click)="showCreateForm.set(true)" [disabled]="showCreateForm()">
|
||||
Create Exception
|
||||
</button>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<!-- Create Exception Form -->
|
||||
@if (showCreateForm()) {
|
||||
@@ -254,31 +247,7 @@ import {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.policy-exception__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.policy-exception__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--color-severity-high);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.policy-exception__header h1 {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-heading);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.policy-exception__lede {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.policy-exception__actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1.25rem;
|
||||
|
||||
@@ -29,20 +29,11 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="policy-lint" [attr.aria-busy]="loading()">
|
||||
<header class="policy-lint__header">
|
||||
<div>
|
||||
<p class="policy-lint__eyebrow">Policy Simulation Studio</p>
|
||||
<h1>Policy Lint</h1>
|
||||
<p class="policy-lint__lede">
|
||||
Check policy syntax, semantics, and best practices.
|
||||
</p>
|
||||
</div>
|
||||
<div class="policy-lint__actions">
|
||||
<button class="btn btn--primary" (click)="runLint()" [disabled]="loading()">
|
||||
{{ loading() ? 'Linting...' : 'Run Lint' }}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="policy-lint__actions">
|
||||
<button class="btn btn--primary" (click)="runLint()" [disabled]="loading()">
|
||||
{{ loading() ? 'Linting...' : 'Run Lint' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Compilation Status -->
|
||||
@if (result()) {
|
||||
@@ -232,31 +223,7 @@ import {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.policy-lint__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.policy-lint__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--color-status-warning);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.policy-lint__header h1 {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-heading);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.policy-lint__lede {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.policy-lint__actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1.25rem;
|
||||
|
||||
@@ -24,16 +24,6 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="merge-preview" [attr.aria-busy]="loading()">
|
||||
<header class="merge-preview__header">
|
||||
<div>
|
||||
<p class="merge-preview__eyebrow">Policy Simulation Studio</p>
|
||||
<h1>Merge Preview</h1>
|
||||
<p class="merge-preview__lede">
|
||||
Preview the result of merging multiple policy packs.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Source Selection -->
|
||||
<div class="merge-preview__sources">
|
||||
<form [formGroup]="sourceForm" (ngSubmit)="generatePreview()">
|
||||
@@ -212,29 +202,6 @@ import {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.merge-preview__header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.merge-preview__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--color-status-success);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.merge-preview__header h1 {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-heading);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.merge-preview__lede {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.merge-preview__sources {
|
||||
background: var(--color-surface-elevated);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
|
||||
@@ -33,18 +33,11 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="promotion-gate" [attr.aria-busy]="loading()">
|
||||
<header class="promotion-gate__header">
|
||||
<div>
|
||||
<p class="promotion-gate__eyebrow">Policy Simulation Studio</p>
|
||||
<h1>Promotion Gate</h1>
|
||||
<p class="promotion-gate__lede">
|
||||
Checklist of requirements before promoting policy to production.
|
||||
</p>
|
||||
</div>
|
||||
<div class="promotion-gate__actions">
|
||||
<button class="btn btn--primary" (click)="checkGate()" [disabled]="loading()">
|
||||
{{ loading() ? 'Checking...' : 'Check Requirements' }}
|
||||
</button>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<!-- Policy Info -->
|
||||
@if (result()) {
|
||||
@@ -239,31 +232,7 @@ import {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.promotion-gate__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.promotion-gate__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--color-status-success);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.promotion-gate__header h1 {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-heading);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.promotion-gate__lede {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.promotion-gate__actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1.25rem;
|
||||
|
||||
@@ -17,23 +17,13 @@ import { ShadowModeStateService } from './shadow-mode-state.service';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="shadow-dashboard" [attr.aria-busy]="loading()">
|
||||
<header class="shadow-dashboard__header">
|
||||
<div>
|
||||
<p class="shadow-dashboard__eyebrow">Policy Simulation Studio</p>
|
||||
<h1>Shadow Mode Dashboard</h1>
|
||||
<p class="shadow-dashboard__lede">
|
||||
Compare shadow policy evaluations against active production policy.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<app-shadow-mode-indicator
|
||||
[config]="config()"
|
||||
[loading]="loading()"
|
||||
(enable)="onEnableShadowMode()"
|
||||
(disable)="onDisableShadowMode()"
|
||||
(viewResults)="loadResults()"
|
||||
/>
|
||||
</header>
|
||||
<app-shadow-mode-indicator
|
||||
[config]="config()"
|
||||
[loading]="loading()"
|
||||
(enable)="onEnableShadowMode()"
|
||||
(disable)="onDisableShadowMode()"
|
||||
(viewResults)="loadResults()"
|
||||
/>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
@if (results()) {
|
||||
@@ -213,33 +203,6 @@ import { ShadowModeStateService } from './shadow-mode-state.service';
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.shadow-dashboard__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.shadow-dashboard__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--color-status-excepted-border);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.shadow-dashboard__header h1 {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-heading);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.shadow-dashboard__lede {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.shadow-dashboard__summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
|
||||
@@ -27,15 +27,8 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="sim-console" [attr.aria-busy]="loading()">
|
||||
<header class="sim-console__header">
|
||||
<div>
|
||||
<p class="sim-console__eyebrow">Policy Simulation Studio</p>
|
||||
<h1>Simulation Console</h1>
|
||||
<p class="sim-console__lede">
|
||||
Run policy simulations against SBOMs to preview evaluation outcomes.
|
||||
</p>
|
||||
</div>
|
||||
@if (result()) {
|
||||
@if (result()) {
|
||||
<div class="sim-console__actions">
|
||||
<div class="sim-console__status">
|
||||
<span
|
||||
class="status-pill"
|
||||
@@ -51,8 +44,8 @@ import {
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="sim-console__layout">
|
||||
<!-- Input Form -->
|
||||
@@ -332,31 +325,7 @@ import {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.sim-console__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.sim-console__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--color-status-info);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sim-console__header h1 {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-heading);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.sim-console__lede {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.sim-console__actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
|
||||
.sim-console__status {
|
||||
display: flex;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { Component, ChangeDetectionStrategy, signal, inject, OnInit } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, signal, computed, inject, OnInit } from '@angular/core';
|
||||
import { RouterModule, Router } from '@angular/router';
|
||||
|
||||
import { ShadowModeIndicatorComponent } from './shadow-mode-indicator.component';
|
||||
@@ -33,6 +33,13 @@ const SIMULATION_TABS: readonly StellaPageTab[] = [
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="simulation">
|
||||
<header class="simulation__header">
|
||||
<div>
|
||||
<h1 class="simulation__title">Policy Simulation</h1>
|
||||
<p class="simulation__subtitle">{{ activeSubtitle() }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- MANDATORY: Shadow Mode Indicator - Visible on all policy views -->
|
||||
<div class="simulation__shadow-banner">
|
||||
<app-shadow-mode-indicator
|
||||
@@ -181,6 +188,10 @@ const SIMULATION_TABS: readonly StellaPageTab[] = [
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.simulation__header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 1rem; }
|
||||
.simulation__title { font-size: 1.5rem; font-weight: var(--font-weight-bold, 700); color: var(--color-text-heading); margin: 0 0 0.25rem; }
|
||||
.simulation__subtitle { font-size: 0.8125rem; color: var(--color-text-secondary); margin: 0; }
|
||||
|
||||
.simulation__shadow-banner {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -284,6 +295,23 @@ export class SimulationDashboardComponent implements OnInit {
|
||||
private readonly shadowModeState = inject(ShadowModeStateService);
|
||||
|
||||
protected readonly activeTab = signal<string>('shadow');
|
||||
|
||||
private static readonly SUBTITLES: Record<string, string> = {
|
||||
shadow: 'Compare shadow policy evaluations against active production policy.',
|
||||
console: 'Run policies against test data and review evaluations.',
|
||||
lint: 'Syntax and semantic validation for policy documents.',
|
||||
coverage: 'Test coverage metrics per rule and policy pack.',
|
||||
effective: 'Which policies apply to which environments and artifacts.',
|
||||
audit: 'Simulation and policy change history.',
|
||||
exceptions: 'Manage temporary policy exception waivers.',
|
||||
promotion: 'Pre-promotion checklist and gate enforcement.',
|
||||
merge: 'Preview pack merge results before applying.',
|
||||
};
|
||||
|
||||
protected readonly activeSubtitle = computed(() =>
|
||||
SimulationDashboardComponent.SUBTITLES[this.activeTab()] ?? ''
|
||||
);
|
||||
|
||||
protected readonly shadowConfig = this.shadowModeState.config;
|
||||
protected readonly shadowLoading = this.shadowModeState.loading;
|
||||
|
||||
|
||||
@@ -28,15 +28,7 @@ import {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="sim-history" [attr.aria-busy]="loading()">
|
||||
<header class="sim-history__header">
|
||||
<div>
|
||||
<p class="sim-history__eyebrow">Policy Simulation Studio</p>
|
||||
<h1>Simulation History</h1>
|
||||
<p class="sim-history__lede">
|
||||
View past simulation runs, verify reproducibility, and compare results.
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="sim-history__actions">
|
||||
<button
|
||||
class="btn btn--secondary"
|
||||
[disabled]="selectedForComparison().length !== 2"
|
||||
@@ -48,7 +40,6 @@ import {
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="sim-history__filters">
|
||||
@@ -384,35 +375,11 @@ import {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.sim-history__header {
|
||||
.sim-history__actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.sim-history__eyebrow {
|
||||
margin: 0;
|
||||
color: var(--color-status-excepted);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sim-history__header h1 {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-heading);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.sim-history__lede {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
|
||||
Reference in New Issue
Block a user