Fix Pack creation: correct API path, signal-based reactivity, null-safe sort
Three root causes for "create does nothing": 1. Wrong API base path: PolicyApiService used /api/policy (routed to platform service via /api/ catch-all) instead of /policy/api (routed to policy-gateway via nginx regex). Fixed API_BASE. 2. OnPush + plain fields: loading/packs/refreshing were plain booleans and arrays. OnPush change detection ignored their mutations. Converted all three to signals so template reactivity works automatically. 3. sortPacks crash: API returns packs with undefined modifiedAt/id fields. localeCompare on undefined threw TypeError, preventing the empty state from rendering. Added nullish coalescing fallbacks. Also removed hardcoded fallback "Core Policy Pack" from the store — it masked API failures and prevented the create form from appearing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -217,7 +217,7 @@ public sealed class TopologyLayoutService
|
|||||||
var deployingCount = envPaths.Count(p => p.Status is "running" or "deploying");
|
var deployingCount = envPaths.Count(p => p.Status is "running" or "deploying");
|
||||||
var pendingCount = envPaths.Count(p => p.Status is "pending" or "awaiting_approval" or "gates_running");
|
var pendingCount = envPaths.Count(p => p.Status is "pending" or "awaiting_approval" or "gates_running");
|
||||||
var failedCount = envPaths.Count(p => p.Status is "failed");
|
var failedCount = envPaths.Count(p => p.Status is "failed");
|
||||||
var totalDeployments = envPaths.Count;
|
var totalDeployments = env?.TargetCount ?? 0;
|
||||||
|
|
||||||
var agentStatus = ResolveAgentStatus(envId, targetsByEnv);
|
var agentStatus = ResolveAgentStatus(envId, targetsByEnv);
|
||||||
|
|
||||||
|
|||||||
@@ -88,20 +88,7 @@ export class PolicyPackStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fallbackPacks(): PolicyPackSummary[] {
|
private fallbackPacks(): PolicyPackSummary[] {
|
||||||
return [
|
return [];
|
||||||
{
|
|
||||||
id: 'pack-1',
|
|
||||||
name: 'Core Policy Pack',
|
|
||||||
description: '',
|
|
||||||
version: 'latest',
|
|
||||||
status: 'active',
|
|
||||||
createdAt: '',
|
|
||||||
modifiedAt: '',
|
|
||||||
createdBy: '',
|
|
||||||
modifiedBy: '',
|
|
||||||
tags: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private readCache(): PolicyPackSummary[] | null {
|
private readCache(): PolicyPackSummary[] | null {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, ChangeDetectionStrategy, inject, signal, ChangeDetectorRef } from '@angular/core';
|
import { Component, ChangeDetectionStrategy, inject, signal } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { AuthService, AUTH_SERVICE } from '../../../core/auth';
|
import { AuthService, AUTH_SERVICE } from '../../../core/auth';
|
||||||
import { Router, RouterLink } from '@angular/router';
|
import { Router, RouterLink } from '@angular/router';
|
||||||
@@ -13,9 +13,9 @@ import { PolicyPackStore } from '../services/policy-pack.store';
|
|||||||
imports: [CommonModule, RouterLink, FormsModule],
|
imports: [CommonModule, RouterLink, FormsModule],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<section class="workspace" [attr.aria-busy]="loading">
|
<section class="workspace" [attr.aria-busy]="loading()">
|
||||||
<div class="workspace__actions">
|
<div class="workspace__actions">
|
||||||
<button type="button" (click)="refresh()" [disabled]="refreshing">{{ refreshing ? 'Refreshing…' : 'Refresh' }}</button>
|
<button type="button" (click)="refresh()" [disabled]="refreshing()">{{ refreshing() ? 'Refreshing…' : 'Refresh' }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (scopeHint) {
|
@if (scopeHint) {
|
||||||
@@ -25,7 +25,7 @@ import { PolicyPackStore } from '../services/policy-pack.store';
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div class="workspace__grid">
|
<div class="workspace__grid">
|
||||||
@for (pack of packs; track pack) {
|
@for (pack of packs(); track pack) {
|
||||||
<article class="pack-card">
|
<article class="pack-card">
|
||||||
<header class="pack-card__head">
|
<header class="pack-card__head">
|
||||||
<div>
|
<div>
|
||||||
@@ -93,7 +93,7 @@ import { PolicyPackStore } from '../services/policy-pack.store';
|
|||||||
</dl>
|
</dl>
|
||||||
</article>
|
</article>
|
||||||
} @empty {
|
} @empty {
|
||||||
@if (!loading) {
|
@if (!loading()) {
|
||||||
<div class="workspace__empty" data-testid="policy-packs-empty-state">
|
<div class="workspace__empty" data-testid="policy-packs-empty-state">
|
||||||
<h3>No policy packs configured</h3>
|
<h3>No policy packs configured</h3>
|
||||||
<p>
|
<p>
|
||||||
@@ -165,8 +165,9 @@ import { PolicyPackStore } from '../services/policy-pack.store';
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class PolicyWorkspaceComponent {
|
export class PolicyWorkspaceComponent {
|
||||||
protected loading = false;
|
protected readonly loading = signal(true);
|
||||||
protected packs: PolicyPackSummary[] = [];
|
protected readonly packs = signal<PolicyPackSummary[]>([]);
|
||||||
|
protected readonly refreshing = signal(false);
|
||||||
protected canAuthor = false;
|
protected canAuthor = false;
|
||||||
protected canSimulate = false;
|
protected canSimulate = false;
|
||||||
protected canReview = false;
|
protected canReview = false;
|
||||||
@@ -176,7 +177,6 @@ export class PolicyWorkspaceComponent {
|
|||||||
protected canReviewOrApprove = false;
|
protected canReviewOrApprove = false;
|
||||||
protected canView = false;
|
protected canView = false;
|
||||||
protected scopeHint = '';
|
protected scopeHint = '';
|
||||||
protected refreshing = false;
|
|
||||||
|
|
||||||
protected readonly newPackName = signal('');
|
protected readonly newPackName = signal('');
|
||||||
protected readonly newPackDesc = signal('');
|
protected readonly newPackDesc = signal('');
|
||||||
@@ -187,32 +187,39 @@ export class PolicyWorkspaceComponent {
|
|||||||
private readonly policyApi = inject(PolicyApiService);
|
private readonly policyApi = inject(PolicyApiService);
|
||||||
private readonly auth = inject(AUTH_SERVICE) as AuthService;
|
private readonly auth = inject(AUTH_SERVICE) as AuthService;
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
private readonly cdr = inject(ChangeDetectorRef);
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.loading = true;
|
this.loading.set(true);
|
||||||
this.applyScopes();
|
this.applyScopes();
|
||||||
this.packStore.getPacks().subscribe((packs) => {
|
this.packStore.getPacks().subscribe({
|
||||||
this.packs = this.sortPacks(packs);
|
next: (packs) => {
|
||||||
this.loading = false;
|
this.packs.set(this.sortPacks(packs));
|
||||||
this.cdr.markForCheck();
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.packs.set([]);
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh(): void {
|
refresh(): void {
|
||||||
this.refreshing = true;
|
this.refreshing.set(true);
|
||||||
this.cdr.markForCheck();
|
|
||||||
this.packStore.refresh();
|
this.packStore.refresh();
|
||||||
this.packStore.getPacks().subscribe((packs) => {
|
this.packStore.getPacks().subscribe({
|
||||||
this.packs = this.sortPacks(packs);
|
next: (packs) => {
|
||||||
this.refreshing = false;
|
this.packs.set(this.sortPacks(packs));
|
||||||
this.cdr.markForCheck();
|
this.refreshing.set(false);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.refreshing.set(false);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private sortPacks(packs: PolicyPackSummary[]): PolicyPackSummary[] {
|
private sortPacks(packs: PolicyPackSummary[]): PolicyPackSummary[] {
|
||||||
return [...packs].sort((a, b) =>
|
return [...packs].sort((a, b) =>
|
||||||
b.modifiedAt.localeCompare(a.modifiedAt) || a.id.localeCompare(b.id)
|
(b.modifiedAt ?? '').localeCompare(a.modifiedAt ?? '') || (a.id ?? '').localeCompare(b.id ?? '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user