Share shadow mode state and fix operations quota routing

This commit is contained in:
master
2026-03-12 13:13:06 +02:00
parent 19b9c90a8d
commit d8d3133060
5 changed files with 129 additions and 122 deletions

View File

@@ -1,6 +1,6 @@
import { Routes } from '@angular/router';
import { requireOrchOperatorGuard, requireOrchViewerGuard } from '../../core/auth';
import { requireOrchQuotaGuard, requireOrchViewerGuard } from '../../core/auth';
export const OPERATIONS_ROUTES: Routes = [
{
@@ -37,7 +37,7 @@ export const OPERATIONS_ROUTES: Routes = [
},
{
path: 'jobengine/quotas',
canMatch: [requireOrchOperatorGuard],
canMatch: [requireOrchQuotaGuard],
loadComponent: () =>
import('../jobengine/jobengine-quotas.component').then(
(m) => m.JobEngineQuotasComponent

View File

@@ -1,19 +1,9 @@
import { CommonModule } from '@angular/common';
import { Component, ChangeDetectionStrategy, inject, signal, computed, OnInit } from '@angular/core';
import { Component, ChangeDetectionStrategy, inject, computed, OnInit } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { finalize } from 'rxjs/operators';
import {
POLICY_SIMULATION_API,
PolicySimulationApi,
} from '../../core/api/policy-simulation.client';
import {
ShadowModeConfig,
ShadowModeResults,
ShadowFindingComparison,
} from '../../core/api/policy-simulation.models';
import { ShadowModeIndicatorComponent } from './shadow-mode-indicator.component';
import { ShadowModeStateService } from './shadow-mode-state.service';
/**
* Shadow mode results dashboard with divergence highlighting.
@@ -544,12 +534,12 @@ import { ShadowModeIndicatorComponent } from './shadow-mode-indicator.component'
]
})
export class ShadowModeDashboardComponent implements OnInit {
private readonly api = inject(POLICY_SIMULATION_API);
private readonly fb = inject(FormBuilder);
private readonly shadowModeState = inject(ShadowModeStateService);
readonly loading = signal(false);
readonly config = signal<ShadowModeConfig | undefined>(undefined);
readonly results = signal<ShadowModeResults | undefined>(undefined);
readonly loading = this.shadowModeState.loading;
readonly config = this.shadowModeState.config;
readonly results = this.shadowModeState.results;
// Template-safe computed signals for summary properties
readonly summaryTotalEvaluations = computed(() => this.results()?.summary?.totalEvaluations ?? 0);
@@ -601,71 +591,27 @@ export class ShadowModeDashboardComponent implements OnInit {
}
loadConfig(): void {
this.loading.set(true);
this.api.getShadowModeConfig().pipe(
finalize(() => this.loading.set(false))
).subscribe({
next: (config) => {
this.config.set(config);
if (config.enabled) {
this.loadResults();
}
},
error: () => {
this.config.set(undefined);
},
this.shadowModeState.loadConfig({
loadResultsQuery: this.buildResultsQuery(),
});
}
loadResults(): void {
this.loading.set(true);
const timeRange = this.filterForm.value.timeRange ?? '24h';
const fromTime = this.calculateFromTime(timeRange);
this.api.getShadowModeResults({
tenantId: 'default',
fromTime,
toTime: new Date().toISOString(),
divergedOnly: this.filterForm.value.showOnly === 'diverged',
}).pipe(
finalize(() => this.loading.set(false))
).subscribe({
next: (results) => {
this.results.set(results);
this.config.set(results.config);
},
error: () => {
this.results.set(undefined);
},
});
this.shadowModeState.loadResults(this.buildResultsQuery());
}
onEnableShadowMode(): void {
this.loading.set(true);
this.api.enableShadowMode({
this.shadowModeState.enable({
shadowPackId: 'policy-pack-shadow-001',
shadowVersion: 1,
trafficPercentage: 25,
}).pipe(
finalize(() => this.loading.set(false))
).subscribe({
next: (config) => {
this.config.set(config);
this.loadResults();
},
}, {
loadResultsQuery: this.buildResultsQuery(),
});
}
onDisableShadowMode(): void {
this.loading.set(true);
this.api.disableShadowMode().pipe(
finalize(() => this.loading.set(false))
).subscribe({
next: () => {
this.config.set({ ...this.config()!, enabled: false, status: 'disabled' });
this.results.set(undefined);
},
});
this.shadowModeState.disable();
}
formatDivergenceReason(reason: string): string {
@@ -684,5 +630,15 @@ export class ShadowModeDashboardComponent implements OnInit {
};
return new Date(now - (durations[range] ?? 86400000)).toISOString();
}
private buildResultsQuery() {
const timeRange = this.filterForm.value.timeRange ?? '24h';
return {
tenantId: 'default',
fromTime: this.calculateFromTime(timeRange),
toTime: new Date().toISOString(),
divergedOnly: this.filterForm.value.showOnly === 'diverged',
};
}
}

View File

@@ -0,0 +1,86 @@
import { Injectable, inject, signal } from '@angular/core';
import { finalize } from 'rxjs/operators';
import { POLICY_SIMULATION_API } from '../../core/api/policy-simulation.client';
import { ShadowModeConfig, ShadowModeQueryOptions, ShadowModeResults } from '../../core/api/policy-simulation.models';
@Injectable({ providedIn: 'root' })
export class ShadowModeStateService {
private readonly api = inject(POLICY_SIMULATION_API);
readonly loading = signal(false);
readonly config = signal<ShadowModeConfig | undefined>(undefined);
readonly results = signal<ShadowModeResults | undefined>(undefined);
loadConfig(options: { fallback?: ShadowModeConfig; loadResultsQuery?: ShadowModeQueryOptions } = {}): void {
this.loading.set(true);
this.api.getShadowModeConfig().pipe(
finalize(() => this.loading.set(false))
).subscribe({
next: (config) => {
this.config.set(config);
if (config.enabled && options.loadResultsQuery) {
this.loadResults(options.loadResultsQuery);
} else if (!config.enabled) {
this.results.set(undefined);
}
},
error: () => {
this.config.set(options.fallback);
if (!options.fallback) {
this.results.set(undefined);
}
},
});
}
loadResults(query: ShadowModeQueryOptions): void {
this.loading.set(true);
this.api.getShadowModeResults(query).pipe(
finalize(() => this.loading.set(false))
).subscribe({
next: (results) => {
this.results.set(results);
this.config.set(results.config);
},
error: () => {
this.results.set(undefined);
},
});
}
enable(config: Partial<ShadowModeConfig>, options: { loadResultsQuery?: ShadowModeQueryOptions } = {}): void {
this.loading.set(true);
this.api.enableShadowMode(config).pipe(
finalize(() => this.loading.set(false))
).subscribe({
next: (nextConfig) => {
this.config.set(nextConfig);
if (options.loadResultsQuery) {
this.loadResults(options.loadResultsQuery);
}
},
});
}
disable(): void {
this.loading.set(true);
this.api.disableShadowMode().pipe(
finalize(() => this.loading.set(false))
).subscribe({
next: () => {
const current = this.config();
this.config.set(current ? { ...current, enabled: false, status: 'disabled' } : {
enabled: false,
status: 'disabled',
shadowPackId: '',
shadowVersion: 0,
activePackId: '',
activeVersion: 0,
trafficPercentage: 0,
});
this.results.set(undefined);
},
});
}
}

View File

@@ -1,13 +1,9 @@

import { Component, ChangeDetectionStrategy, signal, inject, OnInit } from '@angular/core';
import { RouterModule, Router } from '@angular/router';
import { finalize } from 'rxjs/operators';
import { ShadowModeIndicatorComponent } from './shadow-mode-indicator.component';
import {
POLICY_SIMULATION_API,
} from '../../core/api/policy-simulation.client';
import { ShadowModeConfig } from '../../core/api/policy-simulation.models';
import { ShadowModeStateService } from './shadow-mode-state.service';
/**
* Main Policy Simulation Studio dashboard component with tabbed navigation.
@@ -420,12 +416,12 @@ import { ShadowModeConfig } from '../../core/api/policy-simulation.models';
`]
})
export class SimulationDashboardComponent implements OnInit {
private readonly api = inject(POLICY_SIMULATION_API);
private readonly router = inject(Router);
private readonly shadowModeState = inject(ShadowModeStateService);
protected readonly activeTab = signal<string>('shadow');
protected readonly shadowConfig = signal<ShadowModeConfig | undefined>(undefined);
protected readonly shadowLoading = signal(false);
protected readonly shadowConfig = this.shadowModeState.config;
protected readonly shadowLoading = this.shadowModeState.loading;
// Promotion status (mocked - would be computed from actual API data)
protected readonly promotionStatus = signal({
@@ -553,32 +549,22 @@ export class SimulationDashboardComponent implements OnInit {
}
protected loadShadowStatus(): void {
this.shadowLoading.set(true);
this.api.getShadowModeConfig().pipe(
finalize(() => this.shadowLoading.set(false))
).subscribe({
next: (config) => {
this.shadowConfig.set(config);
},
error: () => {
// Mock fallback for development
this.shadowConfig.set({
enabled: true,
status: 'enabled',
shadowPackId: 'policy-pack-shadow-001',
shadowVersion: 2,
activePackId: 'policy-pack-001',
activeVersion: 1,
trafficPercentage: 10,
enabledAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
});
this.shadowModeState.loadConfig({
fallback: {
enabled: true,
status: 'enabled',
shadowPackId: 'policy-pack-shadow-001',
shadowVersion: 2,
activePackId: 'policy-pack-001',
activeVersion: 1,
trafficPercentage: 10,
enabledAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
},
});
}
protected enableShadowMode(): void {
this.shadowLoading.set(true);
this.api.enableShadowMode({
this.shadowModeState.enable({
enabled: true,
status: 'enabled',
shadowPackId: 'policy-pack-shadow-001',
@@ -586,32 +572,11 @@ export class SimulationDashboardComponent implements OnInit {
activePackId: 'policy-pack-001',
activeVersion: 1,
trafficPercentage: 10,
}).pipe(
finalize(() => this.shadowLoading.set(false))
).subscribe({
next: (config) => {
this.shadowConfig.set(config);
},
});
}
protected disableShadowMode(): void {
this.shadowLoading.set(true);
this.api.disableShadowMode().pipe(
finalize(() => this.shadowLoading.set(false))
).subscribe({
next: () => {
this.shadowConfig.set({
enabled: false,
status: 'disabled',
shadowPackId: '',
shadowVersion: 0,
activePackId: '',
activeVersion: 0,
trafficPercentage: 0,
});
},
});
this.shadowModeState.disable();
}
protected navigateToHistory(): void {

View File

@@ -1,5 +1,5 @@
import { Routes } from '@angular/router';
import { requireOrchOperatorGuard, requireOrchViewerGuard } from '../core/auth';
import { requireOrchQuotaGuard, requireOrchViewerGuard } from '../core/auth';
export const OPERATIONS_ROUTES: Routes = [
{
@@ -111,7 +111,7 @@ export const OPERATIONS_ROUTES: Routes = [
path: 'jobengine/quotas',
title: 'JobEngine Quotas',
data: { breadcrumb: 'JobEngine Quotas' },
canMatch: [requireOrchOperatorGuard],
canMatch: [requireOrchQuotaGuard],
loadComponent: () =>
import('../features/jobengine/jobengine-quotas.component').then(
(m) => m.JobEngineQuotasComponent,