Throttle sidebar pending approvals badge refresh
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideRouter, Router } from '@angular/router';
|
||||
import { of } from 'rxjs';
|
||||
import { AppSidebarComponent } from './app-sidebar.component';
|
||||
import { APPROVAL_API, type ApprovalApi } from '../../core/api/approval.client';
|
||||
import {
|
||||
AUTH_SERVICE,
|
||||
MockAuthService,
|
||||
@@ -8,16 +11,32 @@ import {
|
||||
StellaOpsScopes,
|
||||
} from '../../core/auth';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
class DummyRouteComponent {}
|
||||
|
||||
describe('AppSidebarComponent', () => {
|
||||
let authService: MockAuthService;
|
||||
let approvalApi: jasmine.SpyObj<ApprovalApi>;
|
||||
|
||||
beforeEach(async () => {
|
||||
authService = new MockAuthService();
|
||||
approvalApi = jasmine.createSpyObj('ApprovalApi', ['listApprovals']) as jasmine.SpyObj<ApprovalApi>;
|
||||
approvalApi.listApprovals.and.returnValue(of([]));
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AppSidebarComponent],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
provideRouter([
|
||||
{ path: '', component: DummyRouteComponent },
|
||||
{ path: 'ops/integrations/scm', component: DummyRouteComponent },
|
||||
{ path: 'ops/integrations/registries', component: DummyRouteComponent },
|
||||
{ path: 'releases/approvals', component: DummyRouteComponent },
|
||||
]),
|
||||
{ provide: AUTH_SERVICE, useValue: authService },
|
||||
{ provide: APPROVAL_API, useValue: approvalApi },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
@@ -168,6 +187,38 @@ describe('AppSidebarComponent', () => {
|
||||
expect(hrefs).not.toContain('/setup/notifications');
|
||||
});
|
||||
|
||||
it('does not refetch pending approvals badge on unrelated navigation within the refresh window', async () => {
|
||||
setScopes([StellaOpsScopes.UI_READ, StellaOpsScopes.RELEASE_READ]);
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = createComponent();
|
||||
|
||||
expect(approvalApi.listApprovals.calls.count()).toBe(1);
|
||||
|
||||
await router.navigateByUrl('/ops/integrations/scm');
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
await router.navigateByUrl('/ops/integrations/registries');
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(approvalApi.listApprovals.calls.count()).toBe(1);
|
||||
});
|
||||
|
||||
it('forces a pending approvals refresh when navigating into approvals surfaces', async () => {
|
||||
setScopes([StellaOpsScopes.UI_READ, StellaOpsScopes.RELEASE_READ]);
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = createComponent();
|
||||
|
||||
expect(approvalApi.listApprovals.calls.count()).toBe(1);
|
||||
|
||||
await router.navigateByUrl('/releases/approvals');
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(approvalApi.listApprovals.calls.count()).toBe(2);
|
||||
});
|
||||
|
||||
function setScopes(scopes: readonly StellaOpsScope[]): void {
|
||||
const baseUser = authService.user();
|
||||
if (!baseUser) {
|
||||
|
||||
@@ -585,6 +585,8 @@ interface NavSectionGroup {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppSidebarComponent implements AfterViewInit {
|
||||
private static readonly PendingApprovalsBadgeRefreshWindowMs = 30_000;
|
||||
|
||||
private readonly router = inject(Router);
|
||||
private readonly authService = inject(AUTH_SERVICE) as AuthService;
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
@@ -618,6 +620,8 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
private flyoutLeaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
private readonly pendingApprovalsCount = signal(0);
|
||||
private readonly pendingApprovalsBadgeLoadedAt = signal<number | null>(null);
|
||||
private readonly pendingApprovalsBadgeLoading = signal(false);
|
||||
|
||||
/**
|
||||
* Navigation sections - pre-alpha canonical IA.
|
||||
@@ -837,13 +841,13 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.loadPendingApprovalsBadge();
|
||||
this.loadPendingApprovalsBadge(true);
|
||||
this.destroyRef.onDestroy(() => this.clearFlyoutTimers());
|
||||
this.router.events
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((event) => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
this.loadPendingApprovalsBadge();
|
||||
this.loadPendingApprovalsBadge(this.shouldForcePendingApprovalsRefresh(event.urlAfterRedirects));
|
||||
this.doctorTrendService.refresh();
|
||||
this.closeFlyout();
|
||||
}
|
||||
@@ -955,19 +959,49 @@ export class AppSidebarComponent implements AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
private loadPendingApprovalsBadge(): void {
|
||||
private loadPendingApprovalsBadge(force = false): void {
|
||||
if (!this.approvalApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const loadedAt = this.pendingApprovalsBadgeLoadedAt();
|
||||
if (!force) {
|
||||
if (this.pendingApprovalsBadgeLoading()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
loadedAt !== null
|
||||
&& now - loadedAt < AppSidebarComponent.PendingApprovalsBadgeRefreshWindowMs
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.pendingApprovalsBadgeLoading.set(true);
|
||||
this.approvalApi.listApprovals({ statuses: ['pending'] }).pipe(
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe({
|
||||
next: (approvals) => this.pendingApprovalsCount.set(approvals.length),
|
||||
error: () => this.pendingApprovalsCount.set(0),
|
||||
next: (approvals) => {
|
||||
this.pendingApprovalsCount.set(approvals.length);
|
||||
this.pendingApprovalsBadgeLoadedAt.set(now);
|
||||
this.pendingApprovalsBadgeLoading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
if (loadedAt === null) {
|
||||
this.pendingApprovalsCount.set(0);
|
||||
}
|
||||
this.pendingApprovalsBadgeLoading.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private shouldForcePendingApprovalsRefresh(url: string): boolean {
|
||||
const path = (url || '').split('?')[0] ?? '';
|
||||
return path.startsWith('/releases/approvals') || path.startsWith('/releases/promotions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouse-proximity auto-scroll for sidebar nav.
|
||||
* When mouse is within 60px of top/bottom edges, scroll in that direction.
|
||||
|
||||
Reference in New Issue
Block a user