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:
+10
-10
@@ -302,16 +302,16 @@ const baseRoutes = [
|
|||||||
'/cluster-management/clusters/create'
|
'/cluster-management/clusters/create'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// name: 'clusterDetail',
|
name: 'clusterDetail',
|
||||||
// path: '/cluster-management/clusters/detail',
|
path: '/cluster-management/clusters/detail',
|
||||||
// key: 'clusterDetail',
|
key: 'clusterDetail',
|
||||||
// icon: 'icon-cluster2-outline',
|
icon: 'icon-cluster2-outline',
|
||||||
// selectedIcon: 'icon-cluster2-filled',
|
selectedIcon: 'icon-cluster2-filled',
|
||||||
// defaultIcon: 'icon-cluster2-outline',
|
defaultIcon: 'icon-cluster2-outline',
|
||||||
// hideInMenu: true,
|
hideInMenu: true,
|
||||||
// component: './cluster-management/cluster-detail'
|
component: './cluster-management/cluster-detail'
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
name: 'credentials',
|
name: 'credentials',
|
||||||
path: '/cluster-management/credentials',
|
path: '/cluster-management/credentials',
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { clusterDetailAtom } from '@/atoms/clusters';
|
import { clusterDetailAtom } from '@/atoms/clusters';
|
||||||
import Deployments from '@/pages/llmodels/deployments';
|
|
||||||
import GPUList from '@/pages/resources/components/gpus';
|
import GPUList from '@/pages/resources/components/gpus';
|
||||||
import WorkerList from '@/pages/resources/components/workers';
|
import WorkerList from '@/pages/resources/components/workers';
|
||||||
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
import { IconFont } from '@gpustack/core-ui';
|
import { IconFont } from '@gpustack/core-ui';
|
||||||
import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
|
import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
|
||||||
import { Tabs } from 'antd';
|
import { Tabs, type TabsProps } from 'antd';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { PageContainerInner } from '../_components/page-box';
|
import { PageContainerInner } from '../_components/page-box';
|
||||||
import PageBreadcrumb from '../_components/page-breadcrumb';
|
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 (
|
return (
|
||||||
<PageContainerInner
|
<PageContainerInner
|
||||||
header={{
|
header={{
|
||||||
@@ -46,20 +62,15 @@ const ClusterDetailModal = () => {
|
|||||||
key: 'workers',
|
key: 'workers',
|
||||||
label: `Workers`,
|
label: `Workers`,
|
||||||
icon: <IconFont type="icon-resources" />,
|
icon: <IconFont type="icon-resources" />,
|
||||||
children: <WorkerList />
|
children: <WorkerList clusterId={Number(id)} />
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'deployments',
|
|
||||||
label: `Deployments`,
|
|
||||||
icon: <IconFont type="icon-rocket-launch1" />,
|
|
||||||
children: <Deployments></Deployments>
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'gpus',
|
key: 'gpus',
|
||||||
label: `GPUs`,
|
label: `GPUs`,
|
||||||
icon: <IconFont type="icon-gpu1" />,
|
icon: <IconFont type="icon-gpu1" />,
|
||||||
children: <GPUList />
|
children: <GPUList clusterId={Number(id)} />
|
||||||
}
|
},
|
||||||
|
...extraTabs
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</PageContainerInner>
|
</PageContainerInner>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// columns.ts
|
// columns.ts
|
||||||
import { systemConfigAtom } from '@/atoms/system';
|
import { systemConfigAtom } from '@/atoms/system';
|
||||||
import { tableSorter } from '@/config/settings';
|
import { tableSorter } from '@/config/settings';
|
||||||
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
import { StarFilled } from '@ant-design/icons';
|
import { StarFilled } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
AutoTooltip,
|
AutoTooltip,
|
||||||
@@ -11,7 +12,7 @@ import {
|
|||||||
type TableColumnProps as SealColumnProps
|
type TableColumnProps as SealColumnProps
|
||||||
} from '@gpustack/core-ui';
|
} from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip, Typography } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
@@ -79,6 +80,14 @@ const useClusterColumns = (
|
|||||||
): SealColumnProps[] => {
|
): SealColumnProps[] => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const systemConfig = useAtomValue(systemConfigAtom);
|
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) => {
|
const setActionsItems = (row: ClusterListItem) => {
|
||||||
return clusterActionList.filter((item) => {
|
return clusterActionList.filter((item) => {
|
||||||
@@ -101,8 +110,16 @@ const useClusterColumns = (
|
|||||||
span: 3,
|
span: 3,
|
||||||
render: (text: string, record: ClusterListItem) => (
|
render: (text: string, record: ClusterListItem) => (
|
||||||
<>
|
<>
|
||||||
<AutoTooltip ghost>
|
<AutoTooltip ghost title={text}>
|
||||||
<span className="text-primary">{record.name}</span>
|
{nameLinkable ? (
|
||||||
|
<Typography.Link
|
||||||
|
onClick={() => onCellClick?.(record, 'name')}
|
||||||
|
>
|
||||||
|
{record.name}
|
||||||
|
</Typography.Link>
|
||||||
|
) : (
|
||||||
|
<span className="text-primary">{record.name}</span>
|
||||||
|
)}
|
||||||
</AutoTooltip>
|
</AutoTooltip>
|
||||||
{record.is_default && (
|
{record.is_default && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
@@ -11,7 +11,15 @@ import { GPU_DEVICES_API, queryGpuDevicesList } from '../apis';
|
|||||||
import { GPUDeviceItem } from '../config/types';
|
import { GPUDeviceItem } from '../config/types';
|
||||||
import useGPUColumns from '../hooks/use-gpu-columns';
|
import useGPUColumns from '../hooks/use-gpu-columns';
|
||||||
|
|
||||||
const GPUList = () => {
|
// 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<GPUListProps> = ({ clusterId }) => {
|
||||||
const {
|
const {
|
||||||
dataSource,
|
dataSource,
|
||||||
queryParams,
|
queryParams,
|
||||||
@@ -26,7 +34,8 @@ const GPUList = () => {
|
|||||||
key: PaginationKey.GPUs,
|
key: PaginationKey.GPUs,
|
||||||
fetchAPI: queryGpuDevicesList,
|
fetchAPI: queryGpuDevicesList,
|
||||||
polling: true,
|
polling: true,
|
||||||
API: GPU_DEVICES_API
|
API: GPU_DEVICES_API,
|
||||||
|
defaultQueryParams: clusterId ? { cluster_id: clusterId } : undefined
|
||||||
});
|
});
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const page = searchParams.get('page');
|
const page = searchParams.get('page');
|
||||||
@@ -100,7 +109,7 @@ const GPUList = () => {
|
|||||||
handleInputChange={handleNameChange}
|
handleInputChange={handleNameChange}
|
||||||
handleSelectChange={handleClusterChange}
|
handleSelectChange={handleClusterChange}
|
||||||
selectOptions={clusterList}
|
selectOptions={clusterList}
|
||||||
showSelect={true}
|
showSelect={!clusterId}
|
||||||
></FilterBar>
|
></FilterBar>
|
||||||
<ConfigProvider renderEmpty={renderEmpty}>
|
<ConfigProvider renderEmpty={renderEmpty}>
|
||||||
<Table
|
<Table
|
||||||
|
|||||||
@@ -26,7 +26,15 @@ import UpdateLabels from './update-labels';
|
|||||||
import WorkerDetailModal from './worker-detail-modal';
|
import WorkerDetailModal from './worker-detail-modal';
|
||||||
import WorkerRightActions from './worker-right-actions';
|
import WorkerRightActions from './worker-right-actions';
|
||||||
|
|
||||||
const Workers = () => {
|
// 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<WorkersProps> = ({ clusterId }) => {
|
||||||
const {
|
const {
|
||||||
dataSource,
|
dataSource,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
@@ -50,7 +58,8 @@ const Workers = () => {
|
|||||||
contentForDelete: 'resources.worker',
|
contentForDelete: 'resources.worker',
|
||||||
watch: true,
|
watch: true,
|
||||||
API: WORKERS_API,
|
API: WORKERS_API,
|
||||||
updateManually: true
|
updateManually: true,
|
||||||
|
defaultQueryParams: clusterId ? { cluster_id: clusterId } : undefined
|
||||||
});
|
});
|
||||||
const { goToGrafana, ActionButton } = useGranfanaLink({
|
const { goToGrafana, ActionButton } = useGranfanaLink({
|
||||||
type: 'worker'
|
type: 'worker'
|
||||||
@@ -251,7 +260,7 @@ const Workers = () => {
|
|||||||
<>
|
<>
|
||||||
<PageBox>
|
<PageBox>
|
||||||
<FilterBar
|
<FilterBar
|
||||||
showSelect={true}
|
showSelect={!clusterId}
|
||||||
selectHolder={intl.formatMessage({ id: 'clusters.filterBy.cluster' })}
|
selectHolder={intl.formatMessage({ id: 'clusters.filterBy.cluster' })}
|
||||||
marginBottom={22}
|
marginBottom={22}
|
||||||
marginTop={30}
|
marginTop={30}
|
||||||
|
|||||||
Reference in New Issue
Block a user