The legacy ``getCurrentOrganizationId`` helper returned the raw numeric
org id from localStorage, which the GPU-service / storage call sites
then used as the K8s namespace path segment — producing requests like
``/v2/clusters/1/proxy/.../namespaces/5/instances``. The backend
creates a namespace named ``gpustack-{slug}`` (matching
``get_namespace_name``), so the request hit a namespace that doesn't
exist.
Replace the helper with ``getCurrentOrgNamespace`` that:
- prefers the Org the caller is acting under (numeric
``currentOrganizationId`` from localStorage, slug looked up in the
persisted ``organizationList`` or ``allOrganizations`` cache);
- falls back to the selected cluster's owner Org slug for the admin
"All" view, where there is no explicit Org context but the resource
still has to land in some Org's namespace — call sites pass
``currentCluster?.owner_principal_id`` through;
- falls back to ``gpustack-default`` only if neither path resolves a
slug (first load before any cache hydrates, etc.).
``ClusterListItem`` is widened with the optional ``owner_principal_id``
field so TypeScript accepts the fallback argument; the backend has
been returning it via ``ClusterPublic`` all along.
The "chore: hide cluster detail" change had commented out both the
cluster-detail route and the cluster name link in the list. The
detail page component still ships in the OSS tree, so reach is
restored but the link is plugin-gated rather than unconditional:
plain OSS renders the name as a span; with a registered
`clusterDetail.linkableName` flag the name turns into a
`Typography.Link` that fires the parent's existing `onCellClick`
(which navigates to the detail route).
Detail tabs adjustments for the in-cluster context:
- Drop the Deployments tab (deployments are managed from the
top-level Models page; the detail view is scoped to a single
cluster's hardware).
- `WorkerList` and `GPUList` accept an optional `clusterId`. When
set, the list pins `cluster_id` in its query and hides the
cluster-filter dropdown so the user can't scope away from the
cluster they're inside.
`clusterDetail.extraTabs(clusterId, intl)` exposes a plugin slot
that appends additional tab items — used by the enterprise plugin
to inject per-cluster Access / Quotas surfaces. No-op without a
plugin.
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.