Skip to content

feat(ui): add a Fleet tab to the Stage drawer listing governed Targets - #7200

Open
jsbroks wants to merge 7 commits into
mainfrom
justin/stage-fleet-tab
Open

jsbroks wants to merge 7 commits into
mainfrom
justin/stage-fleet-tab

Conversation

@jsbroks

@jsbroks jsbroks commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Builds on #7183, which added the round progress bar and is now on main.

Summary

A target-aware Stage's drawer had no view of the Targets it governs. The Promotions tab shows its PromotionRequests; nothing listed the Targets themselves with where each stands. This adds a Fleet tab, first in order and the initial tab for target-aware Stages, and the Target endpoints it reads from.

API

Cherry-picked from #7138: GET /v1beta1/projects/{project}/targets and GET .../targets/{target}, following the PromotionRequest endpoints (list-then-watch with resourceVersion seeding, sorted by name).

  • labelSelector is applied by the Kubernetes API server, for the list and the watch.
  • stage returns only the Targets that Stage governs. A Stage's selectors are a union, which no single label selector expresses, so this filter is evaluated in-process against each Target and each watch event. A classic Stage yields an empty list; a missing Stage is a 404; a malformed selector is a 400.
  • Selector parsing and the any-selector-matches test move into pkg/api (TargetSelectorsForStage, AnySelectorMatches); ListTargetsForStage parses through the former, so the controller and the API server share one definition of governance.
  • swagger.json and the Go and TypeScript clients are regenerated, now including the Target status types from feat(api): record per-Stage current Freight, health checks, and health on Target status #7174. The user cluster roles already grant read on Targets.

Fleet tab

One row per Target the Stage governs, from the stage filter above:

  • Target with its labels.
  • Freight the Target is running for this Stage, from Target.status.stages[<stage>].currentFreight (feat(api): record per-Stage current Freight, health checks, and health on Target status #7174), one link per origin. A Target whose collection differs from the Stage's current one is marked behind. Until a Promotion has succeeded, the column shows what the round is promoting.
  • Health from status.stages[<stage>].health, with the shared health icon.
  • Promotion phase of the child Promotion in the Stage's latest round, linking to it. A Target the round did not name reads "not in latest round"; a round that ended before creating a child shows the round's phase and its Ready message.
  • When the round finished.

Rows with trouble sort first. Above them, a card in the style of the Requested Freight cards summarizes the round: its Freight, the PromotionRequest's name, phase, finish time, how many Targets succeeded, one chip per phase present, and the progress bar from #7183 as the card's footer. Card and bar are drawn from the rows' own phases rather than the request's summary, so the card and the table cannot disagree.

The table filters from its column headers, as the UI's other filterable tables do: Target by name or label from a text box, Promotion by phase, and Health by health state, each listing only the values present with counts. Targets stay live through a seeded watch scoped to the Stage. The round comes from a new useCurrentRound hook that resolves the request the Stage is promoting through (current, else last, else newest) from the same PromotionRequest list the Promotions tab shows, sharing its fetch and watch.

Classic Stages do not show the tab and open on Promotions as before. Once a user picks a tab, that choice sticks for the drawer's lifetime.

When a round cannot fan out

The Promotions tab used to warn on its own when the Stage's latest request could not progress, quoting the Ready message. That explanation now sits above the tabs, so the Fleet and Promotions tabs both read under it, and it is worded for the person reading it. When the reconciler reports that fan-out is not available in the installation, the alert says so in plain language with a sentence on what it means for the Stage. Any other reason passes through under "Promotion to this Stage cannot progress". While a round is blocked the Fleet tab shows no round card and no phase chips; its rows read "Not attempted" in grey, and the Health and When columns, which have nothing to say, drop out.

A round that did fan out and then had children fail is not blocked, even though the controller also sets Ready to False for it. roundBlock yields nothing for a request that recorded any Target or summary, so the alert is reserved for rounds that never started and a partially failed round keeps its bar and rows.

Verified

  • Go: go test -race on pkg/server and pkg/api, golangci-lint clean.
  • UI: pnpm typecheck, pnpm lint, pnpm vitest run (34 files, 407 tests). fleet-utils.test.ts covers row construction, phase fallback, Freight and health from Target status, the behind marker, sort order, the bar's summary, and the Target text match; promotion-request.test.ts covers roundBlock.
  • Live against a kind cluster with target-aware Stages, a partially failed round, and Target status seeded for two Targets. Captured the tab on three small Stages, a 40-Target Stage with three failures, the same Stage mid-round with all children pending, a Stage whose request carried the status this repository's reconciler writes, and confirmed a classic Stage is unchanged. Screenshots are in the first comment.

Not in this PR

Deep-linkable tabs, a Targets page, and the bar on other surfaces.

Checklist

Eligibility

  • Linked to an existing issue with no blocking labels.
  • Changes documentation only.
  • Changes ten lines or fewer.

Quality

  • Adds or updates corresponding tests.
  • Adds or updates corresponding documentation.

AI Use Disclosure

This PR was written:

  • By a human without AI assistance.
  • By a human with AI assistance. A human has reviewed every line prior to opening the PR.
  • By an AI with human supervision. A human has reviewed every line prior to opening the PR.
  • Entirely by an AI. No human has reviewed this prior to opening the PR.

Sign-Off

All commits:

  • Are signed off by their author (git commit -s) (required)
  • Are cryptographically signed (git commit -S) (encouraged)

🤖 Generated with Claude Code

@kargo-governance-bot kargo-governance-bot Bot added needs/area Issue or PR needs to be labeled to indicate what parts of the code base are affected needs/kind Issue or PR needs to be labeled to clarify its nature needs/priority Priority has not yet been determined; a good signal that maintainers aren't fully committed labels Sep 14, 2026
@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.84615% with 47 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.96%. Comparing base (260e519) to head (6ea133b).

Files with missing lines Patch % Lines
pkg/server/list_targets.go 51.04% 47 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7200      +/-   ##
==========================================
- Coverage   64.97%   64.96%   -0.01%     
==========================================
  Files         511      513       +2     
  Lines       38419    38541     +122     
==========================================
+ Hits        24964    25040      +76     
- Misses      13455    13501      +46     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jsbroks
jsbroks force-pushed the justin/round-progress-bar branch 4 times, most recently from 4ec57a7 to bc38544 Compare September 14, 2026 16:16
@jsbroks
jsbroks force-pushed the justin/stage-fleet-tab branch 3 times, most recently from d15428f to 24d3524 Compare September 14, 2026 16:45
Base automatically changed from justin/round-progress-bar to main September 14, 2026 21:24
@netlify

netlify Bot commented Sep 14, 2026

Copy link
Copy Markdown

Deploy Preview for docs-kargo-io ready!

Name Link
🔨 Latest commit 6ea133b
🔍 Latest deploy log https://app.netlify.com/projects/docs-kargo-io/deploys/6aa9d16616764600084eb8cc
😎 Deploy Preview https://deploy-preview-7200.docs.kargo.io
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@jsbroks
jsbroks force-pushed the justin/stage-fleet-tab branch from 1b6024d to de4fd7f Compare September 14, 2026 22:07
Targets had no REST surface: the UI could see a Stage's targets block and
its PromotionRequests, but never the Targets themselves. This adds

  GET /v1beta1/projects/{project}/targets
  GET /v1beta1/projects/{project}/targets/{target}

following the PromotionRequest endpoints: the list seeds a follow-up watch
(`?watch=true`, `resourceVersion`) through the uncached reader and sorts by
name.

Two filters narrow the list. `labelSelector` is a Kubernetes label selector
the API server applies itself, for both the list and the watch. `stage`
names a Stage and returns only the Targets it governs. A Stage's selectors
are a union that no single label selector can express, so that filter is
evaluated in-process, against each listed Target and each watch event. A
classic Stage governs no Targets and yields an empty list; a missing Stage
is a 404; a malformed selector is a 400.

The selector parsing and the any-selector-matches test move into pkg/api as
TargetSelectorsForStage and AnySelectorMatches, and ListTargetsForStage now
parses through the former, so the controller and the API server share one
definition of governance.

Regenerates swagger.json and the Go and TypeScript clients.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Justin Brooks <[email protected]>
(cherry picked from commit 4b16070)
@jsbroks
jsbroks force-pushed the justin/stage-fleet-tab branch from 900f1da to 9fa5046 Compare September 14, 2026 22:12
@jsbroks
jsbroks marked this pull request as ready for review September 14, 2026 22:21
@jsbroks
jsbroks requested a review from a team as a code owner September 14, 2026 22:21
Justin Brooks and others added 5 commits September 14, 2026 18:58
A target-aware Stage's drawer had no view of the Targets it governs. The
Promotions tab shows its PromotionRequests; nothing listed the Targets
themselves with where each stands.

The Fleet tab shows one row per Target the Stage governs: name and labels,
the Freight the Target is running for this Stage, its health, the phase of
the child Promotion in the Stage's latest round with a link to it, and when
the round finished. Rows with trouble sort first. The round's progress bar
sits above the rows it summarizes, drawn from the rows' own phases so the
two cannot disagree.

Governance comes from the Target list endpoint's `stage` filter, which
evaluates the Stage's selectors exactly as the controller does. Freight and
health per Target read the Target's own status for this Stage, which the
controller writes when a child Promotion succeeds; a Target whose current
collection differs from the Stage's is marked behind. Until a Promotion has
succeeded, the Freight column shows what the round is promoting. The
round's outcome comes from useCurrentRound, which resolves the request the
Stage is promoting through -- current, else last, else newest -- from the
same PromotionRequest list the Promotions tab shows, so the two share one
fetch and one watch. Targets stay live
through a seeded watch scoped to the Stage.

A Target the round did not name -- one that joined the Stage after the
round was created -- reads "not in latest round". A round that ended
before creating a child shows the round's phase and its Ready message.

The tab is first in order and is the initial tab for target-aware Stages;
classic Stages do not show it and open on Promotions as before.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Justin Brooks <[email protected]>
…tabs

The Promotions tab warned when a target-aware Stage's latest
PromotionRequest could not progress, quoting the request's Ready message
under the heading "This Stage promotes to Targets". The Fleet tab, which
now opens first, showed the same round as a red bar and a column of
Errored rows with the reason buried in a tooltip. One screen said two
things, and neither said them plainly.

The explanation moves above the tabs, so the Fleet and Promotions tabs
both sit under it, and it is worded for the person reading it. When the
reconciler reports that fan-out is not available in the installation, the
alert says so in plain language with a sentence on what that means for
the Stage. Any other reason passes through under "Promotion to this Stage
cannot progress". While a round is blocked the Fleet tab shows no progress bar,
since the bar would only restate the same failure per Target.

A round that did fan out and then had children fail is not blocked, even
though the fan-out controller also sets Ready to False for it; its Targets
show the outcome. So roundBlock yields nothing for a request that recorded
any Target or summary, and the alert is reserved for rounds that never
started.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Justin Brooks <[email protected]>
The Fleet tab opened with a bare progress bar and a "2 / 3" figure
floating above the table. It had no label, nothing tied it to the Freight
it described, and its fixed width matched nothing else in the drawer, so
it read as out of place.

The round is now a card in the style of the Requested Freight cards above
the tabs -- the same grey ground and two-pixel border -- with labelled
fields for the Freight the round promotes, its phase, when it finished,
and how many Targets succeeded. The bar becomes the card's footer, spanning
it. It is still drawn from the rows' own phases, so the card and the table
beneath it cannot disagree. A blocked round shows no card, since the
drawer explains the block above the tabs.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Justin Brooks <[email protected]>
…uccesses

Two changes to the Fleet tab for rounds beyond a handful of Targets.

The round card said only "37 of 40 succeeded", leaving the three failures
as a red sliver of the bar. It now carries one chip per phase present in
the round, in the order the bar draws them and with the presentation the
Promotions tab already uses, so every count is legible. The card also
names the PromotionRequest it summarizes, so a reader can find the round
on the Promotions tab.

Past five Targets the tab grows a filter -- by name or label, and the same
phase chips as a toggle to show one phase at a time -- and, when a round
has trouble, its succeeded rows fold into a single line naming the first
few Targets and how many more, expandable in place. The failures become
the table and the green becomes one row. A round with no trouble, a round
with fewer than six successes, and a filtered view all stay flat.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Justin Brooks <[email protected]>
When a target-aware Stage's latest round could not fan out, the drawer
already says why above the tabs. The Fleet tab beneath it still painted
every Target Errored in red with an empty Health column and a finish time
for a round that never started, so the tab read as an outage under a
banner saying nothing ran.

Under a blocked round the tab now lists the governed Targets with a grey
"Not attempted" in the Promotion column, drops the Health and When
columns, which have nothing to say, shows no round card and no phase
chips, and captions the list as the Targets this Stage governs. The
filter by name or label stays for large fleets.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Justin Brooks <[email protected]>
@jsbroks
jsbroks force-pushed the justin/stage-fleet-tab branch from 9fa5046 to 76978fd Compare September 14, 2026 22:58

const targetsQuery = useListTargets(projectName, { stage: stageName });
useWatchTargets(projectName, stageName, !!targetsQuery.data);
const round = useCurrentRound(projectName, stage);

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.

This triggers a watch on Promotion Requests. We call this hook here, and in the parent component.
Let's pass round param instead of initializing it a second time. I think we can get rid of the stage param.

Review of the Fleet tab asked for two changes. Grouping Targets by status
folded a troubled round's successes into one line; a filter is enough, so
the fold goes. And the quick filters above the table listed every phase as
a chip, which is a design the rest of the UI does not use; filters belong
in the column headers, as Ant tables recommend and as the other filterable
tables here do.

The Target column filters by name or label from a text box in its header
menu; the Promotion column filters by phase and the Health column by health
state, each listing only the values present with their counts. The filter
row above the table goes with the fold, and the round card carries the
per-phase chips again, since nothing else now does.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Justin Brooks <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs/area Issue or PR needs to be labeled to indicate what parts of the code base are affected needs/kind Issue or PR needs to be labeled to clarify its nature needs/priority Priority has not yet been determined; a good signal that maintainers aren't fully committed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants