feat(FR-3652): show the resource grid view in the agent detail panel - #9006
Open
yomybaby wants to merge 4 commits into
Open
feat(FR-3652): show the resource grid view in the agent detail panel#9006yomybaby wants to merge 4 commits into
yomybaby wants to merge 4 commits into
Conversation
… block `BAIResourceUnitGrid`'s hover popover is `position: fixed` and writes coordinates measured with `getBoundingClientRect()`, i.e. viewport space. That holds only while no ancestor establishes a containing block for fixed descendants. Every host so far was an untransformed page container, so the assumption was invisible. The agent detail drawer breaks it: `@astryxdesign/lab`'s `Drawer` applies `transform: translateX(0)` to the `<dialog>` element itself while open, which makes the dialog the containing block. Measured in real Chromium at 1920px with the shipped rule set, the popover resolved to viewport left 2240 on an 800px drawer whose left edge sits at 1120 — off-screen, and clipped away by the dialog's `overflow: hidden`. Hovering a session plate inside the drawer did nothing at all: no session detail, no palette picker. Measure that containing block's origin with a zero-size fixed probe (the idiom this file already uses to resolve colors inside its own cascade) and subtract it from the coordinates written to the popover. The probe reports (0, 0) whenever the viewport really is the origin, so the two shipped session list pages and the dashboard panel are byte-identical; the same Chromium measurement puts the drawer popover back at 1120 and hit-testable. Positioning the popover in place, rather than portalling it to `document.body`, keeps it inside the component's own cascade — it reads theme tokens and `light-dark()` the same way the grid's color probes do — and leaves the z-index ladder untouched. The regression test drives the fix through mocked rects: it asserts the written `left` shifts by exactly the measured origin, and fails on the pre-fix code (1144 instead of 24).
The session resource grid (FR-3569..FR-3571) shipped on the user session list and /admin/session, and its spec (#8832) left rolling it out to the other session lists out of scope. The agent detail panel's session list (FR-3252) is where "what does this agent hold, and how hard is it working" is the whole question, so it gets the same Table | Grid toggle behind the same `experimental_session_resource_grid` opt-in, with the same icons, tooltips and i18n keys. No new translation keys, no schema or Relay artifact changes. The drawer's table reads `agentsV2 -> sessions` (Strawberry), so scoping the grid to one agent needed a second path. The grid keeps its own legacy `compute_session_list` query and is scoped through that queryfilter's `agent_ids` field, which the manager maps to an array column (`ComputeSession._queryfilter_fieldspec` -> `ArrayFieldItem`, compiled to `EXISTS(SELECT item FROM unnest(col) WHERE item = :val)`); it has been there since 23.03.9, so no manager version gate is needed. Rebuilding the grid on SessionV2 instead was considered and is not possible today: `live_stat` exists on no Strawberry type, so every cell would render in the "no data" grey that means "no telemetry" — a false statement drawn as a feature. That is why the two views in this one tab read different graphs, and why their status vocabularies and page sizes differ. The drawer instance runs the grid controlled (`viewParams` / `onChangeViewParams` from local state), the seam the dashboard panel already uses. An uncontrolled grid keeps its five settings in app-global URL query keys; a drawer opened over a session list page already in grid view would otherwise share them and silently repaint the page behind it. The toggle itself is local state too, since `AgentList` is mounted on three pages whose URLs would outlive the drawer. Exactly one of the two queries polls at a time: the fetch button reloads the preloaded table query only in table view, and switching back to table forces a network-only reload rather than showing rows that stopped refreshing. Accepted limitations: a multi-node session with kernels on other agents matches the filter and is drawn whole (session-level `occupied_slots` has no per-kernel breakdown, so narrowing only kernel mode would make the two modes disagree); the grid caps at 100 with no pagination while the table paginates at 10; the toggle resets when the drawer closes.
Contributor
Coverage Report for backend-ai-ui-coverage (./packages/backend.ai-ui)
File Coverage
|
||||||||||||||||||||||||||||||||||||||
Contributor
Coverage Report for react-coverage (./react)
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||
…rawer The popover-anchoring fix is locked by a unit test on the coordinate math, but nothing rendered the real component inside a real drawer — the composition that broke it. This story does, and it is how the before/after in the PR was measured: hovering a plate with the panel at viewport left 1120 put the popover at 2268 before the fix (outside the panel, outside the viewport, not hit-testable) and at 1147 after. Storybook does not load `@astryxdesign/lab`'s stylesheet, so the class the dialog already carries for `side="end"` has no rule there and the panel sits at the start edge, where the origin is 0 and the bug cannot show. The measurement above restores that one shipped rule in the page first; the story itself is left as the app writes it.
CodeQL's `js/incomplete-sanitization` on the grid's minilang filter. The escape handled `"` but not `\`, and lark's `ESCAPED_STRING` refuses to close a token after a lone trailing backslash — so such an id would not mis-scope the query, it would make the whole expression unparseable and degrade the grid to its error banner. Escaping the backslash first (the reverse order re-escapes what it just wrote) turns that into a value that parses and simply matches nothing. Practically unreachable either way — agent ids are operator-configured host-style labels — but the failure mode is now the harmless one, and this line stops being the one place in the repo that escapes a minilang interpolation only halfway.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds the experimental session resource-grid view to the agent detail drawer while preserving the existing table view.
Changes:
- Adds the gated Table/Grid control with local grid settings and polling.
- Corrects popover positioning inside transformed drawers.
- Adds regression coverage and a drawer Storybook scenario.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
react/src/components/AgentNodeItems/AgentSessions.tsx |
Implements the agent-scoped grid view and controls. |
react/src/components/AgentDetailDrawerContent.tsx |
Supplies the agent ID for grid filtering. |
packages/backend.ai-ui/src/components/BAIResourceUnitGrid.tsx |
Rebases fixed popovers to transformed containing blocks. |
packages/backend.ai-ui/src/components/BAIResourceUnitGrid.test.tsx |
Tests popover coordinate rebasing. |
packages/backend.ai-ui/src/components/BAIResourceUnitGrid.stories.tsx |
Adds the drawer-hosted grid scenario. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
yomybaby
marked this pull request as ready for review
August 24, 2026 13:59
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.

Resolves #9005 (FR-3652)
What
The session resource grid (FR-3569..FR-3571) ships on the user session list and
/admin/session; its spec (#8832) explicitly left rolling it out to the other session lists out of scope. This adds it to the session list the agent detail panel gained in FR-3252 (#8613), behind the sameexperimental_session_resource_gridopt-in, with the same icons, tooltips and i18n keys.No new i18n keys, no
graphqltag touched, no__generated__churn.Four commits, on purpose
1.
fix(FR-3652)—BAIResourceUnitGridpopover anchoringA prerequisite, and a real defect in the shared component rather than in the new call site.
The grid's hover popover is
position: fixedand writes coordinates measured withgetBoundingClientRect()— viewport space. That is only correct while no ancestor establishes a containing block for fixed descendants, which every host so far satisfied.@astryxdesign/lab'sDrawerappliestransform: translateX(0)to the<dialog>element itself while open, so the dialog takes over as the containing block — and itsoverflow: hiddenclips whatever survives.Measured on the real components in a real drawer (the new Storybook story, panel at viewport left 1120, 1920px viewport), hovering the same plate:
left..rightSo inside the drawer the grid's entire detail affordance — session detail and the palette picker — silently did nothing on hover.
The fix measures the containing block's origin with a zero-size fixed probe (the idiom this file already uses to resolve colors inside its own cascade) and subtracts it. The probe reports
(0, 0)whenever the viewport really is the origin, so the two shipped pages and the dashboard panel are unchanged — confirmed by the same measurement on an untransformed host, where before and after are byte-identical.Positioning in place rather than portalling to
document.bodykeeps the popover inside the component's own cascade (theme tokens,light-dark()) and leaves the z-index ladder untouched.2.
feat(FR-3652)— the agent panel view itself3.
test(FR-3652)— the Storybook story the measurement above runs againstNote for whoever opens that story: Storybook does not load
@astryxdesign/lab's stylesheet, so the class the dialog already carries forside="end"has no rule there and the panel sits at the start edge — where the origin is 0 and this bug cannot show. The measurement restores that one shipped rule (.xhi6v0a { inset-inline-start: auto }, present inlab.css) in the page first.4.
fix(FR-3652)— complete the minilang escapingCodeQL's
js/incomplete-sanitization. The escape handled"but not\, and lark'sESCAPED_STRINGrefuses to close a token after a lone trailing backslash — so such an id would not mis-scope the query, it would make the whole expression unparseable and degrade the grid to its error banner. Escaping the backslash first turns that into a value that parses and matches nothing. Practically unreachable either way (agent ids are operator-configured host-style labels), but the failure mode is now the harmless one.Why the two views in one tab read different graphs
The drawer's table reads
agentsV2 -> sessions(StrawberrySessionV2). The grid keeps its own legacycompute_session_listquery, scoped through that queryfilter'sagent_idsfield — the manager maps it to an array column (ComputeSession._queryfilter_fieldspec→ArrayFieldItem("sessions_agent_ids"), compiled toEXISTS(SELECT item FROM unnest(col) WHERE item = :val)), present since 23.03.9, so no manager version gate is needed.Rebuilding the grid on
SessionV2was considered and is not possible today:live_statexists on no Strawberry type. Every cell would render in the "no data" grey that the grid uses to mean no telemetry — a false statement drawn as a feature. This is also why the two views' status vocabularies and page sizes differ.URL query-state
An uncontrolled grid keeps its five settings (
gridMode,gridResource,gridMetric,gridMemUnit,gridLayout) in app-global nuqs keys. A drawer opened over a session list page already in grid view would share them and silently repaint the page behind it.The drawer instance therefore runs the grid controlled (
viewParams/onChangeViewParamsfrom local state) — the seamSessionResourceGridPanelalready uses for the same reason — so it writes zero URL keys. The toggle is local state too:AgentListis mounted on three pages whose URLs would outlive the drawer.Polling
Exactly one of the two queries polls at a time. The refresh button reloads the preloaded table query only in table view, and switching back to table forces a
network-onlyreload rather than showing rows that stopped refreshing.Accepted limitations
occupied_slotshas no per-kernel breakdown, so narrowing only kernel mode would make the two modes disagree with each other. Say the word if this is unacceptable on an agent panel — it is ~5 lines client-side, using thecontainers.agentfield the query already selects.status != "TERMINATED" & status != "CANCELLED"), matching both shipped pages, which also admits legacy-only statuses the table's 10-enum bucket does not. Counts can legitimately differ for the same radio setting.-created_at; the table's sorter speaks camelCaseSessionV2OrderByand the legacy list speaks snake_case. The grid has no sortable headers, so this only decides which 100 come back.Verification
bash scripts/verify.sh→=== ALL PASS ===backend.ai-ui753 passed / 1 skipped;react1612 passed1144instead of24)git statusclean outside the four changed files — noresources/i18n, no__generated__Still needs a live look (superadmin + real cluster)
Not reachable from this box:
countfor the same radio setting (expect a legitimate difference per the status note above)agent_idshas no index (69c059996cbd_add_agent_ids_col_to_session.pyadds the column withoutcreate_index), so both the count and the slice are sequential scans — fine for a drawer, worth measuring on a large deployment