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.
This commit is contained in:
gitlawr
2026-05-11 20:28:29 +08:00
committed by jialin
parent e25d48cc89
commit 125a5be91f
5 changed files with 76 additions and 30 deletions
+22 -11
View File
@@ -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<TabsProps['items']>[number];
const extraTabs = (getGPUStackPlugin()?.clusterDetail?.extraTabs?.(
Number(id),
intl
) ?? []) as TabItem[];
return (
<PageContainerInner
header={{
@@ -46,20 +62,15 @@ const ClusterDetailModal = () => {
key: 'workers',
label: `Workers`,
icon: <IconFont type="icon-resources" />,
children: <WorkerList />
},
{
key: 'deployments',
label: `Deployments`,
icon: <IconFont type="icon-rocket-launch1" />,
children: <Deployments></Deployments>
children: <WorkerList clusterId={Number(id)} />
},
{
key: 'gpus',
label: `GPUs`,
icon: <IconFont type="icon-gpu1" />,
children: <GPUList />
}
children: <GPUList clusterId={Number(id)} />
},
...extraTabs
]}
/>
</PageContainerInner>
@@ -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) => (
<>
<AutoTooltip ghost>
<span className="text-primary">{record.name}</span>
<AutoTooltip ghost title={text}>
{nameLinkable ? (
<Typography.Link
onClick={() => onCellClick?.(record, 'name')}
>
{record.name}
</Typography.Link>
) : (
<span className="text-primary">{record.name}</span>
)}
</AutoTooltip>
{record.is_default && (
<Tooltip