import { clusterDetailAtom } from '@/atoms/clusters'; 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, type TabsProps } from 'antd'; import { useAtomValue } from 'jotai'; import { PageContainerInner } from '../_components/page-box'; import PageBreadcrumb from '../_components/page-breadcrumb'; import ClusterBasic from './components/detail/cluster-basic'; import ClusterSystemLoad from './components/detail/cluster-system-load'; const ClusterDetailModal = () => { const navigate = useNavigate(); const intl = useIntl(); const [searchParams] = useSearchParams(); const id = searchParams.get('id'); const clusterName = searchParams.get('name'); const clusterDetailData = useAtomValue(clusterDetailAtom); const breadcrumbItems = [ { title: {intl.formatMessage({ id: 'clusters.title' })}, onClick: () => navigate(-1) }, { title: clusterName } ]; // 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 ( }} > , children: ( ) }, { key: 'gpus', label: `GPUs`, icon: , children: }, ...extraTabs ]} /> ); }; export default ClusterDetailModal;