fix: display templates by manufacturer

This commit is contained in:
jialin
2026-05-11 19:49:21 +08:00
committed by jialin
parent 4128de61f9
commit 9e0dd5b003
27 changed files with 614 additions and 194 deletions
@@ -2,6 +2,7 @@ import { PageActionType } from '@/config/types';
import Separator from '@/pages/llmodels/components/separator';
import { SearchOutlined } from '@ant-design/icons';
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Empty, Input, Typography } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
@@ -58,8 +59,10 @@ const AddModal: React.FC<AddModalProps> = ({
data,
onCancel
}) => {
const intl = useIntl();
const form = useRef<any>(null);
const [selectedInstanceType, setSelectedInstanceType] = useState<string>();
const [selectedManufacturer, setSelectedManufacturer] = useState<string>();
const [templateId, setTemplateId] = useState<number>();
const [instanceKeyword, setInstanceKeyword] = useState('');
const [templateKeyword, setTemplateKeyword] = useState('');
@@ -95,17 +98,26 @@ const AddModal: React.FC<AddModalProps> = ({
});
}, [instanceKeyword, instanceTypeList]);
const manufacturerMatchedTemplates = useMemo(() => {
if (!selectedManufacturer) {
return templateList;
}
return templateList.filter(
(item) => item.manufacturer === selectedManufacturer
);
}, [selectedManufacturer, templateList]);
const filteredTemplates = useMemo(() => {
const keyword = templateKeyword.trim().toLowerCase();
if (!keyword) {
return templateList;
return manufacturerMatchedTemplates;
}
return templateList.filter((item) =>
return manufacturerMatchedTemplates.filter((item) =>
[item.name, item.spec?.image, item.spec?.volumeMount].some((text) =>
(text || '').toLowerCase().includes(keyword)
)
);
}, [templateKeyword, templateList]);
}, [templateKeyword, manufacturerMatchedTemplates]);
const handleSubmit = () => {
form.current?.submit();
@@ -124,13 +136,47 @@ const AddModal: React.FC<AddModalProps> = ({
const handleInstanceTypeChange = (item: InstanceTypeItem) => {
const name = item.metadata?.name;
const manufacturer = item.spec?.manufacturer;
setSelectedInstanceType(name);
form.current?.setFieldsValue({
spec: {
type: name,
resources: { accelerator: '1' }
}
});
setSelectedManufacturer(manufacturer);
const currentSpec = form.current?.getFieldsValue()?.spec || {};
const updatedSpec = {
...currentSpec,
type: name,
resources: { ...(currentSpec.resources || {}), accelerator: '1' }
};
const currentTemplate = templateList.find((t) => t.id === templateId);
const isMatched =
!manufacturer ||
(!!currentTemplate && currentTemplate.manufacturer === manufacturer);
if (isMatched) {
form.current?.setFieldsValue({ spec: updatedSpec });
return;
}
const firstTemplate = templateList.find(
(t) => t.manufacturer === manufacturer
);
if (firstTemplate) {
setTemplateId(firstTemplate.id);
form.current?.setFieldsValue({
manufacturer: firstTemplate.manufacturer,
spec: {
...updatedSpec,
...firstTemplate.spec
}
});
} else {
setTemplateId(undefined);
form.current?.setFieldsValue({
manufacturer: undefined,
spec: updatedSpec
});
}
};
const handleTemplateChange = (id: number, item: TemplateItem) => {
@@ -176,7 +222,9 @@ const AddModal: React.FC<AddModalProps> = ({
<Input
allowClear
prefix={<SearchOutlined className="text-tertiary" />}
placeholder="搜索名称、显存、内存或 vCPU"
placeholder={intl.formatMessage({
id: 'gpuservice.instance.search.type.placeholder'
})}
value={instanceKeyword}
onChange={(e) => setInstanceKeyword(e.target.value)}
/>
@@ -208,7 +256,9 @@ const AddModal: React.FC<AddModalProps> = ({
<Input
allowClear
prefix={<SearchOutlined className="text-tertiary" />}
placeholder="搜索模板名称、镜像或挂载路径"
placeholder={intl.formatMessage({
id: 'gpuservice.instance.search.template.placeholder'
})}
value={templateKeyword}
onChange={(e) => setTemplateKeyword(e.target.value)}
/>
@@ -1,4 +1,5 @@
import { AutoTooltip, StatusTag, TemplateCard } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Flex, Tag } from 'antd';
import styled from 'styled-components';
import {
@@ -55,6 +56,7 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
onChange,
dataList = []
}) => {
const intl = useIntl();
const handleSelect = (item: InstanceTypeItem) => {
if (!isAvailable(item)) return;
onChange?.(item);
@@ -104,7 +106,8 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
}}
>
<span>
{item.status?.accelerator?.remaining ?? '-'}
{intl.formatMessage({ id: 'gpuservice.instance.stock' })}{' '}
{item.status?.accelerator?.remaining ?? '-'}
</span>
</Tag>
)}
@@ -114,12 +117,16 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
<span style={{ display: 'flex', height: 15 }}>
{item.spec?.acceleratable && (
<span className="meta-row">
{item.spec?.memory ?? '-'}
{intl.formatMessage({ id: 'gpuservice.instance.memory' })}{' '}
{item.spec?.memory ?? '-'}
</span>
)}
</span>
<span className="meta-row gap-16">
<span> {item.status?.ram?.capacity ?? '-'}</span>
<span>
{intl.formatMessage({ id: 'gpuservice.instance.ram' })}{' '}
{item.status?.ram?.capacity ?? '-'}
</span>
<span>vCPU {item.status?.cpu?.capacity ?? '-'}</span>
</span>
</TypeMeta>
@@ -46,13 +46,15 @@ export const status: Record<string, StatusType> = {
export const rowActionList = [
{
label: '编辑',
label: 'common.button.edit',
key: 'edit',
locale: true,
icon: icons.EditOutlined
},
{
label: '删除',
label: 'common.button.delete',
key: 'delete',
locale: true,
icon: icons.DeleteOutlined,
props: {
danger: true
+72 -80
View File
@@ -1,31 +1,5 @@
export type ImagePullPolicy = 'Always' | 'IfNotPresent' | 'Never';
export interface InstanceEnvVar {
name: string;
value?: string;
}
export interface InstancePort {
port: number;
protocol?: string;
}
export interface InstanceResources {
cpu?: string;
ram?: string;
localStorage?: string;
accelerator?: string;
}
export interface InstanceVolume {
ephemeral?: {
capacity?: string;
};
persistent?: {
name: string;
};
}
// instance form data
export interface FormData {
metadata: {
@@ -38,63 +12,74 @@ export interface FormData {
imagePullPolicy: ImagePullPolicy;
displayName: string;
command: string[];
ports: InstancePort[];
env: InstanceEnvVar[];
ports: {
port: number;
protocol?: string;
}[];
env: {
name: string;
value?: string;
}[];
volumeMount: string;
resources: InstanceResources;
resources: {
cpu?: string;
ram?: string;
localStorage?: string;
accelerator?: string;
};
description: string;
volume: InstanceVolume;
volume: {
ephemeral?: {
capacity?: string;
};
persistent?: {
name: string;
};
};
sshPublicKey: {
name: string;
};
};
}
export interface ManagedField {
manager: string;
operation: string;
apiVersion: string;
time: string;
fieldsType: string;
fieldsV1: Record<string, any>;
}
export type InstanceServicePortProtocol = 'TCP' | 'UDP';
export interface Metadata {
name: string;
namespace?: string;
uid: string;
resourceVersion: string;
creationTimestamp: string;
annotations?: Record<string, string>;
managedFields?: ManagedField[];
export interface InstanceStatus {
hostIPs?: {
ip: string;
}[];
phase?: string;
phaseMessage?: string;
podIPs?: {
ip: string;
}[];
ports?: {
port: number;
nodePort?: number;
protocol?: InstanceServicePortProtocol;
}[];
}
// instance list item
export interface ListItem extends Omit<FormData, 'metadata'> {
id: number;
status?: string;
metadata: Metadata;
}
export interface InstanceItem {
id: number;
name: string;
vram: number; // GiB
ram: number; // GiB
vCPU: number; // cores
gpu_count: number;
status: string; // available, unavailable
}
export interface InstanceTypeSpec {
acceleratable: boolean;
computeCapability?: string;
family?: string;
group: string;
manufacturer?: string;
memory?: string;
product?: string;
sliced?: number;
status?: InstanceStatus;
metadata: {
name: string;
namespace?: string;
uid: string;
resourceVersion: string;
creationTimestamp: string;
annotations?: Record<string, string>;
managedFields?: {
manager: string;
operation: string;
apiVersion: string;
time: string;
fieldsType: string;
fieldsV1: Record<string, any>;
}[];
};
}
type Quality = `${number}Ki` | `${number}Gi`;
@@ -105,15 +90,6 @@ export interface InstanceTypeResource {
remaining?: Quality;
}
export interface InstanceTypeStatus {
accelerator: InstanceTypeResource;
cpu: InstanceTypeResource;
localStorage: InstanceTypeResource;
phase: string;
phaseMessage?: string;
ram: InstanceTypeResource;
}
export interface InstanceTypeItem {
apiVersion: string;
kind: string;
@@ -122,6 +98,22 @@ export interface InstanceTypeItem {
namespace: string;
};
id: number;
spec: InstanceTypeSpec;
status: InstanceTypeStatus;
spec: {
acceleratable: boolean;
computeCapability?: string;
family?: string;
group: string;
manufacturer?: string;
memory?: string;
product?: string;
sliced?: number;
};
status: {
accelerator: InstanceTypeResource;
cpu: InstanceTypeResource;
localStorage: InstanceTypeResource;
phase: string;
phaseMessage?: string;
ram: InstanceTypeResource;
};
}
@@ -1,8 +1,10 @@
import { Input as CInput } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { FormData } from '../config/types';
const Basic = () => {
const intl = useIntl();
return (
<>
<Form.Item<FormData>
@@ -11,14 +13,22 @@ const Basic = () => {
rules={[
{
required: true,
message: '请输入实例名称'
message: intl.formatMessage({
id: 'gpuservice.instance.name.required'
})
}
]}
>
<CInput.Input label="实例名称" required />
<CInput.Input
label={intl.formatMessage({ id: 'gpuservice.instance.name' })}
required
/>
</Form.Item>
<Form.Item<FormData> name={['spec', 'description']}>
<CInput.TextArea label="描述" scaleSize />
<CInput.TextArea
label={intl.formatMessage({ id: 'common.table.description' })}
scaleSize
/>
</Form.Item>
</>
);
+24 -12
View File
@@ -9,6 +9,7 @@ import {
useScrollActiveChange,
useWrapperContext
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import {
forwardRef,
@@ -77,6 +78,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
namespace = 'default',
onFinish
} = props;
const intl = useIntl();
const [form] = Form.useForm<InstanceFormValues>();
const scrollTabsRef = useRef<any>(null);
const { detailData, fetchData } = useGetSshkey();
@@ -98,9 +100,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
useEffect(() => {
if (open) {
fetchData({}).then((res) => {
form?.setFieldValue(['spec', 'sshPublicKey', 'name'], res.name);
});
fetchData({});
}
}, [open]);
@@ -108,30 +108,36 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
() => [
{
value: TABKeysMap.BASIC,
label: '基础信息',
label: intl.formatMessage({
id: 'gpuservice.instance.section.basic'
}),
icon: <IconFont type="icon-basic" />,
field: 'name'
},
{
value: TABKeysMap.INSTANCE_TYPE,
label: '实例类型',
label: intl.formatMessage({ id: 'gpuservice.instance.section.type' }),
icon: <IconFont type="icon-gpu1" />,
field: 'instanceType'
},
{
value: TABKeysMap.TEMPLATE,
label: '实例模板',
label: intl.formatMessage({
id: 'gpuservice.instance.section.template'
}),
icon: <IconFont type="icon-model" />,
field: 'template'
},
{
value: TABKeysMap.STORAGE,
label: '存储卷',
label: intl.formatMessage({
id: 'gpuservice.instance.section.storage'
}),
icon: <IconFont type="icon-storage-outlined" />,
field: 'storage'
}
],
[]
[intl]
);
const ports = (Form.useWatch(['spec', 'ports'], form) ||
@@ -211,7 +217,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
const handleFinish = async (values: InstanceFormValues) => {
const payload = buildPayload(values);
await onFinish(payload);
await onFinish(values);
};
useImperativeHandle(ref, () => ({
@@ -275,19 +281,25 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
items={[
{
key: TABKeysMap.INSTANCE_TYPE,
label: '实例类型',
label: intl.formatMessage({
id: 'gpuservice.instance.section.type'
}),
forceRender: true,
children: <InstanceTypeFormItem />
},
{
key: TABKeysMap.TEMPLATE,
label: '实例模板',
label: intl.formatMessage({
id: 'gpuservice.instance.section.template'
}),
forceRender: true,
children: <TemplateBasicForm page="instance" />
},
{
key: TABKeysMap.STORAGE,
label: '存储卷',
label: intl.formatMessage({
id: 'gpuservice.instance.section.storage'
}),
forceRender: true,
children: <StorageVolume />
}
@@ -1,4 +1,5 @@
import { AutoTooltip, InputNumber as CInputNumber } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Flex, Form, Tag } from 'antd';
import { useEffect, useMemo } from 'react';
import styled from 'styled-components';
@@ -47,6 +48,7 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
value,
dataList
}) => {
const intl = useIntl();
const selected = useMemo(() => {
return dataList.find((item) => (item.metadata?.name || '') === value);
}, [value, dataList]);
@@ -67,17 +69,24 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
color: 'var(--ant-color-text-tertiary)'
}}
>
{selected?.status?.accelerator?.remaining ?? '-'}
{intl.formatMessage({ id: 'gpuservice.instance.stock' })}{' '}
{selected?.status?.accelerator?.remaining ?? '-'}
</Tag>
)}
</Flex>
</SummaryTitle>
<SummaryMeta>
{selected?.spec?.acceleratable && (
<span> {selected?.spec?.memory ?? '-'}</span>
<span>
{intl.formatMessage({ id: 'gpuservice.instance.memory' })}{' '}
{selected?.spec?.memory ?? '-'}
</span>
)}
<Flex gap={16}>
<span> {selected?.status?.ram?.capacity ?? '-'}</span>
<span>
{intl.formatMessage({ id: 'gpuservice.instance.ram' })}{' '}
{selected?.status?.ram?.capacity ?? '-'}
</span>
<span>vCPU {selected?.status?.cpu?.capacity ?? '-'}</span>
</Flex>
</SummaryMeta>
@@ -88,6 +97,7 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
};
const InstanceTypeFormItem = () => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const typeName = Form.useWatch(['spec', 'type'], form) as string | undefined;
@@ -132,14 +142,16 @@ const InstanceTypeFormItem = () => {
}, [form, typeName, maxGpuCount, dataList]);
return (
<>
<FieldBlock data-field="instanceType">
<div data-field="instanceType">
<FieldBlock>
<Form.Item
name={['spec', 'type']}
rules={[
{
required: true,
message: '请选择实例类型'
message: intl.formatMessage({
id: 'gpuservice.instance.type.required'
})
}
]}
>
@@ -152,13 +164,20 @@ const InstanceTypeFormItem = () => {
rules={[
{
required: true,
message: '请输入 GPU 数量'
message: intl.formatMessage({
id: 'gpuservice.instance.gpuCount.required'
})
},
{
validator: (_, value) => {
if (value > maxGpuCount) {
return Promise.reject(
new Error(`当前实例类型最多支持 ${maxGpuCount} 个 GPU`)
new Error(
intl.formatMessage(
{ id: 'gpuservice.instance.gpuCount.max' },
{ count: maxGpuCount }
)
)
);
}
return Promise.resolve();
@@ -170,12 +189,12 @@ const InstanceTypeFormItem = () => {
min={1}
max={maxGpuCount}
precision={0}
label="GPU 数量"
label={intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })}
required
/>
</Form.Item>
)}
</>
</div>
);
};
@@ -1,6 +1,7 @@
import { PageAction } from '@/config';
import FormOverlayView from '@/pages/_components/form-overlay-view';
import { ModalFooter } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useCallback, useRef } from 'react';
import { FormData as StorageFormData } from '../../storage/config/types';
import GPUServiceStorageForm from '../../storage/forms';
@@ -18,6 +19,7 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
onCancel,
onSubmit
}) => {
const intl = useIntl();
const formRef = useRef<any>(null);
const handleSubmit = () => {
@@ -43,7 +45,7 @@ const StorageOverlay: React.FC<StorageOverlayProps> = ({
return (
<FormOverlayView
title="添加存储"
title={intl.formatMessage({ id: 'gpuservice.storage.add' })}
open={open}
width={600}
onCancel={handleCancel}
@@ -1,5 +1,6 @@
import { useModel } from '@@/plugin-model';
import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Button, Flex, Form, Radio } from 'antd';
import { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
@@ -17,6 +18,7 @@ const FieldBlock = styled.div`
const DEFAULT_TEMP_CAPACITY_GB = 50;
const StorageVolume = () => {
const intl = useIntl();
const { initialState } = useModel('@@initialState');
const { fetchData: createStorage } = useCreateStorage();
const { detailData: storageData, fetchData: fetchStorage } =
@@ -92,8 +94,16 @@ const StorageVolume = () => {
value={storageMode}
onChange={(e) => handleModeChange(e.target.value)}
options={[
{ label: '持久存储', value: StorageModeValueMap.Existing },
{ label: '临时存储', value: StorageModeValueMap.Temporary }
{
label: intl.formatMessage({
id: 'gpuservice.storage.persistent'
}),
value: StorageModeValueMap.Existing
},
{
label: intl.formatMessage({ id: 'gpuservice.storage.temporary' }),
value: StorageModeValueMap.Temporary
}
]}
/>
<Button
@@ -102,7 +112,7 @@ const StorageVolume = () => {
style={{ marginBottom: 6 }}
onClick={() => setOverlayOpen(true)}
>
{intl.formatMessage({ id: 'gpuservice.storage.add' })}
</Button>
</Flex>
@@ -112,11 +122,19 @@ const StorageVolume = () => {
rules={[
{
required: true,
message: '请选择持久卷'
message: intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.required'
})
}
]}
>
<Select label="持久卷" required options={storageOptions} />
<Select
label={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume'
})}
required
options={storageOptions}
/>
</Form.Item>
)}
@@ -132,11 +150,20 @@ const StorageVolume = () => {
rules={[
{
required: true,
message: '请输入本地临时存储容量'
message: intl.formatMessage({
id: 'gpuservice.storage.tempCapacity.required'
})
}
]}
>
<CInputNumber min={1} precision={0} label="存储容量 (GB)" required />
<CInputNumber
min={1}
precision={0}
label={intl.formatMessage({
id: 'gpuservice.storage.tempCapacity'
})}
required
/>
</Form.Item>
)}
@@ -1,4 +1,5 @@
import { AutoTooltip, IconFont, TemplateCard } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import styled from 'styled-components';
import { ListItem as TemplateItem } from '../../templates/config/types';
@@ -51,6 +52,7 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
onChange,
dataList = []
}) => {
const intl = useIntl();
return (
<TemplateGrid>
{dataList.map((item: TemplateItem) => (
@@ -71,7 +73,11 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
</div>
<div className="info">
<span>
<IconFont className="icon" type="icon-model" /> :
<IconFont className="icon" type="icon-model" />{' '}
{intl.formatMessage({
id: 'gpuservice.instance.template.image'
})}
:
</span>
<AutoTooltip ghost minWidth={20}>
<span className="value">{item.spec?.image || '-'}</span>
@@ -79,7 +85,11 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
</div>
<div className="info">
<span>
<IconFont className="icon" type="icon-storage-outlined" /> :
<IconFont className="icon" type="icon-storage-outlined" />{' '}
{intl.formatMessage({
id: 'gpuservice.instance.template.mount'
})}
:
</span>
<span className="value">{item.spec?.volumeMount || '-'}</span>
</div>
@@ -1,5 +1,6 @@
import { tableSorter } from '@/config/settings';
import { AutoTooltip, DropdownButtons, StatusTag } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import { useMemo } from 'react';
@@ -15,10 +16,11 @@ const useInstancesColumns = ({
handleSelect,
sortOrder
}: ColumnsHookProps): ColumnsType<ListItem> => {
const intl = useIntl();
return useMemo(() => {
return [
{
title: '名称',
title: intl.formatMessage({ id: 'common.table.name' }),
dataIndex: ['metadata', 'name'],
key: 'name',
sorter: tableSorter(1),
@@ -32,7 +34,7 @@ const useInstancesColumns = ({
)
},
{
title: '状态',
title: intl.formatMessage({ id: 'common.table.status' }),
dataIndex: ['status', 'phase'],
key: 'status',
sorter: tableSorter(2),
@@ -48,7 +50,7 @@ const useInstancesColumns = ({
}
},
{
title: '镜像',
title: intl.formatMessage({ id: 'gpuservice.template.image' }),
dataIndex: ['spec', 'image'],
key: 'image',
sorter: tableSorter(3),
@@ -60,7 +62,7 @@ const useInstancesColumns = ({
)
},
{
title: '创建时间',
title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: ['metadata', 'creationTimestamp'],
key: 'creationTimestamp',
sorter: tableSorter(5),
@@ -74,7 +76,7 @@ const useInstancesColumns = ({
)
},
{
title: '操作',
title: intl.formatMessage({ id: 'common.table.operation' }),
key: 'operation',
dataIndex: 'operation',
render: (_text, record) => (
@@ -85,7 +87,7 @@ const useInstancesColumns = ({
)
}
];
}, [handleSelect, sortOrder]);
}, [handleSelect, sortOrder, intl]);
};
export default useInstancesColumns;
+21 -13
View File
@@ -32,6 +32,7 @@ import useCreateInstance from './services/use-create-instance';
import useUpdateInstance from './services/use-update-instance';
const GPUService: React.FC = () => {
const intl = useIntl();
const { initialState } = useModel('@@initialState');
const namespace = initialState?.currentUser?.org_name || 'default';
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
@@ -97,7 +98,7 @@ const GPUService: React.FC = () => {
deleteAPI: deleteInstance,
watch: true,
API: GPU_SERVICE_INSTANCES_API({ namespace, clusterID }),
contentForDelete: 'GPU 实例'
contentForDelete: intl.formatMessage({ id: 'gpuservice.instance' })
});
const { fetchData: createInstance } = useCreateInstance();
@@ -107,7 +108,6 @@ const GPUService: React.FC = () => {
cancelRequest: cancelClusterRequest,
clusterList
} = useQueryClusterList();
const intl = useIntl();
const k8sClusterList = useMemo(
() =>
@@ -155,7 +155,7 @@ const GPUService: React.FC = () => {
const handleAddInstance = () => {
setOpenAddModalStatus({
action: PageAction.CREATE,
title: '添加 GPU 实例',
title: intl.formatMessage({ id: 'gpuservice.instance.add' }),
open: true,
currentData: null
});
@@ -164,7 +164,7 @@ const GPUService: React.FC = () => {
const handleEditInstance = (row: ListItem) => {
setOpenAddModalStatus({
action: PageAction.EDIT,
title: '编辑 GPU 实例',
title: intl.formatMessage({ id: 'gpuservice.instance.edit' }),
open: true,
currentData: row
});
@@ -192,9 +192,9 @@ const GPUService: React.FC = () => {
fetchData();
closeModal();
message.success('操作成功');
message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) {
message.error('操作失败');
message.error(intl.formatMessage({ id: 'common.message.fail' }));
}
};
@@ -224,13 +224,19 @@ const GPUService: React.FC = () => {
loading={dataSource.loading}
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-instances-outlined" />}
image={<IconFont type="icon-cloud-outlined" />}
filters={_.omit(queryParams, ['sort_by'])}
noFoundText="未找到匹配的 GPU 实例"
title="暂无 GPU 实例"
subTitle="创建一个 GPU 实例后会显示在这里"
noFoundText={intl.formatMessage({
id: 'noresult.gpuservice.instance.nofound'
})}
title={intl.formatMessage({
id: 'noresult.gpuservice.instance.title'
})}
subTitle={intl.formatMessage({
id: 'noresult.gpuservice.instance.subTitle'
})}
onClick={handleAddInstance}
buttonText="立即添加"
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
/>
);
};
@@ -267,8 +273,10 @@ const GPUService: React.FC = () => {
showSelect={false}
selectOptions={k8sClusterList}
select={{ showSearch: true }}
selectHolder="按集群过滤"
buttonText="添加 GPU 实例"
selectHolder={intl.formatMessage({
id: 'gpuservice.instance.filter.cluster'
})}
buttonText={intl.formatMessage({ id: 'gpuservice.instance.add' })}
handleSearch={handleSearch}
handleSelectChange={handleClusterChange}
handleDeleteByBatch={handleDeleteBatch}
+13 -8
View File
@@ -7,23 +7,26 @@ export const StorageTypeValueMap = {
};
export const StorageTypeLabelMap: Record<string, string> = {
[StorageTypeValueMap.Local]: '本地存储',
[StorageTypeValueMap.Shared]: '共享存储',
[StorageTypeValueMap.Object]: '对象存储'
[StorageTypeValueMap.Local]: 'gpuservice.storage.type.local',
[StorageTypeValueMap.Shared]: 'gpuservice.storage.type.shared',
[StorageTypeValueMap.Object]: 'gpuservice.storage.type.object'
};
export const StorageTypeOptions = [
{
label: StorageTypeLabelMap[StorageTypeValueMap.Local],
value: StorageTypeValueMap.Local
value: StorageTypeValueMap.Local,
locale: true
},
{
label: StorageTypeLabelMap[StorageTypeValueMap.Shared],
value: StorageTypeValueMap.Shared
value: StorageTypeValueMap.Shared,
locale: true
},
{
label: StorageTypeLabelMap[StorageTypeValueMap.Object],
value: StorageTypeValueMap.Object
value: StorageTypeValueMap.Object,
locale: true
}
];
@@ -76,13 +79,15 @@ export const ReClaimPolicyOptions = [
export const rowActionList = [
{
label: '编辑',
label: 'common.button.edit',
key: 'edit',
locale: true,
icon: icons.EditOutlined
},
{
label: '删除',
label: 'common.button.delete',
key: 'delete',
locale: true,
icon: icons.DeleteOutlined,
props: {
danger: true
+23 -6
View File
@@ -4,12 +4,14 @@ import {
Select as SealSelect,
useAppUtils
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Flex, Form } from 'antd';
import { useEffect } from 'react';
import { FormData } from '../config/types';
import useQueryStorageClass from '../services/use-query-storage-class';
const Basic = ({ open }: { open: boolean }) => {
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
const { storageClassList, fetchData, loading } = useQueryStorageClass();
@@ -26,11 +28,17 @@ const Basic = ({ open }: { open: boolean }) => {
rules={[
{
required: true,
message: getRuleMessage('input', '名称')
message: getRuleMessage(
'input',
intl.formatMessage({ id: 'common.table.name' })
)
}
]}
>
<CInput.Input label="名称" required />
<CInput.Input
label={intl.formatMessage({ id: 'common.table.name' })}
required
/>
</Form.Item>
<Flex gap={16}>
<div style={{ flex: 1 }}>
@@ -39,12 +47,15 @@ const Basic = ({ open }: { open: boolean }) => {
rules={[
{
required: true,
message: getRuleMessage('select', '存储类型')
message: getRuleMessage(
'select',
intl.formatMessage({ id: 'gpuservice.storage.type' })
)
}
]}
>
<SealSelect
label="存储类型"
label={intl.formatMessage({ id: 'gpuservice.storage.type' })}
required
loading={loading}
options={storageClassList}
@@ -61,11 +72,17 @@ const Basic = ({ open }: { open: boolean }) => {
rules={[
{
required: true,
message: getRuleMessage('input', '容量')
message: getRuleMessage(
'input',
intl.formatMessage({ id: 'gpuservice.storage.capacity' })
)
}
]}
>
<InputNumber label="容量" required />
<InputNumber
label={intl.formatMessage({ id: 'gpuservice.storage.capacity' })}
required
/>
</Form.Item>
</div>
</Flex>
@@ -1,5 +1,6 @@
import { tableSorter } from '@/config/settings';
import { AutoTooltip, DropdownButtons } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import { useMemo } from 'react';
@@ -15,10 +16,11 @@ const useStorageColumns = ({
handleSelect,
sortOrder
}: ColumnsHookProps): ColumnsType<ListItem> => {
const intl = useIntl();
return useMemo(() => {
return [
{
title: '名称',
title: intl.formatMessage({ id: 'common.table.name' }),
dataIndex: ['metadata', 'name'],
key: 'name',
sorter: tableSorter(1),
@@ -32,28 +34,31 @@ const useStorageColumns = ({
)
},
{
title: '类型',
title: intl.formatMessage({ id: 'common.table.type' }),
dataIndex: ['spec', 'type'],
key: 'type',
sorter: tableSorter(2),
render: (value: string) => StorageTypeLabelMap[value] || value || '-'
render: (value: string) =>
StorageTypeLabelMap[value]
? intl.formatMessage({ id: StorageTypeLabelMap[value] })
: value || '-'
},
{
title: '容量',
title: intl.formatMessage({ id: 'gpuservice.storage.capacity' }),
dataIndex: ['spec', 'capacity'],
key: 'capacity',
sorter: tableSorter(3),
render: (value: string) => value ?? '-'
},
{
title: '访问模式',
title: intl.formatMessage({ id: 'gpuservice.storage.accessMode' }),
dataIndex: ['spec', 'accessMode'],
key: 'accessMode',
sorter: tableSorter(4),
render: (value: string) => value || '-'
},
{
title: '创建时间',
title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: ['metadata', 'creationTimestamp'],
key: 'creationTimestamp',
sorter: tableSorter(5),
@@ -67,7 +72,7 @@ const useStorageColumns = ({
)
},
{
title: '操作',
title: intl.formatMessage({ id: 'common.table.operation' }),
key: 'operation',
dataIndex: 'operation',
render: (_text, record) => (
@@ -78,7 +83,7 @@ const useStorageColumns = ({
)
}
];
}, [handleSelect, sortOrder]);
}, [handleSelect, sortOrder, intl]);
};
export default useStorageColumns;
+18 -12
View File
@@ -33,6 +33,7 @@ import useCreateStorage from './services/use-create-storage';
import useUpdateStorage from './services/use-update-storage';
const GPUServiceStorage: React.FC = () => {
const intl = useIntl();
const { initialState } = useModel('@@initialState');
const namespace = initialState?.currentUser?.org_name || 'default';
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
@@ -98,7 +99,7 @@ const GPUServiceStorage: React.FC = () => {
deleteAPI: deleteStorage,
watch: false,
API: GPU_SERVICE_STORAGE_API({ namespace, clusterID }),
contentForDelete: '存储'
contentForDelete: intl.formatMessage({ id: 'gpuservice.storage' })
});
const { fetchData: createStorage } = useCreateStorage();
@@ -108,7 +109,6 @@ const GPUServiceStorage: React.FC = () => {
cancelRequest: cancelClusterRequest,
clusterList
} = useQueryClusterList();
const intl = useIntl();
const k8sClusterList = useMemo(
() =>
@@ -156,7 +156,7 @@ const GPUServiceStorage: React.FC = () => {
const handleAddStorage = () => {
setOpenAddModalStatus({
action: PageAction.CREATE,
title: '添加存储',
title: intl.formatMessage({ id: 'gpuservice.storage.add' }),
open: true,
currentData: null
});
@@ -165,7 +165,7 @@ const GPUServiceStorage: React.FC = () => {
const handleEditStorage = (row: ListItem) => {
setOpenAddModalStatus({
action: PageAction.EDIT,
title: '编辑存储',
title: intl.formatMessage({ id: 'gpuservice.storage.edit' }),
open: true,
currentData: row
});
@@ -193,9 +193,9 @@ const GPUServiceStorage: React.FC = () => {
fetchData();
closeModal();
message.success('操作成功');
message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) {
message.error('操作失败');
message.error(intl.formatMessage({ id: 'common.message.fail' }));
}
};
@@ -227,11 +227,15 @@ const GPUServiceStorage: React.FC = () => {
dataSource={dataSource.dataList}
image={<IconFont type="icon-storage-outlined" />}
filters={_.omit(queryParams, ['sort_by'])}
noFoundText="未找到匹配的存储"
title="暂无存储"
subTitle="创建一个存储后会显示在这里"
noFoundText={intl.formatMessage({
id: 'noresult.gpuservice.storage.nofound'
})}
title={intl.formatMessage({ id: 'noresult.gpuservice.storage.title' })}
subTitle={intl.formatMessage({
id: 'noresult.gpuservice.storage.subTitle'
})}
onClick={handleAddStorage}
buttonText="立即添加"
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
/>
);
};
@@ -269,8 +273,10 @@ const GPUServiceStorage: React.FC = () => {
showSelect={false}
selectOptions={k8sClusterList}
select={{ showSearch: true }}
selectHolder="按集群过滤"
buttonText="添加存储"
selectHolder={intl.formatMessage({
id: 'gpuservice.storage.filter.cluster'
})}
buttonText={intl.formatMessage({ id: 'gpuservice.storage.add' })}
handleSearch={handleSearch}
handleSelectChange={handleClusterChange}
handleDeleteByBatch={handleDeleteBatch}
@@ -34,6 +34,7 @@ const Basic: React.FC<BasicProps> = ({ page = 'template' }) => {
return (
<>
<div data-field="template"></div>
{page === 'template' && (
<>
<Form.Item<FormData>