Skip to content

api, ui: show and filter sandboxes by organization - #1248

Merged
ac-pplx merged 5 commits into
ac/admin-ui-sandbox-lookupfrom
ac/admin-ui-sandbox-list-org
Nov 2, 2026
Merged

ac-pplx merged 5 commits into
ac/admin-ui-sandbox-lookupfrom
ac/admin-ui-sandbox-list-org

Conversation

@ac-pplx

@ac-pplx ac-pplx commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The sandbox list can now answer "which org owns this" and "show me this org's sandboxes".

The list spans every organization but had no organization column and no organization filter, so answering either question meant opening rows one by one. It also hid every sandbox older than 24 hours by default ("Last 24h"), and the long-running sandboxes support cases tend to be about are exactly those.

Changes:

  • An Org column names each sandbox's organization and links to it.
  • An org filter is applied by the admin API (GET /admin/api/v1/sandboxes?org_id=). The loaded rows are therefore that organization's newest sandboxes, not a filtered slice of everyone's newest. It is kept in the URL as ?org=, which is used only when it is a UUID; anything else is ignored instead of failing every list request with 400.
  • The org selector is searchable, and an org whose name another org shares is labelled name (slug). When an org is selected, the capped-list notice names it, and a sandbox found by direct lookup says whose it is: "It belongs to , not ." (or "another organization" or "no organization"), or that it is not among the newest rows of the selected org. The notices and the error alert use the selector's labels, so orgs that share a name are told apart by slug.
  • Only the newest list request applies its rows, its error and the end of loading: every request (initial load, polling, Refresh, a change of org or size, Reset) takes the next number. An older answer, even for the same org after a change away and back, is dropped. A poll or Refresh whose org or size is no longer selected by the time it runs takes no number and is skipped: the view is not marked fresh, and Refresh says it was skipped.
  • When the load for a newly selected org or size fails, the previous rows are cleared and the error, naming the org, shows above the table with Retry. That load counts as failed: the view is not marked fresh, and Refresh does not say "Sandboxes refreshed" (the alert reports it, without a toast). Selecting another org or size clears the error. A failed refresh of the same org and size keeps its rows.
  • Each org and size keeps its own refresh timestamp (dashboard:sandboxes:org-<id>:limit-<n>), so windows showing different orgs do not share one and slow each other's refresh. Only the unscoped list at the default size keeps the plain key; the unscoped 100-row list has its own.
  • Admin API: an org-scoped list (org_id set) no longer appends the rows nodes report without a database row (rowless warm-pool slots, observed-local sandboxes). Those rows belong to no organization, so an org-filtered list used to include the whole fleet's pool slots, and the org page's sandbox total counted them. The unscoped list still carries them. The change is in crates/libs/api/src/routes/admin/sandboxes.rs, with a new database-backed test, org_scoped_list_leaves_out_observed_only_rows.
  • The age window now defaults to all time. Links that set age= keep working, and Reset returns to the new default.

Stack

Stacked on #1244 (which is stacked on #1242). Merge those first. This branch is restacked on #1244's review follow-ups. "ui: offer Start, Resume and Restore in sandbox rows" (#1257) is stacked on this PR.

Validation

  • npm ci, npm test (79 pass, including src/lib/org-filter.test.mjs for the ?org= check, the repeated-name labels, the per-scope refresh keys and the lookup notice) and npm run build in ui/.
  • cargo fmt --all -- --check; cargo test -p pplx-space-api --all-features --lib -- routes::admin::sandboxes:: (16 pass, with the new test against a Postgres test container); cargo clippy -p pplx-space-api --all-targets --all-features -- -D warnings.
  • A Playwright loop against a local Kind stack with synthetic data (three orgs), run on the parent branch (production build) and on this branch (the Kind space-ui image):
    • ?org=<acme> shows exactly the rows GET /admin/api/v1/sandboxes?org_id=<acme> returns;
    • every Org cell names that org;
    • the default list matches the API's full list;
    • Acme, then Globex, then Acme again, with the first Acme answer delayed past the others: it never shows (before the round-2 change it showed until the next poll), and the list is not left loading;
    • a synthetic 503 for a newly selected org clears the previous rows and shows the error with Retry, Retry loads the org, and a failed Refresh of the same org keeps its rows;
    • an org-scoped list stores its own refresh key;
    • looking up another org's sandbox by ID while Acme is selected says which org it belongs to, and with a synthetic second org named "Acme Synthetic" it says "It belongs to Acme Synthetic (acme-synthetic-eu), not Acme Synthetic (acme-synthetic).";
    • a poll that captured Acme and runs (its lock callback delayed) after Globex is selected starts no Acme load, and the rows stay Globex's (before this change they showed Acme's);
    • after a failed load for a newly selected org, Refresh shows no "Sandboxes refreshed" and leaves the org's refresh timestamp unchanged, and selecting another org clears the error at once.
  • No new console errors, page errors or failed requests compared with main.

Visual confirmation

Before After: filtered to one org
before after

after: filtering by org

Video: after.

Severity and risk

Severity: medium. This is the first pivot of most org-level support questions.

Risk: low. The default view now includes sandboxes of any age, which can mean more rows on large fleets. The list is still capped by "Load newest". The admin list's org-scoped answer drops node-reported rows, which belong to no org. In this repository only the console passes org_id (this list and the org page); the unscoped list is unchanged. Roll back by reverting the commits.

AI assistance: Claude Code wrote the code and this description, and ran the validation; a maintainer reviews it.

@github-actions github-actions Bot added the triage:pending Waiting for a maintainer to triage label Oct 31, 2026
@github-actions

Copy link
Copy Markdown

Thanks for opening this pull request! A SPACE maintainer will take a look.

CONTRIBUTING.md explains how we review outside contributions.

The sandbox list spans every organization but had no organization
column or filter, so "show me this org's sandboxes" meant opening each
row. It also hid every sandbox older than 24 hours by default, which are
the long-running sandboxes support cases tend to be about.

The list now has an Org column linking to the organization, and an org
filter that the admin API applies (`org_id`), so the loaded rows are
that organization's newest sandboxes rather than a filtered slice of
everyone's; it is kept in the URL as `?org=`. The age window defaults to
all time; links that set `age=` keep working.
…ads with

Reset cleared the organization selector but not the value the list
loads with, and did not reload: the selector read "Org: any" while the
rows, and every later poll, stayed filtered to the previous
organization. Reset now clears the organization filter the same way the
selector does, which reloads the list for every organization.
The admin sandbox list appended node-reported rows (rowless warm-pool
slots, observed-local sandboxes) to every answer. Those rows belong to
no organization, so an org-filtered list carried the whole fleet's pool
slots and the org page's sandbox total counted them. Only the unscoped
list carries them now; a new database-backed test covers both answers.

In the console, `?org=` is used only when it is a UUID, so a mistyped
link no longer fails every list request with 400. The org selector is
searchable and labels repeated names with their slug, and the
capped-list notice names the selected org.
- The list compared an answer's org and size with the current ones, so
  after a change from org A to B and back to A, the first A answer
  could still arrive last and replace the newer one, and an outdated
  request ended `loading` for the newer one. Every list request now
  takes the next number, and only the newest applies its rows, its
  error and the end of loading: the initial load, polling, Refresh, org
  and size changes and Reset alike.
- When the load for a newly selected org or size fails, the previous
  scope's rows are cleared, and the error, naming the org, shows above
  the table with Retry. A failed refresh of the same scope keeps its
  rows, as before.
- The refresh key includes the org and the size
  (`dashboard:sandboxes:org-<id>:limit-<n>`), taken from the same scope
  as the request, so windows showing different orgs no longer share one
  timestamp and starve each other. The unscoped default keeps the plain
  key.
- A sandbox found by direct lookup while an org is selected says whose
  it is: "It belongs to <org>, not <selected org>." (or "another
  organization" / "no organization"), or that it is not among the
  newest rows of the selected org.
…d loads as failures

- A poll or Refresh captured its org and size before its Web Locks
  callback ran, but took its request number inside it: a poll that
  captured org A and ran after the operator selected org B got the
  newer number, B's answer was dropped, and A's rows showed under B
  until the next poll. A refresh whose scope is no longer selected now
  returns without a number, and is reported as skipped: the view is
  not marked fresh, and Refresh says it was skipped.
- A failed load for a newly selected org or size set the error and
  returned normally, so the view was marked fresh and Refresh said
  "Sandboxes refreshed" next to the error. It now fails, with the error
  marked as shown, so no toast repeats the alert.
- Selecting another org or size clears the previous scope's error.
- Only the unscoped list at the default size uses the plain refresh key;
  the unscoped 100-row list gets its own.
- The lookup notice and the alerts name orgs by the selector's labels,
  which add the slug where a name repeats.
- A comment in sandbox-id.ts still said every admin list appends
  node-reported rows; only the unscoped one does.
@ac-pplx
ac-pplx marked this pull request as ready for review November 1, 2026 04:24
zibi-pplx
zibi-pplx previously approved these changes Nov 1, 2026

@zibi-pplx zibi-pplx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One nit.

  • [nit] The title doesn't mention the admin API change. crates/libs/api/src/routes/admin/sandboxes.rs:107 changes what GET /admin/api/v1/sandboxes?org_id= returns (it no longer includes observed-only rows). The title is ui: only, so a reader of the log or release notes won't see an API behaviour change. #1262, on a separate branch, also relies on it. Consider an api, ui: title, or a line about it in the commit message.

@zibi-pplx
zibi-pplx dismissed their stale review November 1, 2026 22:30

Withdrawing this approval for now. Any feedback above still stands.

@ac-pplx ac-pplx changed the title ui: show and filter sandboxes by organization api, ui: show and filter sandboxes by organization Nov 2, 2026
@ac-pplx
ac-pplx added this pull request to stack #1283 November 2, 2026 04:30

@johnlouie-pplx johnlouie-pplx 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.

Approving the org-filter changes at 244193e. I rechecked that the server filters before the limit and still enforces managing-org admin access. The component tests I ran confirm that the newest org response wins, a failed scope change clears the old rows, and the #1244 spinner case is fixed. All 79 UI tests and the production build pass at this head. The CI log also explicitly records org_scoped_list_leaves_out_observed_only_rows passing. The separate lookup follow-up is on #1244; I haven't reproduced the author's browser tests.

@ac-pplx
ac-pplx added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 8e0939f Nov 2, 2026
23 checks passed
@ac-pplx
ac-pplx deleted the ac/admin-ui-sandbox-list-org branch November 2, 2026 06:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage:pending Waiting for a maintainer to triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants