Update compose config, policy simulation, and workflow replay
- devops/compose: README, docker-compose, hosts updates - Policy simulation: pre-promotion and test-validate panels, routes, dashboard, and spec updates - Workflow visualization: run-graph replay page template update - Claude settings update Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,14 +8,14 @@ import { PolicySimulationStudioComponent } from './policy-simulation.component';
|
||||
@Component({ selector: 'app-shadow-mode-dashboard', template: '', standalone: true })
|
||||
class MockShadowModeDashboardComponent {}
|
||||
|
||||
@Component({ selector: 'app-simulation-console', template: '', standalone: true })
|
||||
class MockSimulationConsoleComponent {}
|
||||
@Component({ selector: 'app-promotion-gate', template: '', standalone: true })
|
||||
class MockPromotionGateComponent {}
|
||||
|
||||
@Component({ selector: 'app-coverage-fixture', template: '', standalone: true })
|
||||
class MockCoverageFixtureComponent {}
|
||||
@Component({ selector: 'app-simulation-test-validate-panel', template: '', standalone: true })
|
||||
class MockSimulationTestValidatePanelComponent {}
|
||||
|
||||
@Component({ selector: 'app-policy-audit-log', template: '', standalone: true })
|
||||
class MockPolicyAuditLogComponent {}
|
||||
@Component({ selector: 'app-simulation-pre-promotion-panel', template: '', standalone: true })
|
||||
class MockSimulationPrePromotionPanelComponent {}
|
||||
|
||||
@Component({ selector: 'app-effective-policy-viewer', template: '', standalone: true })
|
||||
class MockEffectivePolicyViewerComponent {}
|
||||
@@ -23,27 +23,6 @@ class MockEffectivePolicyViewerComponent {}
|
||||
@Component({ selector: 'app-policy-exception', template: '', standalone: true })
|
||||
class MockPolicyExceptionComponent {}
|
||||
|
||||
@Component({ selector: 'app-policy-lint', template: '', standalone: true })
|
||||
class MockPolicyLintComponent {}
|
||||
|
||||
@Component({ selector: 'app-promotion-gate', template: '', standalone: true })
|
||||
class MockPromotionGateComponent {}
|
||||
|
||||
@Component({ selector: 'app-policy-diff-viewer', template: '', standalone: true })
|
||||
class MockPolicyDiffViewerComponent {}
|
||||
|
||||
@Component({ selector: 'app-policy-merge-preview', template: '', standalone: true })
|
||||
class MockPolicyMergePreviewComponent {}
|
||||
|
||||
@Component({ selector: 'app-simulation-history', template: '', standalone: true })
|
||||
class MockSimulationHistoryComponent {}
|
||||
|
||||
@Component({ selector: 'app-conflict-detection', template: '', standalone: true })
|
||||
class MockConflictDetectionComponent {}
|
||||
|
||||
@Component({ selector: 'app-batch-evaluation', template: '', standalone: true })
|
||||
class MockBatchEvaluationComponent {}
|
||||
|
||||
describe('PolicySimulationStudioComponent', () => {
|
||||
let component: PolicySimulationStudioComponent;
|
||||
let fixture: ComponentFixture<PolicySimulationStudioComponent>;
|
||||
@@ -53,36 +32,22 @@ describe('PolicySimulationStudioComponent', () => {
|
||||
imports: [
|
||||
PolicySimulationStudioComponent,
|
||||
MockShadowModeDashboardComponent,
|
||||
MockSimulationConsoleComponent,
|
||||
MockCoverageFixtureComponent,
|
||||
MockPolicyAuditLogComponent,
|
||||
MockPromotionGateComponent,
|
||||
MockSimulationTestValidatePanelComponent,
|
||||
MockSimulationPrePromotionPanelComponent,
|
||||
MockEffectivePolicyViewerComponent,
|
||||
MockPolicyExceptionComponent,
|
||||
MockPolicyLintComponent,
|
||||
MockPromotionGateComponent,
|
||||
MockPolicyDiffViewerComponent,
|
||||
MockPolicyMergePreviewComponent,
|
||||
MockSimulationHistoryComponent,
|
||||
MockConflictDetectionComponent,
|
||||
MockBatchEvaluationComponent,
|
||||
],
|
||||
})
|
||||
.overrideComponent(PolicySimulationStudioComponent, {
|
||||
set: {
|
||||
imports: [
|
||||
MockShadowModeDashboardComponent,
|
||||
MockSimulationConsoleComponent,
|
||||
MockCoverageFixtureComponent,
|
||||
MockPolicyAuditLogComponent,
|
||||
MockPromotionGateComponent,
|
||||
MockSimulationTestValidatePanelComponent,
|
||||
MockSimulationPrePromotionPanelComponent,
|
||||
MockEffectivePolicyViewerComponent,
|
||||
MockPolicyExceptionComponent,
|
||||
MockPolicyLintComponent,
|
||||
MockPromotionGateComponent,
|
||||
MockPolicyDiffViewerComponent,
|
||||
MockPolicyMergePreviewComponent,
|
||||
MockSimulationHistoryComponent,
|
||||
MockConflictDetectionComponent,
|
||||
MockBatchEvaluationComponent,
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -103,46 +68,34 @@ describe('PolicySimulationStudioComponent', () => {
|
||||
});
|
||||
|
||||
it('should have all tab definitions', () => {
|
||||
expect(component.tabs.length).toBe(13);
|
||||
expect(component.tabs.length).toBe(6);
|
||||
});
|
||||
|
||||
it('should have tab IDs for all expected features', () => {
|
||||
const tabIds = component.tabs.map((t) => t.id);
|
||||
expect(tabIds).toContain('shadow');
|
||||
expect(tabIds).toContain('simulation');
|
||||
expect(tabIds).toContain('coverage');
|
||||
expect(tabIds).toContain('lint');
|
||||
expect(tabIds).toContain('audit');
|
||||
expect(tabIds).toContain('promotion');
|
||||
expect(tabIds).toContain('testing');
|
||||
expect(tabIds).toContain('review');
|
||||
expect(tabIds).toContain('effective');
|
||||
expect(tabIds).toContain('exceptions');
|
||||
expect(tabIds).toContain('promotion');
|
||||
expect(tabIds).toContain('diff');
|
||||
expect(tabIds).toContain('merge');
|
||||
expect(tabIds).toContain('history');
|
||||
expect(tabIds).toContain('conflicts');
|
||||
expect(tabIds).toContain('batch');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tab Navigation', () => {
|
||||
it('should change active tab on setActiveTab', () => {
|
||||
component.setActiveTab('simulation');
|
||||
expect(component.activeTab()).toBe('simulation');
|
||||
component.setActiveTab('promotion');
|
||||
expect(component.activeTab()).toBe('promotion');
|
||||
});
|
||||
|
||||
it('should change active tab to coverage', () => {
|
||||
component.setActiveTab('coverage');
|
||||
expect(component.activeTab()).toBe('coverage');
|
||||
it('should change active tab to testing', () => {
|
||||
component.setActiveTab('testing');
|
||||
expect(component.activeTab()).toBe('testing');
|
||||
});
|
||||
|
||||
it('should change active tab to audit', () => {
|
||||
component.setActiveTab('audit');
|
||||
expect(component.activeTab()).toBe('audit');
|
||||
});
|
||||
|
||||
it('should change active tab to lint', () => {
|
||||
component.setActiveTab('lint');
|
||||
expect(component.activeTab()).toBe('lint');
|
||||
it('should change active tab to review', () => {
|
||||
component.setActiveTab('review');
|
||||
expect(component.activeTab()).toBe('review');
|
||||
});
|
||||
|
||||
it('should change active tab to effective', () => {
|
||||
@@ -159,7 +112,7 @@ describe('PolicySimulationStudioComponent', () => {
|
||||
describe('Template Rendering', () => {
|
||||
it('should render navigation tabs', () => {
|
||||
const navTabs = fixture.nativeElement.querySelectorAll('.nav-tab');
|
||||
expect(navTabs.length).toBe(13);
|
||||
expect(navTabs.length).toBe(6);
|
||||
});
|
||||
|
||||
it('should mark active tab with active class', () => {
|
||||
@@ -171,12 +124,12 @@ describe('PolicySimulationStudioComponent', () => {
|
||||
it('should display tab labels', () => {
|
||||
const tabLabels = fixture.nativeElement.querySelectorAll('.nav-tab__label');
|
||||
expect(tabLabels[0].textContent).toContain('Shadow Mode');
|
||||
expect(tabLabels[1].textContent).toContain('Simulation Console');
|
||||
expect(tabLabels[1].textContent).toContain('Promotion Gate');
|
||||
});
|
||||
|
||||
it('should display tab icons', () => {
|
||||
const tabIcons = fixture.nativeElement.querySelectorAll('.nav-tab__icon');
|
||||
expect(tabIcons.length).toBe(13);
|
||||
expect(tabIcons.length).toBe(6);
|
||||
});
|
||||
|
||||
it('should have navigation with tablist role', () => {
|
||||
@@ -186,7 +139,7 @@ describe('PolicySimulationStudioComponent', () => {
|
||||
|
||||
it('should have buttons with tab role', () => {
|
||||
const tabButtons = fixture.nativeElement.querySelectorAll('button[role="tab"]');
|
||||
expect(tabButtons.length).toBe(13);
|
||||
expect(tabButtons.length).toBe(6);
|
||||
});
|
||||
|
||||
it('should set aria-selected on active tab', () => {
|
||||
@@ -207,15 +160,15 @@ describe('PolicySimulationStudioComponent', () => {
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.activeTab()).toBe('simulation');
|
||||
expect(component.activeTab()).toBe('promotion');
|
||||
}));
|
||||
|
||||
it('should update active class on tab change', fakeAsync(() => {
|
||||
component.setActiveTab('coverage');
|
||||
component.setActiveTab('testing');
|
||||
fixture.detectChanges();
|
||||
|
||||
const activeTab = fixture.nativeElement.querySelector('.nav-tab--active');
|
||||
expect(activeTab.textContent).toContain('Coverage');
|
||||
expect(activeTab.textContent).toContain('Test & Validate');
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -225,44 +178,34 @@ describe('PolicySimulationStudioComponent', () => {
|
||||
expect(shadowComponent).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display simulation console when selected', fakeAsync(() => {
|
||||
component.setActiveTab('simulation');
|
||||
it('should display promotion gate when selected', fakeAsync(() => {
|
||||
component.setActiveTab('promotion');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
const simulationComponent = fixture.debugElement.query(By.directive(MockSimulationConsoleComponent));
|
||||
expect(simulationComponent).toBeTruthy();
|
||||
const promotionComponent = fixture.debugElement.query(By.directive(MockPromotionGateComponent));
|
||||
expect(promotionComponent).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should display coverage fixture when selected', fakeAsync(() => {
|
||||
component.setActiveTab('coverage');
|
||||
it('should display test & validate panel when selected', fakeAsync(() => {
|
||||
component.setActiveTab('testing');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
const coverageComponent = fixture.debugElement.query(By.directive(MockCoverageFixtureComponent));
|
||||
expect(coverageComponent).toBeTruthy();
|
||||
const testingComponent = fixture.debugElement.query(By.directive(MockSimulationTestValidatePanelComponent));
|
||||
expect(testingComponent).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should display policy lint when selected', fakeAsync(() => {
|
||||
component.setActiveTab('lint');
|
||||
it('should display pre-promotion review panel when selected', fakeAsync(() => {
|
||||
component.setActiveTab('review');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
const lintComponent = fixture.debugElement.query(By.directive(MockPolicyLintComponent));
|
||||
expect(lintComponent).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should display audit log when selected', fakeAsync(() => {
|
||||
component.setActiveTab('audit');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
const auditComponent = fixture.debugElement.query(By.directive(MockPolicyAuditLogComponent));
|
||||
expect(auditComponent).toBeTruthy();
|
||||
const reviewComponent = fixture.debugElement.query(By.directive(MockSimulationPrePromotionPanelComponent));
|
||||
expect(reviewComponent).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should display effective policies when selected', fakeAsync(() => {
|
||||
@@ -284,66 +227,6 @@ describe('PolicySimulationStudioComponent', () => {
|
||||
const exceptionsComponent = fixture.debugElement.query(By.directive(MockPolicyExceptionComponent));
|
||||
expect(exceptionsComponent).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should display promotion gate when selected', fakeAsync(() => {
|
||||
component.setActiveTab('promotion');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
const promotionComponent = fixture.debugElement.query(By.directive(MockPromotionGateComponent));
|
||||
expect(promotionComponent).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should display diff viewer when selected', fakeAsync(() => {
|
||||
component.setActiveTab('diff');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
const diffComponent = fixture.debugElement.query(By.directive(MockPolicyDiffViewerComponent));
|
||||
expect(diffComponent).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should display merge preview when selected', fakeAsync(() => {
|
||||
component.setActiveTab('merge');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
const mergeComponent = fixture.debugElement.query(By.directive(MockPolicyMergePreviewComponent));
|
||||
expect(mergeComponent).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should display simulation history when selected', fakeAsync(() => {
|
||||
component.setActiveTab('history');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
const historyComponent = fixture.debugElement.query(By.directive(MockSimulationHistoryComponent));
|
||||
expect(historyComponent).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should display conflict detection when selected', fakeAsync(() => {
|
||||
component.setActiveTab('conflicts');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
const conflictsComponent = fixture.debugElement.query(By.directive(MockConflictDetectionComponent));
|
||||
expect(conflictsComponent).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should display batch evaluation when selected', fakeAsync(() => {
|
||||
component.setActiveTab('batch');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
const batchComponent = fixture.debugElement.query(By.directive(MockBatchEvaluationComponent));
|
||||
expect(batchComponent).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('Tab Configuration', () => {
|
||||
|
||||
@@ -2,40 +2,38 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
|
||||
import { ShadowModeDashboardComponent } from './shadow-mode-dashboard.component';
|
||||
import { SimulationConsoleComponent } from './simulation-console.component';
|
||||
import { CoverageFixtureComponent } from './coverage-fixture.component';
|
||||
import { PolicyAuditLogComponent } from './policy-audit-log.component';
|
||||
import { PromotionGateComponent } from './promotion-gate.component';
|
||||
import { SimulationTestValidatePanelComponent } from './simulation-test-validate-panel.component';
|
||||
import { SimulationPrePromotionPanelComponent } from './simulation-pre-promotion-panel.component';
|
||||
import { EffectivePolicyViewerComponent } from './effective-policy-viewer.component';
|
||||
import { PolicyExceptionComponent } from './policy-exception.component';
|
||||
import { PolicyLintComponent } from './policy-lint.component';
|
||||
import { PromotionGateComponent } from './promotion-gate.component';
|
||||
import { PolicyDiffViewerComponent } from './policy-diff-viewer.component';
|
||||
import { PolicyMergePreviewComponent } from './policy-merge-preview.component';
|
||||
import { SimulationHistoryComponent } from './simulation-history.component';
|
||||
import { ConflictDetectionComponent } from './conflict-detection.component';
|
||||
import { BatchEvaluationComponent } from './batch-evaluation.component';
|
||||
import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/stella-page-tabs/stella-page-tabs.component';
|
||||
|
||||
/**
|
||||
* Main Policy Simulation Studio component with tabbed navigation.
|
||||
* Provides access to Shadow Mode, Simulation Console, Coverage, Audit Log,
|
||||
* Effective Policies, and Exceptions management.
|
||||
*
|
||||
* Rationalized from 13 tabs to 6:
|
||||
* 1. Shadow Mode (default) - unchanged
|
||||
* 2. Promotion Gate - unchanged
|
||||
* 3. Test & Validate - merges Simulation Console + Coverage + Lint
|
||||
* 4. Pre-Promotion Review - merges Diff Viewer + Merge Preview + Conflict Detection
|
||||
* 5. Effective Policies - unchanged
|
||||
* 6. Exceptions - unchanged (has badge)
|
||||
*
|
||||
* Removed from tabs:
|
||||
* - Audit Log -> /evidence/audit-log?module=policy-simulation
|
||||
* - History -> accessible from Shadow Mode via action
|
||||
* - Batch Evaluation -> accessible from Simulation Console
|
||||
*
|
||||
* @sprint SPRINT_20251229_021b_FE
|
||||
*/
|
||||
const POLICY_SIM_TABS: readonly StellaPageTab[] = [
|
||||
{ id: 'shadow', label: 'Shadow Mode', icon: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z|||M12 12m-3 0a3 3 0 1 0 6 0 3 3 0 1 0-6 0' },
|
||||
{ id: 'simulation', label: 'Simulation Console', icon: 'M5 3l14 9-14 9V3z' },
|
||||
{ id: 'coverage', label: 'Coverage', icon: 'M18 20V10|||M12 20V4|||M6 20v-6' },
|
||||
{ id: 'lint', label: 'Lint', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6|||M16 13H8|||M16 17H8|||M10 9H8' },
|
||||
{ id: 'audit', label: 'Audit Log', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6|||M16 13H8|||M16 17H8|||M10 9H8' },
|
||||
{ id: 'promotion', label: 'Promotion Gate', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
|
||||
{ id: 'testing', label: 'Test & Validate', icon: 'M5 3l14 9-14 9V3z' },
|
||||
{ id: 'review', label: 'Pre-Promotion Review', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6' },
|
||||
{ id: 'effective', label: 'Effective Policies', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
|
||||
{ id: 'exceptions', label: 'Exceptions', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
|
||||
{ id: 'promotion', label: 'Promotion Gate', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
|
||||
{ id: 'diff', label: 'Diff Viewer', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6' },
|
||||
{ id: 'merge', label: 'Merge Preview', icon: 'M6 3v12|||M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M18 9a9 9 0 0 1-9 9' },
|
||||
{ id: 'history', label: 'History', icon: 'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0|||M12 6v6l4 2' },
|
||||
{ id: 'conflicts', label: 'Conflicts', icon: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z|||M12 9v4|||M12 17h.01' },
|
||||
{ id: 'batch', label: 'Batch Evaluation', icon: 'M18 20V10|||M12 20V4|||M6 20v-6' },
|
||||
];
|
||||
|
||||
@Component({
|
||||
@@ -43,18 +41,11 @@ const POLICY_SIM_TABS: readonly StellaPageTab[] = [
|
||||
standalone: true,
|
||||
imports: [
|
||||
ShadowModeDashboardComponent,
|
||||
SimulationConsoleComponent,
|
||||
CoverageFixtureComponent,
|
||||
PolicyAuditLogComponent,
|
||||
PromotionGateComponent,
|
||||
SimulationTestValidatePanelComponent,
|
||||
SimulationPrePromotionPanelComponent,
|
||||
EffectivePolicyViewerComponent,
|
||||
PolicyExceptionComponent,
|
||||
PolicyLintComponent,
|
||||
PromotionGateComponent,
|
||||
PolicyDiffViewerComponent,
|
||||
PolicyMergePreviewComponent,
|
||||
SimulationHistoryComponent,
|
||||
ConflictDetectionComponent,
|
||||
BatchEvaluationComponent,
|
||||
StellaPageTabsComponent,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@@ -70,14 +61,14 @@ const POLICY_SIM_TABS: readonly StellaPageTab[] = [
|
||||
@if (activeTab() === 'shadow') {
|
||||
<app-shadow-mode-dashboard />
|
||||
}
|
||||
@if (activeTab() === 'simulation') {
|
||||
<app-simulation-console />
|
||||
@if (activeTab() === 'promotion') {
|
||||
<app-promotion-gate />
|
||||
}
|
||||
@if (activeTab() === 'coverage') {
|
||||
<app-coverage-fixture />
|
||||
@if (activeTab() === 'testing') {
|
||||
<app-simulation-test-validate-panel />
|
||||
}
|
||||
@if (activeTab() === 'audit') {
|
||||
<app-policy-audit-log />
|
||||
@if (activeTab() === 'review') {
|
||||
<app-simulation-pre-promotion-panel />
|
||||
}
|
||||
@if (activeTab() === 'effective') {
|
||||
<app-effective-policy-viewer />
|
||||
@@ -85,27 +76,6 @@ const POLICY_SIM_TABS: readonly StellaPageTab[] = [
|
||||
@if (activeTab() === 'exceptions') {
|
||||
<app-policy-exception />
|
||||
}
|
||||
@if (activeTab() === 'lint') {
|
||||
<app-policy-lint />
|
||||
}
|
||||
@if (activeTab() === 'promotion') {
|
||||
<app-promotion-gate />
|
||||
}
|
||||
@if (activeTab() === 'diff') {
|
||||
<app-policy-diff-viewer />
|
||||
}
|
||||
@if (activeTab() === 'merge') {
|
||||
<app-policy-merge-preview />
|
||||
}
|
||||
@if (activeTab() === 'history') {
|
||||
<app-simulation-history />
|
||||
}
|
||||
@if (activeTab() === 'conflicts') {
|
||||
<app-conflict-detection />
|
||||
}
|
||||
@if (activeTab() === 'batch') {
|
||||
<app-batch-evaluation />
|
||||
}
|
||||
</stella-page-tabs>
|
||||
</div>
|
||||
`,
|
||||
@@ -131,4 +101,14 @@ export class PolicySimulationStudioComponent {
|
||||
readonly activeTab = signal<string>('shadow');
|
||||
|
||||
readonly POLICY_SIM_TABS = POLICY_SIM_TABS;
|
||||
|
||||
/** Alias for spec compatibility. */
|
||||
get tabs(): readonly StellaPageTab[] {
|
||||
return this.POLICY_SIM_TABS;
|
||||
}
|
||||
|
||||
/** Alias for spec compatibility. */
|
||||
setActiveTab(tabId: string): void {
|
||||
this.activeTab.set(tabId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,36 +2,24 @@
|
||||
* @file policy-simulation.routes.ts
|
||||
* @sprint SPRINT_20251229_021b_FE
|
||||
* @description Routes for Policy Simulation Studio at /policy/simulation
|
||||
*
|
||||
* Rationalized from 13 routes to 6 tab destinations:
|
||||
* 1. Shadow Mode (default)
|
||||
* 2. Promotion Gate
|
||||
* 3. Test & Validate (merges console, coverage, lint)
|
||||
* 4. Pre-Promotion Review (merges diff, merge, conflicts)
|
||||
* 5. Effective Policies
|
||||
* 6. Exceptions
|
||||
*
|
||||
* Legacy routes (console, coverage, lint, diff, merge, conflicts) redirect
|
||||
* to the new merged tabs. Audit Log redirects to the central evidence page.
|
||||
* History and Batch Evaluation remain accessible as non-tab routes.
|
||||
*/
|
||||
|
||||
import { Routes } from '@angular/router';
|
||||
import { requireAuthGuard } from '../../core/auth/auth.guard';
|
||||
import { StellaOpsScopes } from '../../core/auth/scopes';
|
||||
|
||||
/**
|
||||
* Policy Simulation Studio Routes
|
||||
*
|
||||
* Provides interfaces for policy testing and validation:
|
||||
* - Dashboard (overview and quick actions)
|
||||
* - Shadow Mode (A/B policy comparison) - MANDATORY visibility before production promotion
|
||||
* - Simulation Console (run policy against test data)
|
||||
* - Policy Lint (syntax and semantic validation)
|
||||
* - Coverage (test coverage per rule)
|
||||
* - Effective Policies (which policies apply where)
|
||||
* - Audit Log (change history)
|
||||
* - Policy Diff (version comparison)
|
||||
* - Promotion Gate (checklist enforcement)
|
||||
* - Exceptions (policy exception management)
|
||||
* - Merge Preview (pack merge visualization)
|
||||
*
|
||||
* Promotion gates require:
|
||||
* - Shadow duration: 7 days minimum
|
||||
* - Coverage: 80%+ test coverage
|
||||
* - Lint: Clean (no errors)
|
||||
* - Compile: Success
|
||||
* - Security review: Approved
|
||||
* - Stakeholder approval: Signed off
|
||||
*/
|
||||
export const policySimulationRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
@@ -42,6 +30,7 @@ export const policySimulationRoutes: Routes = [
|
||||
(m) => m.SimulationDashboardComponent
|
||||
),
|
||||
children: [
|
||||
// ── Default: Shadow Mode ──
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () =>
|
||||
@@ -58,54 +47,8 @@ export const policySimulationRoutes: Routes = [
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
|
||||
},
|
||||
{
|
||||
path: 'console',
|
||||
loadComponent: () =>
|
||||
import('./simulation-console.component').then(
|
||||
(m) => m.SimulationConsoleComponent
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_SIMULATE] },
|
||||
},
|
||||
{
|
||||
path: 'lint',
|
||||
loadComponent: () =>
|
||||
import('./policy-lint.component').then(
|
||||
(m) => m.PolicyLintComponent
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
|
||||
},
|
||||
{
|
||||
path: 'coverage',
|
||||
loadComponent: () =>
|
||||
import('./coverage-fixture.component').then(
|
||||
(m) => m.CoverageFixtureComponent
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
|
||||
},
|
||||
{
|
||||
path: 'effective',
|
||||
loadComponent: () =>
|
||||
import('./effective-policy-viewer.component').then(
|
||||
(m) => m.EffectivePolicyViewerComponent
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
|
||||
},
|
||||
{
|
||||
path: 'audit',
|
||||
loadComponent: () =>
|
||||
import('./policy-audit-log.component').then(
|
||||
(m) => m.PolicyAuditLogComponent
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_AUDIT] },
|
||||
},
|
||||
{
|
||||
path: 'diff/:policyPackId',
|
||||
loadComponent: () =>
|
||||
import('./policy-diff-viewer.component').then(
|
||||
(m) => m.PolicyDiffViewerComponent
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
|
||||
},
|
||||
|
||||
// ── Promotion Gate ──
|
||||
{
|
||||
path: 'promotion',
|
||||
loadComponent: () =>
|
||||
@@ -114,6 +57,38 @@ export const policySimulationRoutes: Routes = [
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_APPROVE] },
|
||||
},
|
||||
|
||||
// ── Test & Validate (merged: console + coverage + lint) ──
|
||||
{
|
||||
path: 'testing',
|
||||
loadComponent: () =>
|
||||
import('./simulation-test-validate-panel.component').then(
|
||||
(m) => m.SimulationTestValidatePanelComponent
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_SIMULATE] },
|
||||
},
|
||||
|
||||
// ── Pre-Promotion Review (merged: diff + merge + conflicts) ──
|
||||
{
|
||||
path: 'review',
|
||||
loadComponent: () =>
|
||||
import('./simulation-pre-promotion-panel.component').then(
|
||||
(m) => m.SimulationPrePromotionPanelComponent
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
|
||||
},
|
||||
|
||||
// ── Effective Policies ──
|
||||
{
|
||||
path: 'effective',
|
||||
loadComponent: () =>
|
||||
import('./effective-policy-viewer.component').then(
|
||||
(m) => m.EffectivePolicyViewerComponent
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
|
||||
},
|
||||
|
||||
// ── Exceptions ──
|
||||
{
|
||||
path: 'exceptions',
|
||||
loadComponent: () =>
|
||||
@@ -130,14 +105,20 @@ export const policySimulationRoutes: Routes = [
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
|
||||
},
|
||||
{
|
||||
path: 'merge',
|
||||
loadComponent: () =>
|
||||
import('./policy-merge-preview.component').then(
|
||||
(m) => m.PolicyMergePreviewComponent
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
|
||||
},
|
||||
|
||||
// ── Legacy redirects: merged into Test & Validate ──
|
||||
{ path: 'console', redirectTo: 'testing', pathMatch: 'full' },
|
||||
{ path: 'coverage', redirectTo: 'testing', pathMatch: 'full' },
|
||||
{ path: 'lint', redirectTo: 'testing', pathMatch: 'full' },
|
||||
|
||||
// ── Legacy redirects: merged into Pre-Promotion Review ──
|
||||
{ path: 'merge', redirectTo: 'review', pathMatch: 'full' },
|
||||
{ path: 'conflicts', redirectTo: 'review', pathMatch: 'full' },
|
||||
|
||||
// ── Audit Log: redirect to central evidence page ──
|
||||
{ path: 'audit', redirectTo: '/evidence/audit-log', pathMatch: 'full' },
|
||||
|
||||
// ── Non-tab routes (still accessible via actions) ──
|
||||
{
|
||||
path: 'history',
|
||||
loadComponent: () =>
|
||||
@@ -146,14 +127,6 @@ export const policySimulationRoutes: Routes = [
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
|
||||
},
|
||||
{
|
||||
path: 'conflicts',
|
||||
loadComponent: () =>
|
||||
import('./conflict-detection.component').then(
|
||||
(m) => m.ConflictDetectionComponent
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
|
||||
},
|
||||
{
|
||||
path: 'batch',
|
||||
loadComponent: () =>
|
||||
@@ -162,6 +135,14 @@ export const policySimulationRoutes: Routes = [
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_SIMULATE] },
|
||||
},
|
||||
{
|
||||
path: 'diff/:policyPackId',
|
||||
loadComponent: () =>
|
||||
import('./policy-diff-viewer.component').then(
|
||||
(m) => m.PolicyDiffViewerComponent
|
||||
),
|
||||
data: { requiredScopes: [StellaOpsScopes.POLICY_READ] },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -5,11 +5,23 @@ import { RouterModule, Router } from '@angular/router';
|
||||
import { ShadowModeIndicatorComponent } from './shadow-mode-indicator.component';
|
||||
import { ShadowModeStateService } from './shadow-mode-state.service';
|
||||
import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/stella-page-tabs/stella-page-tabs.component';
|
||||
import { StellaQuickLinksComponent, type StellaQuickLink } from '../../shared/components/stella-quick-links/stella-quick-links.component';
|
||||
|
||||
/**
|
||||
* Main Policy Simulation Studio dashboard component with tabbed navigation.
|
||||
* Provides access to Shadow Mode, Simulation Console, Coverage, Audit Log,
|
||||
* Effective Policies, and Exceptions management.
|
||||
*
|
||||
* Rationalized from 9 tabs to 6:
|
||||
* 1. Shadow Mode (default) - unchanged
|
||||
* 2. Promotion Gate - unchanged
|
||||
* 3. Test & Validate - merges Simulation Console + Coverage + Lint
|
||||
* 4. Pre-Promotion Review - merges Diff Viewer + Merge Preview + Conflict Detection
|
||||
* 5. Effective Policies - unchanged
|
||||
* 6. Exceptions - unchanged (has badge)
|
||||
*
|
||||
* Removed from tabs:
|
||||
* - Audit Log -> /evidence/audit-log?module=policy-simulation
|
||||
* - History -> accessible from Shadow Mode via action
|
||||
* - Batch Evaluation -> accessible from Simulation Console
|
||||
*
|
||||
* MANDATORY: Shadow mode indicator is visible on all policy views before production promotion.
|
||||
*
|
||||
@@ -17,19 +29,16 @@ import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/
|
||||
*/
|
||||
const SIMULATION_TABS: readonly StellaPageTab[] = [
|
||||
{ id: 'shadow', label: 'Shadow Mode', icon: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z|||M12 12m-3 0a3 3 0 1 0 6 0 3 3 0 1 0-6 0' },
|
||||
{ id: 'console', label: 'Simulation Console', icon: 'M5 3l14 9-14 9V3z' },
|
||||
{ id: 'lint', label: 'Lint', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6|||M16 13H8|||M16 17H8|||M10 9H8' },
|
||||
{ id: 'coverage', label: 'Coverage', icon: 'M18 20V10|||M12 20V4|||M6 20v-6', badge: '72%', status: 'warn', statusHint: '72% coverage (80%+ required)' },
|
||||
{ id: 'effective', label: 'Effective Policies', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
|
||||
{ id: 'audit', label: 'Audit Log', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6|||M16 13H8|||M16 17H8|||M10 9H8' },
|
||||
{ id: 'exceptions', label: 'Exceptions', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z', badge: 3 },
|
||||
{ id: 'promotion', label: 'Promotion Gate', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
|
||||
{ id: 'merge', label: 'Merge Preview', icon: 'M6 3v12|||M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M18 9a9 9 0 0 1-9 9' },
|
||||
{ id: 'testing', label: 'Test & Validate', icon: 'M5 3l14 9-14 9V3z' },
|
||||
{ id: 'review', label: 'Pre-Promotion Review', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6' },
|
||||
{ id: 'effective', label: 'Effective Policies', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
|
||||
{ id: 'exceptions', label: 'Exceptions', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z', badge: 3 },
|
||||
];
|
||||
|
||||
@Component({
|
||||
selector: 'app-simulation-dashboard',
|
||||
imports: [RouterModule, ShadowModeIndicatorComponent, StellaPageTabsComponent],
|
||||
imports: [RouterModule, ShadowModeIndicatorComponent, StellaPageTabsComponent, StellaQuickLinksComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="simulation">
|
||||
@@ -38,6 +47,9 @@ const SIMULATION_TABS: readonly StellaPageTab[] = [
|
||||
<h1 class="simulation__title">Policy Simulation</h1>
|
||||
<p class="simulation__subtitle">{{ activeSubtitle() }}</p>
|
||||
</div>
|
||||
<aside class="page-aside">
|
||||
<stella-quick-links [links]="quickLinks" label="Quick Links" layout="strip" />
|
||||
</aside>
|
||||
</header>
|
||||
|
||||
<!-- MANDATORY: Shadow Mode Indicator - Visible on all policy views -->
|
||||
@@ -192,6 +204,8 @@ const SIMULATION_TABS: readonly StellaPageTab[] = [
|
||||
.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; }
|
||||
|
||||
.page-aside { flex: 0 1 60%; min-width: 0; }
|
||||
|
||||
.simulation__shadow-banner {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -296,16 +310,20 @@ export class SimulationDashboardComponent implements OnInit {
|
||||
|
||||
protected readonly activeTab = signal<string>('shadow');
|
||||
|
||||
readonly quickLinks: readonly StellaQuickLink[] = [
|
||||
{ label: 'Governance', route: '/ops/policy/governance', description: 'Risk budgets and compliance profiles' },
|
||||
{ label: 'Policy Packs', route: '/ops/policy/packs', description: 'Author and manage policy pack rules' },
|
||||
{ label: 'VEX & Exceptions', route: '/ops/policy/vex', description: 'Vulnerability exceptions and waivers' },
|
||||
{ label: 'Audit Events', route: '/evidence/audit-log', description: 'Cross-module audit trail' },
|
||||
];
|
||||
|
||||
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.',
|
||||
testing: 'Run policies against test data, check coverage, and validate syntax.',
|
||||
review: 'Diff versions, preview merges, and detect conflicts before promotion.',
|
||||
effective: 'Which policies apply to which environments and artifacts.',
|
||||
exceptions: 'Manage temporary policy exception waivers.',
|
||||
};
|
||||
|
||||
protected readonly activeSubtitle = computed(() =>
|
||||
@@ -328,14 +346,11 @@ export class SimulationDashboardComponent implements OnInit {
|
||||
|
||||
private static readonly TAB_ROUTES: Record<string, string> = {
|
||||
shadow: './shadow',
|
||||
console: './console',
|
||||
lint: './lint',
|
||||
coverage: './coverage',
|
||||
effective: './effective',
|
||||
audit: './audit',
|
||||
exceptions: './exceptions',
|
||||
promotion: './promotion',
|
||||
merge: './merge',
|
||||
testing: './testing',
|
||||
review: './review',
|
||||
effective: './effective',
|
||||
exceptions: './exceptions',
|
||||
};
|
||||
|
||||
protected readonly SIMULATION_TABS: readonly StellaPageTab[] = SIMULATION_TABS;
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
|
||||
import { PolicyDiffViewerComponent } from './policy-diff-viewer.component';
|
||||
import { PolicyMergePreviewComponent } from './policy-merge-preview.component';
|
||||
import { ConflictDetectionComponent } from './conflict-detection.component';
|
||||
|
||||
/**
|
||||
* Wrapper panel that merges Diff Viewer, Merge Preview, and Conflict Detection
|
||||
* into a single "Pre-Promotion Review" tab with toggle sub-sections.
|
||||
*
|
||||
* Replaces three separate top-level tabs with a single tab and
|
||||
* toggle buttons to switch between sub-views.
|
||||
*/
|
||||
type ReviewSection = 'diff' | 'merge' | 'conflicts';
|
||||
|
||||
interface SectionOption {
|
||||
id: ReviewSection;
|
||||
label: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const SECTIONS: readonly SectionOption[] = [
|
||||
{
|
||||
id: 'diff',
|
||||
label: 'Diff Viewer',
|
||||
icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6',
|
||||
},
|
||||
{
|
||||
id: 'merge',
|
||||
label: 'Merge Preview',
|
||||
icon: 'M6 3v12 M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M18 9a9 9 0 0 1-9 9',
|
||||
},
|
||||
{
|
||||
id: 'conflicts',
|
||||
label: 'Conflict Detection',
|
||||
icon: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z M12 9v4 M12 17h.01',
|
||||
},
|
||||
] as const;
|
||||
|
||||
@Component({
|
||||
selector: 'app-simulation-pre-promotion-panel',
|
||||
standalone: true,
|
||||
imports: [
|
||||
PolicyDiffViewerComponent,
|
||||
PolicyMergePreviewComponent,
|
||||
ConflictDetectionComponent,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="panel">
|
||||
<div class="panel__toggles" role="group" aria-label="Pre-promotion review sections">
|
||||
@for (section of sections; track section.id) {
|
||||
<button
|
||||
class="panel__toggle"
|
||||
[class.panel__toggle--active]="activeSection() === section.id"
|
||||
[attr.aria-pressed]="activeSection() === section.id"
|
||||
(click)="activeSection.set(section.id)"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
@for (d of section.icon.split(' M'); track $index) {
|
||||
<path [attr.d]="$index === 0 ? d : 'M' + d" />
|
||||
}
|
||||
</svg>
|
||||
<span>{{ section.label }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="panel__content">
|
||||
@if (activeSection() === 'diff') {
|
||||
<app-policy-diff-viewer />
|
||||
}
|
||||
@if (activeSection() === 'merge') {
|
||||
<app-policy-merge-preview />
|
||||
}
|
||||
@if (activeSection() === 'conflicts') {
|
||||
<app-conflict-detection />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
:host { display: block; }
|
||||
|
||||
.panel__toggles {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem 1rem 0;
|
||||
border-bottom: 1px solid var(--color-border-subtle, rgba(255,255,255,0.06));
|
||||
background: var(--color-surface-secondary, #1a1a2e);
|
||||
}
|
||||
|
||||
.panel__toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.875rem;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: var(--font-weight-medium, 500);
|
||||
cursor: pointer;
|
||||
transition: color 150ms ease, border-color 150ms ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.panel__toggle:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.panel__toggle--active {
|
||||
color: var(--color-brand-primary, #7c5cfc);
|
||||
border-bottom-color: var(--color-brand-primary, #7c5cfc);
|
||||
}
|
||||
|
||||
.panel__content {
|
||||
padding: 0;
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class SimulationPrePromotionPanelComponent {
|
||||
readonly sections = SECTIONS;
|
||||
readonly activeSection = signal<ReviewSection>('diff');
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
|
||||
import { SimulationConsoleComponent } from './simulation-console.component';
|
||||
import { CoverageFixtureComponent } from './coverage-fixture.component';
|
||||
import { PolicyLintComponent } from './policy-lint.component';
|
||||
|
||||
/**
|
||||
* Wrapper panel that merges Simulation Console, Coverage, and Lint
|
||||
* into a single "Test & Validate" tab with toggle sub-sections.
|
||||
*
|
||||
* Replaces three separate top-level tabs with a single tab and
|
||||
* toggle buttons to switch between sub-views.
|
||||
*/
|
||||
type TestValidateSection = 'console' | 'coverage' | 'lint';
|
||||
|
||||
interface SectionOption {
|
||||
id: TestValidateSection;
|
||||
label: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const SECTIONS: readonly SectionOption[] = [
|
||||
{
|
||||
id: 'console',
|
||||
label: 'Simulation Console',
|
||||
icon: 'M5 3l14 9-14 9V3z',
|
||||
},
|
||||
{
|
||||
id: 'coverage',
|
||||
label: 'Coverage',
|
||||
icon: 'M18 20V10 M12 20V4 M6 20v-6',
|
||||
},
|
||||
{
|
||||
id: 'lint',
|
||||
label: 'Lint',
|
||||
icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6 M16 13H8 M16 17H8 M10 9H8',
|
||||
},
|
||||
] as const;
|
||||
|
||||
@Component({
|
||||
selector: 'app-simulation-test-validate-panel',
|
||||
standalone: true,
|
||||
imports: [
|
||||
SimulationConsoleComponent,
|
||||
CoverageFixtureComponent,
|
||||
PolicyLintComponent,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="panel">
|
||||
<div class="panel__toggles" role="group" aria-label="Test and validate sections">
|
||||
@for (section of sections; track section.id) {
|
||||
<button
|
||||
class="panel__toggle"
|
||||
[class.panel__toggle--active]="activeSection() === section.id"
|
||||
[attr.aria-pressed]="activeSection() === section.id"
|
||||
(click)="activeSection.set(section.id)"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
@for (d of section.icon.split(' M'); track $index) {
|
||||
<path [attr.d]="$index === 0 ? d : 'M' + d" />
|
||||
}
|
||||
</svg>
|
||||
<span>{{ section.label }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="panel__content">
|
||||
@if (activeSection() === 'console') {
|
||||
<app-simulation-console />
|
||||
}
|
||||
@if (activeSection() === 'coverage') {
|
||||
<app-coverage-fixture />
|
||||
}
|
||||
@if (activeSection() === 'lint') {
|
||||
<app-policy-lint />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
:host { display: block; }
|
||||
|
||||
.panel__toggles {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem 1rem 0;
|
||||
border-bottom: 1px solid var(--color-border-subtle, rgba(255,255,255,0.06));
|
||||
background: var(--color-surface-secondary, #1a1a2e);
|
||||
}
|
||||
|
||||
.panel__toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.875rem;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: var(--font-weight-medium, 500);
|
||||
cursor: pointer;
|
||||
transition: color 150ms ease, border-color 150ms ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.panel__toggle:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.panel__toggle--active {
|
||||
color: var(--color-brand-primary, #7c5cfc);
|
||||
border-bottom-color: var(--color-brand-primary, #7c5cfc);
|
||||
}
|
||||
|
||||
.panel__content {
|
||||
padding: 0;
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class SimulationTestValidatePanelComponent {
|
||||
readonly sections = SECTIONS;
|
||||
readonly activeSection = signal<TestValidateSection>('console');
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
[backLabel]="returnTo() ? 'Return to previous context' : null"
|
||||
(backClick)="returnToSource()"
|
||||
>
|
||||
<a header-actions routerLink="/releases/runs" class="run-workspace__back-link">Back to release runs</a>
|
||||
<a header-actions routerLink="/releases/deployments" class="run-workspace__back-link">Back to deployments</a>
|
||||
</app-context-header>
|
||||
|
||||
<app-tabbed-nav
|
||||
|
||||
Reference in New Issue
Block a user