Skip to content

Applications: filter-dropdown fixes, app-stats 401 retry, and deploy-from-Git source/commit UX - #5424

Merged
norman-abramovitz merged 5 commits into
cloudfoundry:developfrom
nabramovitz:feat/applications-improvements
Jun 5, 2026
Merged

Applications: filter-dropdown fixes, app-stats 401 retry, and deploy-from-Git source/commit UX#5424
norman-abramovitz merged 5 commits into
cloudfoundry:developfrom
nabramovitz:feat/applications-improvements

Conversation

@nabramovitz

Copy link
Copy Markdown
Contributor

A batch of Applications-area improvements found while working the deploy-from-Git flow and the per-CF tabs. Five focused commits:

1. feat(cf-apps): populate the per-CF Org/Space filter dropdowns

The per-CF Applications tab's Org/Space dropdowns only ever showed "All" — unlike the other per-CF tabs (Routes/Services/Users) which read the shared EndpointDataService, the apps config keeps its own lazy catalog that's gated behind an onOpen hook the tab never wired. Add the hook (scoped to the route's CNSI). Audited the sibling tabs — Apps was the only gap.

2. feat(cf-apps): disambiguate same-named spaces in the filter dropdown

With Org = All, the Space dropdown listed every space by bare name, so a CF with space_1 in N orgs showed N identical rows. Label each "<space> - <org>" when no org is selected (matching Routes/Services/Users); bare name once an org is picked. Applies to the per-CF tab and the global app wall.

3. fix(cf): retry app-stats reads once on a token-boundary 401

The app-detail stats poll 401s once per ~20-min token cycle: a poll firing just before expiry is rejected by CF by the time it validates, and the proactive refresh can't catch a token that was valid at build time. Add callWithCapiRetry — a reusable helper that rebuilds the client and replays once on a 401 (by retry time the token has genuinely expired, so the rebuild refreshes it). Both app-stats handlers route through it.

4. feat(deploy): tabbed access modes on the GitHub/GitLab source step

The source step crammed an optional Enterprise URL + token ahead of the required Project/Branch, with no labels and no sign that the "optional" auth gates the repo lookup. Replace it with Public / Private / Enterprise tabs that reveal only each path's fields in dependency order, with labels + required markers; provider-aware third-tab label (GitHub Enterprise / Self-hosted GitLab). Presentation only — no backend/store change. Also labels the Git URL + Docker source types.

5. feat(deploy): allow deploying the latest commit without pinning a SHA

The commit step always pinned a specific SHA. Add a "Deploy the latest commit on {branch} (don't pin a commit)" checkbox that sends an empty commit so CF deploys whatever HEAD points at (the backend already skips the reset when the commit is empty).

Addresses two items of #5400 (source-step UX, deploy-latest-HEAD) plus follow-ups to the per-CF tab work.

Testing

  • Unit tests added for each behavioural change (cf-apps space labels & dropdown wiring; Go retry tests; gitMode derivation/clearing; commit step validity & pin-vs-HEAD).
  • Full make check gate green across all five commits (backend + frontend + prod AOT build).
  • Live-verified: per-CF dropdowns populate with disambiguated space labels; the source step's three tabs render correctly and still resolve a real repo + branches. (The deploy-latest-HEAD toggle is unit + AOT verified; full wizard drive to the commit step pending.)

The per-CF Applications tab's Organization and Space filter dropdowns
only ever showed "All". Unlike the other per-CF tabs (Routes, Services,
Users) which read the shared EndpointDataService the parent CF page
hydrates, the apps config keeps its OWN lazy org/space catalog so the
multi-CF application wall doesn't saturate the network on mount. That
catalog is gated behind ensureNamesLoaded(), triggered by the toolbar
dropdowns' onOpen hook — which the per-CF tab never wired (only the
global app wall did).

Add the onOpen hook to both dropdowns, scoped to the route's single
CNSI (tighter than the wall's all-endpoints default). First open of
either dropdown now fetches this CF's org+space catalog; the service-
side promise dedupes repeated opens to one fanout.

Audited the sibling per-CF tabs: Routes/Services/Users already populate
via the shared EndpointDataService, and Marketplace has no scope
dropdowns by design — so the Applications tab was the only gap.

Adds TestBed coverage asserting the dropdowns carry no locked CF entry
and that first-open triggers ensureNamesLoaded for the scoped CNSI.
When no organization is selected, the applications space filter dropdown
listed every space by its bare name. CF space names are only unique
within an org, so a CF with space_1 in fifty different orgs rendered
fifty identical "space_1" rows — impossible to tell apart or pick from.

Label each space "<space> - <org>" when Org = All (sorted by space name
then org name), collapsing back to the bare name once an org is selected
and the scope is unambiguous. This matches what the Routes, Services and
Users tabs already do, and applies to both the per-CF Applications tab
and the global application wall (shared config service).

Org names come from the same per-CF catalog that backs the Organization
dropdown; an as-yet-unresolved org name falls back to the bare space
name rather than a trailing " - ".
The app-detail stats poll fires on a ~30s cadence against a per-CF token
with a ~20-minute life. A poll that fires in the final moments before
expiry travels to CF (~700ms RTT) and is rejected with a 401 by the time
CF validates it — newCapiClient's proactive expiry refresh can't catch a
token that was still valid when the client was built. The result is a
recurring 401 once per token cycle on a long-lived read.

Add callWithCapiRetry: a small generic helper that runs an operation
against a fresh capi client and, on a 401, rebuilds the client once and
replays the operation. By retry time the stored token has genuinely
expired, so the rebuild's newCapiClient proactively refreshes it and the
replay succeeds. This is the reactive companion to the proactive check,
mirroring the inline pattern in native_audit_events_reads.go but reusable
by any native read/write handler.

Wire both app-stats handlers through it: the single-app endpoint replays
its lookup + stats pair, and the batched endpoint retries on its gating
web-process lookup (the first CAPI call, which catches the expired token
before the stats fan-out) so the rebuilt client carries the fresh token
into the fan-out.
The deploy-from-Git source step crammed the gitscm fields onto one flat,
placeholder-only list: an optional GitHub Enterprise URL and access token
led, the required Project and Branch followed unmarked, and nothing
showed that the "optional" auth actually gates the repo lookup for
private / Enterprise repos.

Replace the flat form with a Public / Private / Enterprise tab strip that
reveals only the fields each path needs, in the order they're required:

  Public      → Project, Branch
  Private     → Access token, Project, Branch        (host private repo)
  Enterprise  → Base URL, Access token, Project, Branch

The third tab is provider-aware — "GitHub Enterprise" for GitHub,
"Self-hosted GitLab" for GitLab (the gitscm sub-form serves both). Every
field now has a real label and a required marker; switching tabs clears
the auth fields that don't belong to the new mode so a stale URL / token
is never carried into the deploy.

This is presentation only — the active mode is local component state
derived from the existing githubEnterpriseUrl + accessToken bindings; no
backend or store change. GitLab's token forwarding remains GitHub-only as
before (a pre-existing, separately-tested behaviour).

Also adds labels + required/optional markers to the Git URL and Docker
source types for consistency.

deriveGitMode + setGitMode are unit-tested; the three tabs, per-mode
validity gating, and repo resolution were verified live.
The deploy-from-Git commit step always pinned a specific commit: it
auto-selected the newest commit and the step was only valid once a row
was chosen, so there was no way to say "just deploy whatever's at the
head of the branch." The backend already supports it — it skips the
`git reset` when the commit is empty — but the UI never sent an empty
commit.

Add a "Deploy the latest commit on {branch} (don't pin a commit)"
checkbox above the commit list. When on, the list is hidden, the step is
valid with no row selected, and the deploy sends an empty commit so CF
builds whatever HEAD points at (and every redeploy picks up new commits
automatically). Off, the pick-a-commit behaviour is unchanged.

Validity is now `useLatestHead || a commit is selected`; onNext sends ''
for latest-HEAD or the selected SHA otherwise. Toggling the checkbox
clears the radio selection while on and restores the newest-commit
default when switched back off.

@norman-abramovitz norman-abramovitz left a comment

Copy link
Copy Markdown
Contributor

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 e7fe246 into cloudfoundry:develop Jun 5, 2026
12 checks passed
@nabramovitz
nabramovitz deleted the feat/applications-improvements branch June 5, 2026 05:46
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