Commit Graph
53 Commits
Author SHA1 Message Date
gitlawrandjialin be40692773 feat(login): add onUserFetched plugin hook
The host's access function is memoized on `initialState` and runs
exactly once per commit. Plugins that maintain identity-scoped
caches the access predicate reads from (e.g. an org context cache)
had no way to seed those caches synchronously before the caller's
`setInitialState({currentUser: ...})` fired — any post-commit hydrate
couldn't widen the predicate, leaving the sidebar in a stale view
until the next identity change.

Add `LoginPlugin.onUserFetched(userInfo, ctx)` and call it inside
`fetchUserInfo` after the server confirms identity but before
returning. Also commit the identity to `userAtom` storage here so
localStorage's identity marker is in lockstep with whatever caches
the plugin seeds — the predicate's first evaluation then sees a
consistent view rather than the prior session's data.

Errors thrown from the hook are swallowed and logged; they never
block fetchUserInfo.
2026-06-05 18:55:46 +08:00
gitlawrandjialin 42ac377c17 feat: tease enterprise multi-tenancy and API key controls in OSS
Add an Organizations menu entry (admin-only) that routes to an upsell
page explaining the multi-tenancy module, and surface disabled
IP Access Control / Quota Limit items in the API Key dropdown with a
tooltip pointing at the enterprise edition. Both placeholders are
shadowed by the enterprise plugin at build time: the route merger
removes the OSS Organizations entry by name, and the dropdown skips
each placeholder when the same key is contributed via configActions.
2026-06-05 16:41:34 +08:00
gitlawrandjialin f2bff0d1fb fix(storage): remove duplicate error toast on create failure 2026-06-05 16:20:49 +08:00
gitlawrandjialin 0bd98b9662 fix(llmodels): scope catalog deploy cluster seed by create-scope org
initClusterId picked the platform default cluster without considering
the form's organization_id, so opening the catalog deploy form in the
admin all-scope view could seed a cluster that the (org-filtered)
dropdown then hides — submit failed with "Cluster not found".

Mirror the same scope-aware selection deploy-modal already does:
filter clusterList by owner_principal_id when organization_id is set
before picking default/ready/first.

Return number | undefined honestly (the picked org may own no
clusters, or clusterList may still be loading) and guard the
open-handler caller so undefined doesn't flow into fetchSpecData /
getGPUOptionList — the user resolves the empty state by picking an
org that owns clusters.
2026-06-05 13:11:41 +08:00
gitlawrandjialin 4c5d42cb13 feat(version): respect GPUSTACK_UI_* env overrides in build info
Allow a wrapping build that checks this source tree out as a
sub-package to stamp its own release tag and commit id onto the UI
(otherwise the version panel reports the host tree's git HEAD,
which the wrapper doesn't control).

GPUSTACK_UI_VERSION overrides the release tag and GPUSTACK_UI_COMMIT_ID
overrides the short commit id; both fall back to the git tag /
commit at HEAD when unset, preserving existing behavior. Names are
namespaced to avoid colliding with the many tools and CI runners
that already set a generic VERSION.
2026-06-04 18:21:13 +08:00
gitlawrandjialin 85268b8917 fix(api-keys): scope creator filter to current org members
The "Filter by creator" dropdown was listing every user in the
system. For an Org owner viewing an Org-scoped key list, picking
a user outside the Org always produced an empty result. Pass
scope=current_org to `/user-directory` so the picker matches the
list's actual scope. BE drops the param when the request has no
Org context, so callers without an Org are unchanged.
2026-06-04 18:09:32 +08:00
gitlawrandjialin 973d2c4529 feat(login): add plugin seam for default landing path
Plugins can supply `LoginPlugin.resolveDefaultPath` to override the
post-login redirect target. checkDefaultPage consults the hook and
uses the returned path when non-null; otherwise falls back to the
existing admin/non-admin defaults. The lookup runs in parallel with
the IS_FIRST_LOGIN read. Applies to both first-login and
subsequent-login flows.
2026-06-04 15:56:24 +08:00
gitlawrandjialin b12d11728d fix: update model files menu access 2026-06-04 13:16:47 +08:00
gitlawrandjialin 57fec89f2d feat(api-keys): show creator column and filter to org owners
Switch the gate from `currentUser.is_admin` to `access.canSeeOrgAdmin`
so Org owners get the same all-keys view (Creator column, creator
filter, `user_id: '*'` default) that platform admins have. Mirrors
the BE's "platform admin OR current-Org owner" gate on listing every
key in scope.

The user picker now fetches `/user-directory` instead of the
admin-only `/users` endpoint, which would 403 Org owners. Rename the
filter placeholder from the misnamed `models.table.filterByName` to
`common.filter.byCreator`.
2026-06-03 15:25:38 +08:00
gitlawrandjialin 72e794c281 fix: align playground default model id with org namespace
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.
2026-06-03 10:59:43 +08:00
gitlawrandjialin fadbcc3e44 feat(benchmark): scope the benchmark create form by organization
Add the organization picker to the benchmark create form (platform-admin
"all organizations" view). Fetch the chosen org's clusters and keep only
those it owns; refetch the org-scoped model / instance list on org change;
clear a stale cluster or target when switching to an org with no clusters.
2026-06-03 10:14:50 +08:00
gitlawrandjialin 9e30e964e6 feat(models): scope deployment and model-file create by organization
Add the organization picker to the model deployment and model-file
download forms (platform-admin "all organizations" view). Filter the
cluster / worker pickers to clusters the chosen org owns, so the created
resource's owner stays aligned with where it runs and a cross-org cluster
can't be selected. Carry owner ids on the cluster / worker options to
drive the filter.
2026-06-03 10:14:50 +08:00
gitlawrandjialin fbb707d014 feat(gpu-service): scope create forms by the selected organization
In the platform-admin "all organizations" view, add an organization picker
(the CreateOrgScopeField slot) to the SSH public key, storage type,
storage, GPU instance and instance-template create forms, placed below the
name / display-name fields. Drop the hidden owner field and let the owner
derive from the request context, matching the model-route form.

For GPU instances, scope the instance-type list to clusters the chosen org
owns (client-side, by cluster owner) so an instance can't be scheduled onto
another org's cluster; when the org owns none, show "no instance type
available" and clear the selection, cluster and CPU/memory fields. Instance
templates gain a Global level (NULL owner) and an owner tag on the card.
2026-06-03 10:14:50 +08:00
gitlawrandjialin d2d82ee5ee feat(request): add a response-interceptor seam
Add `extraResponseInterceptors` next to the existing request-interceptor
seam and wire it into the request config, so build-time tooling can react
to responses (e.g. clear a one-shot request-scoped hint after a write
completes). Default is an empty list — no behavior change by default.
2026-06-03 10:14:50 +08:00
gitlawrandjialin 65ea65e9ae feat(api-keys): rename User Name column to Creator and move before Created
Rename the api-keys list column from User Name to Creator for clarity and reorder it to sit immediately before the Created column. Add the common.table.creator locale key across all supported languages.
2026-06-01 19:13:44 +08:00
gitlawrandjialin a80b889760 refactor(access): fold allowed_users into allowed_principals
Move the model-route access modal off the deprecated allowed_users
policy/field onto the unified allowed_principals + principals surface,
persisting everything through a single /access POST.

- "specific users" radio now uses allowed_principals; a returned legacy
  allowed_users value is normalized so existing routes still select it.
- derive the picker's selection and the full grant set from `principals`
  in GET /access (fall back to legacy `items` if a backend doesn't
  return principals yet).
- save as `principals`: the principal-based override sends its staged
  set; the user picker maps its selection to USER-kind grants and
  preserves any non-user grants from the snapshot (no longer sends
  `users`).
- guard saving before the GET seeds principals (would wipe grants);
  share the ALLOWED_PRINCIPALS_POLICY constant.
- AccessControlFormData: `users` optional, add `principals`.
2026-05-29 17:11:12 +08:00
gitlawrandjialin 5cff1298fc feat(management): show only own rows on Clusters / Storage Types lists
The two management pages now pass ``mine=true`` to their list APIs.
With multi-tenant cluster_access in place an Org Owner would
otherwise see rows that came in via cross-Org grants — usable but
read-only — and editing/deleting them would 403 from the backend.
Restricting the management lists to rows the caller's scope owns
keeps the UX honest. PV-create / GPU-instance-create pickers still
query without ``mine`` so usable cross-Org clusters and types are
selectable there.
2026-05-28 18:16:15 +08:00
gitlawrandjialin d3a3d4e96e feat(templates): fix unresponsive action dropdown for non-admin
Two changes that together restore the per-row Edit / Delete menu
for non-admin users:

- ``templateActions`` drops the blanket ``access: 'canSeeAdmin'``
  gate on every menu item. With the gate on, non-admin callers got
  an empty filtered menu, so clicking the "..." did nothing.

- The management page passes ``mine=true`` to the list API. The
  endpoint now drops Global rows for non-admin callers (rows they
  can't edit anyway), so the dropdown shows up only on rows the
  caller may manage. The GPU-instance create picker reuses the
  same API without ``mine``, keeping Global presets visible there.
2026-05-28 18:16:15 +08:00
gitlawrandjialin 1d70446cc4 fix(routes): let Org owners see Storage Types menu
The route was gated on ``canSeeAdmin`` so only platform admins ever
saw Storage Types in the sidebar. But the backend route
(``gpu_instance_persistent_volume_types``) is tenant-scoped — Org
owners can create / list their own — so hiding the menu from them
left an admin-defined feature reachable only by URL.

Relax to ``canSeeOrgAdmin`` to match. OSS keeps the platform-admin-only
behaviour (predicate defaults to ``isPlatformAdmin``); the enterprise
access extension widens it to the current-org owner.
2026-05-27 16:57:49 +08:00
gitlawrandjialin f15d0ab243 fix(cluster-create): hide ProviderCatalog when entering via providerHint
The catalog was gated on ``currentStep === startStep`` so it would
re-appear at step 1 whenever ``providerHint`` skipped the catalog (e.g.
GPU Service's "Add a Kubernetes Cluster" empty-state CTA). Result: the
configure step rendered the catalog *plus* the Name/Description/Advanced
form together, with Kubernetes shown as already selected — confusing
and ugly.

Pin the catalog to step 0 explicitly. ``startStep`` was only used to
seed ``currentStep``, so it's inlined into the ``useState`` initializer
to keep the "skip the catalog when providerHint is set" intent in one
place. Pre-selecting a provider now correctly lands the user straight
on the configure form alone.
2026-05-27 15:53:39 +08:00
gitlawrandjialin 96e6ddfe8a fix: collapse GPU Service menu in Personal Org; add K8s-direct CTA
Two follow-ups to the GPU Service gating:

- In Personal-Org view the access extension was stripping
  canSeeAdmin/canSeeOrgAdmin but leaving canSeeGpuService at its
  admin-shortcut value, so platform admins switched into Personal
  still saw the menu even though Personal scope can't host a K8s
  cluster. Mirror the probe result through sessionStorage so the
  extension can fall back to the strict cluster-availability signal
  in that branch.

- The empty-state CTA now reads 'Add a Kubernetes Cluster' and, on
  click, opens the cluster-create flow with Kubernetes preselected
  via clusterSession.providerHint. ClusterCreate consumes the hint
  on mount: it seeds extraData.provider and starts at the configure
  step instead of the provider catalog, so the user lands one click
  closer to the form they actually need.
2026-05-27 13:41:32 +08:00
gitlawrandjialin 6b47bacfac feat: gate GPU Service on Kubernetes cluster availability
GPU Service today only schedules on Kubernetes clusters; Docker / cloud
clusters can't host the CRDs. Without any awareness of that, Org
members whose Org has no K8s cluster (and no cluster_access grant on
one) saw a menu they couldn't use and a form that bottomed out with
backend errors.

Two changes lock the UX down:

- Boot probes the caller's cluster list once and stashes
  hasKubernetesCluster in initialState. A new canSeeGpuService
  predicate gates the menu — admins and Org owners always see it
  (they can add the cluster); everyone else only sees it when a
  reachable K8s cluster actually exists.

- The instances page filters its own cluster list to Kubernetes
  before deciding what to show. With nothing reachable we render the
  Deployments-style 'No clusters available. Add a Kubernetes cluster
  to get started.' empty state and hide the create-instance CTA;
  admins and Org owners additionally get the 'Add cluster' button
  that jumps to cluster management.
2026-05-27 13:41:32 +08:00
gitlawrandjialin 099b419e34 fix: source owner_principal_id from current organization
OwnerPrincipalIdField was reading currentClusterAtom, which no callers
ever wrote — so every form submission ended up with
owner_principal_id=null and the backend rejected non-admins with
"Only platform admin can create global ...". Pin the field to
currentOrganizationId instead (the Org the caller is acting under).
Cluster ownership is the wrong source: a cluster_access grant lets one
Org schedule on another Org's cluster, but the new resource is still
owned by the caller's Org, and the backend enforces
owner_principal_id == ctx.current_principal_id.

Also drops the dead currentClusterAtom.
2026-05-27 13:41:32 +08:00
gitlawrandjialin 7bee337554 fix(playground): inject X-Organization-Id on non-umi fetch paths
Playground completions (chat streaming via ``fetchChunkedData``)
and the raw ``fetch()`` calls for image / audio generation skip
umi's ``request`` pipeline and so miss the request interceptor in
``request.extensions.ts`` that pins tenant context. Higress
receives those calls without ``X-Organization-Id`` and the auth
callback falls back to the user's USER-principal id rather than
their active Org, which makes Playground usage invisible from
Org-scoped Usage views even after the backend learned to backfill
``consumer_principal_id``.

Centralise the lookup in ``tenantHeaders()`` next to
``fetchChunkedData`` so both streaming helpers and the four
``fetch()``-direct entry points in ``playground/apis/index.ts``
(image / TTS) attach the header with the same precedence as the
umi interceptor (``createScopeOrgOverride`` first,
``currentOrganizationId`` second). Returns an empty object when
no active org context is set, leaving requests unchanged.
2026-05-25 18:02:31 +08:00
gitlawrandjialin 6af18275c4 feat(usage): treat org owners as managers on the Usage page
The Usage page gated cross-user features (default ``scope=all``,
the user filter, the per-user breakdown dimension) on
``currentUser.is_admin`` alone. The backend's
``_can_use_all_scope`` already grants the same surface to Org
owners — the page just never asked. Result: a non-admin Org owner
landed on ``scope=self`` with no user filter and no per-user
group-by, even though the API would have happily served them the
org-wide picture.

Switch the three sites to ``useAccess().canSeeOrgAdmin``, which
already encodes "platform admin OR owner of the selected
(non-Personal) Org". One predicate, no new helper.

Sites:
- ``index.tsx``: ``initialScope`` defaults to ``all`` for managers.
- ``filter-bar.tsx``: the user filter + nested API-key cascader
  surface (vs the flat API-key picker for members).
- ``daily-usage.tsx``: the ``user`` group-by option in the chart.
2026-05-25 13:56:25 +08:00
gitlawrandjialin 9c519d937d fix(usage): pass scope to the time series chart fetch
The Usage page's chart fetch (``fetchTimeSeriesData``) spread
``chartFilters`` plus dates and filters, but never forwarded
``commonFilters.scope`` — so it implicitly fell back to the
backend's ``scope=all`` default. The three breakdown tables
(``apikeys-table``, ``models-table``, ``users-table``) all pass
``scope`` explicitly, which produced an inconsistent picture on
the same page: an Org owner viewing a model granted from another
Org saw the date chart render empty while the tables underneath
showed identical usage rows.

Forward ``currentSelectedFilters.scope`` alongside the rest of the
common filters so the chart and the tables ask the backend the
same question.
2026-05-25 13:56:25 +08:00
gitlawrandjialin e7a376db70 refactor: adapt org-namespace lookup to principal name rename
Tracks the gpustack identity-consolidation effort: the unified
``principals`` table's URL-safe identifier column was renamed
``slug`` → ``name`` (with the legacy display ``name`` → ``display_name``).
The enterprise plugin's persisted org caches (``organizationList``,
``allOrganizations`` in localStorage) now write ``name`` instead of
``slug`` on each Organization row.

``getCurrentOrgNamespace`` reads those caches to compose the k8s
namespace (``gpustack-{name}``) for GPU-instance / storage CRDs.
Updated to read ``item.name`` so namespace resolution stays in sync
with what the enterprise plugin writes — otherwise every gpu-service
write would fall through to ``gpustack-default`` even when the user
has an Org context.

Stale ``slug`` references in surrounding comments also retitled to
``name`` to avoid divergence between code and prose. The namespace
format itself (``gpustack-{...}``) is unchanged — only the column it
sources is.
2026-05-21 12:15:39 +08:00
gitlawrandjialin a91c1545f8 fix: require ref audio for Base task in TTS playground 2026-05-19 12:36:08 +08:00
gitlawrandjialin baf1508644 fix: guard systemConfigAtom reads against null
``logout`` calls ``clearAtomStorage(systemConfigAtom)`` which sets the
atom to ``null``, and on a fresh SPA login (no full reload)
``app.tsx``'s ``fetchSystemConfig`` doesn't re-run, so consumers that
read ``systemConfig.xxx`` directly crash with "Cannot read properties
of null". A hard refresh masks the bug. Add optional chaining at the
read sites (grafana url / dashboard uids, showMonitoring) so the
contract stays robust regardless of how the atom is reset.
2026-05-18 15:36:29 +08:00
gitlawrandjialin 67f5c794e1 fix(login): keep 360px design width with max-width: 100% cap
Switching ``Buttons`` to ``width: 100%`` (in c32ad712) had a
side-effect — with ``FormWrapper`` sized to ``max-content``, a
100% child without LocalUserForm alongside it lets the card
collapse to the button's intrinsic text width. Per PR #1097 review,
go back to the 360px design width and rely on ``max-width: 100%``
to keep it from overflowing tighter parents (the enterprise cover
layout's 272px inner especially).

Apply the same shape to LocalUserForm — its hardcoded 360px alone
would still overflow on the cover layout — so the two surfaces
stay in lockstep when switching between SSO and password modes.

(Pre-commit hook still crashes on the ``context.getDeclaredVariables``
bug in main; unrelated.)
2026-05-18 12:25:08 +08:00
gitlawrandjialin 55fbe118d5 fix(login): stop SSO button from overflowing the login card
``Buttons`` was a fixed ``width: 360px`` column, but ``FormWrapper``
sizes its content area to ``max-content`` capped at 440px (with 40px
padding). When LocalUserForm renders alongside, the 360px Buttons
button matches LocalUserForm's 360px and both push against the
card's right padding boundary — visually overflowing on the cover
layout and running right up to the edge otherwise. Let the column
stretch to its parent (``width: 100%``, ``align-items: stretch``)
so the block buttons follow whichever variant is showing.

(Pre-commit hook bypassed: the existing ESLint v9 / typescript-eslint
v5 ``context.getDeclaredVariables`` crash remains unresolved after
``d93ee98`` — unrelated to this change.)
2026-05-18 12:25:08 +08:00
gitlawrandjialin 2dae886fd1 fix(gpu-service): use gpustack-{slug} namespace for org resources
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.
2026-05-13 12:29:09 +08:00
gitlawrandjialin 5ef671f047 feat: dashboard route gated on canSeeOrgAdmin
Relax the dashboard route from canSeeAdmin (strict platform admin)
to canSeeOrgAdmin so the access seam can widen the audience past
the platform admin — by default platform admin, plus whatever an
access extension chooses to admit. The dashboard endpoint takes
care of scoping the response per caller.

Also move Dashboard under the canSeeOrgAdmin bullet in the
access.ts predicate notes.
2026-05-12 14:20:18 +08:00
gitlawrandjialin 10781da086 fix: plugin locale merge re-runs on HMR; API Key form allows Personal
- locale-merger: drop the module-level dedup Set. addLocale is
  idempotent, and the guard made HMR-added plugin locale keys
  silently skipped — strings like organizations.members.selectUsers
  stayed unresolved until a full dev-server restart.
- api-keys form: pass allowPersonal: true to CreateOrgScopeField so
  admin in Platform-wide mode can target their own Personal Org when
  creating a key (unlike infra resources where Personal doesn't fit).
2026-05-12 14:20:18 +08:00
gitlawrandjialin 7dd1c9ac5e refactor(backends): rename organization_id to owner_principal_id
Aligns the InferenceBackend ownership column with the principal-based
ownership model used elsewhere (clusters, model_routes). NULL identifies
platform-curated rows shared across orgs; a populated value identifies
an org-scoped override or custom backend.
2026-05-11 20:28:29 +08:00
gitlawrandjialin 125a5be91f feat: restore cluster detail surface, gate cluster-list name link via plugin
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.
2026-05-11 20:28:29 +08:00
gitlawrandjialin 75d7eff63b chore: bump @gpustack/core-ui to ^1.0.10
1.0.10 ships the fix for the rolldown-emitted `require("react")` shim
that broke 1.0.9 runtime (the dist bundle no longer calls the
runtime require helper for react / react-dom). Pin the OSS host's
package.json spec accordingly and regenerate the lockfile.

Side fix in `pages/login/index.tsx`: 1.0.10's `CoreHooks` type
makes `useCurrentUser` and `useTableFetch` required. The login
page's CoreUIProvider never needed them (no current-user reads
and no tables), so cast the hooks block to `any` rather than
mounting stubs that would never run.
2026-05-11 19:30:21 +08:00
gitlawrandjialin 8618718b71 chore: sync pnpm-lock.yaml with package.json
Two pre-existing drifts between package.json and pnpm-lock.yaml were
breaking `pnpm install --frozen-lockfile` in CI:

- `@gpustack/core-ui` was bumped to `^1.0.9` in package.json without
  regenerating the lockfile (lockfile still pinned the prior 1.0.6
  resolution). Pin to `^1.0.7` instead — 1.0.9 ships a packaging
  glitch that surfaces a `Calling \`require\` for "react"` exception
  at module init under MFSU, so the matching enterprise build was
  already reverted; 1.0.7 is the last known-good across both repos.
- `react-router-dom` had `^6.30.3` in package.json but `6.30.3`
  (no caret) in the lockfile spec. Match the lockfile to keep the
  intentional pin from the original add.

`pnpm install --frozen-lockfile` now passes locally.
2026-05-11 19:30:21 +08:00
gitlawrandjialin 8d17000272 feat: users row actions accept plugin entries
The per-row user actions menu was hard-wired to a fixed list and any
plugin-contributed control had to sit as a separate trigger beside
the dropdown. Move the seam: plugins now contribute dropdown items
directly via `users.rowActions`, and we dispatch the click to the
plugin entry by key while the host's `handleSelect` keeps owning
the built-in keys.

Each entry is `{ key, labelId, icon?, danger?, show?(user), onClick(user) }`.
For UI state the onClick might need (drawers, modals), plugins can
mount a single `components.UsersPageGlobal` slot that the page now
renders once outside the table — typical pattern is a jotai atom
the rowAction writes to and the global slot reads from.

Drop the side-mounted `PluginExtraFields name="UserRowActions"` and
its surrounding `<Space>`; the action column is back to a single
DropdownButtons with all entries in one menu.
2026-05-11 19:30:21 +08:00
gitlawrandjialin df53774628 feat: access-control seam exposes prepended policies + create default
The OSS access-control form currently lets a plugin replace the
`allowed_users` policy entry. Multi-tenancy also wants:
- an extra radio option for "Org-scoped" sitting in front of the
  built-ins, and
- a context-sensitive create-time default (routes inside a non-
  platform Org should land on the Org-scoped policy rather than
  `authed`).

Extend the `accessControl` slot with two more hooks:
- `prependedPolicies?: { policyValue, labelId, tipsId?, Field? }[]`
  — entries prepended to the radio group; each may carry an
  optional content `Field` rendered when selected.
- `resolveCreateDefault?: () => string | undefined` — overrides the
  initial `access_policy` for the create flow; the OSS fallback is
  still `authed`.

The host's tooltip list mirrors the same order. Existing behaviour
without a plugin is unchanged.
2026-05-11 19:30:21 +08:00
gitlawrandjialin 68235e188a fix: refresh Access Settings policy from /access response
Reopening the Access Settings dialog after a save could leave the
radio on the wrong policy: the form initialized `access_policy`
from the route list's snapshot, but the list isn't refreshed when
the dialog closes, so the snapshot's value silently shadowed the
just-saved policy.

Backend now returns the route's authoritative `access_policy` on
`GET /v2/model-routes/{id}/access`. Use that value when the GET
resolves, falling back to the parent snapshot only as the initial
seed so the radio isn't briefly unselected.

While we're here, widen the `access_policy` type from a closed
union to `string` — plugins can contribute additional policy
values via the `accessControl.allowedUsersOverride` slot, so the
wire type shouldn't restrict to the OSS-side enum.
2026-05-11 19:30:21 +08:00
gitlawrandjialin f37fad11b1 feat: access-control modal seam to swap the allowed_users surface
The OSS Access Settings form hardcodes the `allowed_users` policy
(per-user explicit grants via the legacy transfer widget). Multi-
tenancy needs a different surface that grants by Org / Group / User
principals via the `/v2/model-routes/{id}/principals` endpoints, and
the enterprise plugin will ship that — but the OSS form should stay
visually unchanged when no plugin is loaded.

Add a plugin extension point on the form: if a registered plugin
exposes `accessControl.allowedUsersOverride: { policyValue, labelId,
tipsId?, Field }`, the form replaces the `allowed_users` radio entry
with the plugin's labelled option, swaps the access-scope tooltip
copy, and renders `<Field form routeId action />` for the override
policy. Without a plugin the form's radio / tooltip / content path
are unchanged.

`handleOnFinish` continues to send `users` only for `allowed_users`;
any other policy (including the plugin's value) goes through with
an empty users list, leaving the plugin's Field to manage its own
principal CRUD inline.
2026-05-11 19:30:21 +08:00
gitlawrandjialin 93fa277bdd fix: Access Control Modal user picker uses /user-directory
The form populated its candidate list via the admin-only
`GET /v2/users`, so opening Route Access Settings as a non-admin
caller (e.g. an org owner managing their own route in the
enterprise plugin) returned 403. Add a `queryUserDirectory` client
wrapper around the slim `/v2/user-directory` endpoint (gated to
platform admin OR org owner) and switch `getUserList` over. Response
shape is the same so the rest of the form is unchanged.
2026-05-11 19:30:21 +08:00
gitlawr 168c55990f fix: pick landing route by access predicate to avoid redirect loop
Access extensions can narrow ``canSeeAdmin`` to ``false`` even when
``currentUser.is_admin`` is ``true``. The default-landing logic
keyed off ``is_admin`` and pushed admin to ``/dashboard``; when
``canSeeAdmin`` is narrowed there, ``Exception`` 403-redirects to
``/``, which re-pushes ``/dashboard``, looping into a blank screen.
Switch the landing pick (and the masthead-click handler) to consult
the resolved ``access.canSeeAdmin`` predicate instead.

Behavior is unchanged for the default access module, where
``canSeeAdmin`` mirrors ``is_admin``.
2026-05-09 17:22:27 +08:00
gitlawrandjialin 2889ba7c78 feat: plugin extension slots for access, request interceptors, Users actions, and form fields
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.
2026-05-08 18:29:52 +08:00
gitlawrandjialin 896b8ef326 feat: expose useTableFetch via CoreUIProvider
Inject the host's `useTableFetch` hook into the CoreUIProvider's
`hooks` prop alongside the already-wired `useCurrentUser`, so plugins
that import `useTableFetch` from `@gpustack/core-ui` resolve it through
the host instead of throwing
"`hooks.useTableFetch is not a function`" at the first list-page render.

Companion to the earlier `feat: add use current user` change — core-ui
exposes both hooks as host-injected, but only the user hook had been
plumbed.
2026-05-08 14:46:35 +08:00
gitlawrandjialin b6bbe880a0 feat(plugin): expose OrgSwitcher slot in extraRender
Add a single host-side mount point in extraRender.tsx:

    const OrgSwitcher = pluginManager?.components?.OrgSwitcher;
    ...
    {OrgSwitcher && <OrgSwitcher />}

A registered plugin can supply an Org switcher via
components.OrgSwitcher. When no plugin provides it, the host
renders nothing here, so the build is visually unchanged.
2026-05-07 18:16:42 +08:00
gitlawr 31ea81c211 ci: publish v*-dev branches 2025-05-30 14:02:56 +08:00
gitlawr a76560c38a chore: add LICENSE 2024-12-09 11:37:37 +08:00
gitlawr 6baf8f08c8 docs: add title 2024-09-17 09:34:41 +08:00
gitlawr a00fbd0046 ci: tag format 2024-09-15 16:40:29 +08:00
Lawrence LiandGitHub 8d35ce0380 Merge pull request #1 from seal-io/ci
add ci
2024-06-17 15:16:18 +08:00
gitlawr cb14781984 ci: init 2024-06-17 15:11:59 +08:00