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