Reserve a stable block for the empty/loading state (matching the NoResult
height) and fade the empty state in, so entering a list page no longer jumps
between spinner, empty result, and rows. Apply scroll-table to the antd main
lists and pass emptyMinHeight to the SealTable-based lists.
Add `usePluginListColumns(pageKey)` so a registered plugin can splice
extra columns into a list page's columns hook keyed by page id. The
slot is wired into 11 list pages (Models, Model Routes, Clusters,
Cloud Credentials, MaaS Providers, Model Files, Model Instances,
GPU Service Instances / Public Keys / Storage / Storage Types) so any
consumer of the seam reaches every org-scoped list without per-page
plumbing.
The slot accepts either a static array or a hook function — the hook
form lets the registrant use React hooks to decide visibility without
the host having to evaluate it. Columns carry a `placement`:
`after-name` is the default (sits next to the row's identifying
column); `before-time` / `before-operation` are kept for back-compat
with the existing per-page `modelRoutes.extraColumns` slot.
SealTable-grid pages (Models / Model Routes / Clusters) absorb the
plugin column's span by shrinking the widest right-side columns so
the 24-unit grid stays balanced. Model Routes' `CREATE_TIME_MIN_SPAN`
drops from 3 to 2 because with two plugin columns active the grid
was 1 unit over and the action dropdown wrapped onto a new row; the
date string ellipsizes cleanly at the lower min.
ListItem types for Model Instances, MaaS Providers, Cloud Credentials,
and Model Files gain an optional `owner_principal_id` field — the
backend already emits it (denormalized from the parent resource), the
TS types just hadn't declared it.
The GPU Instances page splices the plugin column right before the
Cluster column (rather than after Name); the two read together since
Cluster narrows down to one Org.
Two QA-reported bugs on the "Open in Playground" path. Both come
down to the model id the playground submits not matching what
``/v1/models`` / the dispatcher key off:
* Routes page (OSS UI) emitted ``default/<name>`` for routes in the
platform Org and 404'd. ``use-open-playground`` keyed off the
``is_platform`` flag alone; stale caches that drop the flag
slipped through. Also accept the well-known ``name === 'default'``
(``PLATFORM_PRINCIPAL_NAME`` on the backend) as a fallback signal.
* My Models page (enterprise UI, non-admin) emitted bare ``<name>``
with no Org prefix for non-platform routes. Use ``model.name``
from ``/v2/my-models`` verbatim — the backend ("fix: principal
prefix in my-models") now rewrites that field to the OpenAI-style
id server-side, which also closes the cross-Org grant gap a
client-side cache lookup can't (the granting Org isn't in the
caller's member list). Card title now shows the prefixed id, so
users can tell apart same-named models from different Orgs.
Routes page keeps ``useOpenPlayground`` — that surface is always
scoped to the caller's own Org, the local cache is sufficient, and
``/model-routes`` still returns the raw ``name``.
Also drops the now-unused ``onClick`` prop on ``ModelItem``: the
card had ``clickable={false}`` so the parent-passed handler was
already dead code; the Button drives the playground navigation.
Opening an org-scoped deployment in the Playground pre-selected the bare
model name (e.g. `qwen3-0.6b`), which never matched the org-namespaced
option the `/v1/models` dropdown actually lists (`org1/qwen3-0.6b`), so
the selection showed an unmatched value.
Resolve the owning org from the route row's `owner_principal_id` (falling
back to the org the caller is currently acting under for the admin "All"
view) and reconstruct the same `{org}/{name}` id the server reports. The
platform org carries no prefix, matching the server's behaviour.
- user.ts: add `getOrgById`/`getCurrentOrg` that scan both org caches
(`organizationList` + admin-only `allOrganizations`) with string-
normalised id comparison; `getOrgNameById` is now a thin wrapper.
Helpers accept `undefined` so optional row fields type-check.
- use-open-playground: build the prefix from the resolved org record,
keying the skip-prefix decision off `is_platform`.
- RouteItem: declare the `owner_principal_id` the list API returns.
- RouteTargetFormItem: drop the duplicate `overridden_model_name`
declaration that TS flagged as a duplicate identifier.
Introduce `pages/api-keys/plugin.tsx` and `pages/model-routes/plugin.tsx`
defining a unified `{ key, labelId, icon, priority, danger, form,
useCreate }` contract that plugins use to contribute per-row
"configure this record" actions. The host renders one dropdown entry
per registered action and mounts each entry's form, ordered by a
single numeric priority (built-ins are ranked on the same scale; rows
flagged `danger` sink to the bottom).
Drops the older `apiKeys.rowActions` / `modelRoutes.rowActions` slots
and the implicit drawer half of
`<PluginExtraFields name="APIKeysPageGlobal" />`. The model-routes
mount point stays for the page-level quota-defaults bulk-fetch and now
also accepts a `refreshToken` so per-row saves can invalidate derived
page data without changing the row set.
Four purely-additive seams that let build-time tooling extend host
behaviour without forking files:
* `src/access.extensions.ts` — identity `applyAccessExtensions`
(mirrors `config/routes.extensions.ts`). `src/access.ts` runs the
resolved predicate object through it. Adds two new predicates,
`canSeeOrgAdmin` and `canManageCurrentOrg`, alongside the existing
`canSeeAdmin`. Resources, Models children, Cluster Management, and
Resources/Backends are retagged from `canSeeAdmin` to
`canSeeOrgAdmin`. Users and Dashboard stay strict.
* `src/request.extensions.ts` — identity-empty
`extraRequestInterceptors`. `src/request-config.tsx` spreads it
into the existing `requestInterceptors` list so extensions can
inject context-aware headers without forking the request config.
* Users page action column — renders
`getGPUStackPlugin()?.components?.UserRowActions` next to the
existing DropdownButtons inside a Space when a plugin component
is registered. If absent, the cell renders exactly as before.
* `src/components/plugin-extra-fields.tsx` — generic component-slot
helper. Renders `pluginManager.components.<name>` if registered,
forwarding a `context` payload. Used by create/edit forms to let
plugins inject extra `Form.Item` fields. Mounted in the relevant
create forms — API Keys, Cloud Credentials, Clusters, Model
Routes, Model Providers, and Inference Backends — under the slot
name `CreateOrgScopeField`. Resources whose org is implicit from a
parent (Models / Workers / Benchmarks / Worker Pools / Model Files
inherit from the chosen Cluster) deliberately don't mount the slot.