Share shadow mode state and fix operations quota routing
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user