diff --git a/config/routes.ts b/config/routes.ts index 1956cb63..a9e3abbb 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -302,16 +302,16 @@ const baseRoutes = [ '/cluster-management/clusters/create' ] }, - // { - // name: 'clusterDetail', - // path: '/cluster-management/clusters/detail', - // key: 'clusterDetail', - // icon: 'icon-cluster2-outline', - // selectedIcon: 'icon-cluster2-filled', - // defaultIcon: 'icon-cluster2-outline', - // hideInMenu: true, - // component: './cluster-management/cluster-detail' - // }, + { + name: 'clusterDetail', + path: '/cluster-management/clusters/detail', + key: 'clusterDetail', + icon: 'icon-cluster2-outline', + selectedIcon: 'icon-cluster2-filled', + defaultIcon: 'icon-cluster2-outline', + hideInMenu: true, + component: './cluster-management/cluster-detail' + }, { name: 'credentials', path: '/cluster-management/credentials', diff --git a/src/pages/cluster-management/cluster-detail.tsx b/src/pages/cluster-management/cluster-detail.tsx index f6df61b2..69db24ee 100644 --- a/src/pages/cluster-management/cluster-detail.tsx +++ b/src/pages/cluster-management/cluster-detail.tsx @@ -1,10 +1,10 @@ import { clusterDetailAtom } from '@/atoms/clusters'; -import Deployments from '@/pages/llmodels/deployments'; import GPUList from '@/pages/resources/components/gpus'; import WorkerList from '@/pages/resources/components/workers'; +import { getGPUStackPlugin } from '@/plugins'; import { IconFont } from '@gpustack/core-ui'; import { useIntl, useNavigate, useSearchParams } from '@umijs/max'; -import { Tabs } from 'antd'; +import { Tabs, type TabsProps } from 'antd'; import { useAtomValue } from 'jotai'; import { PageContainerInner } from '../_components/page-box'; import PageBreadcrumb from '../_components/page-breadcrumb'; @@ -29,6 +29,22 @@ const ClusterDetailModal = () => { } ]; + // Extension slot: a registered plugin may inject additional tab + // items (e.g. per-cluster access / quota panels) by exporting + // ``clusterDetail.extraTabs(clusterId, intl)`` that returns an + // ``items`` array. The plugin's items are appended after the + // built-in tabs; without a plugin this is just ``[]`` and the + // tab strip is unchanged. ``intl`` is threaded through so plugin + // tabs can resolve i18n labels to plain strings — antd Tabs' + // icon-text gap collapses when ``label`` is a ReactNode rather + // than a string, so matching the built-in tabs' shape keeps the + // gap visually consistent. + type TabItem = NonNullable[number]; + const extraTabs = (getGPUStackPlugin()?.clusterDetail?.extraTabs?.( + Number(id), + intl + ) ?? []) as TabItem[]; + return ( { key: 'workers', label: `Workers`, icon: , - children: - }, - { - key: 'deployments', - label: `Deployments`, - icon: , - children: + children: }, { key: 'gpus', label: `GPUs`, icon: , - children: - } + children: + }, + ...extraTabs ]} /> diff --git a/src/pages/cluster-management/hooks/use-cluster-columns.tsx b/src/pages/cluster-management/hooks/use-cluster-columns.tsx index e63beb16..c2bbd4ea 100644 --- a/src/pages/cluster-management/hooks/use-cluster-columns.tsx +++ b/src/pages/cluster-management/hooks/use-cluster-columns.tsx @@ -1,6 +1,7 @@ // columns.ts import { systemConfigAtom } from '@/atoms/system'; import { tableSorter } from '@/config/settings'; +import { getGPUStackPlugin } from '@/plugins'; import { StarFilled } from '@ant-design/icons'; import { AutoTooltip, @@ -11,7 +12,7 @@ import { type TableColumnProps as SealColumnProps } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; -import { Tooltip } from 'antd'; +import { Tooltip, Typography } from 'antd'; import dayjs from 'dayjs'; import { useAtomValue } from 'jotai'; import { useMemo } from 'react'; @@ -79,6 +80,14 @@ const useClusterColumns = ( ): SealColumnProps[] => { const intl = useIntl(); const systemConfig = useAtomValue(systemConfigAtom); + // The cluster-detail page is shipped in OSS source, but OSS keeps + // it unreachable from the cluster list — the link is only + // surfaced when a plugin opts in via + // `clusterDetail.linkableName`. Without a plugin we render the + // name as plain text (matches the pre-restore behaviour); with one + // we use Typography.Link wired to the parent's `onCellClick`. + const nameLinkable: boolean = !!getGPUStackPlugin()?.clusterDetail + ?.linkableName; const setActionsItems = (row: ClusterListItem) => { return clusterActionList.filter((item) => { @@ -101,8 +110,16 @@ const useClusterColumns = ( span: 3, render: (text: string, record: ClusterListItem) => ( <> - - {record.name} + + {nameLinkable ? ( + onCellClick?.(record, 'name')} + > + {record.name} + + ) : ( + {record.name} + )} {record.is_default && ( { +// Optional ``clusterId`` pins the list to a single cluster (used by +// the cluster-detail page) and hides the cluster-filter dropdown so +// the user can't change scope away from the cluster they're already +// inside. +interface GPUListProps { + clusterId?: number; +} + +const GPUList: React.FC = ({ clusterId }) => { const { dataSource, queryParams, @@ -26,7 +34,8 @@ const GPUList = () => { key: PaginationKey.GPUs, fetchAPI: queryGpuDevicesList, polling: true, - API: GPU_DEVICES_API + API: GPU_DEVICES_API, + defaultQueryParams: clusterId ? { cluster_id: clusterId } : undefined }); const [searchParams] = useSearchParams(); const page = searchParams.get('page'); @@ -100,7 +109,7 @@ const GPUList = () => { handleInputChange={handleNameChange} handleSelectChange={handleClusterChange} selectOptions={clusterList} - showSelect={true} + showSelect={!clusterId} > { +// Optional ``clusterId`` pins the list to a single cluster (used by +// the cluster-detail page) and hides the cluster-filter dropdown so +// the user can't change scope away from the cluster they're already +// inside. +interface WorkersProps { + clusterId?: number; +} + +const Workers: React.FC = ({ clusterId }) => { const { dataSource, rowSelection, @@ -50,7 +58,8 @@ const Workers = () => { contentForDelete: 'resources.worker', watch: true, API: WORKERS_API, - updateManually: true + updateManually: true, + defaultQueryParams: clusterId ? { cluster_id: clusterId } : undefined }); const { goToGrafana, ActionButton } = useGranfanaLink({ type: 'worker' @@ -251,7 +260,7 @@ const Workers = () => { <>