feat: templates, storage

This commit is contained in:
jialin
2026-05-11 19:49:21 +08:00
committed by jialin
parent 3ae49dc826
commit 7ea7f6666d
52 changed files with 3604 additions and 188 deletions
@@ -0,0 +1,64 @@
import { request } from '@umijs/max';
import { mockStorageData } from '../config/mock-data';
import { FormData, ListItem } from '../config/types';
export const GPU_SERVICE_STORAGE_API = '/gpu-service-storage';
export async function queryGPUServiceStorage(
params: Global.SearchParams,
options?: any
) {
// return request<Global.PageResponse<ListItem>>(GPU_SERVICE_STORAGE_API, {
// method: 'GET',
// params,
// cancelToken: options?.token
// });
const page = params.page || 1;
const perPage = params.perPage || 10;
const search = params.search?.toLowerCase();
const clusterId = params.cluster_id;
const filteredData = mockStorageData.filter((item) => {
const matchSearch = search
? item.name.toLowerCase().includes(search)
: true;
const matchCluster = clusterId ? item.cluster_id === clusterId : true;
return matchSearch && matchCluster;
});
const start = (page - 1) * perPage;
const items = filteredData.slice(start, start + perPage);
return {
items,
pagination: {
total: filteredData.length,
totalPage: Math.ceil(filteredData.length / perPage),
page,
perPage
}
} as Global.PageResponse<ListItem>;
}
export async function createGPUServiceStorage(params: { data: FormData }) {
// return request<ListItem>(GPU_SERVICE_STORAGE_API, {
// method: 'POST',
// data: params.data
// });
return true;
}
export async function updateGPUServiceStorage(params: {
id: number;
data: FormData;
}) {
// return request<ListItem>(`${GPU_SERVICE_STORAGE_API}/${params.id}`, {
// method: 'PUT',
// data: params.data
// });
return true;
}
export async function deleteGPUServiceStorage(id: number) {
return request(`${GPU_SERVICE_STORAGE_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 GPUServiceStorageForm 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'
}}
/>
}
>
<GPUServiceStorageForm
ref={form}
action={action}
currentData={data}
onFinish={onFinish}
open={open}
/>
</FormDrawer>
);
};
export default AddModal;
@@ -0,0 +1,73 @@
import { StatusMaps } from '@/config';
import { StatusType } from '@/config/types';
import { icons } from '@gpustack/core-ui';
export const StorageTypeValueMap = {
Local: 'local',
Shared: 'shared',
Object: 'object'
};
export const StorageTypeLabelMap: Record<string, string> = {
[StorageTypeValueMap.Local]: '默认存储',
[StorageTypeValueMap.Shared]: '默认存储',
[StorageTypeValueMap.Object]: '对象存储'
};
export const StorageAccessModeValueMap = {
ReadWriteOnce: 'ReadWriteOnce',
ReadOnlyMany: 'ReadOnlyMany',
ReadWriteMany: 'ReadWriteMany'
};
export const StorageAccessModeOptions = [
{
label: 'ReadWriteOnce',
value: StorageAccessModeValueMap.ReadWriteOnce
},
{
label: 'ReadWriteMany',
value: StorageAccessModeValueMap.ReadWriteMany
},
{
label: 'ReadOnlyMany',
value: StorageAccessModeValueMap.ReadOnlyMany
}
];
export const StorageStatusValueMap = {
Available: 'Available',
Bound: 'Bound',
Released: 'Released',
Failed: 'Failed'
};
export const StorageStatusLabelMap: Record<string, string> = {
[StorageStatusValueMap.Available]: 'Available',
[StorageStatusValueMap.Bound]: 'Bound',
[StorageStatusValueMap.Released]: 'Released',
[StorageStatusValueMap.Failed]: 'Failed'
};
export const status: Record<string, StatusType> = {
[StorageStatusValueMap.Available]: StatusMaps.success,
[StorageStatusValueMap.Bound]: StatusMaps.transitioning,
[StorageStatusValueMap.Released]: StatusMaps.inactive,
[StorageStatusValueMap.Failed]: StatusMaps.error
};
export const rowActionList = [
{
label: '编辑',
key: 'edit',
icon: icons.EditOutlined
},
{
label: '删除',
key: 'delete',
icon: icons.DeleteOutlined,
props: {
danger: true
}
}
];
@@ -0,0 +1,69 @@
import { StorageStatusValueMap, StorageTypeValueMap } from '.';
import { ListItem } from './types';
export const mockStorageData: ListItem[] = [
{
id: 1,
name: 'local-nvme-cache',
type: StorageTypeValueMap.Local,
capacity_gb: 500,
mount_path: '/mnt/nvme',
access_modes: ['ReadWriteOnce'],
parameters: {
path: '/mnt/nvme'
},
status: StorageStatusValueMap.Available,
cluster_id: 1,
description: 'Local NVMe cache for hot model files',
created_at: '2026-04-01T10:00:00Z',
updated_at: '2026-04-10T10:00:00Z'
},
{
id: 2,
name: 'shared-model-store',
type: StorageTypeValueMap.Shared,
capacity_gb: 2048,
mount_path: '/models',
access_modes: ['ReadWriteMany'],
parameters: {
storageClassName: 'nfs-client'
},
status: StorageStatusValueMap.Bound,
cluster_id: 1,
description: 'Shared storage for model weights',
created_at: '2026-04-02T10:00:00Z',
updated_at: '2026-04-11T10:00:00Z'
},
{
id: 3,
name: 'object-dataset-bucket',
type: StorageTypeValueMap.Object,
capacity_gb: 10240,
mount_path: '/datasets',
access_modes: ['ReadOnlyMany'],
parameters: {
bucket: 'datasets'
},
status: StorageStatusValueMap.Released,
cluster_id: 2,
description: 'Object storage mounted for datasets',
created_at: '2026-04-03T10:00:00Z',
updated_at: '2026-04-12T10:00:00Z'
},
{
id: 4,
name: 'failed-shared-cache',
type: StorageTypeValueMap.Shared,
capacity_gb: 1024,
mount_path: '/failed-cache',
access_modes: ['ReadWriteMany'],
parameters: {
storageClassName: 'nfs-client'
},
status: StorageStatusValueMap.Failed,
cluster_id: 2,
description: 'Shared cache waiting for storage backend recovery',
created_at: '2026-04-04T10:00:00Z',
updated_at: '2026-04-13T10:00:00Z'
}
];
@@ -0,0 +1,17 @@
export interface FormData {
name: string;
description?: string;
type?: string;
capacity_gb?: number;
mount_path?: string;
access_modes?: string[];
parameters?: Record<string, any>;
status?: string;
cluster_id?: number;
}
export interface ListItem extends FormData {
id: number;
created_at?: string;
updated_at?: string;
}
@@ -0,0 +1,81 @@
import {
Input as CInput,
InputNumber as CInputNumber,
Select as SealSelect
} from '@gpustack/core-ui';
import { Form } from 'antd';
import { StorageAccessModeOptions } from '../config';
import { FormData } from '../config/types';
const Basic = () => {
const form = Form.useFormInstance<FormData>();
const parameters = Form.useWatch('parameters', form);
const handleParametersChange = (value: Record<string, any>) => {
form.setFieldValue('parameters', value);
};
return (
<>
<Form.Item<FormData>
name="name"
rules={[
{
required: true,
message: '请输入存储名称'
}
]}
>
<CInput.Input label="名称" required />
</Form.Item>
<div style={{ display: 'flex', gap: 16 }}>
<div style={{ flex: 1 }}>
<Form.Item<FormData>
name="type"
rules={[
{
required: true,
message: '请选择存储类型'
}
]}
>
<SealSelect label="存储类型" required options={[]}></SealSelect>
</Form.Item>
</div>
<div style={{ flex: 1 }}>
<Form.Item<FormData>
name="capacity_gb"
rules={[
{
required: true,
message: '请输入容量'
}
]}
>
<CInputNumber min={1} precision={0} label="容量 (GB)" required />
</Form.Item>
</div>
</div>
<Form.Item<FormData> name="access_modes">
<SealSelect
label="存储卷访问方式"
allowClear
options={StorageAccessModeOptions}
></SealSelect>
</Form.Item>
{/* <Form.Item<FormData> name="parameters">
<LabelSelector
label="存储卷配置参数"
labels={parameters || {}}
btnText="添加参数"
onChange={handleParametersChange}
/>
</Form.Item> */}
<Form.Item<FormData> name="description">
<CInput.TextArea label="描述" scaleSize />
</Form.Item>
</>
);
};
export default Basic;
@@ -0,0 +1,71 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { Form } from 'antd';
import { forwardRef, useEffect, useImperativeHandle } from 'react';
import { StorageStatusValueMap } from '../config';
import { FormData, ListItem } from '../config/types';
import Basic from './basic';
interface StorageFormProps {
ref?: any;
open: boolean;
action: PageActionType;
currentData?: ListItem | null;
onFinish: (values: FormData) => Promise<void>;
}
const GPUServiceStorageForm: React.FC<StorageFormProps> = 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,
type: currentData.type,
capacity_gb: currentData.capacity_gb,
mount_path: currentData.mount_path,
access_modes: currentData.access_modes,
parameters: currentData.parameters,
status: currentData.status
});
return;
}
form.setFieldsValue({
type: '默认存储',
parameters: {},
status: StorageStatusValueMap.Available
});
}, [action, currentData, form, open]);
useImperativeHandle(ref, () => ({
submit: () => {
form.submit();
},
resetFields: () => {
form.resetFields();
}
}));
return (
<Form
name="gpuServiceStorageForm"
form={form}
onFinish={onFinish}
preserve={false}
>
<Basic />
</Form>
);
}
);
export default GPUServiceStorageForm;
@@ -0,0 +1,113 @@
import { tableSorter } from '@/config/settings';
import { AutoTooltip, DropdownButtons, StatusTag } from '@gpustack/core-ui';
import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import { useMemo } from 'react';
import {
rowActionList,
status,
StorageStatusLabelMap,
StorageStatusValueMap,
StorageTypeLabelMap
} from '../config';
import { ListItem } from '../config/types';
interface ColumnsHookProps {
handleSelect: (val: string, record: ListItem) => void;
sortOrder: string[];
}
const useStorageColumns = ({
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: 'type',
key: 'type',
sorter: tableSorter(2),
render: (value: string) => StorageTypeLabelMap[value] || value || '-'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
sorter: tableSorter(3),
render: (statusValue: string) => {
const value = statusValue || StorageStatusValueMap.Available;
return (
<StatusTag
statusValue={{
status: status[value],
text: StorageStatusLabelMap[value] || value
}}
/>
);
}
},
{
title: '容量 (GB)',
dataIndex: 'capacity_gb',
key: 'capacity_gb',
sorter: tableSorter(4),
render: (value: number) => value ?? '-'
},
// {
// title: '挂载路径',
// dataIndex: 'mount_path',
// key: 'mount_path',
// 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 useStorageColumns;
+241 -2
View File
@@ -1,7 +1,246 @@
import PageBox from '../../_components/page-box';
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 { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
import {
BaseSelect,
DeleteModal,
FilterBar,
IconFont,
NoResult
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, Divider, Flex, message, Table } from 'antd';
import _ from 'lodash';
import { useEffect, useState } from 'react';
import { PageContainerInner } from '../../_components/page-box';
import {
createGPUServiceStorage,
deleteGPUServiceStorage,
GPU_SERVICE_STORAGE_API,
queryGPUServiceStorage,
updateGPUServiceStorage
} from './apis';
import AddModal from './components/add-modal';
import { FormData, ListItem } from './config/types';
import useStorageColumns from './hooks/use-storage-columns';
const GPUServiceStorage: React.FC = () => {
return <PageBox> GPU Service Storage Page</PageBox>;
const {
dataSource,
rowSelection,
queryParams,
sortOrder,
modalRef,
handleDelete,
handleDeleteBatch,
fetchData,
handlePageChange,
handleTableChange,
handleQueryChange,
handleSearch,
handleNameChange
} = useTableFetch<ListItem>({
key: PaginationKey.Storage,
fetchAPI: queryGPUServiceStorage,
deleteAPI: deleteGPUServiceStorage,
watch: false,
API: GPU_SERVICE_STORAGE_API,
contentForDelete: '存储'
});
const {
fetchClusterList,
cancelRequest: cancelClusterRequest,
clusterList
} = useQueryClusterList();
const intl = useIntl();
const [openAddModalStatus, setOpenAddModalStatus] = useState<{
action: PageActionType;
open: boolean;
title: string;
currentData?: ListItem | null;
}>({
action: PageAction.CREATE,
title: '',
open: false,
currentData: null
});
useEffect(() => {
fetchClusterList({ page: -1 }).then((clusters) => {
if (clusters.length > 0) {
handleQueryChange({
cluster_id: clusters[0].id,
page: 1
});
}
});
return () => {
cancelClusterRequest();
};
}, []);
const handleAddStorage = () => {
setOpenAddModalStatus({
action: PageAction.CREATE,
title: '添加存储',
open: true,
currentData: null
});
};
const handleEditStorage = (row: ListItem) => {
setOpenAddModalStatus({
action: PageAction.EDIT,
title: '编辑存储',
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 updateGPUServiceStorage({
id: openAddModalStatus.currentData!.id,
data
});
} else {
await createGPUServiceStorage({ data });
}
fetchData();
closeModal();
message.success('操作成功');
} catch (error) {
message.error('操作失败');
}
};
const handleSelect = useMemoizedFn((val: string, row: ListItem) => {
if (val === 'edit') {
handleEditStorage(row);
} else if (val === 'delete') {
handleDelete({ ...row, name: row.name });
}
});
const handleClusterChange = (value: number) => {
handleQueryChange({
cluster_id: value,
page: 1
});
};
const renderEmpty = (type?: string) => {
if (type !== 'Table') return;
return (
<NoResult
loading={dataSource.loading}
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-storage-outlined" />}
filters={_.omit(queryParams, ['sort_by'])}
noFoundText="未找到匹配的存储"
title="暂无存储"
subTitle="创建一个存储后会显示在这里"
onClick={handleAddStorage}
buttonText="立即添加"
/>
);
};
const columns = useStorageColumns({
handleSelect,
sortOrder
});
return (
<>
<PageContainerInner
leftContent={
<Flex align="center">
<span>{intl.formatMessage({ id: 'menu.gpuService.storage' })}</span>
<Divider
orientation="vertical"
style={{
marginLeft: 16
}}
/>
<BaseSelect
variant="borderless"
value={queryParams.cluster_id}
options={clusterList}
style={{ minWidth: 120, fontWeight: 500 }}
onChange={handleClusterChange}
></BaseSelect>
</Flex>
}
>
<FilterBar
marginBottom={22}
marginTop={30}
showSelect={false}
selectOptions={clusterList}
select={{ showSearch: true }}
selectHolder="按集群过滤"
buttonText="添加存储"
handleSearch={handleSearch}
handleSelectChange={handleClusterChange}
handleDeleteByBatch={handleDeleteBatch}
handleClickPrimary={handleAddStorage}
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>
</PageContainerInner>
<AddModal
open={openAddModalStatus.open}
action={openAddModalStatus.action}
title={openAddModalStatus.title}
data={openAddModalStatus.currentData}
onCancel={closeModal}
onOk={handleModalOk}
/>
<DeleteModal ref={modalRef} />
</>
);
};
export default GPUServiceStorage;