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

View File

@@ -1,19 +1,9 @@
import { CommonModule } from '@angular/common'; 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 { 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 { ShadowModeIndicatorComponent } from './shadow-mode-indicator.component';
import { ShadowModeStateService } from './shadow-mode-state.service';
/** /**
* Shadow mode results dashboard with divergence highlighting. * Shadow mode results dashboard with divergence highlighting.
@@ -544,12 +534,12 @@ import { ShadowModeIndicatorComponent } from './shadow-mode-indicator.component'
] ]
}) })
export class ShadowModeDashboardComponent implements OnInit { export class ShadowModeDashboardComponent implements OnInit {
private readonly api = inject(POLICY_SIMULATION_API);
private readonly fb = inject(FormBuilder); private readonly fb = inject(FormBuilder);
private readonly shadowModeState = inject(ShadowModeStateService);
readonly loading = signal(false); readonly loading = this.shadowModeState.loading;
readonly config = signal<ShadowModeConfig | undefined>(undefined); readonly config = this.shadowModeState.config;
readonly results = signal<ShadowModeResults | undefined>(undefined); readonly results = this.shadowModeState.results;
// Template-safe computed signals for summary properties // Template-safe computed signals for summary properties
readonly summaryTotalEvaluations = computed(() => this.results()?.summary?.totalEvaluations ?? 0); readonly summaryTotalEvaluations = computed(() => this.results()?.summary?.totalEvaluations ?? 0);
@@ -601,71 +591,27 @@ export class ShadowModeDashboardComponent implements OnInit {
} }
loadConfig(): void { loadConfig(): void {
this.loading.set(true); this.shadowModeState.loadConfig({
this.api.getShadowModeConfig().pipe( loadResultsQuery: this.buildResultsQuery(),
finalize(() => this.loading.set(false))
).subscribe({
next: (config) => {
this.config.set(config);
if (config.enabled) {
this.loadResults();
}
},
error: () => {
this.config.set(undefined);
},
}); });
} }
loadResults(): void { loadResults(): void {
this.loading.set(true); this.shadowModeState.loadResults(this.buildResultsQuery());
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);
},
});
} }
onEnableShadowMode(): void { onEnableShadowMode(): void {
this.loading.set(true); this.shadowModeState.enable({
this.api.enableShadowMode({
shadowPackId: 'policy-pack-shadow-001', shadowPackId: 'policy-pack-shadow-001',
shadowVersion: 1, shadowVersion: 1,
trafficPercentage: 25, trafficPercentage: 25,
}).pipe( }, {
finalize(() => this.loading.set(false)) loadResultsQuery: this.buildResultsQuery(),
).subscribe({
next: (config) => {
this.config.set(config);
this.loadResults();
},
}); });
} }
onDisableShadowMode(): void { onDisableShadowMode(): void {
this.loading.set(true); this.shadowModeState.disable();
this.api.disableShadowMode().pipe(
finalize(() => this.loading.set(false))
).subscribe({
next: () => {
this.config.set({ ...this.config()!, enabled: false, status: 'disabled' });
this.results.set(undefined);
},
});
} }
formatDivergenceReason(reason: string): string { formatDivergenceReason(reason: string): string {
@@ -684,5 +630,15 @@ export class ShadowModeDashboardComponent implements OnInit {
}; };
return new Date(now - (durations[range] ?? 86400000)).toISOString(); 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 { Component, ChangeDetectionStrategy, signal, inject, OnInit } from '@angular/core';
import { RouterModule, Router } from '@angular/router'; import { RouterModule, Router } from '@angular/router';
import { finalize } from 'rxjs/operators';
import { ShadowModeIndicatorComponent } from './shadow-mode-indicator.component'; import { ShadowModeIndicatorComponent } from './shadow-mode-indicator.component';
import { import { ShadowModeStateService } from './shadow-mode-state.service';
POLICY_SIMULATION_API,
} from '../../core/api/policy-simulation.client';
import { ShadowModeConfig } from '../../core/api/policy-simulation.models';
/** /**
* Main Policy Simulation Studio dashboard component with tabbed navigation. * 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 { export class SimulationDashboardComponent implements OnInit {
private readonly api = inject(POLICY_SIMULATION_API);
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly shadowModeState = inject(ShadowModeStateService);
protected readonly activeTab = signal<string>('shadow'); protected readonly activeTab = signal<string>('shadow');
protected readonly shadowConfig = signal<ShadowModeConfig | undefined>(undefined); protected readonly shadowConfig = this.shadowModeState.config;
protected readonly shadowLoading = signal(false); protected readonly shadowLoading = this.shadowModeState.loading;
// Promotion status (mocked - would be computed from actual API data) // Promotion status (mocked - would be computed from actual API data)
protected readonly promotionStatus = signal({ protected readonly promotionStatus = signal({
@@ -553,32 +549,22 @@ export class SimulationDashboardComponent implements OnInit {
} }
protected loadShadowStatus(): void { protected loadShadowStatus(): void {
this.shadowLoading.set(true); this.shadowModeState.loadConfig({
this.api.getShadowModeConfig().pipe( fallback: {
finalize(() => this.shadowLoading.set(false)) enabled: true,
).subscribe({ status: 'enabled',
next: (config) => { shadowPackId: 'policy-pack-shadow-001',
this.shadowConfig.set(config); shadowVersion: 2,
}, activePackId: 'policy-pack-001',
error: () => { activeVersion: 1,
// Mock fallback for development trafficPercentage: 10,
this.shadowConfig.set({ enabledAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
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 { protected enableShadowMode(): void {
this.shadowLoading.set(true); this.shadowModeState.enable({
this.api.enableShadowMode({
enabled: true, enabled: true,
status: 'enabled', status: 'enabled',
shadowPackId: 'policy-pack-shadow-001', shadowPackId: 'policy-pack-shadow-001',
@@ -586,32 +572,11 @@ export class SimulationDashboardComponent implements OnInit {
activePackId: 'policy-pack-001', activePackId: 'policy-pack-001',
activeVersion: 1, activeVersion: 1,
trafficPercentage: 10, trafficPercentage: 10,
}).pipe(
finalize(() => this.shadowLoading.set(false))
).subscribe({
next: (config) => {
this.shadowConfig.set(config);
},
}); });
} }
protected disableShadowMode(): void { protected disableShadowMode(): void {
this.shadowLoading.set(true); this.shadowModeState.disable();
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,
});
},
});
} }
protected navigateToHistory(): void { protected navigateToHistory(): void {

View File

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