Absorb Release Gates into Deployments Approvals tab
Product rationale: Users asking "why is my release blocked?" had to check 3 places (Policy > Release Gates, Deployments > Approvals, Release Detail > Gates). Now the Deployments Approvals tab is the single surface for all blocking information with action capability. Approvals tab enriched: - Summary cards: Pending Approvals, Gate Failures, Gates Passed counts - Table columns: Release, Promotion, Gate Type, Status, Reason, Requested, Actions - Gate Type column shows approval/policy/security/freeze/dependency - Reason column explains blocking status - Contextual action: "Policy" link to policy packs when gates are blocking - Approve/Reject buttons for pending human approvals Policy sidebar: - Release Gates removed from Policy group (5 items → 4: Packs, Governance, Simulation, VEX & Exceptions, Audit) - /ops/policy/gates redirects to /releases/deployments?view=approvals - Sub-routes (gates/catalog, gates/simulate, etc.) retained for deep links - Policy shell subtitle updated to remove "release gates" mention No functionality loss — policy authors use Simulation to test rules and Audit to review change history. The gate evaluation impact on releases is now visible in context where operators can act on it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -204,14 +204,30 @@ function deriveOutcomeIcon(status: string): string {
|
||||
} @else if (filteredApprovals().length === 0) {
|
||||
<div class="empty-state">No approvals match the active gate filters.</div>
|
||||
} @else {
|
||||
<!-- Summary cards -->
|
||||
<div class="gate-summary">
|
||||
<div class="gate-summary__card gate-summary__card--pending">
|
||||
<strong>{{ countByStatus('pending') }}</strong>
|
||||
<span>Pending Approvals</span>
|
||||
</div>
|
||||
<div class="gate-summary__card gate-summary__card--blocked">
|
||||
<strong>{{ countByGateResult(false) }}</strong>
|
||||
<span>Gate Failures</span>
|
||||
</div>
|
||||
<div class="gate-summary__card gate-summary__card--passed">
|
||||
<strong>{{ countByGateResult(true) }}</strong>
|
||||
<span>Gates Passed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="stella-table stella-table--striped stella-table--hoverable stella-table--bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Release</th>
|
||||
<th>Promotion</th>
|
||||
<th>Gate Type</th>
|
||||
<th>Status</th>
|
||||
<th>Urgency</th>
|
||||
<th>Gates</th>
|
||||
<th>Reason</th>
|
||||
<th>Requested</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
@@ -226,9 +242,11 @@ function deriveOutcomeIcon(status: string): string {
|
||||
}
|
||||
</td>
|
||||
<td>{{ apr.sourceEnvironment }} → {{ apr.targetEnvironment }}</td>
|
||||
<td><span class="gate-type-chip" [attr.data-gate]="deriveGateType(apr)">{{ deriveGateType(apr) }}</span></td>
|
||||
<td><span class="status-chip" [attr.data-status]="apr.status">{{ apr.status }}</span></td>
|
||||
<td><span class="urgency-chip" [attr.data-urgency]="apr.urgency">{{ apr.urgency }}</span></td>
|
||||
<td><span [class.text-success]="apr.gatesPassed" [class.text-danger]="!apr.gatesPassed">{{ apr.gatesPassed ? 'Passed' : 'Failed' }}</span></td>
|
||||
<td class="muted" [title]="apr.gatesPassed ? 'All gates passed' : 'One or more gates blocking'">
|
||||
{{ apr.gatesPassed ? 'All gates passed' : 'Gates blocking — review required' }}
|
||||
</td>
|
||||
<td class="muted">{{ formatDate(apr.requestedAt) }}<br>by {{ apr.requestedBy }}</td>
|
||||
<td>
|
||||
<div class="row-actions">
|
||||
@@ -236,12 +254,15 @@ function deriveOutcomeIcon(status: string): string {
|
||||
<button class="apc__btn apc__btn--approve" (click)="onApprove(apr)" type="button">Approve</button>
|
||||
<button class="apc__btn apc__btn--reject" (click)="onReject(apr)" type="button">Reject</button>
|
||||
}
|
||||
@if (!apr.gatesPassed) {
|
||||
<a class="apc__btn apc__btn--view" routerLink="/ops/policy/packs">Policy</a>
|
||||
}
|
||||
<button class="apc__btn apc__btn--view" (click)="onView(apr)" type="button">View</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr><td colspan="7" style="text-align:center;padding:1.5rem;color:var(--color-text-muted)">No approvals match the active gate filters.</td></tr>
|
||||
<tr><td colspan="7" style="text-align:center;padding:1.5rem;color:var(--color-text-muted)">No approvals or gate evaluations match the current filters.</td></tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -395,6 +416,14 @@ function deriveOutcomeIcon(status: string): string {
|
||||
|
||||
.apc__status-row{display:flex;gap:.35rem;padding:0 .6rem .45rem;flex-wrap:wrap}
|
||||
|
||||
.gate-summary{display:flex;gap:1rem;margin-bottom:1rem}
|
||||
.gate-summary__card{flex:1;padding:0.75rem 1rem;border:1px solid var(--color-border-primary);border-radius:var(--radius-md);display:flex;align-items:center;gap:0.5rem}
|
||||
.gate-summary__card strong{font-size:1.25rem}
|
||||
.gate-summary__card span{font-size:0.75rem;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.04em}
|
||||
.gate-summary__card--pending{border-left:3px solid var(--color-status-warning)}
|
||||
.gate-summary__card--blocked{border-left:3px solid var(--color-severity-high)}
|
||||
.gate-summary__card--passed{border-left:3px solid var(--color-status-success)}
|
||||
.gate-type-chip{font-size:0.6875rem;font-weight:600;text-transform:uppercase;letter-spacing:0.03em;padding:0.125rem 0.5rem;border-radius:var(--radius-sm);background:var(--color-surface-tertiary)}
|
||||
.pending-lane{margin-bottom:1rem;padding:1rem;border:1px solid var(--color-status-warning-border);border-radius:var(--radius-lg);background:var(--color-status-warning-bg);animation:lane-enter 300ms ease-out both;overflow:hidden}
|
||||
.pending-lane--exiting{animation:lane-exit 250ms ease-in forwards}
|
||||
@keyframes lane-enter{from{opacity:0;max-height:0;padding:0;margin:0;border-width:0}to{opacity:1;max-height:500px}}
|
||||
@@ -523,6 +552,14 @@ export class ReleasesActivityComponent implements OnInit, OnDestroy {
|
||||
|
||||
readonly approvalSearchQuery = signal('');
|
||||
|
||||
countByStatus(status: string): number {
|
||||
return this.filteredApprovals().filter(a => a.status === status).length;
|
||||
}
|
||||
|
||||
countByGateResult(passed: boolean): number {
|
||||
return this.filteredApprovals().filter(a => a.gatesPassed === passed).length;
|
||||
}
|
||||
|
||||
readonly gateToggleState = signal<Record<string, boolean>>({
|
||||
gated: false,
|
||||
policy: false,
|
||||
|
||||
@@ -777,7 +777,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
id: 'ops-environments',
|
||||
label: 'Environments',
|
||||
icon: 'globe',
|
||||
route: '/environments/regions',
|
||||
route: '/environments/overview',
|
||||
menuGroupId: 'release-control',
|
||||
menuGroupLabel: 'Release Control',
|
||||
requireAnyScope: [
|
||||
@@ -821,15 +821,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
menuGroupLabel: 'Policy',
|
||||
requireAnyScope: [StellaOpsScopes.VEX_READ, StellaOpsScopes.EXCEPTION_READ],
|
||||
},
|
||||
{
|
||||
id: 'ops-policy-gates',
|
||||
label: 'Release Gates',
|
||||
icon: 'check-square',
|
||||
route: '/ops/policy/gates',
|
||||
menuGroupId: 'policy',
|
||||
menuGroupLabel: 'Policy',
|
||||
requireAnyScope: [StellaOpsScopes.POLICY_READ],
|
||||
},
|
||||
// Release Gates absorbed into Deployments > Approvals tab
|
||||
{
|
||||
id: 'ops-policy-audit',
|
||||
label: 'Policy Audit',
|
||||
|
||||
Reference in New Issue
Block a user