feat: add intances list

This commit is contained in:
jialin
2026-05-11 19:49:21 +08:00
committed by jialin
parent 75d7eff63b
commit 3ae49dc826
18 changed files with 692 additions and 6 deletions
+1 -1
View File
@@ -13,4 +13,4 @@
.swc
.DS_Store
.idea
.claude
.claude
+3
View File
@@ -0,0 +1,3 @@
### Create form table list
### Create a form
+41
View File
@@ -179,6 +179,47 @@ const baseRoutes = [
}
]
},
{
name: 'gpuService',
path: '/gpu-service',
key: 'gpuService',
routes: [
{
path: '/gpu-service',
redirect: '/gpu-service/list'
},
{
name: 'instances',
path: '/gpu-service/list',
key: 'gpuServiceList',
icon: 'icon-instances-outlined',
selectedIcon: 'icon-instances-filled',
defaultIcon: 'icon-instances-outlined',
access: 'canSeeAdmin',
component: './gpu-service/instances'
},
{
name: 'templates',
path: '/gpu-service/templates',
key: 'gpuServiceTemplates',
icon: 'icon-instance-template-outlined',
selectedIcon: 'icon-instance-template-filled',
defaultIcon: 'icon-instance-template-outlined',
access: 'canSeeAdmin',
component: './gpu-service/templates'
},
{
name: 'storage',
path: '/gpu-service/storage',
key: 'gpuServiceStorage',
icon: 'icon-storage-outlined',
selectedIcon: 'icon-storage-filled',
defaultIcon: 'icon-storage-outlined',
access: 'canSeeAdmin',
component: './gpu-service/storage'
}
]
},
{
name: 'resources',
path: '/resources',
+5 -1
View File
@@ -40,5 +40,9 @@ export default {
'menu.clusterManagement.credentials': 'Cloud Credentials',
'menu.clusterManagement.clusterDetail': 'Cluster Detail',
'menu.clusterManagement.clusterCreate': 'Create Cluster',
'menu.resources.backendsList': 'Inference Backends'
'menu.resources.backendsList': 'Inference Backends',
'menu.gpuService': 'GPU Service',
'menu.gpuService.instances': 'GPU Instances',
'menu.gpuService.templates': 'Instance Templates',
'menu.gpuService.storage': 'Storage Management'
};
+5 -1
View File
@@ -40,7 +40,11 @@ export default {
'menu.models.userModels': 'My Models',
'menu.clusterManagement.clusterDetail': 'Cluster Detail',
'menu.clusterManagement.clusterCreate': 'Create Cluster',
'menu.resources.backendsList': 'Inference Backends'
'menu.resources.backendsList': 'Inference Backends',
'menu.gpuService': 'GPU Service',
'menu.gpuService.instances': 'GPU Instances',
'menu.gpuService.templates': 'Instance Templates',
'menu.gpuService.storage': 'Storage Management'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
+5 -1
View File
@@ -40,7 +40,11 @@ export default {
'menu.clusterManagement.clusterDetail': 'Детали кластера',
'menu.clusterManagement.clusterCreate': 'Создать кластер',
'menu.resources.backendsList': 'Бэкенды запуска',
'menu.settings': 'Settings'
'menu.settings': 'Settings',
'menu.gpuService': 'GPU Service',
'menu.gpuService.instances': 'GPU Instances',
'menu.gpuService.templates': 'Instance Templates',
'menu.gpuService.storage': 'Storage Management'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
+5 -1
View File
@@ -40,7 +40,11 @@ export default {
'menu.clusterManagement.clusterCreate': 'Küme Oluştur',
'menu.resources.backendsList': 'Çıkarım Altyapıları',
'menu.models.instances': 'Instances',
'menu.settings': 'Settings'
'menu.settings': 'Settings',
'menu.gpuService': 'GPU Service',
'menu.gpuService.instances': 'GPU Instances',
'menu.gpuService.templates': 'Instance Templates',
'menu.gpuService.storage': 'Storage Management'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
+5 -1
View File
@@ -40,5 +40,9 @@ export default {
'menu.clusterManagement.credentials': '云凭证',
'menu.clusterManagement.clusterDetail': '集群详情',
'menu.clusterManagement.clusterCreate': '创建集群',
'menu.resources.backendsList': '推理后端'
'menu.resources.backendsList': '推理后端',
'menu.gpuService': 'GPU 服务',
'menu.gpuService.instances': 'GPU 实例',
'menu.gpuService.templates': '实例模板',
'menu.gpuService.storage': '存储管理'
};
@@ -0,0 +1,38 @@
import { request } from '@umijs/max';
import { FormData, ListItem } from '../config/types';
export const GPU_SERVICE_INSTANCES_API = '/gpu-service-instances';
export async function queryGPUServiceInstances(
params: Global.SearchParams,
options?: any
) {
return request<Global.PageResponse<ListItem>>(GPU_SERVICE_INSTANCES_API, {
method: 'GET',
params,
cancelToken: options?.token
});
}
export async function createGPUServiceInstance(params: { data: FormData }) {
return request<ListItem>(GPU_SERVICE_INSTANCES_API, {
method: 'POST',
data: params.data
});
}
export async function updateGPUServiceInstance(params: {
id: number;
data: FormData;
}) {
return request<ListItem>(`${GPU_SERVICE_INSTANCES_API}/${params.id}`, {
method: 'PUT',
data: params.data
});
}
export async function deleteGPUServiceInstance(id: number) {
return request(`${GPU_SERVICE_INSTANCES_API}/${id}`, {
method: 'DELETE'
});
}
@@ -0,0 +1,72 @@
import { PageActionType } from '@/config/types';
import { ModalFooter } from '@gpustack/core-ui';
import { useRef } from 'react';
import FormDrawer from '../../../_components/form-drawer';
import { FormData, ListItem } from '../config/types';
import GPUServiceInstanceForm from '../forms';
type AddModalProps = {
title: string;
action: PageActionType;
open: boolean;
onOk: (values: FormData) => void;
data?: ListItem | null;
onCancel: () => void;
};
const AddModal: React.FC<AddModalProps> = ({
title,
action,
open,
onOk,
data,
onCancel
}) => {
const form = useRef<any>(null);
const handleSubmit = () => {
form.current?.submit();
};
const handleCancel = () => {
form.current?.resetFields();
onCancel();
};
const onFinish = async (values: FormData) => {
onOk({
...values
});
};
return (
<FormDrawer
title={title}
open={open}
onCancel={handleCancel}
onSubmit={handleSubmit}
width={600}
footer={
<ModalFooter
onOk={handleSubmit}
onCancel={handleCancel}
style={{
padding: '16px 24px 8px',
display: 'flex',
justifyContent: 'flex-end'
}}
/>
}
>
<GPUServiceInstanceForm
ref={form}
action={action}
currentData={data}
onFinish={onFinish}
open={open}
/>
</FormDrawer>
);
};
export default AddModal;
@@ -0,0 +1,31 @@
import { icons } from '@gpustack/core-ui';
export const InstanceStatusValueMap = {
Running: 'running',
Pending: 'pending',
Stopped: 'stopped',
Failed: 'failed'
};
export const InstanceStatusLabelMap: Record<string, string> = {
[InstanceStatusValueMap.Running]: '运行中',
[InstanceStatusValueMap.Pending]: '等待中',
[InstanceStatusValueMap.Stopped]: '已停止',
[InstanceStatusValueMap.Failed]: '失败'
};
export const rowActionList = [
{
label: '编辑',
key: 'edit',
icon: icons.EditOutlined
},
{
label: '删除',
key: 'delete',
icon: icons.DeleteOutlined,
props: {
danger: true
}
}
];
@@ -0,0 +1,15 @@
export interface FormData {
name: string;
description?: string;
image?: string;
gpu_count?: number;
replicas?: number;
}
export interface ListItem extends FormData {
id: number;
status?: string;
endpoint?: string;
created_at?: string;
updated_at?: string;
}
@@ -0,0 +1,68 @@
import {
Input as CInput,
InputNumber as CInputNumber
} from '@gpustack/core-ui';
import { Form } from 'antd';
import { FormData } from '../config/types';
const Basic = () => {
return (
<>
<Form.Item<FormData>
name="name"
rules={[
{
required: true,
message: '请输入实例名称'
}
]}
>
<CInput.Input label="实例名称" required />
</Form.Item>
<Form.Item<FormData>
name="image"
rules={[
{
required: true,
message: '请输入镜像'
}
]}
>
<CInput.Input label="镜像" required />
</Form.Item>
<div style={{ display: 'flex', gap: 16 }}>
<div style={{ flex: 1 }}>
<Form.Item<FormData>
name="gpu_count"
rules={[
{
required: true,
message: '请输入 GPU 数量'
}
]}
>
<CInputNumber min={0} precision={0} label="GPU 数量" required />
</Form.Item>
</div>
<div style={{ flex: 1 }}>
<Form.Item<FormData>
name="replicas"
rules={[
{
required: true,
message: '请输入副本数'
}
]}
>
<CInputNumber min={1} precision={0} label="副本数" required />
</Form.Item>
</div>
</div>
<Form.Item<FormData> name="description">
<CInput.TextArea label="描述" scaleSize />
</Form.Item>
</>
);
};
export default Basic;
@@ -0,0 +1,66 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { Form } from 'antd';
import { forwardRef, useEffect, useImperativeHandle } from 'react';
import { FormData, ListItem } from '../config/types';
import Basic from './basic';
interface InstanceFormProps {
ref?: any;
open: boolean;
action: PageActionType;
currentData?: ListItem | null;
onFinish: (values: FormData) => Promise<void>;
}
const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
(props, ref) => {
const { action, currentData, open, onFinish } = props;
const [form] = Form.useForm<FormData>();
useEffect(() => {
if (!open) {
form.resetFields();
return;
}
if (action === PageAction.EDIT && currentData) {
form.setFieldsValue({
name: currentData.name,
description: currentData.description,
image: currentData.image,
gpu_count: currentData.gpu_count,
replicas: currentData.replicas
});
return;
}
form.setFieldsValue({
gpu_count: 1,
replicas: 1
});
}, [action, currentData, form, open]);
useImperativeHandle(ref, () => ({
submit: () => {
form.submit();
},
resetFields: () => {
form.resetFields();
}
}));
return (
<Form
name="gpuServiceInstanceForm"
form={form}
onFinish={onFinish}
preserve={false}
>
<Basic />
</Form>
);
}
);
export default GPUServiceInstanceForm;
@@ -0,0 +1,132 @@
import { tableSorter } from '@/config/settings';
import { AutoTooltip, DropdownButtons } from '@gpustack/core-ui';
import { Tag } from 'antd';
import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import { useMemo } from 'react';
import {
InstanceStatusLabelMap,
InstanceStatusValueMap,
rowActionList
} from '../config';
import { ListItem } from '../config/types';
interface ColumnsHookProps {
handleSelect: (val: string, record: ListItem) => void;
sortOrder: string[];
}
const statusColorMap: Record<string, string> = {
[InstanceStatusValueMap.Running]: 'success',
[InstanceStatusValueMap.Pending]: 'processing',
[InstanceStatusValueMap.Stopped]: 'default',
[InstanceStatusValueMap.Failed]: 'error'
};
const useInstancesColumns = ({
handleSelect,
sortOrder
}: ColumnsHookProps): ColumnsType<ListItem> => {
return useMemo(() => {
return [
{
title: '实例名称',
dataIndex: 'name',
key: 'name',
sorter: tableSorter(1),
ellipsis: {
showTitle: false
},
render: (text: string) => (
<AutoTooltip ghost style={{ maxWidth: 360 }}>
<span className="text-primary">{text}</span>
</AutoTooltip>
)
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
sorter: tableSorter(2),
render: (status: string) => {
const value = status || InstanceStatusValueMap.Pending;
return (
<Tag
color={statusColorMap[value] || 'default'}
style={{ marginRight: 0 }}
>
{InstanceStatusLabelMap[value] || value}
</Tag>
);
}
},
{
title: '镜像',
dataIndex: 'image',
key: 'image',
ellipsis: {
showTitle: false
},
render: (text: string) => (
<AutoTooltip ghost minWidth={20}>
{text || '-'}
</AutoTooltip>
)
},
{
title: 'GPU 数量',
dataIndex: 'gpu_count',
key: 'gpu_count',
sorter: tableSorter(3),
render: (value: number) => value ?? '-'
},
{
title: '副本数',
dataIndex: 'replicas',
key: 'replicas',
sorter: tableSorter(4),
render: (value: number) => value ?? '-'
},
{
title: '访问端点',
dataIndex: 'endpoint',
key: 'endpoint',
ellipsis: {
showTitle: false
},
render: (text: string) => (
<AutoTooltip ghost minWidth={20}>
{text || '-'}
</AutoTooltip>
)
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
sorter: tableSorter(5),
ellipsis: {
showTitle: false
},
render: (text: string) => (
<AutoTooltip ghost>
{text ? dayjs(text).format('YYYY-MM-DD HH:mm:ss') : '-'}
</AutoTooltip>
)
},
{
title: '操作',
key: 'operation',
dataIndex: 'operation',
render: (_text, record) => (
<DropdownButtons
items={rowActionList}
onSelect={(val) => handleSelect(val, record)}
/>
)
}
];
}, [handleSelect, sortOrder]);
};
export default useInstancesColumns;
+186
View File
@@ -0,0 +1,186 @@
import { PageAction } from '@/config';
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
import type { PageActionType } from '@/config/types';
import useTableFetch from '@/hooks/use-table-fetch';
import { DeleteModal, FilterBar, IconFont, NoResult } from '@gpustack/core-ui';
import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, message, Table } from 'antd';
import _ from 'lodash';
import { useState } from 'react';
import PageBox from '../../_components/page-box';
import {
createGPUServiceInstance,
deleteGPUServiceInstance,
GPU_SERVICE_INSTANCES_API,
queryGPUServiceInstances,
updateGPUServiceInstance
} from './apis';
import AddModal from './components/add-modal';
import { FormData, ListItem } from './config/types';
import useInstancesColumns from './hooks/use-instances-columns';
const GPUService: React.FC = () => {
const {
dataSource,
rowSelection,
queryParams,
sortOrder,
modalRef,
handleDelete,
handleDeleteBatch,
fetchData,
handlePageChange,
handleTableChange,
handleSearch,
handleNameChange
} = useTableFetch<ListItem>({
key: PaginationKey.Instances,
fetchAPI: queryGPUServiceInstances,
deleteAPI: deleteGPUServiceInstance,
watch: false,
API: GPU_SERVICE_INSTANCES_API,
contentForDelete: 'GPU 实例'
});
const [openAddModalStatus, setOpenAddModalStatus] = useState<{
action: PageActionType;
open: boolean;
title: string;
currentData?: ListItem | null;
}>({
action: PageAction.CREATE,
title: '',
open: false,
currentData: null
});
const handleAddInstance = () => {
setOpenAddModalStatus({
action: PageAction.CREATE,
title: '创建 GPU 实例',
open: true,
currentData: null
});
};
const handleEditInstance = (row: ListItem) => {
setOpenAddModalStatus({
action: PageAction.EDIT,
title: '编辑 GPU 实例',
open: true,
currentData: row
});
};
const closeModal = () => {
setOpenAddModalStatus({
action: PageAction.CREATE,
title: '',
open: false,
currentData: null
});
};
const handleModalOk = async (data: FormData) => {
try {
if (openAddModalStatus.action === PageAction.EDIT) {
await updateGPUServiceInstance({
id: openAddModalStatus.currentData!.id,
data
});
} else {
await createGPUServiceInstance({ data });
}
fetchData();
closeModal();
message.success('操作成功');
} catch (error) {
message.error('操作失败');
}
};
const handleSelect = useMemoizedFn((val: string, row: ListItem) => {
if (val === 'edit') {
handleEditInstance(row);
} else if (val === 'delete') {
handleDelete({ ...row, name: row.name });
}
});
const renderEmpty = (type?: string) => {
if (type !== 'Table') return;
return (
<NoResult
loading={dataSource.loading}
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-layers" />}
filters={_.omit(queryParams, ['sort_by'])}
noFoundText="未找到匹配的 GPU 实例"
title="暂无 GPU 实例"
subTitle="创建一个 GPU 实例后会显示在这里"
onClick={handleAddInstance}
buttonText="创建"
/>
);
};
const columns = useInstancesColumns({
handleSelect,
sortOrder
});
return (
<>
<PageBox>
<FilterBar
marginBottom={22}
marginTop={30}
buttonText="创建"
handleSearch={handleSearch}
handleDeleteByBatch={handleDeleteBatch}
handleClickPrimary={handleAddInstance}
handleInputChange={handleNameChange}
rowSelection={rowSelection}
widths={{ input: 300 }}
/>
<ConfigProvider renderEmpty={renderEmpty}>
<Table
columns={columns}
dataSource={dataSource.dataList}
rowSelection={rowSelection}
loading={{
spinning: dataSource.loading,
size: 'middle'
}}
sortDirections={TABLE_SORT_DIRECTIONS}
showSorterTooltip={false}
rowKey="id"
onChange={handleTableChange}
pagination={{
size: 'middle',
showSizeChanger: true,
pageSize: queryParams.perPage,
current: queryParams.page,
total: dataSource.total,
hideOnSinglePage: queryParams.perPage === 10,
onChange: handlePageChange
}}
/>
</ConfigProvider>
</PageBox>
<AddModal
open={openAddModalStatus.open}
action={openAddModalStatus.action}
title={openAddModalStatus.title}
data={openAddModalStatus.currentData}
onCancel={closeModal}
onOk={handleModalOk}
/>
<DeleteModal ref={modalRef} />
</>
);
};
export default GPUService;
+7
View File
@@ -0,0 +1,7 @@
import PageBox from '../../_components/page-box';
const GPUServiceStorage: React.FC = () => {
return <PageBox> GPU Service Storage Page</PageBox>;
};
export default GPUServiceStorage;
@@ -0,0 +1,7 @@
import PageBox from '../../_components/page-box';
const GPUServiceTemplates: React.FC = () => {
return <PageBox> GPU Service Templates Page</PageBox>;
};
export default GPUServiceTemplates;