Fix pending lane animation, approvals table, and Pipeline-only visibility
Pending actions lane: - Only visible on Pipeline tab (not Approvals), uses showPendingLane computed - Exit animation now works: pendingLaneHidden signal delays @if removal until the 250ms CSS collapse animation completes - Re-shows when switching back to Pipeline tab Approvals tab: - Replaced horizontal card scroll with a proper sortable table - Columns: Release, Promotion (source → target), Status, Urgency, Gates, Requested, Actions - Shows requestedAt date + requestedBy in the Requested column - Approve/Reject/View buttons inline in the Actions column - Empty state message when no approvals match gate filters Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -70,8 +70,8 @@ function deriveOutcomeIcon(status: string): string {
|
||||
<p>Deployment runs, approvals, and promotion activity.</p>
|
||||
</header>
|
||||
|
||||
<!-- Pending approvals inline lane (dashboard-style action cards) -->
|
||||
@if (pendingApprovals().length > 0 && viewMode() !== 'approvals') {
|
||||
<!-- Pending approvals inline lane (only visible on Pipeline tab) -->
|
||||
@if (showPendingLane()) {
|
||||
<div class="pending-lane" [class.pending-lane--exiting]="pendingLaneExiting()">
|
||||
<div class="pending-lane__header">
|
||||
<h2 class="pending-lane__title">
|
||||
@@ -197,49 +197,47 @@ function deriveOutcomeIcon(status: string): string {
|
||||
} @else if (filteredApprovals().length === 0) {
|
||||
<div class="empty-state">No approvals match the active gate filters.</div>
|
||||
} @else {
|
||||
<div class="apc-lane-wrapper" [class.can-scroll-left]="showApcLeft()" [class.can-scroll-right]="showApcRight()">
|
||||
@if (showApcLeft()) {
|
||||
<button class="apc-scroll apc-scroll--left" (click)="scrollApc('left')" type="button" aria-label="Scroll left">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
</button>
|
||||
}
|
||||
<div class="apc-lane" #apcScroll (scroll)="onApcScroll()">
|
||||
<table class="stella-table stella-table--striped stella-table--hoverable stella-table--bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Release</th>
|
||||
<th>Promotion</th>
|
||||
<th>Status</th>
|
||||
<th>Urgency</th>
|
||||
<th>Gates</th>
|
||||
<th>Requested</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (apr of filteredApprovals(); track apr.id) {
|
||||
<div class="apc" [class.apc--prod]="isProductionEnv(apr.targetEnvironment)" [class.apc--expiring]="isExpiringSoon(apr.expiresAt)">
|
||||
<div class="apc__head">
|
||||
<div class="apc__id"><span class="apc__name">{{ apr.releaseName }}</span>@if (apr.releaseVersion && apr.releaseVersion !== apr.releaseName) {<span class="apc__ver">{{ apr.releaseVersion }}</span>}</div>
|
||||
<span class="urgency-chip" [attr.data-urgency]="apr.urgency">{{ apr.urgency }}</span>
|
||||
</div>
|
||||
<div class="apc__envs">
|
||||
<span class="apc__env">{{ apr.sourceEnvironment }}</span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="apc__arr"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
<span class="apc__env" [class.apc__env--prod]="isProductionEnv(apr.targetEnvironment)">{{ apr.targetEnvironment }}</span>
|
||||
</div>
|
||||
<div class="apc__meta">
|
||||
<span class="apc__who">by {{ apr.requestedBy }}</span>
|
||||
<span class="apc__gate" [class.apc__gate--pass]="apr.gatesPassed" [class.apc__gate--fail]="!apr.gatesPassed">{{ apr.gatesPassed ? 'Gates OK' : 'Gates fail' }}</span>
|
||||
<span class="apc__exp" [class.text-warning]="isExpiringSoon(apr.expiresAt)">{{ timeRemaining(apr.expiresAt) }}</span>
|
||||
</div>
|
||||
<div class="apc__status-row">
|
||||
<span class="status-chip" [attr.data-status]="apr.status">{{ apr.status }}</span>
|
||||
<span class="gate-chip" [attr.data-gate]="deriveGateType(apr)">{{ deriveGateType(apr) }}</span>
|
||||
</div>
|
||||
<div class="apc__btns">
|
||||
@if (apr.status === 'pending') {
|
||||
<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>
|
||||
<tr [class.row--prod]="isProductionEnv(apr.targetEnvironment)">
|
||||
<td>
|
||||
<strong>{{ apr.releaseName }}</strong>
|
||||
@if (apr.releaseVersion && apr.releaseVersion !== apr.releaseName) {
|
||||
<br><span class="muted mono">{{ apr.releaseVersion }}</span>
|
||||
}
|
||||
<button class="apc__btn apc__btn--view" (click)="onView(apr)" type="button">View</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ apr.sourceEnvironment }} → {{ apr.targetEnvironment }}</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">{{ formatDate(apr.requestedAt) }}<br>by {{ apr.requestedBy }}</td>
|
||||
<td>
|
||||
<div class="row-actions">
|
||||
@if (apr.status === 'pending') {
|
||||
<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>
|
||||
}
|
||||
<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>
|
||||
}
|
||||
</div>
|
||||
@if (showApcRight()) {
|
||||
<button class="apc-scroll apc-scroll--right" (click)="scrollApc('right')" type="button" aria-label="Scroll right">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
} @else {
|
||||
<!-- Existing deployment views: filters + timeline/table/correlations -->
|
||||
@@ -733,12 +731,17 @@ export class ReleasesActivityComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
readonly pendingLaneExiting = signal(false);
|
||||
readonly showPendingLane = computed(() =>
|
||||
this.pendingApprovals().length > 0 && this.viewMode() === 'timeline' && !this.pendingLaneHidden()
|
||||
);
|
||||
private readonly pendingLaneHidden = signal(false);
|
||||
|
||||
onTabChange(tab: string): void {
|
||||
// Animate pending lane out before switching to approvals
|
||||
if (tab === 'approvals' && this.pendingApprovals().length > 0 && this.viewMode() !== 'approvals') {
|
||||
if (tab === 'approvals' && this.pendingApprovals().length > 0 && this.viewMode() === 'timeline') {
|
||||
this.pendingLaneExiting.set(true);
|
||||
setTimeout(() => {
|
||||
this.pendingLaneHidden.set(true);
|
||||
this.pendingLaneExiting.set(false);
|
||||
this.viewMode.set('approvals');
|
||||
this.loadApprovals();
|
||||
@@ -746,6 +749,10 @@ export class ReleasesActivityComponent implements OnInit, OnDestroy {
|
||||
}, 250);
|
||||
return;
|
||||
}
|
||||
// When switching back to pipeline, show pending lane again
|
||||
if (tab === 'timeline') {
|
||||
this.pendingLaneHidden.set(false);
|
||||
}
|
||||
this.viewMode.set(tab as 'timeline' | 'approvals');
|
||||
if (tab === 'approvals') this.loadApprovals();
|
||||
this.applyFilters();
|
||||
|
||||
Reference in New Issue
Block a user