chore: add models and gpus in cluster detail page

This commit is contained in:
jialin
2026-01-12 20:30:24 +08:00
parent 42943a33fe
commit 0cde6f7dbc
11 changed files with 134 additions and 90 deletions
+3
View File
@@ -1,3 +1,4 @@
import { ClusterListItem } from '@/pages/cluster-management/config/types';
import { atom } from 'jotai'; import { atom } from 'jotai';
// models expand keys: create, update , delete, // models expand keys: create, update , delete,
@@ -63,3 +64,5 @@ export const clusterSessionAtom = atom<{
firstAddWorker: boolean; firstAddWorker: boolean;
firstAddCluster: boolean; firstAddCluster: boolean;
} | null>(null); } | null>(null);
export const clusterDetailAtom = atom<ClusterListItem | null>(null);
@@ -1,7 +1,11 @@
import { clusterDetailAtom } from '@/atoms/clusters';
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import Deployments from '@/pages/llmodels/index';
import GPUList from '@/pages/resources/components/gpus';
import WorkerList from '@/pages/resources/components/workers'; import WorkerList from '@/pages/resources/components/workers';
import { useIntl, useNavigate, useSearchParams } from '@umijs/max'; import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
import { Tabs } from 'antd'; import { Tabs } from 'antd';
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';
import ClusterBasic from './components/detail/cluster-basic'; import ClusterBasic from './components/detail/cluster-basic';
@@ -13,6 +17,7 @@ const ClusterDetailModal = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const id = searchParams.get('id'); const id = searchParams.get('id');
const clusterName = searchParams.get('name'); const clusterName = searchParams.get('name');
const clusterDetailData = useAtomValue(clusterDetailAtom);
const breadcrumbItems = [ const breadcrumbItems = [
{ {
@@ -39,7 +44,7 @@ const ClusterDetailModal = () => {
items={[ items={[
{ {
key: 'workers', key: 'workers',
label: 'Workers', label: `Workers (${clusterDetailData?.workers ?? 0})`,
icon: <IconFont type="icon-resources" />, icon: <IconFont type="icon-resources" />,
children: ( children: (
<WorkerList <WorkerList
@@ -53,15 +58,15 @@ const ClusterDetailModal = () => {
}, },
{ {
key: 'deployments', key: 'deployments',
label: 'Deployments', label: `Deployments (${clusterDetailData?.models ?? 0})`,
icon: <IconFont type="icon-rocket-launch1" />, icon: <IconFont type="icon-rocket-launch1" />,
children: <div>Deployments Content</div> children: <Deployments clusterId={Number(id)}></Deployments>
}, },
{ {
key: 'gpus', key: 'gpus',
label: 'GPUs', label: `GPUs (${clusterDetailData?.gpus ?? 0})`,
icon: <IconFont type="icon-gpu1" />, icon: <IconFont type="icon-gpu1" />,
children: <div>GPUs Content</div> children: <GPUList clusterId={Number(id)} widths={{ input: 360 }} />
} }
]} ]}
/> />
+1 -1
View File
@@ -254,7 +254,7 @@ const Clusters: React.FC = () => {
const handleOnCell = useMemoizedFn((record: ClusterListItem, dataIndex) => { const handleOnCell = useMemoizedFn((record: ClusterListItem, dataIndex) => {
if (dataIndex === 'name') { if (dataIndex === 'name') {
navigate( navigate(
`/cluster-management/clusters/detail?id=${record.id}&name=${record.name}` `/cluster-management/clusters/detail?id=${record.id}&name=${record.name}&page=clusters`
); );
} }
}); });
@@ -1,5 +1,5 @@
import CardWrapper from '@/components/card-wrapper'; import CardWrapper from '@/components/card-wrapper';
import { Col, Progress, Row } from 'antd'; import { Col, Progress, Row, Tag } from 'antd';
import { round } from 'lodash'; import { round } from 'lodash';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -11,9 +11,12 @@ const Container = styled.div`
height: 86px; height: 86px;
gap: 16px; gap: 16px;
.title { .title {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: var(--ant-color-text-secondary); color: var(--ant-color-text-tertiary);
} }
.value-wrapper { .value-wrapper {
@@ -42,26 +45,30 @@ const ClusterSystemLoad: React.FC<{ clusterId: number }> = ({ clusterId }) => {
} }
}, [clusterId]); }, [clusterId]);
const renderStepsProgress = (percent: number) => { const renderStepsProgress = (
percent: number,
tag: { color: string; text: string }
) => {
return ( return (
<Progress <Progress
percent={percent} percent={percent}
size={{ type="circle"
height: 8 size={50}
}} strokeWidth={8}
styles={{ showInfo={true}
root: { format={() => (
width: '100%' <Tag
}, color={tag?.color || 'blue'}
track: { style={{
flex: 1 fontSize: 11,
}, borderRadius: 12,
body: { backgroundColor: 'unset',
display: 'flex', fontWeight: 500
width: '100%' }}
} >
}} {tag?.text || ''}
showInfo={false} </Tag>
)}
></Progress> ></Progress>
); );
}; };
@@ -73,11 +80,18 @@ const ClusterSystemLoad: React.FC<{ clusterId: number }> = ({ clusterId }) => {
<Col span={6}> <Col span={6}>
<CardWrapper style={{ padding: '16px', height: 120 }}> <CardWrapper style={{ padding: '16px', height: 120 }}>
<Container> <Container>
<div className="title">GPU Utilization</div> <div className="title">
<span>GPU Utilization</span>
</div>
<div className="value-wrapper"> <div className="value-wrapper">
<div className="value"> <div className="value">
<span>{`${round(systemLoad.current.gpu, 1)}%`}</span> <span>{`${round(systemLoad.current.gpu, 1)}%`}</span>
{renderStepsProgress(round(systemLoad.current.gpu, 1))} </div>
<div className="chart">
{renderStepsProgress(round(systemLoad.current.gpu, 1), {
color: 'magenta',
text: 'GPU'
})}
</div> </div>
</div> </div>
</Container> </Container>
@@ -86,17 +100,16 @@ const ClusterSystemLoad: React.FC<{ clusterId: number }> = ({ clusterId }) => {
<Col span={6}> <Col span={6}>
<CardWrapper style={{ padding: '16px', height: 120 }}> <CardWrapper style={{ padding: '16px', height: 120 }}>
<Container> <Container>
<div className="title">VRAM Utilization</div> <div className="title">
<span>VRAM Utilization</span>
</div>
<div className="value-wrapper"> <div className="value-wrapper">
<div className="value">{`${round(systemLoad.current.vram, 1)}%`}</div> <div className="value">{`${round(systemLoad.current.vram, 1)}%`}</div>
<div className="chart"> <div className="chart">
<Progress {renderStepsProgress(round(systemLoad.current.vram, 1), {
percent={round(systemLoad.current.vram, 1)} color: 'purple',
type="circle" text: 'VRAM'
size={50} })}
strokeWidth={8}
showInfo={false}
></Progress>
</div> </div>
</div> </div>
</Container> </Container>
@@ -105,11 +118,18 @@ const ClusterSystemLoad: React.FC<{ clusterId: number }> = ({ clusterId }) => {
<Col span={6}> <Col span={6}>
<CardWrapper style={{ padding: '16px', height: 120 }}> <CardWrapper style={{ padding: '16px', height: 120 }}>
<Container> <Container>
<div className="title">CPU Utilization</div> <div className="title">
<span>CPU Utilization</span>
</div>
<div className="value-wrapper"> <div className="value-wrapper">
<div className="value"> <div className="value">
<span>{`${round(systemLoad.current.cpu, 1)}%`}</span> <span>{`${round(systemLoad.current.cpu, 1)}%`}</span>
{renderStepsProgress(round(systemLoad.current.cpu, 1))} </div>
<div className="chart">
{renderStepsProgress(round(systemLoad.current.cpu, 1), {
color: 'cyan',
text: 'CPU'
})}
</div> </div>
</div> </div>
</Container> </Container>
@@ -118,19 +138,18 @@ const ClusterSystemLoad: React.FC<{ clusterId: number }> = ({ clusterId }) => {
<Col span={6}> <Col span={6}>
<CardWrapper style={{ padding: '16px', height: 120 }}> <CardWrapper style={{ padding: '16px', height: 120 }}>
<Container> <Container>
<div className="title">Memo Utilization</div> <div className="title">
<span>Memo Utilization</span>
</div>
<div className="value-wrapper"> <div className="value-wrapper">
<div className="value"> <div className="value">
<span>{`${round(systemLoad.current.ram, 1)}%`}</span> <span>{`${round(systemLoad.current.ram, 1)}%`}</span>
</div> </div>
<div className="chart"> <div className="chart">
<Progress {renderStepsProgress(round(systemLoad.current.ram, 1), {
percent={round(systemLoad.current.ram, 1)} color: 'green',
type="circle" text: 'RAM'
size={50} })}
strokeWidth={8}
showInfo={false}
></Progress>
</div> </div>
</div> </div>
</Container> </Container>
@@ -6,7 +6,7 @@ import StatusTag from '@/components/status-tag';
import { tableSorter } from '@/config/settings'; import { tableSorter } from '@/config/settings';
import { StarFilled } from '@ant-design/icons'; import { StarFilled } from '@ant-design/icons';
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 { useMemo } from 'react'; import { useMemo } from 'react';
import { import {
@@ -42,10 +42,9 @@ const useClusterColumns = (
render: (text: string, record: ClusterListItem) => ( render: (text: string, record: ClusterListItem) => (
<> <>
<AutoTooltip ghost title={text}> <AutoTooltip ghost title={text}>
{/* <Typography.Link onClick={() => onCellClick?.(record, 'name')}> <Typography.Link onClick={() => onCellClick?.(record, 'name')}>
{record.name} {record.name}
</Typography.Link> */} </Typography.Link>
{record.name}
</AutoTooltip> </AutoTooltip>
{record.is_default && ( {record.is_default && (
<Tooltip <Tooltip
@@ -101,11 +100,12 @@ const useClusterColumns = (
title: intl.formatMessage({ id: 'common.table.status' }), title: intl.formatMessage({ id: 'common.table.status' }),
dataIndex: 'state', dataIndex: 'state',
span: 3, span: 3,
render: (value: number) => ( render: (value: number, record: ClusterListItem) => (
<StatusTag <StatusTag
statusValue={{ statusValue={{
status: ClusterStatus[value], status: ClusterStatus[value],
text: ClusterStatusLabelMap[value] text: ClusterStatusLabelMap[value],
message: record.state_message || undefined
}} }}
/> />
) )
@@ -1,6 +1,8 @@
import { clusterDetailAtom } from '@/atoms/clusters';
import { createAxiosToken } from '@/hooks/use-chunk-request'; import { createAxiosToken } from '@/hooks/use-chunk-request';
import { useRequest } from 'ahooks'; import { useRequest } from 'ahooks';
import { CancelTokenSource } from 'axios'; import { CancelTokenSource } from 'axios';
import { useAtom } from 'jotai';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { queryClusterDetail, queryClusterItem } from '../apis'; import { queryClusterDetail, queryClusterItem } from '../apis';
import { ClusterListItem } from '../config/types'; import { ClusterListItem } from '../config/types';
@@ -79,6 +81,7 @@ export const useClusterDetail = () => {
const [clusterDetail, setClusterDetail] = useState<ClusterListItem>( const [clusterDetail, setClusterDetail] = useState<ClusterListItem>(
{} as ClusterListItem {} as ClusterListItem
); );
const [, setClusterDetailAtom] = useAtom(clusterDetailAtom);
const { const {
run: fetchClusterDetail, run: fetchClusterDetail,
@@ -96,9 +99,11 @@ export const useClusterDetail = () => {
manual: true, manual: true,
onSuccess: (response) => { onSuccess: (response) => {
setClusterDetail(response); setClusterDetail(response);
setClusterDetailAtom(response);
}, },
onError: () => { onError: () => {
setClusterDetail({} as ClusterListItem); setClusterDetail({} as ClusterListItem);
setClusterDetailAtom(null);
} }
} }
); );
@@ -526,6 +526,8 @@ const InstanceItem: React.FC<InstanceItemProps> = ({
is_main: false, is_main: false,
vram: calcTotalVram(item.computed_resource_claim?.vram || {}), vram: calcTotalVram(item.computed_resource_claim?.vram || {}),
gpu_index: _.keys(item.computed_resource_claim?.vram) gpu_index: _.keys(item.computed_resource_claim?.vram)
.map((i: string) => Number(i))
.sort((a: number, b: number) => a - b)
}; };
}); });
+33 -27
View File
@@ -17,7 +17,7 @@ import PageBox from '@/pages/_components/page-box';
import useNoResourceResult from '@/pages/llmodels/hooks/use-no-resource-result'; import useNoResourceResult from '@/pages/llmodels/hooks/use-no-resource-result';
import { handleBatchRequest } from '@/utils'; import { handleBatchRequest } from '@/utils';
import { DownOutlined, SearchOutlined, SyncOutlined } from '@ant-design/icons'; import { DownOutlined, SearchOutlined, SyncOutlined } from '@ant-design/icons';
import { useIntl, useNavigate } from '@umijs/max'; import { useIntl, useNavigate, useSearchParams } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { Button, Input, Space, message } from 'antd'; import { Button, Input, Space, message } from 'antd';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
@@ -139,6 +139,8 @@ const Models: React.FC<ModelsProps> = ({
getWorkerList, getWorkerList,
workerList workerList
} = useFormInitialValues(); } = useFormInitialValues();
const [searchParams] = useSearchParams();
const page = searchParams.get('page');
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [updateFormInitials, setUpdateFormInitials] = useState<{ const [updateFormInitials, setUpdateFormInitials] = useState<{
data: any; data: any;
@@ -656,18 +658,20 @@ const Models: React.FC<ModelsProps> = ({
onChange={handleCategoryChange} onChange={handleCategoryChange}
options={modelCategories.filter((item) => item.value)} options={modelCategories.filter((item) => item.value)}
></BaseSelect> ></BaseSelect>
<BaseSelect {page !== 'clusters' && (
allowClear <BaseSelect
showSearch={false} allowClear
placeholder={intl.formatMessage({ showSearch={false}
id: 'clusters.filterBy.cluster' placeholder={intl.formatMessage({
})} id: 'clusters.filterBy.cluster'
style={{ width: 160 }} })}
size="large" style={{ width: 160 }}
maxTagCount={1} size="large"
onChange={handleClusterChange} maxTagCount={1}
options={clusterList} onChange={handleClusterChange}
></BaseSelect> options={clusterList}
></BaseSelect>
)}
<BaseSelect <BaseSelect
allowClear allowClear
showSearch={false} showSearch={false}
@@ -690,21 +694,23 @@ const Models: React.FC<ModelsProps> = ({
} }
right={ right={
<Space size={20}> <Space size={20}>
<DropDownActions {page !== 'clusters' && (
menu={{ <DropDownActions
items: sourceOptions, menu={{
onClick: handleClickDropdown items: sourceOptions,
}} onClick: handleClickDropdown
placement="bottomRight" }}
> placement="bottomRight"
<Button
icon={<DownOutlined></DownOutlined>}
type="primary"
iconPlacement="end"
> >
{intl?.formatMessage?.({ id: 'models.button.deploy' })} <Button
</Button> icon={<DownOutlined></DownOutlined>}
</DropDownActions> type="primary"
iconPlacement="end"
>
{intl?.formatMessage?.({ id: 'models.button.deploy' })}
</Button>
</DropDownActions>
)}
<DropdownButtons <DropdownButtons
items={ButtonList} items={ButtonList}
extra={ extra={
+2 -2
View File
@@ -16,7 +16,7 @@ import {
import TableList from './components/table-list'; import TableList from './components/table-list';
import { ListItem } from './config/types'; import { ListItem } from './config/types';
const Models: React.FC = () => { const Models: React.FC<{ clusterId?: number }> = ({ clusterId }) => {
const { sortOrder, handleMultiSortChange } = useTableMultiSort(); const { sortOrder, handleMultiSortChange } = useTableMultiSort();
const { setChunkRequest, createAxiosToken } = useSetChunkRequest(); const { setChunkRequest, createAxiosToken } = useSetChunkRequest();
const { setChunkRequest: setModelInstanceChunkRequest } = const { setChunkRequest: setModelInstanceChunkRequest } =
@@ -45,7 +45,7 @@ const Models: React.FC = () => {
page: 1, page: 1,
perPage: 10, perPage: 10,
search: '', search: '',
cluster_id: 0, cluster_id: clusterId || 0,
categories: [], categories: [],
state: '', state: '',
sort_by: '' sort_by: ''
+13 -6
View File
@@ -5,7 +5,7 @@ import useTableFetch from '@/hooks/use-table-fetch';
import NoResult from '@/pages/_components/no-result'; import NoResult from '@/pages/_components/no-result';
import PageBox from '@/pages/_components/page-box'; import PageBox from '@/pages/_components/page-box';
import { queryClusterList } from '@/pages/cluster-management/apis'; import { queryClusterList } from '@/pages/cluster-management/apis';
import { useIntl } from '@umijs/max'; import { useIntl, useSearchParams } from '@umijs/max';
import { ConfigProvider, Table } from 'antd'; import { ConfigProvider, Table } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
@@ -13,7 +13,10 @@ 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: React.FC = () => { const GPUList: React.FC<{ clusterId?: number; widths: { input: number } }> = ({
clusterId,
widths
}) => {
const { const {
dataSource, dataSource,
queryParams, queryParams,
@@ -27,9 +30,13 @@ const GPUList: React.FC = () => {
} = useTableFetch<GPUDeviceItem>({ } = useTableFetch<GPUDeviceItem>({
fetchAPI: queryGpuDevicesList, fetchAPI: queryGpuDevicesList,
polling: true, polling: true,
API: GPU_DEVICES_API API: GPU_DEVICES_API,
defaultQueryParams: {
cluster_id: clusterId
}
}); });
const [searchParams] = useSearchParams();
const page = searchParams.get('page');
const intl = useIntl(); const intl = useIntl();
const [clusterList, setClusterList] = useState<Global.BaseOption<number>[]>( const [clusterList, setClusterList] = useState<Global.BaseOption<number>[]>(
[] []
@@ -97,8 +104,8 @@ const GPUList: React.FC = () => {
handleInputChange={handleNameChange} handleInputChange={handleNameChange}
handleSelectChange={handleClusterChange} handleSelectChange={handleClusterChange}
selectOptions={clusterList} selectOptions={clusterList}
showSelect showSelect={page !== 'clusters'}
widths={{ input: 200 }} widths={{ input: widths?.input || 200 }}
></FilterBar> ></FilterBar>
<ConfigProvider renderEmpty={renderEmpty}> <ConfigProvider renderEmpty={renderEmpty}>
<Table <Table
@@ -274,10 +274,7 @@ const useWorkerColumns = ({
return useMemo<ColumnsType<ListItem>>( return useMemo<ColumnsType<ListItem>>(
() => [ () => [
{ {
title: title: intl.formatMessage({ id: 'common.table.name' }),
sourceType === 'resources'
? intl.formatMessage({ id: 'common.table.name' })
: intl.formatMessage({ id: 'resources.table.workername' }),
dataIndex: 'name', dataIndex: 'name',
width: 100, width: 100,
sorter: tableSorter(1), sorter: tableSorter(1),