Skip to content

fix: regression sweep — edit-stepper caches, refresh-spin, sort, list counters#5365

Merged
norman-abramovitz merged 9 commits into
cloudfoundry:developfrom
nabramovitz:fix/edit-stepper-stale-summary
May 23, 2026
Merged

fix: regression sweep — edit-stepper caches, refresh-spin, sort, list counters#5365
norman-abramovitz merged 9 commits into
cloudfoundry:developfrom
nabramovitz:fix/edit-stepper-stale-summary

Conversation

@nabramovitz
Copy link
Copy Markdown
Contributor

@nabramovitz nabramovitz commented May 23, 2026

Summary

Eight-commit regression sweep from a dev.98 adepttech verify session. Each commit is a focused single-file-family fix; all stack cleanly on develop.

# Commit Regression
1 3a96e524 After CnsiOrgsSource.update / CnsiSpacesSource.update, the auto-redirect to /summary rendered stale entity (old name/SSH/quota) until hard reload — the EndpointData list cache was patched but OrgDataService._org / SpaceDataService._space weren't
2 25d782ec Direct URL to /edit-space rendered empty form (SpaceName blank, SSH "Disabled" regardless of backend state) — CloudFoundrySpaceService had no load() call analogous to CloudFoundryOrganizationService
3 57c051e2 "Total Organizations: N" (and the other 8 L5 sub-nav counters) dropped to 0 when a filter had no matches — was bound to view.totalFilteredResults instead of unfiltered total
4 6a406c88 Routes toolbar missing the locked-CF scope indicator that Services + Marketplace lead with
5 f1096293 Endpoint disconnect cleared endpoint.user, so reconnect dialog opened with empty username — even when reconnecting as the same user. localStorage prefill restores the previous username
6 f9ca59ee Refresh-button spinner never engaged after the first load — isAnyLoading was wired to !hasLoadedOnce() on 17 list pages. Self-contained fix in signal-list component (internal isRefreshing signal), no per-config churn
7 d5d3b576 dev.99 version bump
8 7c194a2e ViewPipeline default sort was raw < / > so capital-O orgs jumped above lowercase, and org_10 landed between org_1 and org_2. Switch to localeCompare { numeric: true, sensitivity: 'base' } for case-insensitive natural sort

Live-verified on dev.99 adepttech: refresh-spin engages 31ms after click, persists through the XHR, clears on completion; all five mutation fixes verified browser-side.

A separate feature PR is planned to consolidate sort into the single naturalCompare from @stratosui/core and migrate sort dropdowns to support an inline match_case toggle per string column.

Test plan

  • bun run test --run for each touched spec — all green (added 7 new tests across remembered-username, view-pipeline, signal-list, org-data, space-data)
  • Browser-verified on dev.99 adepttech (https://console.run.adepttech.ca):
    • Add Org / Add Space — list updates without manual refresh
    • Edit-Org name — summary reflects new name without hard reload
    • Edit-Space form populated on direct URL navigation
    • Refresh button animation visible during XHR (orgs ~25s, persisted spinner)
    • "Total Organizations" stays anchored to dataset total when filter narrows
    • Org list sorts case-insensitively with natural numeric order
  • CI on this branch

After CnsiOrgsSource.update / CnsiSpacesSource.update, the steppers
auto-navigated to /summary while OrgDataService._org and
SpaceDataService._space still held the pre-edit values. Those signals
are private caches with warm-cache short-circuits — CnsiXSource patches
the EndpointDataService _orgs/_spaces list cache but doesn't reach the
detail signal the summary view reads from, so the new name/SSH/quota
only appeared after a hard reload.

Add patch(p) to both data services that merges into the cached entity
in place, and call it from each stepper after the canonical update
completes. Mirrors the existing eds.updateOrg(guid, patch) pattern.

Verified on dev.98 with browser playwright that the regression
reproduces; tests cover the new patch + the no-op-before-load case.
CloudFoundryOrganizationService kicks off orgDataService.load() in its
constructor so any consumer reading the org signal sees a populated
value, including the /edit-org direct URL. CloudFoundrySpaceService
had no equivalent call, so direct navigation to /edit-space landed
with spaceDataService.space()===null and the form prefilled empty
(Space Name blank, SSH "Disabled" regardless of backend state). The
form only populated when the user reached /edit-space via the summary
page, which had already warmed the cache through
cloud-foundry-space-base.

Add the matching load() on construction. Warm-cache short-circuit
makes it a no-op on the common summary→edit path.
The "Total X" L5 sub-nav title (e.g. "Total Organizations") was bound
to view.totalFilteredResults so a non-matching filter drove the
headline to 0 while the underlying dataset still held N items. Users
saw "Total Organizations: 0" on an endpoint with 56 orgs — jarring
and inconsistent with the label semantics.

Add view.totalItems (raw items().length) alongside totalFilteredResults
on ViewPipeline (and the endpoints-page local copy of the same class)
so the headline can stay anchored to the dataset size while the
filter/sort/paginator chain continues to feed off the filtered view.

Repoint every L5 sub-nav binding to view.totalItems: orgs, spaces,
space-quotas, org-quotas, users (cf/org/space), apps wall, routes-tab,
variables-tab, endpoints. The remaining totalFilteredResults reads in
the same component files are pass-through to SignalListConfig where
the paginator + empty-state branches genuinely want the filtered
count — those are unchanged.
Services + Marketplace tabs lead their filter row with a Cloud Foundry
dropdown locked to the URL-pinned CNSI so the user can always see
which endpoint they're scoped to. Routes was missing the same
indicator — the toolbar started with Organization, leaving the CF
scope implicit. Adds a single-option Cloud Foundry dropdown (label =
endpoint name) at the head of the filter row, disabled, mirroring the
existing services + marketplace pattern.

Endpoint name is sourced from the existing cfEndpointService.endpoint
signal; falls back to the cfGuid string if the EndpointModel hasn't
hydrated yet.
The backend clears endpoint.user on disconnect, so the reconnect dialog
opened with the credentials form empty even when the same user was
about to reconnect to the same endpoint. Friction every time and a
likely source of typos (long Cloud Foundry usernames).

Cache the username in localStorage on a successful credentials connect
(keyed by endpoint guid), and prefill it into the dialog's authValues
group when the form has a username control. Token / SSO forms have no
username field, so the prefill is a no-op there. Storage is wrapped in
try/catch so private-browsing quota failures degrade silently — the
user just types the username again, no error surfaces.
The signal-list template gated the inline refresh spinner on
config.isAnyLoading(), but most config services wire that to
!hasLoadedOnce() — true only during the initial load. Every refresh
click after that ran a real HTTP fetch (verified XHR fires) but
showed no visual feedback, so the button looked dead. The quota
config services were fixed in 2b26a07 by adding a dedicated
loading signal; the other 15+ list pages were never updated.

Self-contained fix at the signal-list layer: an internal
isRefreshing signal flips during invokeRefresh(), and the template
gates the spinner on (config.isAnyLoading() || isRefreshing()).
Components keep their existing onRefresh wiring; no per-config
churn. The page-level loading indicator (empty + loading branch)
inherits the same gate for consistency.

Verified live on adepttech orgs page: refresh button click fires
GET /pp/v1/cf/orgs (7.7s on this dataset), signal repopulates the
list — only the animation was missing, this fix restores it.
Orgs list rendered as:
  OrgNoSelectedQuota, e2e, opensource, org_1, org_10, org_11, ...,
  org_19, org_2, org_20, ...

— two visible problems:
1. Capital-O org jumped above lowercase orgs (raw `<` on strings uses
   ASCII codepoint order)
2. org_10 lands between org_1 and org_2 (lexicographic, not natural)

Other places in the codebase already use
`localeCompare(b, undefined, { numeric: true })` for the same need
(routes config, services-wall, marketplace). Pull that into the shared
ViewPipeline default comparator with sensitivity:'base' for the
case-insensitive piece. Mirrors the same change into the endpoints-
page local ViewPipeline copy.

Numbers and non-string comparisons keep their existing paths
unchanged. Date-as-ISO-string columns still sort correctly under
localeCompare since ISO 8601 is lexicographically ordered.
The Total X counter fix (57c051e) repointed L5 sub-nav bindings
from view.totalFilteredResults to view.totalItems on 10 list
components. Six component specs stub the routesConfig.view (etc.)
with { pagedItems, totalFilteredResults, totalPages } and now need a
totalItems entry too — without it the compiled template threw
"ctx.count is not a function" when the new view.totalItems signal was
read by the page's count binding.

Adds totalItems to each mock with a value matching the rest of the
stub's scale (computed for routes/variables specs that already model
a filtered pipeline, signal(0) for the simpler stubs).
Copy link
Copy Markdown
Contributor

@norman-abramovitz norman-abramovitz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@norman-abramovitz norman-abramovitz merged commit 90f1c50 into cloudfoundry:develop May 23, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants