Skip to content

Commit 2593cd6

Browse files
authored
Fix two regressions surfaced after the routes slice (5308) merge (#5311)
* Fix home card placeholder render under showMode=false Home cards rendered titles but `-` counts and placeholder rows when "Show all connected endpoints" was unchecked. The parent's QueryList-walking dispatch (setCardsToLoad) raced with child Input bindings: when `endpointElements.changes` fired, a child whose `endpoint` Input wasn't yet bound surfaced as `guid: undefined`, got skipped silently, and was never re-dispatched. Result: wrapper.load() never ran -> CFHomeCardComponent.load() never ran -> endpointDataService stayed null -> placeholders forever. Fix is to drive the load from the child's own ngOnInit, after Angular has bound the @input. Retire the parent's dispatchedGuids / setCardsToLoad / processCardsToLoad / viewport-scroll plumbing — the registry's mergeMap(maxConcurrentCards) already throttles the HTTP fan-out, so centralized dispatch isn't load-bearing for concurrency. * Restore Add Route button on app Routes tab Slice 3.5 rebuilt the AddRoutesComponent stepper but the Routes tab's entry point to it dropped out: the legacy <app-add-route-button> was retired, and the new SignalListConfig didn't populate headerActions. Result: the stepper at /applications/{cnsi}/{appGuid}/add-route was unreachable from the UI on signal-native foundations. Wire headerActions on the tab's listConfig with a single "Add Route" entry that navigates to the stepper. Keeps the existing row actions (Unmap / Delete) untouched. Mirrors the SignalListHeaderAction pattern already used by cloud-foundry-space-users.
1 parent fff9dc8 commit 2593cd6

4 files changed

Lines changed: 35 additions & 161 deletions

File tree

src/frontend/packages/cloud-foundry/src/features/applications/application/application-tabs-base/tabs/routes-tab/routes-tab/routes-tab.component.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import {
99
inject,
1010
signal,
1111
} from '@angular/core';
12+
import { Router } from '@angular/router';
1213

1314
import {
1415
ConfirmationDialogConfig,
1516
ConfirmationDialogService,
1617
SignalListColumn,
1718
SignalListComponent,
1819
SignalListConfig,
20+
SignalListHeaderAction,
1921
SignalListRowAction,
2022
TailwindSnackBarService,
2123
} from '@stratosui/core';
@@ -60,6 +62,7 @@ export class RoutesTabComponent implements OnInit, OnDestroy {
6062
private routesConfig = inject(CfAppRoutesSignalConfigService);
6163
private confirmDialog = inject(ConfirmationDialogService);
6264
private snackBar = inject(TailwindSnackBarService);
65+
private router = inject(Router);
6366

6467
/** Loading projection for the signal-list framework. */
6568
private readonly _isAnyLoading: Signal<boolean> = computed(() => this.dataService.loading().routes);
@@ -80,6 +83,21 @@ export class RoutesTabComponent implements OnInit, OnDestroy {
8083
return col;
8184
});
8285

86+
const headerActions: readonly SignalListHeaderAction[] = [
87+
{
88+
label: 'Add Route',
89+
icon: 'add',
90+
invoke: () => {
91+
void this.router.navigate([
92+
'/applications',
93+
this.dataService.cnsiGuid,
94+
this.dataService.appGuid,
95+
'add-route',
96+
]);
97+
},
98+
},
99+
];
100+
83101
this.listConfig = {
84102
pagedItems: this.routesConfig.view.pagedItems,
85103
totalFilteredResults: this.routesConfig.view.totalFilteredResults,
@@ -102,6 +120,7 @@ export class RoutesTabComponent implements OnInit, OnDestroy {
102120
onClear: () => this.routesConfig.clearFilters(),
103121
viewMode: this.routesConfig.viewMode,
104122
sort: this.routesConfig.sort,
123+
headerActions,
105124
};
106125
}
107126

src/frontend/packages/core/src/features/home/home/home-page-endpoint-card/home-page-endpoint-card.component.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ export class HomePageEndpointCardComponent implements OnInit, OnDestroy, AfterVi
192192
};
193193
})
194194
);
195+
196+
// Self-trigger load now that @Input endpoint is bound. This used to be
197+
// driven by the parent's QueryList walk, which raced with input
198+
// bindings and stranded cards on the showMode=false render path.
199+
this.load();
195200
}
196201

197202
ngOnDestroy() {

src/frontend/packages/core/src/features/home/home/home-page.component.html

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,10 @@ <h1>Home</h1>
4545
<div class="home-grid"
4646
[class.columns-1]="columns === 1"
4747
[class.columns-2]="columns === 2"
48-
[class.columns-3]="columns === 3"
49-
#endpointsPanel>
48+
[class.columns-3]="columns === 3">
5049
@for (ep of endpoints(); track ep?.guid ?? $index) {
51-
<div #endpointElements class="home-grid-item" [style.grid-column]="'span ' + getEffectiveSpan(ep)">
52-
<app-home-page-endpoint-card (loaded)="cardLoaded()" [layout]="layout()" [endpoint]="ep">
50+
<div class="home-grid-item" [style.grid-column]="'span ' + getEffectiveSpan(ep)">
51+
<app-home-page-endpoint-card [layout]="layout()" [endpoint]="ep">
5352
</app-home-page-endpoint-card>
5453
</div>
5554
}

src/frontend/packages/core/src/features/home/home/home-page.component.ts

Lines changed: 8 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { CommonModule } from '@angular/common';
2-
import { ScrollDispatcher } from '@angular/cdk/scrolling';
3-
import { ChangeDetectionStrategy, AfterViewInit, Component, computed, ElementRef, HostListener, Injector, OnDestroy, OnInit, QueryList, Signal, ViewChild, ViewChildren, effect, signal, inject } from '@angular/core';
4-
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
2+
import { ChangeDetectionStrategy, Component, computed, Injector, OnInit, Signal, effect, signal, inject } from '@angular/core';
3+
import { toSignal } from '@angular/core/rxjs-interop';
54
import { Router } from '@angular/router';
65
import {
76
IUserFavoritesGroups,
87
EndpointModel,
98
entityCatalog,
109
UserFavoriteManager } from '@stratosui/store';
11-
import { combineLatest, Observable, Subscription } from 'rxjs';
12-
import { take, debounceTime, filter, map, startWith, switchMap } from 'rxjs/operators';
10+
import { Observable } from 'rxjs';
11+
import { take, filter, map, switchMap } from 'rxjs/operators';
1312

1413
import { EndpointsService } from '../../../core/endpoints.service';
1514
import { SessionService } from '../../../core/session.service';
@@ -51,13 +50,12 @@ const noFavoritesMsg = (endpointCount: number, favoriteCount: number) => ({
5150
],
5251
changeDetection: ChangeDetectionStrategy.OnPush
5352
})
54-
export class HomePageComponent implements AfterViewInit, OnInit, OnDestroy {
53+
export class HomePageComponent implements OnInit {
5554
endpointsService = inject(EndpointsService);
5655
private router = inject(Router);
5756
private sessionService = inject(SessionService);
5857
private prefs = inject(DashboardPreferencesService);
5958
userFavoriteManager = inject(UserFavoriteManager);
60-
private scrollDispatcher = inject(ScrollDispatcher);
6159
private registry = inject(EndpointDataRegistry);
6260
private injector = inject(Injector);
6361

@@ -120,25 +118,6 @@ export class HomePageComponent implements AfterViewInit, OnInit, OnDestroy {
120118

121119
noneAvailableMsg = noFavoritesMsg(0, 0);
122120

123-
@ViewChild('endpointsPanel', { static: false }) endpointsPanel: ElementRef;
124-
@ViewChildren(HomePageEndpointCardComponent) endpointCards: QueryList<HomePageEndpointCardComponent>;
125-
@ViewChildren('endpointElements') endpointElements!: QueryList<ElementRef>;
126-
127-
notLoadedCardIndices: number[] = [];
128-
cardsToLoad: HomePageEndpointCardComponent[] = [];
129-
// Tracks which CNSI guids have already been dispatched to card.load().
130-
// Prevents duplicate wrapper.load() → cfhome.load() invocations when
131-
// QueryList.changes fires repeatedly. Registry.acquire() is also
132-
// idempotent on duplicate guid, but this set avoids the redundant chain.
133-
private dispatchedGuids = new Set<string>();
134-
135-
private viewMonitorSub!: Subscription;
136-
private cardChangesSub!: Subscription;
137-
private _checkLayout = signal<number>(0);
138-
private check$ = toObservable(this._checkLayout).pipe(
139-
debounceTime(100) // Debounce the check signal itself
140-
);
141-
142121
private redirectChecked = false;
143122
private layoutInitialized = false;
144123

@@ -197,7 +176,9 @@ export class HomePageComponent implements AfterViewInit, OnInit, OnDestroy {
197176
private concurrencyConfigured = false;
198177

199178
ngOnInit() {
200-
// Wire EndpointDataRegistry concurrency from backend session config (one-shot)
179+
// Wire EndpointDataRegistry concurrency from backend session config (one-shot).
180+
// Each card self-triggers load() in its own ngOnInit; the registry's
181+
// mergeMap(maxConcurrentCards) throttles the actual HTTP fan-out.
201182
effect(() => {
202183
const config = this.sessionService.config();
203184
if (!config || this.concurrencyConfigured) {
@@ -209,132 +190,6 @@ export class HomePageComponent implements AfterViewInit, OnInit, OnDestroy {
209190
}
210191
this.concurrencyConfigured = true;
211192
}, { injector: this.injector });
212-
213-
const scroll$ = this.scrollDispatcher.scrolled().pipe(
214-
map((e: any) => {
215-
const el = e.elementRef.nativeElement;
216-
return el.scrollTop;
217-
}),
218-
debounceTime(100), // Debounce scroll events
219-
startWith(0)
220-
);
221-
222-
// Load cards as they come into view
223-
this.viewMonitorSub = combineLatest([scroll$, this.check$]).pipe(
224-
debounceTime(150) // Reduce debounce time for better responsiveness
225-
).subscribe(([scrollTop]) => {
226-
// Skip if no cards to check
227-
if (this.notLoadedCardIndices.length === 0) {
228-
return;
229-
}
230-
231-
// User has scrolled - check the remaining cards that have not been loaded to see if any are now visible
232-
const remaining: number[] = [];
233-
const cardsArray = this.endpointElements.toArray();
234-
const cardsComponentArray = this.endpointCards.toArray();
235-
236-
// Early exit if arrays are empty or mismatched
237-
if (cardsArray.length === 0 || cardsComponentArray.length === 0) {
238-
return;
239-
}
240-
241-
const panelParent = this.endpointsPanel?.nativeElement?.offsetParent;
242-
if (!panelParent) {
243-
return;
244-
}
245-
246-
const height = panelParent.offsetHeight;
247-
const scrollBottom = scrollTop + height;
248-
249-
for (const index of this.notLoadedCardIndices) {
250-
const cardElement = cardsArray[index];
251-
if (!cardElement) {
252-
continue;
253-
}
254-
255-
const cardTop = cardElement.nativeElement.offsetTop;
256-
const cardBottom = cardTop + cardElement.nativeElement.offsetHeight;
257-
258-
// Check if the card is in view - either its top or bottom must be within the visible scroll area
259-
if ((cardTop >= scrollTop && cardTop <= scrollBottom) || (cardBottom >= scrollTop && cardBottom <= scrollBottom)) {
260-
const card = cardsComponentArray[index];
261-
if (card) {
262-
this.cardsToLoad.push(card);
263-
}
264-
} else {
265-
remaining.push(index);
266-
}
267-
}
268-
269-
this.notLoadedCardIndices = remaining;
270-
this.processCardsToLoad();
271-
});
272-
}
273-
274-
processCardsToLoad() {
275-
// No mutex — EndpointDataRegistry controls concurrency via mergeMap(N).
276-
while (this.cardsToLoad.length > 0) {
277-
const card = this.cardsToLoad.shift();
278-
if (card) {
279-
card.load();
280-
}
281-
}
282-
}
283-
284-
ngOnDestroy() {
285-
if (this.viewMonitorSub) {
286-
this.viewMonitorSub.unsubscribe();
287-
}
288-
if (this.cardChangesSub) {
289-
this.cardChangesSub.unsubscribe();
290-
}
291-
}
292-
293-
ngAfterViewInit(): void {
294-
this.cardChangesSub = this.endpointElements.changes.subscribe(cards => this.setCardsToLoad(cards));
295-
if (this.endpointElements.toArray().length > 0) {
296-
this.setCardsToLoad(this.endpointElements.toArray());
297-
}
298-
}
299-
300-
setCardsToLoad(_cards: ElementRef[]) {
301-
// Viewport gating reliably stranded below-fold cards because no scroll
302-
// event fires on first render, so the gate never re-checks. Dispatch
303-
// every card directly — the registry's mergeMap(maxConcurrentCards)
304-
// enforces bounded concurrency, and dispatchedGuids prevents duplicate
305-
// wrapper.load() calls when QueryList.changes re-emits.
306-
this.notLoadedCardIndices = [];
307-
const cardsArr = this.endpointCards?.toArray() ?? [];
308-
let dispatched = 0;
309-
for (const card of cardsArr) {
310-
const guid = (card as any)?.endpoint?.guid;
311-
if (!guid || this.dispatchedGuids.has(guid)) { continue; }
312-
this.dispatchedGuids.add(guid);
313-
this.cardsToLoad.push(card);
314-
dispatched++;
315-
}
316-
if (dispatched > 0) {
317-
this.processCardsToLoad();
318-
}
319-
}
320-
321-
// This is called after a card has loaded - we call the scroll handler again
322-
// to check if there are more cards that are visible and thus can be loaded
323-
cardLoaded() {
324-
if (this.notLoadedCardIndices.length > 0 && this.cardsToLoad.length === 0) {
325-
this.checkCardsInView();
326-
}
327-
}
328-
329-
@HostListener('window:resize')
330-
onResize() {
331-
// If we resize the window and make it larger then new cards may come into view
332-
this.checkCardsInView();
333-
}
334-
335-
// Check the cards in view
336-
checkCardsInView() {
337-
this._checkLayout.update(v => v + 1);
338193
}
339194

340195
public toggleShowAllEndpoints() {
@@ -352,10 +207,6 @@ export class HomePageComponent implements AfterViewInit, OnInit, OnDestroy {
352207

353208
// Persist the state
354209
this.prefs.setHomeLayout(this.layoutID);
355-
356-
// Ensure we check again if any cards are now visible
357-
// Schedule the check so it happens after the cards have been laid out
358-
setTimeout(() => this.checkCardsInView(), 1);
359210
}
360211

361212
// Order the endpoint cards:

0 commit comments

Comments
 (0)