Skip to content

feat(FR-3652): show the resource grid view in the agent detail panel - #9006

Open
yomybaby wants to merge 4 commits into
mainfrom
feat/FR-3652-agent-panel-grid-view
Open

feat(FR-3652): show the resource grid view in the agent detail panel#9006
yomybaby wants to merge 4 commits into
mainfrom
feat/FR-3652-agent-panel-grid-view

Conversation

@yomybaby

@yomybaby yomybaby commented Aug 24, 2026

Copy link
Copy Markdown
Member

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 same experimental_session_resource_grid opt-in, with the same icons, tooltips and i18n keys.

No new i18n keys, no graphql tag touched, no __generated__ churn.

Flag off tab renders exactly as it does today, no toggle
Flag on Table | Grid segmented control between the running/finished radio group and the refresh button

Four commits, on purpose

1. fix(FR-3652)BAIResourceUnitGrid popover anchoring

A prerequisite, and a real defect in the shared component rather than in the new call site.

The grid's hover popover is position: fixed and writes coordinates measured with getBoundingClientRect() — viewport space. That is only correct while no ancestor establishes a containing block for fixed descendants, which every host so far satisfied. @astryxdesign/lab's Drawer applies transform: translateX(0) to the <dialog> element itself while open, so the dialog takes over as the containing block — and its overflow: hidden clips 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:

popover left..right inside the panel in viewport hit-testable
before 2268..2468
after 1147..1347
before — hover produces nothingafter

So 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.body keeps 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 itself

3. test(FR-3652) — the Storybook story the measurement above runs against

Note for whoever opens that story: 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 this bug cannot show. The measurement restores that one shipped rule (.xhi6v0a { inset-inline-start: auto }, present in lab.css) in the page first.

4. fix(FR-3652) — complete the minilang escaping

CodeQL's js/incomplete-sanitization. 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 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 (Strawberry SessionV2). The grid keeps its own legacy compute_session_list query, scoped through that queryfilter's agent_ids field — the manager maps it to an array column (ComputeSession._queryfilter_fieldspecArrayFieldItem("sessions_agent_ids"), compiled to EXISTS(SELECT item FROM unnest(col) WHERE item = :val)), present since 23.03.9, so no manager version gate is needed.

Rebuilding the grid on SessionV2 was considered and is not possible today: live_stat exists 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 / onChangeViewParams from local state) — the seam SessionResourceGridPanel already uses for the same reason — so it writes zero URL keys. The toggle is local state too: AgentList is 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-only reload rather than showing rows that stopped refreshing.

Accepted limitations

  • Multi-node bleed. A 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 with each other. Say the word if this is unacceptable on an agent panel — it is ~5 lines client-side, using the containers.agent field the query already selects.
  • Cap vs. pagination. Grid = first 100, newest first, with the existing truncation banner; table = page N of 10. Both shipped pages already have this mismatch.
  • Status vocabulary. The grid sends the complement form (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.
  • Ordering is fixed to -created_at; the table's sorter speaks camelCase SessionV2OrderBy and the legacy list speaks snake_case. The grid has no sortable headers, so this only decides which 100 come back.
  • The toggle and grid settings reset when the drawer closes.

Verification

  • bash scripts/verify.sh=== ALL PASS ===
  • backend.ai-ui 753 passed / 1 skipped; react 1612 passed
  • New unit test drives the popover fix through mocked rects and fails on the pre-fix code (1144 instead of 24)
  • Real-component before/after measurement in headless Chromium (table and screenshots above), plus the same measurement on an untransformed host showing no change
  • git status clean outside the four changed files — no resources/i18n, no __generated__
  • CI green on every check (CodeQL included)

Still needs a live look (superadmin + real cluster)

Not reachable from this box:

  • Grid toolbar wrapping inside the fixed 800px drawer on a cluster with ≥2 accelerator families
  • Grid group count vs. the table's count for the same radio setting (expect a legitimate difference per the status note above)
  • How the multi-node bleed actually reads on a real multi-node session
  • agent_ids has no index (69c059996cbd_add_agent_ids_col_to_session.py adds the column without create_index), so both the count and the slice are sequential scans — fine for a drawer, worth measuring on a large deployment

… 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.
@github-actions github-actions Bot added the size:L 100~500 LoC label Aug 24, 2026
Comment thread react/src/components/AgentNodeItems/AgentSessions.tsx Fixed
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for backend-ai-ui-coverage (./packages/backend.ai-ui)

Status Category Percentage Covered / Total
🔵 Lines 43.06% 4036 / 9371
🔵 Statements 37.45% 4657 / 12435
🔵 Functions 39.46% 770 / 1951
🔵 Branches 27.51% 3213 / 11676
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/backend.ai-ui/src/components/BAIResourceUnitGrid.tsx 89.17% 75.77% 89.09% 91.41% 64-66, 100-104, 289, 311, 337, 346, 354-355, 359-362, 404-405, 420, 433-434, 434-441, 469-470, 532, 258-337
Generated in workflow #70 for commit 1acdbbf by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for react-coverage (./react)

Status Category Percentage Covered / Total
🔵 Lines 14.89% 5143 / 34536
🔵 Statements 12.38% 6155 / 49696
🔵 Functions 12.44% 789 / 6340
🔵 Branches 8.85% 4209 / 47541
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
react/src/components/AgentDetailDrawerContent.tsx 0% 0% 0% 0% 47-87
react/src/components/AgentNodeItems/AgentSessions.tsx 0% 0% 0% 0% 50-189
Generated in workflow #191 for commit 1acdbbf by the Vitest Coverage Report Action

…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.

Copilot AI 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.

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
yomybaby marked this pull request as ready for review August 24, 2026 13:59

@agatha197 agatha197 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.

Image I still can't see the tooltip.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100~500 LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show the session resource grid view in the agent detail panel

4 participants