diff --git a/src/atoms/user.ts b/src/atoms/user.ts index e75d3067..67370036 100644 --- a/src/atoms/user.ts +++ b/src/atoms/user.ts @@ -28,13 +28,74 @@ export const initialPasswordAtom = atomWithStorage( '' ); -export const getCurrentOrganizationId = (): string => { +// Namespace the server creates for an Org's resources on each Kubernetes +// cluster. The format must match the backend's ``get_namespace_name`` +// helper — ``gpustack-{slug}`` — because the GPU-instance / storage CRDs +// (worker.gpustack.ai/v1) are namespaced and the server-side admission +// keys off this exact name. +// +// Resolution path: +// 1. The Org the caller is currently acting under — the enterprise +// plugin persists ``currentOrganizationId`` (numeric) when the user +// picks an Org via OrgSwitcher. +// 2. The cluster's own owner Org — used in the platform-admin "All" +// view, where the caller has no Org context but the resource still +// has to land in *some* Org's namespace. +// 3. ``gpustack-default`` as a last resort (first load before any +// cache is hydrated, or a cluster whose owner Org is missing from +// both caches). +// +// Called outside React (umi page utilities) so it reads localStorage +// directly rather than going through a Jotai hook. The org caches are +// kept fresh by the enterprise plugin's atomWithStorage atoms, and the +// OrgSwitcher reloads the page on switch so we don't need in-process +// reactivity here. +export const getCurrentOrgNamespace = ( + clusterOwnerPrincipalId?: number | null +): string => { + return ( + lookupOrgNamespace(getStoredCurrentOrgId()) ?? + lookupOrgNamespace(clusterOwnerPrincipalId ?? null) ?? + 'gpustack-default' + ); +}; + +const getStoredCurrentOrgId = (): number | null => { try { const raw = localStorage.getItem('currentOrganizationId'); - if (raw) { - const value = JSON.parse(raw); - if (value != null) return String(value); + if (!raw) return null; + const value = JSON.parse(raw); + return typeof value === 'number' ? value : null; + } catch { + return null; + } +}; + +// Org caches the enterprise plugin persists. ``organizationList`` is +// the caller's member orgs; ``allOrganizations`` is admin-only (every +// Org on the platform) so admin sessions can resolve any owner Org id. +// Both are checked because ``currentOrganizationId`` is null in the +// admin "All" view but a member org's slug might still cover the +// cluster-owner fallback. +const ORG_CACHE_KEYS = ['organizationList', 'allOrganizations'] as const; + +const lookupOrgNamespace = (id: number | null): string | null => { + if (id == null) return null; + // Normalise both sides to strings — the stored id type varies between + // localStorage payloads (some writers stringify, others persist as a + // JSON number); strict equality would silently miss those cases. + const target = String(id); + for (const key of ORG_CACHE_KEYS) { + try { + const raw = localStorage.getItem(key); + if (!raw) continue; + const list = JSON.parse(raw) as Array<{ id: number; slug?: string }>; + if (!Array.isArray(list)) continue; + const match = list.find((item) => String(item?.id) === target); + if (match?.slug) return `gpustack-${match.slug}`; + } catch { + // ignore malformed cache; continue checking other keys } - } catch {} - return 'gpustack-default'; + } + return null; }; diff --git a/src/pages/cluster-management/config/types.ts b/src/pages/cluster-management/config/types.ts index ab05735e..c587c453 100644 --- a/src/pages/cluster-management/config/types.ts +++ b/src/pages/cluster-management/config/types.ts @@ -88,6 +88,9 @@ export interface ClusterListItem { state_message: string; worker_pools: NodePoolListItem[]; k8s_volume_mounts?: VolumeMount[]; + // Backend ClusterPublic carries this; admin-"All" namespace + // resolution falls back to the cluster's owner Org slug. + owner_principal_id?: number; } export interface ClusterFormData { diff --git a/src/pages/gpu-service/instances/forms/storage-volume.tsx b/src/pages/gpu-service/instances/forms/storage-volume.tsx index db15c367..ee867d44 100644 --- a/src/pages/gpu-service/instances/forms/storage-volume.tsx +++ b/src/pages/gpu-service/instances/forms/storage-volume.tsx @@ -1,7 +1,9 @@ -import { getCurrentOrganizationId } from '@/atoms/user'; +import { currentClusterAtom } from '@/atoms/gpuservice'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Button, Flex, Form, Radio } from 'antd'; +import { useAtomValue } from 'jotai'; import { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { FormData as StorageFormData } from '../../storage/config/types'; @@ -29,7 +31,8 @@ const StorageVolume = () => { const form = Form.useFormInstance(); - const namespace = getCurrentOrganizationId(); + const currentCluster = useAtomValue(currentClusterAtom); + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); useEffect(() => { fetchStorage({}); diff --git a/src/pages/gpu-service/instances/index.tsx b/src/pages/gpu-service/instances/index.tsx index 051ad130..c892f9d6 100644 --- a/src/pages/gpu-service/instances/index.tsx +++ b/src/pages/gpu-service/instances/index.tsx @@ -1,5 +1,5 @@ import { currentClusterAtom } from '@/atoms/gpuservice'; -import { getCurrentOrganizationId } from '@/atoms/user'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { PageAction } from '@/config'; import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings'; import type { PageActionType } from '@/config/types'; @@ -33,9 +33,11 @@ import useUpdateInstance from './services/use-update-instance'; const GPUService: React.FC = () => { const intl = useIntl(); - const namespace = getCurrentOrganizationId(); const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom); const clusterID = currentCluster?.id; + // In admin "All" view there's no Org context, so the helper falls + // back to the selected cluster's owner Org slug. + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); const deleteInstance = useCallback( (id: number) => deleteGPUServiceInstance({ namespace, clusterID, id }), diff --git a/src/pages/gpu-service/instances/services/use-create-instance.ts b/src/pages/gpu-service/instances/services/use-create-instance.ts index a3196b10..efefffa8 100644 --- a/src/pages/gpu-service/instances/services/use-create-instance.ts +++ b/src/pages/gpu-service/instances/services/use-create-instance.ts @@ -1,5 +1,5 @@ import { currentClusterAtom } from '@/atoms/gpuservice'; -import { getCurrentOrganizationId } from '@/atoms/user'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { useQueryData } from '@gpustack/core-ui'; import { useAtomValue } from 'jotai'; import { useCallback } from 'react'; @@ -12,9 +12,11 @@ interface CreateInstanceParams { } export default function useCreateInstance() { - const namespace = getCurrentOrganizationId(); const currentCluster = useAtomValue(currentClusterAtom); const clusterID = currentCluster?.id; + // Admin "All" view has no Org context — fall back to the + // selected cluster's owner Org slug for the K8s namespace. + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); const fetchDetail = useCallback( (params: CreateInstanceParams, option?: any) => diff --git a/src/pages/gpu-service/instances/services/use-update-instance.ts b/src/pages/gpu-service/instances/services/use-update-instance.ts index 239dd378..00cdcfb9 100644 --- a/src/pages/gpu-service/instances/services/use-update-instance.ts +++ b/src/pages/gpu-service/instances/services/use-update-instance.ts @@ -1,5 +1,5 @@ import { currentClusterAtom } from '@/atoms/gpuservice'; -import { getCurrentOrganizationId } from '@/atoms/user'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { useQueryData } from '@gpustack/core-ui'; import { useAtomValue } from 'jotai'; import { useCallback } from 'react'; @@ -13,9 +13,11 @@ interface UpdateInstanceParams { } export default function useUpdateInstance() { - const namespace = getCurrentOrganizationId(); const currentCluster = useAtomValue(currentClusterAtom); const clusterID = currentCluster?.id; + // Admin "All" view has no Org context — fall back to the + // selected cluster's owner Org slug for the K8s namespace. + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); const fetchDetail = useCallback( (params: UpdateInstanceParams, option?: any) => diff --git a/src/pages/gpu-service/storage/index.tsx b/src/pages/gpu-service/storage/index.tsx index 74ce04e7..aa55e0f5 100644 --- a/src/pages/gpu-service/storage/index.tsx +++ b/src/pages/gpu-service/storage/index.tsx @@ -1,5 +1,5 @@ import { currentClusterAtom } from '@/atoms/gpuservice'; -import { getCurrentOrganizationId } from '@/atoms/user'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { PageAction } from '@/config'; import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings'; import type { PageActionType } from '@/config/types'; @@ -34,9 +34,11 @@ import useUpdateStorage from './services/use-update-storage'; const GPUServiceStorage: React.FC = () => { const intl = useIntl(); - const namespace = getCurrentOrganizationId(); const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom); const clusterID = currentCluster?.id; + // Admin "All" view falls back to the cluster's owner Org slug — + // see :func:`getCurrentOrgNamespace`. + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); const deleteStorage = useCallback( (id: number) => deleteGPUServiceStorage({ namespace, clusterID, id }), diff --git a/src/pages/gpu-service/storage/services/use-create-storage.ts b/src/pages/gpu-service/storage/services/use-create-storage.ts index b36c62b5..745e55ec 100644 --- a/src/pages/gpu-service/storage/services/use-create-storage.ts +++ b/src/pages/gpu-service/storage/services/use-create-storage.ts @@ -1,5 +1,5 @@ import { currentClusterAtom } from '@/atoms/gpuservice'; -import { getCurrentOrganizationId } from '@/atoms/user'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { useQueryData } from '@gpustack/core-ui'; import { useAtomValue } from 'jotai'; import { useCallback } from 'react'; @@ -12,9 +12,11 @@ interface CreateStorageParams { } export default function useCreateStorage() { - const namespace = getCurrentOrganizationId(); const currentCluster = useAtomValue(currentClusterAtom); const clusterID = currentCluster?.id; + // Admin "All" view has no Org context — fall back to the + // selected cluster's owner Org slug for the K8s namespace. + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); const fetchDetail = useCallback( (params: CreateStorageParams, option?: any) => diff --git a/src/pages/gpu-service/storage/services/use-query-storage.ts b/src/pages/gpu-service/storage/services/use-query-storage.ts index 70f36d84..c49a868e 100644 --- a/src/pages/gpu-service/storage/services/use-query-storage.ts +++ b/src/pages/gpu-service/storage/services/use-query-storage.ts @@ -1,5 +1,5 @@ import { currentClusterAtom } from '@/atoms/gpuservice'; -import { getCurrentOrganizationId } from '@/atoms/user'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { useQueryData } from '@gpustack/core-ui'; import { useAtomValue } from 'jotai'; import { useCallback } from 'react'; @@ -7,9 +7,11 @@ import { queryGPUServiceStorage } from '../apis'; import { ListItem } from '../config/types'; export default function useQueryStorage() { - const namespace = getCurrentOrganizationId(); const currentCluster = useAtomValue(currentClusterAtom); const clusterID = currentCluster?.id; + // Admin "All" view has no Org context — fall back to the + // selected cluster's owner Org slug for the K8s namespace. + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); const fetchDetail = useCallback( (params: Global.K8sSearchParams = {}, options?: any) => diff --git a/src/pages/gpu-service/storage/services/use-update-storage.ts b/src/pages/gpu-service/storage/services/use-update-storage.ts index d3645c23..47941f6d 100644 --- a/src/pages/gpu-service/storage/services/use-update-storage.ts +++ b/src/pages/gpu-service/storage/services/use-update-storage.ts @@ -1,5 +1,5 @@ import { currentClusterAtom } from '@/atoms/gpuservice'; -import { getCurrentOrganizationId } from '@/atoms/user'; +import { getCurrentOrgNamespace } from '@/atoms/user'; import { useQueryData } from '@gpustack/core-ui'; import { useAtomValue } from 'jotai'; import { useCallback } from 'react'; @@ -13,9 +13,11 @@ interface UpdateStorageParams { } export default function useUpdateStorage() { - const namespace = getCurrentOrganizationId(); const currentCluster = useAtomValue(currentClusterAtom); const clusterID = currentCluster?.id; + // Admin "All" view has no Org context — fall back to the + // selected cluster's owner Org slug for the K8s namespace. + const namespace = getCurrentOrgNamespace(currentCluster?.owner_principal_id); const fetchDetail = useCallback( (params: UpdateStorageParams, option?: any) =>