Wave CDEF: signal-native ngrx cleanup, mutation-refresh, fw-capi async-delete#5362
Merged
norman-abramovitz merged 24 commits intoMay 23, 2026
Conversation
Introduce MetricsDataService — a signal-native HTTP client for CF
metrics that replaces the V2 MetricsAction / EntityServiceFactory /
EntityMonitor wire. Exposes a one-shot fetch(req) and a signal-bound
observe(requestSignal, { pollIntervalMs }) that returns
{ metrics, fetching, error, refresh, stop } signals.
Pattern-setter for B1 PR-A1: chart components + range selectors + 4
consumers move to MetricsRequest signals in subsequent commits of this
wave. cf-metrics.actions.ts and metrics.effects.ts stay for now;
PR-A2 deletes them once all V2 consumers are gone.
Refactor MetricsChartComponent, MetricsRangeSelectorComponent,
MetricsParentRangeSelectorComponent and the range-selector services
to consume Signal<MetricsRequest> via MetricsDataService.observe()
instead of dispatching MetricsAction objects through the V2 ngrx
METRICS_START effect + EntityMonitor wire.
Chart owns a writable requestSignal; ContentChild range selector
emits a MetricsRequest output that the chart applies to the signal;
parent range selector mutates each child chart's signal via the
new chart.applyRequest() seam.
Migrated 7 consumers off MetricsAction construction:
- cf: metrics-tab, application-instance-chart,
cloud-foundry-cell.service, cf-cell-apps-signal-config
- k8s: pod-metrics, kubernetes-node-metrics,
kubernetes-node-metrics-chart
cloud-foundry-cell.service now picks the live health-metric (HEALTHY
vs HEALTHY_DEP) by direct fetch-and-check instead of the CfCellHelper
ngrx pagination probe.
Also:
- drop dead <app-metrics-range-selector> block from list.component
(showCustomTime is set false everywhere; V2 list framework)
- MetricsDataService.observe() anchors effect() to the service's
captured injector via runInInjectionContext so callers from
ngOnInit (not themselves in an injection context) work
- update 2 inline-mocked specs (cloud-foundry-cell-charts/-summary)
for the MetricsConfig.request field rename
cf-metrics.actions.ts, metrics.effects.ts and kubernetes metrics
actions are now dead code; PR-A2 deletes them.
Now that the chart layer + 7 consumers are off MetricsAction, strip the
dead V2 wiring end-to-end:
Live consumer migrations:
- KubernetesNodeService.setupMetricObservable now drives the node
stats cards via MetricsDataService.observe() with a 30s poll;
drops MetricsAction + EntityMonitor + store.dispatch.
- CloudFoundryCellBaseComponent's "wait for cell metric" gate is
derived from cellMetric$ readiness (Observable<boolean>) instead
of LoadingPage's V2 entityId/entitySchema entity-monitor path.
Effect + helper deletions:
- cf-metrics.actions.ts (FetchCF*Metrics*, FetchApplication*Metrics*)
- cf-cell.helpers.ts (CfCellHelper — pagination-based health-metric
probe; replaced by direct fetch-and-check in cloud-foundry-cell
.service)
- cf-app-instances-metrics-action.ts helper
- app.effects.ts:clearCellMetrics$ (post-scale-up immediate refresh;
chart's 10s poll picks up the change instead — same end state ~10s
later)
- metrics.effects.ts (METRICS_START handler) + store.module wiring
- FetchKubernetesMetricsAction + FetchKubernetesChartMetricsAction
- MetricsAction + MetricsChartAction base classes + METRICS_START*
constants in metrics.actions.ts (kept MetricQueryConfig and
getFullMetricQueryQuery which MetricsRequest still uses)
Entity-catalog cleanup — metricEntityType registrations are dead now
that nothing dispatches MetricsAction:
- stratos-entity-factory (const + base schema)
- cf-entity-factory + cf-entity-generator.generateCFMetrics
- kubernetes-entity-factory + kubernetes-entity-generator
.generateMetricEntity
- autoscaler-entity-factory + autoscaler-entity-generator
.generateMetricEntity
- public-api re-export
V2 list-framework metrics path (verified dead — no consumer set
showCustomTime=true):
- drop metricsAction field + updateMetricsAction method from
list-data-source + interface
- drop showCustomTime / customTimeWindows /
customTimeInitialValue / customTimePollingInterval /
customTimeValidation from list config
- drop handleTimeWindowChange hook
Spec updates:
- cell-base.spec: HttpClientTesting + override component-level
CloudFoundryCellService provider with the existing mock
- cell-summary.spec: drop healthyMetricId from mock
- metrics-chart.spec: drop dead-code commented FetchApplication ref
The apps-wall page is already signal-native (CfAppsSignalConfigService);
the V2 CfAppsDataSource only survived because two stepper components
still round-tripped through ngrx pagination state to discover the
wall's currently-selected cf/org/space when the user navigated
"+ Create Application" or "+ Deploy Application" from the wall.
CfAppsSignalConfigService is providedIn: 'root', so its
selectedCnsi / selectedOrg / selectedSpace signals persist across
the route change. The steppers now read those signals directly:
create-application: ngOnInit
deploy-application: ngOnInit else-branch (no appGuid path)
Drops:
- cf-apps-data-source.ts (126 lines — the V2 data source that
dispatched GetAllApplications + CreatePagination + per-row
appStats fetches; entire chain is dead since the wall stopped
using it)
- paginationStateSub + selectCfPaginationState wire in create
- entityKey + selectPaginationState wire in deploy
Behavior preserved one-for-one: create-application keeps its
`cf && org` / `cf && org && space` precondition gating; deploy
keeps its independent cf/org/space assignments.
cf-user.service.isConnectedUserAdmin was the only direct ngrx selector consumer in this file outside the V2 list-framework pagination cache path. Swap it for the existing signal-service facade (CfCurrentUserRolesSignalService.cfEndpointRolesState$), which returns the same role-state shape without touching Store. The wider `createPaginationAction` rewrite (line ~366, selectCfPaginationState) is deferred — it has 3 live consumers (cf-admin-add-user-warning, org-base, space-base) that each switch on the returned action shape, and the V2 "is-maxed" semantics don't translate cleanly to CfUsersSignalConfigService yet.
Delete UpdateAppEffects and drop its registration from the cloud-foundry
store + test modules. The effect listened for CF_APP_UPDATE_SUCCESS and
dispatched fanout actions (env vars, stats, summary) so the V2 ngrx app
cache stayed coherent after a mutation.
No live consumer dispatches UpdateExistingApplication anymore; the
edit-application step now calls AppDetailDataService.update() (PATCH
/pp/v1/cf/apps/:cnsi/:guid) which already refreshes the app entity, and
the component itself triggers detail.refresh('stats') after a scale.
Env vars / summary are owned by the same signal-native service. The
post-scale-up metrics refresh that lived in app.effects.ts:clearCellMetrics
was already retired in W-a4, so the entire ngrx fanout is dead code.
The CF_APP_UPDATE_SUCCESS action type and UpdateExistingApplication
class are left in place — they remain referenced by the entity-catalog
action-builder registration (cleanup belongs to a later wave that
retires the application entity action surface).
W-e of the ngrx-removal sweep. cfInfo was the only handler in
CloudFoundryEffects — the V2 action listener dispatched GetCFInfo,
hit /pp/v1/cf/info/{guid}, and wrapped the response in the legacy
WrapperRequestActionSuccess/Failed envelope for the request reducer.
The signal-native CfInfoDataService already covers the same fetch
(introduced in a prior wave) and is the live source for every
visible consumer (CloudFoundryEndpointService.info$, card-cf-info,
endpoint summary header). The effect was kept alive only by the
EndpointHealthCheck callback in cf-entity-generator dispatching
cfEntityCatalog.cfInfo.api.get(endpoint.guid).
Migration:
* Added CfInfoDataService.refresh() — bypasses the warm-cache
short-circuit so the health-check pulse still produces a fresh
HTTP fetch (load() would otherwise short-circuit forever once
warm). In-flight dedup still applies.
* Added cf-info-helper.ts with a module-level injector capture
(same pattern as autoscaler-available.ts) so the static
healthCheck lambda can reach CfInfoDataRegistry without an
Angular injection context.
* Wired setCfInfoHelperInjector(inject(Injector)) into the
CloudFoundryPackageModule constructor.
* Rewired the CF endpoint healthCheck callback to call
refreshCfInfo(endpoint.guid) instead of dispatching GetCFInfo.
Deletions (now fully orphaned):
* store/effects/cloud-foundry.effects.ts (only effect in the file)
* actions/cloud-foundry.actions.ts (GET_CF_INFO + GetCFInfo only)
* entity-action-builders/cf-info.action-builders.ts
* generateCFInfoEntity() and the cfEntityCatalog.cfInfo registration
* cfInfoEntityType constant + CFInfoSchema entry
* CloudFoundryEffects registrations from cloud-foundry.store.module
and cloud-foundry-test.module.
* cfInfoEntityType from public_api.ts (not referenced externally).
The /pp/v1/cf/info/{cnsi} wire path is unchanged; the V3-only
Stratos-native handler still returns the legacy ICfV2Info shape,
just now consumed directly by CfInfoDataService instead of being
dispatched through the request-pipeline reducer.
Remove the WrapperRequestActionSuccess dispatch paths for orgs and
spaces from EndpointDataShim. Earlier waves retired every consumer of
the pagination state the shim used to populate:
- cfEntityCatalog.org/space.actions.getMultiple readers are gone;
cloudfoundry-endpoint.service.orgs$ now sources from
EndpointDataService.orgs() (W-b path).
- CfOrgSpaceDataService fetches orgs/spaces directly via HTTP into its
own signal state rather than reading the entity catalog pagination
store.
- selectCfEntity(organizationEntityType / spaceEntityType, guid) reads
by bare guid, while the shim wrote under FWT-934 composite keys
(cnsiGuid:guid), so even single-entity selectors didn't pick up the
shim's writes.
What was removed:
- dispatchOrgs(): action = cfEntityCatalog.org.actions.getMultiple +
WrapperRequestActionSuccess under paginationKey 'endpoint-{cnsi}'.
- dispatchSpaces(): action = cfEntityCatalog.space.actions.getMultiple
+ WrapperRequestActionSuccess under paginationKey 'spaces-bulk-{cnsi}'.
- detectCollisions(): store.select probe of state.request[entityKey]
to count entity-key-collision-avoided events; its observability
value was tied to the write path being live.
- Inline toOrgResource/toSpaceResource builders (StOrg->APIResource
envelope is now only needed by st-org-adapter.ts for the
cloudfoundry-endpoint.service bridge — kept there, not duplicated).
What stays in place:
- write() still emits entity-size-sample diagnostics per org and per
space, now sampling the StOrg/StSpace payload directly instead of
the APIResource envelope.
- EndpointDataService.loadDetails().finalize() still calls shim.write()
— consumer-side removal is a separate scope.
Spec updated to assert the new behavior: no dispatches, diagnostics
samples only, apps still stay out of the shim.
Tests 1-4 of duplicate-url-collision.spec.ts asserted on dispatched WrapperRequestActionSuccess actions emitted from EndpointDataShim.write(). W-f retired all such dispatches, so those assertions can no longer pass. Test 5 (cfEntityId composition) is redundant with cf-entity-ref.spec.ts. The integration/ directory is empty after removal.
The signal-list migration (28d183f, ef8cd15) rebuilt the Address column as plain text and dropped the warning icon FWT-929 had wired up (cfe9fb3). TableCellEndpointAddressComponent — with its existing isDuplicate$ logic — is still in the tree, just orphaned. Restore it via the framework's existing kind:'template' mechanism plus a projected <ng-template appSignalListCell="address"> at the consumer site. The signal-list framework stays domain-agnostic; the endpoints page owns its cell choice. If a second consumer ever needs the same duplicate-URL cell, both share the component directly — no framework growth required.
Picks up the Location-header parse fix extended across all 9 delete handlers (orgs / spaces / domains / users / security_groups / brokers / service_instances / route_bindings / buildpacks). Stratos issues the async-job delete envelope on those resources; without the upstream fix the 202 job ref was lost in the response wrapper, causing the client to spin without a terminal state. The `replace` directive still points at norman-abramovitz/fw-capi until the fix lands on the fivetwenty-io fork's tagged release.
Phase 1 of the mutation-refresh architecture. The post-Wave-3 signal migration left every mutation's "refresh the affected pages" path on ad-hoc per-page logic — silently broken in several cases (org delete not repainting, refresh button no-op, snackbar flash). Plumbs the generic foundation here so Phase 2 (source classes) and Phase 3 (SignalListConfig wiring) have something to dispatch into. EndpointDataService gains: - _orgsStale / _spacesStale / _appsStale / etc. signals + readonly accessors. The cache-predicate in loadDetails / loadOrgs / loadApps / loadSpaces now considers staleness alongside lastFetched, so the next read after a mutation re-fetches even when the cache is warm. - 14 patch helpers (removeOrg/addOrg/updateOrg + same for spaces / apps / SI; remove/addServiceCredentialBinding) so source classes can update the canonical cache atomically with each write. - markStale(entity), applyCascade(key), refreshOrgs/refreshApps/ refreshSpaces/refreshDetails — the entry points sources call after a mutation lands. cascade-registry.ts declares the cross-entity staleness rules in one typed map (org.delete → [spaces, apps, serviceInstances, bindings]; space.delete → [apps, SI, bindings]; etc.). The "what should this mutation invalidate" question becomes a one-line lookup instead of re-deriving it at every callsite. cascadeFor(key) is the only external entry — sources call applyCascade(key) which delegates here. DIAGNOSTIC_CODE_FAMILIES gains the 'cascade-apply' enum value so applyCascade emissions surface in the diagnostics tab.
Thin mutation surfaces over /pp/v1/cf/organizations/{cnsi} and
/pp/v1/cf/spaces/{cnsi}. Each source extends CnsiEntitySource and
exposes create/update/delete that:
1. Issues the HTTP write (delete goes through writeWithJob so CF's
async 202 → /v3/jobs/{guid} terminal state is honoured).
2. Patches its own _items via patchItems (so consumers reading the
source's items() signal repaint immediately).
3. Calls into EndpointDataService's matching patch helper (add/
remove/update Org or Space), keeping the canonical cache aligned.
4. Fires applyCascade('org.delete' | 'org.create' | 'space.delete'
| 'space.create' | ...) to mark dependent entities stale per the
cascade-registry rules.
These two were missing — orgs and spaces were the entities most often
mutated by the user-facing flows (Add Org, Delete Org, Add Space,
Delete Space), and the Wave-3 migration retired their ngrx effects
without ever wiring a replacement repaint trigger. Sources here fill
that gap.
Brings the four pre-existing mutation sources up to the contract that
the new orgs / spaces sources established:
CnsiServiceInstancesSource — gains create/update/delete; existing
write path moves to writeWithJob; EDS patch + applyCascade fires
on terminal state.
CnsiServiceBindingsSource — retrofitted to use the canonical
StServiceCredentialBinding from stratos-types (had a local
StServiceBinding placeholder that didn't carry type / serviceInstance
/ createdAt). The new EDS addServiceCredentialBinding signature would
not type-check against the placeholder.
CnsiRoutesSource — delete now goes through writeWithJob + patchItems
+ EDS patch + applyCascade('route.delete') so the apps cascade-stale
fires (app-detail routes lists were the silently-broken consumer).
CnsiAppsSource — applyCascade calls added for app.delete, app.update,
route.create. The eds ctor arg is optional so existing tests don't
break.
cnsi-routes-source.spec.ts + cnsi-service-bindings-source.spec.ts
updated to assert the new contract (HttpResponse mock + writeWithJob
flow). cnsi-service-instances-source.spec.ts is new.
…ase 3) Pulls the orgs / spaces / apps SignalListConfig services off their ad-hoc mutation paths and onto the new CnsiOrgsSource / CnsiSpacesSource contracts. Each config now: - Instantiates the source once per initialize() (sharing EDS via the registry), caching it for subsequent mutations on the same CF. - Routes delete (and create/update where exposed) through the source, so the cascade fires once and EDS patches itself. - refresh() now calls EDS.refreshOrgs / refreshApps / refreshSpaces (which bypass the cache predicate) instead of loadDetails — which short-circuited on cache hit, leaving the user-facing refresh button a no-op. CloudFoundryEndpointService.fetchApps() flips from loadDetails to refreshDetails for the same reason — fetchApps is the "polling indicator clicked / page-level refresh" entry point.
…bars isAnyLoading on both toolbars was wired to a hardcoded signal(false), so the refresh button's spinner never engaged on either page. Pipe in the real EndpointsDataService.loading signal so clicking refresh shows the spinner while getAll() drains, matching the cf-services / cf-apps behaviour. The endpoints-page config exposes the loading signal via a `loading` readonly accessor; the k8s-endpoints config reads its EndpointsDataService directly (already injected for the endpoints projection). Same change shape so the two pages stay in sync.
The page-sub-nav CLI Info button rendered a `keyboard` glyph, which reads as "key input" rather than "open a terminal session." Material Icons' `terminal` glyph is the conventional choice for CLI affordances. One-line cosmetic.
…Events Apps wall already exposed CF / Org / Space dropdowns; the rest of the CF navs that have org/space membership had no parallel — users couldn't narrow Routes / Services / Users / Events without leaving the page. Each affected config service gains: - selectedOrg / selectedSpace WritableSignals (null = "All"). - orgOptions / spaceOptions computed lists, sorted natural-order (numeric-aware), "All" first. - A filter-effect clause that drops rows whose org/space don't match. - A cascade effect that clears selectedSpace when selectedOrg switches to a different specific org (the stale space is no longer reachable through that org). Org → All preserves the space selection. Space dropdown labels are cascade-aware: - Org selected → label is the space name on its own. - Org = All → label is "<space> - <org>" so the picker disambiguates identical space names across orgs (typical CF naming has many "dev" / "prod" spaces). Sort order: space name natural-sort, then org name natural-sort within the collision group. Per-page consumers (cf-services, cf-users, services-wall, cf-routes, cf-events-list) get the new dropdowns wired into their SignalListConfig filterDropdowns slot. cf-events-list conditionally renders them only on the foundation-wide page — the per-org / per-space / per-app sub-pages pin scope via basePredicate and elect to omit dropdowns that would let the user pick mismatched values. Service-instances config is multi-CNSI, so the new dropdowns union orgs / spaces across the in-scope CNSIs (or narrow to the selected CF when the CF dropdown is locked, as on per-CF tabs). EDS acquisition inside computed()s would refcount-leak, so initialize / initializeForX swap a tracked _edsByCnsi map through a refcount-balanced helper. Specs updated to mirror the new stub shape: services-wall expects 3 dropdowns instead of 1; cf-users stub exposes orgOptions/spaceOptions/ selectedOrg/selectedSpace; cf-service-instances spec's FakeDs gains orgs()/spaces() and the registry mock gains release().
Drops the four-tab Org+Space filter bundle + Phase 1-3 mutation refresh + fw-capi .11 onto adepttech for browser verification.
Adds the all-orgs "space - org" labels + natural sort iteration on top of dev.96 so the deployed environment matches the working tree.
Add create() to both sources so consumers can route app and route mutations through the canonical EDS-patching path instead of raw http.post. Each call patches the source items list, calls the matching EDS patch helper, and fires the cascade verb (app.create or route.create) so dependent surfaces invalidate.
…asses Migrate every remaining direct http.post/patch/delete that targeted an EDS-cached entity to its CnsiXSource so the canonical list cache patches in place and the matching cascade verb fires. Eleven steppers + a per-app detail-page PATCH now share the source-routing pattern. - create-organization-step : http.post -> CnsiOrgsSource.create - edit-organization-step : OrgWriteService.updateOrg -> CnsiOrgsSource.update - create-space-step : http.post -> CnsiSpacesSource.create - edit-space-step (name + SSH) : http.patch -> CnsiSpacesSource.update - edit-space-step (quota detach) : keeps raw http but fires space.update cascade - edit-space-step (quota attach) : keeps raw http but fires space.update cascade - create-application-step3 (app) : http.post -> CnsiAppsSource.create - create-application-step3 (route) : http.post -> CnsiRoutesSource.create - app-detail-data.update : http.patch -> CnsiAppsSource.update - detach-service-instance.detachOne : http.delete -> CnsiServiceBindingsSource.delete Steppers now acquire/release the EndpointDataService on the registry so the cache is alive for the duration of the write; for create-application two acquires (app + route) are reference-counted and released on destroy. Drops two dead routes that were already bypassed by the new sources: OrgWriteService (no remaining callers) and the unused deleteSpace on CloudFoundryOrganizationService (callers go through CfSpacesSignalConfig).
isAnyLoading on both quota tabs was tied to !hasLoadedOnce() so the refresh-button spinner engaged only on the very first load. Add a dedicated `loading` signal to each signal-config service that flips true around load/refresh and read it directly from the components. Matches the endpoints + k8s page fix already shipped this branch.
norman-abramovitz
approved these changes
May 23, 2026
Contributor
norman-abramovitz
left a comment
There was a problem hiding this comment.
more removal of ngrx and other fixes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Umbrella branch covering four interlocking workstreams that prepare Stratos for full V3-native CF data access and retire the remaining ngrx coupling in the cloud-foundry package.
1. Wave 4 ngrx cleanup (W-a .. W-f)
Removes ngrx Store/effects/selectors from six remaining surfaces that the earlier waves left behind. Each slice retires its V2 ngrx code while a signal-native replacement carries the traffic.
2. EDS mutation-refresh architecture (Phase 1-3)
After every write (create / update / delete) the relevant cached list now invalidates immediately instead of waiting for a hard reload.
3. fw-capi v3.216.4-fix-apps-delete.11
Extends the Location-header parser across nine v3 delete handlers (orgs, spaces, domains, users, security_groups, brokers, service_instances, route_bindings, buildpacks). Pure CAPI-client plumbing; the jetstream-side wiring still uses writeWithJob.
4. UX + quality-of-life
Merge strategy
This PR is intentionally a stack of small slices. Please Create a merge commit rather than squash or rebase so the slice identity stays in history — squashing a 23-commit slice stack destroys the ability to bisect inside the umbrella.
Test plan