fix: api test

This commit is contained in:
jialin
2026-05-11 19:49:21 +08:00
committed by jialin
parent fc900c8ce6
commit 4128de61f9
29 changed files with 687 additions and 380 deletions
+6
View File
@@ -0,0 +1,6 @@
import { ClusterListItem } from '@/pages/cluster-management/config/types';
import { atom } from 'jotai';
export const currentClusterAtom = atom<
(Partial<ClusterListItem> & { label?: string; value?: number }) | null
>(null);
+43 -16
View File
@@ -1,18 +1,29 @@
import { request } from '@umijs/max';
import { InstanceTypeItem, ListItem } from '../config/types';
export const GPU_SERVICE_INSTANCES_API = (namespace: string) =>
`/proxy/apis/worker.gpustack.ai/v1/namespaces/${namespace}/instances`;
export const GPU_SERVICE_INSTANCES_API = (params: {
namespace: string;
clusterID?: number;
}) =>
`/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances`;
export const GPU_SERVICE_INSTANCES_TYPE_API =
'/proxy/apis/worker.gpustack.ai/v1/instancetypes';
export const GPU_SERVICE_INSTANCES_TYPE_API = (params: {
clusterID?: number;
}) =>
`/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/instancetypes`;
export async function queryGPUServiceInstances(
params: Global.K8sSearchParams,
params: Global.K8sSearchParams & {
namespace: string;
clusterID?: number;
},
options?: any
) {
return request<Global.K8sPageResponse<ListItem>>(
GPU_SERVICE_INSTANCES_API(params.namespace || ''),
GPU_SERVICE_INSTANCES_API({
namespace: params.namespace,
clusterID: params.clusterID
}),
{
method: 'GET',
params,
@@ -24,27 +35,38 @@ export async function queryGPUServiceInstances(
export async function createGPUServiceInstance(
params: {
namespace: string;
clusterID?: number;
data: Global.K8sCommonData;
},
option?: any
) {
return request<ListItem>(GPU_SERVICE_INSTANCES_API(params.namespace), {
method: 'POST',
data: params.data,
cancelToken: option?.token
});
return request<ListItem>(
GPU_SERVICE_INSTANCES_API({
namespace: params.namespace,
clusterID: params.clusterID
}),
{
method: 'POST',
data: params.data,
cancelToken: option?.token
}
);
}
export async function updateGPUServiceInstance(
params: {
namespace: string;
clusterID?: number;
id: number;
data: Global.K8sCommonData;
},
option?: any
) {
return request<ListItem>(
`${GPU_SERVICE_INSTANCES_API(params.namespace)}/${params.id}`,
`${GPU_SERVICE_INSTANCES_API({
namespace: params.namespace,
clusterID: params.clusterID
})}/${params.id}`,
{
method: 'PUT',
data: params.data,
@@ -56,12 +78,16 @@ export async function updateGPUServiceInstance(
export async function deleteGPUServiceInstance(
params: {
namespace: string;
clusterID?: number;
id: number;
},
option?: any
) {
return request(
`${GPU_SERVICE_INSTANCES_API(params.namespace)}/${params.id}`,
`${GPU_SERVICE_INSTANCES_API({
namespace: params.namespace,
clusterID: params.clusterID
})}/${params.id}`,
{
method: 'DELETE',
cancelToken: option?.token
@@ -72,11 +98,11 @@ export async function deleteGPUServiceInstance(
// =========== Instance Types ===========
export async function queryGPUServiceInstanceTypes(
params: Global.K8sSearchParams,
params: Global.K8sSearchParams & { clusterID?: number },
options?: any
) {
return request<Global.K8sPageResponse<InstanceTypeItem>>(
GPU_SERVICE_INSTANCES_TYPE_API,
GPU_SERVICE_INSTANCES_TYPE_API({ clusterID: params.clusterID }),
{
method: 'GET',
params,
@@ -88,11 +114,12 @@ export async function queryGPUServiceInstanceTypes(
export async function queryGPUServiceInstanceTypeItems(
params: {
name: string;
clusterID?: number;
},
options?: any
) {
return request<InstanceTypeItem>(
`${GPU_SERVICE_INSTANCES_TYPE_API}/${params.name}`,
`${GPU_SERVICE_INSTANCES_TYPE_API({ clusterID: params.clusterID })}/${params.name}`,
{
method: 'GET',
params,
@@ -5,8 +5,8 @@ import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
import { Empty, Input, Typography } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import useQueryTemplates from '../../templates/services/use-query-templates';
import { ListItem as TemplateItem } from '../../templates/config/types';
import useQueryTemplates from '../../templates/services/use-query-templates';
import { FormData, InstanceTypeItem, ListItem } from '../config/types';
import GPUServiceInstanceForm from '../forms';
import TemplateSelector from '../forms/template-selector';
@@ -59,7 +59,7 @@ const AddModal: React.FC<AddModalProps> = ({
onCancel
}) => {
const form = useRef<any>(null);
const [instanceTypeName, setInstanceTypeName] = useState<string>();
const [selectedInstanceType, setSelectedInstanceType] = useState<string>();
const [templateId, setTemplateId] = useState<number>();
const [instanceKeyword, setInstanceKeyword] = useState('');
const [templateKeyword, setTemplateKeyword] = useState('');
@@ -84,7 +84,7 @@ const AddModal: React.FC<AddModalProps> = ({
return instanceTypeList;
}
return instanceTypeList.filter((item) => {
const name = item.metadata?.name || item.name || '';
const name = item.metadata?.name || '';
return [
name,
item.spec?.memory ?? '',
@@ -123,28 +123,23 @@ const AddModal: React.FC<AddModalProps> = ({
};
const handleInstanceTypeChange = (item: InstanceTypeItem) => {
const name = item.metadata?.name || item.name;
setInstanceTypeName(name);
const name = item.metadata?.name;
setSelectedInstanceType(name);
form.current?.setFieldsValue({
type: name,
spec: { resources: { accelerator: '1' } }
spec: {
type: name,
resources: { accelerator: '1' }
}
});
};
const handleTemplateChange = (id: number, item: TemplateItem) => {
setTemplateId(id);
form.current?.setFieldsValue({
manufacturer: item.manufacturer,
spec: {
image: item.spec?.image,
imagePullPolicy: item.spec?.imagePullPolicy,
command: item.spec?.command || [],
ports: item.spec?.ports || [],
env: item.spec?.env || [],
volumeMount: item.spec?.volumeMount,
resources: {
cpu: item.spec?.resources?.cpu,
ram: item.spec?.resources?.ram
}
...form.current?.getFieldsValue()?.spec,
...item.spec
}
});
};
@@ -188,7 +183,7 @@ const AddModal: React.FC<AddModalProps> = ({
</div>
{filteredInstanceTypes.length > 0 ? (
<InstanceTypeList
value={instanceTypeName}
value={selectedInstanceType}
dataList={filteredInstanceTypes}
onChange={handleInstanceTypeChange}
/>
@@ -1,7 +1,11 @@
import { AutoTooltip, TemplateCard } from '@gpustack/core-ui';
import { AutoTooltip, StatusTag, TemplateCard } from '@gpustack/core-ui';
import { Flex, Tag } from 'antd';
import styled from 'styled-components';
import { InstanceTypePhaseValueMap } from '../config';
import {
InstanceTypePhaseLabelMap,
InstanceTypePhaseStatus,
InstanceTypePhaseValueMap
} from '../config';
import { InstanceTypeItem } from '../config/types';
const TypeGrid = styled.div`
@@ -44,7 +48,7 @@ interface InstanceTypeListProps {
}
const isAvailable = (item: InstanceTypeItem) =>
item.status?.phase === InstanceTypePhaseValueMap.Available;
item.status?.phase === InstanceTypePhaseValueMap.Active;
const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
value,
@@ -60,35 +64,60 @@ const InstanceTypeList: React.FC<InstanceTypeListProps> = ({
<TypeGrid>
{dataList.map((item) => {
const disabled = !isAvailable(item);
const name = item.metadata?.name || item.name;
const name = item.metadata?.name;
return (
<TemplateCard
key={name}
clickable
ghost
hoverable
height={104}
height={106}
active={value === name}
disabled={disabled}
onClick={() => handleSelect(item)}
>
<TypeName>
<Flex gap={16}>
<Flex gap={8} align="center">
<AutoTooltip ghost minWidth={20}>
{name}
</AutoTooltip>
</Flex>
<Tag
style={{
fontWeight: 400,
color: 'var(--ant-color-text-tertiary)'
}}
>
<span> {item.status?.accelerator?.remaining ?? '-'}</span>
</Tag>
<Flex gap={8} align="center">
{item.status?.phase && (
<StatusTag
statusValue={{
status:
InstanceTypePhaseStatus[item.status.phase] ??
'inactive',
text:
InstanceTypePhaseLabelMap[item.status.phase] ??
item.status.phase,
message: ''
}}
/>
)}
{item.spec?.acceleratable && (
<Tag
style={{
fontWeight: 400,
color: 'var(--ant-color-text-tertiary)'
}}
>
<span>
{item.status?.accelerator?.remaining ?? '-'}
</span>
</Tag>
)}
</Flex>
</TypeName>
<TypeMeta>
<span className="meta-row"> {item.spec?.memory ?? '-'}</span>
<span style={{ display: 'flex', height: 15 }}>
{item.spec?.acceleratable && (
<span className="meta-row">
{item.spec?.memory ?? '-'}
</span>
)}
</span>
<span className="meta-row gap-16">
<span> {item.status?.ram?.capacity ?? '-'}</span>
<span>vCPU {item.status?.cpu?.capacity ?? '-'}</span>
+100 -9
View File
@@ -1,23 +1,47 @@
import { StatusMaps } from '@/config';
import { StatusType } from '@/config/types';
import { icons } from '@gpustack/core-ui';
import _ from 'lodash';
import {
InstanceTypeItem,
InstanceTypeResource,
InstanceTypeStatus
} from './types';
export const InstanceStatusValueMap = {
Ready: 'ready',
Pending: 'pending',
Error: 'error'
Scheduling: 'Scheduling',
Pending: 'Pending',
Scheduled: 'Scheduled',
Initializing: 'Initializing',
InitializeFailed: 'InitializeFailed',
Initialized: 'Initialized',
Preparing: 'Preparing',
NotReady: 'NotReady',
Ready: 'Ready'
};
export const InstanceStatusLabelMap: Record<string, string> = {
[InstanceStatusValueMap.Ready]: 'Ready',
[InstanceStatusValueMap.Scheduling]: 'Scheduling',
[InstanceStatusValueMap.Pending]: 'Pending',
[InstanceStatusValueMap.Error]: 'Error'
[InstanceStatusValueMap.Scheduled]: 'Scheduled',
[InstanceStatusValueMap.Initializing]: 'Initializing',
[InstanceStatusValueMap.InitializeFailed]: 'InitializeFailed',
[InstanceStatusValueMap.Initialized]: 'Initialized',
[InstanceStatusValueMap.Preparing]: 'Preparing',
[InstanceStatusValueMap.NotReady]: 'NotReady',
[InstanceStatusValueMap.Ready]: 'Ready'
};
export const status: Record<string, StatusType> = {
[InstanceStatusValueMap.Ready]: StatusMaps.success,
[InstanceStatusValueMap.Scheduling]: StatusMaps.transitioning,
[InstanceStatusValueMap.Pending]: StatusMaps.transitioning,
[InstanceStatusValueMap.Error]: StatusMaps.error
[InstanceStatusValueMap.Scheduled]: StatusMaps.transitioning,
[InstanceStatusValueMap.Initializing]: StatusMaps.transitioning,
[InstanceStatusValueMap.InitializeFailed]: StatusMaps.error,
[InstanceStatusValueMap.Initialized]: StatusMaps.transitioning,
[InstanceStatusValueMap.Preparing]: StatusMaps.transitioning,
[InstanceStatusValueMap.NotReady]: StatusMaps.warning,
[InstanceStatusValueMap.Ready]: StatusMaps.success
};
export const rowActionList = [
@@ -37,8 +61,23 @@ export const rowActionList = [
];
export const InstanceTypePhaseValueMap = {
Available: 'Available',
Unavailable: 'Unavailable'
// Available: 'Available',
// Unavailable: 'Unavailable'
PreParing: 'Preparing',
Inactive: 'Inactive',
Active: 'Active'
};
export const InstanceTypePhaseLabelMap: Record<string, string> = {
[InstanceTypePhaseValueMap.PreParing]: 'Preparing',
[InstanceTypePhaseValueMap.Inactive]: 'Inactive',
[InstanceTypePhaseValueMap.Active]: 'Active'
};
export const InstanceTypePhaseStatus: Record<string, StatusType> = {
[InstanceTypePhaseValueMap.PreParing]: StatusMaps.transitioning,
[InstanceTypePhaseValueMap.Inactive]: StatusMaps.inactive,
[InstanceTypePhaseValueMap.Active]: StatusMaps.success
};
export const StorageModeValueMap = {
@@ -48,3 +87,55 @@ export const StorageModeValueMap = {
// Constant SSH public key resource name used when SSH is enabled
export const DEFAULT_SSH_PUBLIC_KEY_NAME = 'default';
const KI_TO_GI = 1024 * 1024;
export const convertKiToGi = (value?: string): string | undefined => {
if (!value) return value;
const match = /^(-?\d+(?:\.\d+)?)Ki$/.exec(value);
if (!match) return value;
return `${_.round(Number(match[1]) / KI_TO_GI, 2)}Gi`;
};
export const transformInstanceTypeResource = (
resource?: InstanceTypeResource
): InstanceTypeResource | undefined => {
if (!resource) return resource;
return {
...resource,
capacity: convertKiToGi(
resource.capacity
) as InstanceTypeResource['capacity'],
onceMaxRequest: convertKiToGi(
resource.onceMaxRequest
) as InstanceTypeResource['onceMaxRequest'],
remaining: convertKiToGi(
resource.remaining
) as InstanceTypeResource['remaining']
};
};
export const transformInstanceType = (
item: InstanceTypeItem
): InstanceTypeItem => {
if (!item?.status) return item;
const status = item.status;
return {
...item,
status: {
...status,
accelerator: transformInstanceTypeResource(
status.accelerator
) as InstanceTypeStatus['accelerator'],
cpu: transformInstanceTypeResource(
status.cpu
) as InstanceTypeStatus['cpu'],
localStorage: transformInstanceTypeResource(
status.localStorage
) as InstanceTypeStatus['localStorage'],
ram: transformInstanceTypeResource(
status.ram
) as InstanceTypeStatus['ram']
}
};
};
@@ -50,12 +50,30 @@ export interface FormData {
};
}
export interface ManagedField {
manager: string;
operation: string;
apiVersion: string;
time: string;
fieldsType: string;
fieldsV1: Record<string, any>;
}
export interface Metadata {
name: string;
namespace?: string;
uid: string;
resourceVersion: string;
creationTimestamp: string;
annotations?: Record<string, string>;
managedFields?: ManagedField[];
}
// instance list item
export interface ListItem extends FormData {
export interface ListItem extends Omit<FormData, 'metadata'> {
id: number;
status?: string;
created_at?: string;
updated_at?: string;
metadata: Metadata;
}
export interface InstanceItem {
@@ -79,10 +97,12 @@ export interface InstanceTypeSpec {
sliced?: number;
}
type Quality = `${number}Ki` | `${number}Gi`;
export interface InstanceTypeResource {
capacity?: string;
onceMaxRequest?: string;
remaining?: string;
capacity?: Quality;
onceMaxRequest?: Quality;
remaining?: Quality;
}
export interface InstanceTypeStatus {
@@ -102,7 +122,6 @@ export interface InstanceTypeItem {
namespace: string;
};
id: number;
name: string;
spec: InstanceTypeSpec;
status: InstanceTypeStatus;
}
+36 -67
View File
@@ -1,7 +1,6 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import {
CheckboxField,
Input as CInput,
CollapsePanel,
IconFont,
@@ -18,9 +17,10 @@ import {
useMemo,
useRef
} from 'react';
import useGetSshkey from '../../public-keys/services/use-get-sshkey';
import { DefaultImagePullPolicy } from '../../templates/config';
import TemplateBasicForm from '../../templates/forms/basic';
import { DEFAULT_SSH_PUBLIC_KEY_NAME, StorageModeValueMap } from '../config';
import { DEFAULT_SSH_PUBLIC_KEY_NAME } from '../config';
import { FormData, InstancePort, ListItem } from '../config/types';
import Basic from './basic';
import InstanceTypeFormItem from './instance-type';
@@ -29,10 +29,6 @@ import StorageVolume from './storage-volume';
const SSH_PORT = 22;
type InstanceFormValues = FormData & {
// storage holders
storage_mode?: string;
storage_name?: string;
local_storage_size_gb?: number;
// ssh holder
enable_ssh?: boolean;
};
@@ -68,7 +64,7 @@ const requiredFields = {
},
[TABKeysMap.STORAGE]: {
sort: 4,
fields: ['storage_mode', 'storage_name', 'local_storage_size_gb']
fields: ['volume']
}
};
@@ -83,6 +79,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
} = props;
const [form] = Form.useForm<InstanceFormValues>();
const scrollTabsRef = useRef<any>(null);
const { detailData, fetchData } = useGetSshkey();
const { getScrollElementScrollableHeight } = useWrapperContext();
const {
activeKey,
@@ -99,6 +96,14 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
]
});
useEffect(() => {
if (open) {
fetchData({}).then((res) => {
form?.setFieldValue(['spec', 'sshPublicKey', 'name'], res.name);
});
}
}, [open]);
const segmentOptions = useMemo(
() => [
{
@@ -153,14 +158,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
}
if (action === PageAction.EDIT && currentData) {
const persistentName = currentData.spec?.volume?.persistent?.name;
const ephemeralCapacity = currentData.spec?.volume?.ephemeral?.capacity;
const isExisting = !!persistentName;
const isTemporary = !!ephemeralCapacity;
const localSizeGb = ephemeralCapacity
? Number(String(ephemeralCapacity).replace(/Gi$/i, '')) || undefined
: undefined;
form.setFieldsValue({
metadata: {
name: currentData.metadata?.name,
@@ -171,39 +168,10 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
imagePullPolicy:
currentData.spec?.imagePullPolicy || DefaultImagePullPolicy
},
storage_mode: isExisting
? StorageModeValueMap.Existing
: isTemporary
? StorageModeValueMap.Temporary
: StorageModeValueMap.Existing,
storage_name: persistentName,
local_storage_size_gb: localSizeGb,
enable_ssh: !!currentData.spec?.sshPublicKey?.name
});
return;
}
form.setFieldsValue({
metadata: {
namespace
},
spec: {
description: '',
displayName: '',
image: '',
imagePullPolicy: DefaultImagePullPolicy,
command: [],
ports: [],
env: [],
volumeMount: '',
resources: {
accelerator: '1'
}
} as Partial<FormData['spec']> as FormData['spec'],
storage_mode: StorageModeValueMap.Existing,
local_storage_size_gb: 50,
enable_ssh: false
});
}, [action, currentData, form, open, namespace]);
const buildPayload = (values: InstanceFormValues): FormData => {
@@ -223,19 +191,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
}
} as FormData['spec'];
// Storage volume mapping
if (values.storage_mode === StorageModeValueMap.Existing) {
spec.volume = { persistent: { name: values.storage_name || '' } };
} else if (values.storage_mode === StorageModeValueMap.Temporary) {
spec.volume = {
ephemeral: {
capacity: values.local_storage_size_gb
? `${values.local_storage_size_gb}Gi`
: ''
}
};
}
// SSH public key — include only when checkbox is checked AND port 22 is in ports
const portsList = (values.spec?.ports || []) as InstancePort[];
const has22 = portsList.some((p) => Number(p?.port) === SSH_PORT);
@@ -268,7 +223,8 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
},
setFieldsValue: (values: Partial<InstanceFormValues>) => {
form.setFieldsValue(values as any);
}
},
getFieldsValue: () => form.getFieldsValue()
}));
return (
@@ -290,6 +246,26 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
onFinish={handleFinish}
onFinishFailed={handleOnFinishFailed}
preserve={false}
initialValues={{
spec: {
description: '',
displayName: '',
image: '',
imagePullPolicy: DefaultImagePullPolicy,
command: [],
ports: [],
env: [],
volumeMount: '',
resources: {
accelerator: '1'
},
volume: { persistent: { name: '' } },
sshPublicKey: {
name: 'gpustack-organization-ssh-public-key'
}
},
enable_ssh: false
}}
>
<Basic />
<CollapsePanel
@@ -318,17 +294,10 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
]}
/>
{hasSshPort && (
<Form.Item
name="enable_ssh"
valuePropName="checked"
style={{ marginBottom: 8 }}
>
<CheckboxField label="SSH Terminal Access" />
<Form.Item<FormData> hidden name={['spec', 'sshPublicKey', 'name']}>
<CInput.Input />
</Form.Item>
)}
<Form.Item<FormData> hidden name={['spec', 'sshPublicKey', 'name']}>
<CInput.Input />
</Form.Item>
</Form>
</ScrollSpyTabs>
);
@@ -1,8 +1,4 @@
import {
AutoTooltip,
Input as CInput,
InputNumber as CInputNumber
} from '@gpustack/core-ui';
import { AutoTooltip, InputNumber as CInputNumber } from '@gpustack/core-ui';
import { Flex, Form, Tag } from 'antd';
import { useEffect, useMemo } from 'react';
import styled from 'styled-components';
@@ -44,19 +40,15 @@ const SummaryMeta = styled.div`
interface InstanceTypePickerProps {
value?: string;
onChange?: (value: string) => void;
dataList: InstanceTypeItem[];
}
const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
value,
onChange,
dataList
}) => {
const selected = useMemo(() => {
return dataList.find(
(item) => (item.metadata?.name || item.name) === value
);
return dataList.find((item) => (item.metadata?.name || '') === value);
}, [value, dataList]);
return (
@@ -66,10 +58,9 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
<SummaryTitle>
<Flex gap={16} align="center">
<AutoTooltip ghost minWidth={20}>
{(selected?.metadata?.name || selected?.name) ??
'请选择实例类型'}
{selected?.metadata?.name || '-'}
</AutoTooltip>
{selected && (
{selected && selected.spec?.acceleratable && (
<Tag
style={{
fontWeight: 400,
@@ -82,7 +73,9 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
</Flex>
</SummaryTitle>
<SummaryMeta>
<span> {selected?.spec?.memory ?? '-'}</span>
{selected?.spec?.acceleratable && (
<span> {selected?.spec?.memory ?? '-'}</span>
)}
<Flex gap={16}>
<span> {selected?.status?.ram?.capacity ?? '-'}</span>
<span>vCPU {selected?.status?.cpu?.capacity ?? '-'}</span>
@@ -95,8 +88,8 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
};
const InstanceTypeFormItem = () => {
const form = Form.useFormInstance<FormData & { type?: string }>();
const typeName = Form.useWatch('type', form) as string | undefined;
const form = Form.useFormInstance<FormData>();
const typeName = Form.useWatch(['spec', 'type'], form) as string | undefined;
const { detailData, fetchData } = useQueryInstanceTypes();
@@ -107,9 +100,7 @@ const InstanceTypeFormItem = () => {
const dataList = detailData?.items || [];
const selectedInstanceType = useMemo(() => {
return dataList.find(
(item) => (item.metadata?.name || item.name) === typeName
);
return dataList.find((item) => (item.metadata?.name || '') === typeName);
}, [dataList, typeName]);
const maxGpuCount = useMemo(() => {
@@ -121,13 +112,10 @@ const InstanceTypeFormItem = () => {
useEffect(() => {
if (!typeName && dataList.length > 0) {
const defaultType = dataList.find(
(item) => item.status?.phase === InstanceTypePhaseValueMap.Available
(item) => item.status?.phase === InstanceTypePhaseValueMap.Active
);
if (defaultType) {
form.setFieldValue(
'type',
defaultType.metadata?.name || defaultType.name
);
form.setFieldValue(['spec', 'type'], defaultType.metadata?.name || '');
}
return;
}
@@ -147,7 +135,7 @@ const InstanceTypeFormItem = () => {
<>
<FieldBlock data-field="instanceType">
<Form.Item
name="type"
name={['spec', 'type']}
rules={[
{
required: true,
@@ -155,54 +143,40 @@ const InstanceTypeFormItem = () => {
}
]}
>
<InstanceTypePicker dataList={dataList} />
<InstanceTypePicker dataList={dataList} value={typeName} />
</Form.Item>
</FieldBlock>
<Form.Item
name={['spec', 'resources', 'accelerator']}
rules={[
{
required: true,
message: '请输入 GPU 数量'
},
{
validator: (_, value) => {
if (value > maxGpuCount) {
return Promise.reject(
new Error(`当前实例类型最多支持 ${maxGpuCount} 个 GPU`)
);
{selectedInstanceType?.spec?.acceleratable && (
<Form.Item
name={['spec', 'resources', 'accelerator']}
rules={[
{
required: true,
message: '请输入 GPU 数量'
},
{
validator: (_, value) => {
if (value > maxGpuCount) {
return Promise.reject(
new Error(`当前实例类型最多支持 ${maxGpuCount} 个 GPU`)
);
}
return Promise.resolve();
}
return Promise.resolve();
}
}
]}
>
<CInputNumber
min={1}
max={maxGpuCount}
precision={0}
label="GPU 数量"
required
/>
</Form.Item>
<Form.Item name="type_display" hidden>
<CInput.Input />
</Form.Item>
]}
>
<CInputNumber
min={1}
max={maxGpuCount}
precision={0}
label="GPU 数量"
required
/>
</Form.Item>
)}
</>
);
};
export const useSelectedInstanceType = () => {
const form = Form.useFormInstance<FormData & { type?: string }>();
const typeName = Form.useWatch('type', form) as string | undefined;
const { detailData } = useQueryInstanceTypes();
const dataList = detailData?.items || [];
return useMemo(() => {
return dataList.find(
(item) => (item.metadata?.name || item.name) === typeName
);
}, [dataList, typeName]);
};
export default InstanceTypeFormItem;
@@ -1,44 +1,48 @@
import { useModel } from '@@/plugin-model';
import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui';
import { Button, Flex, Form, message, Radio } from 'antd';
import { Button, Flex, Form, Radio } from 'antd';
import { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { FormData as StorageFormData } from '../../storage/config/types';
import useCreateStorage from '../../storage/services/use-create-storage';
import useQueryStorage from '../../storage/services/use-query-storage';
import { StorageModeValueMap } from '../config';
import { FormData } from '../config/types';
import { FormData, InstanceVolume } from '../config/types';
import StorageOverlay from './storage-overlay';
const FieldBlock = styled.div`
margin-bottom: 24px;
`;
type StorageHolderFields = {
storage_mode?: string;
storage_name?: string;
local_storage_size_gb?: number;
};
const DEFAULT_TEMP_CAPACITY_GB = 50;
const StorageVolume = () => {
const { initialState } = useModel('@@initialState');
const namespace = initialState?.currentUser?.org_name || 'default';
const { fetchData: createStorage } = useCreateStorage();
const { detailData: storageData, fetchData: fetchStorage } =
useQueryStorage();
const [overlayOpen, setOverlayOpen] = useState(false);
const [storageMode, setStorageMode] = useState<string>(
StorageModeValueMap.Existing
);
const form = Form.useFormInstance<FormData & StorageHolderFields>();
const storageMode = Form.useWatch('storage_mode', form) as string | undefined;
const storageName = Form.useWatch('storage_name', form) as string | undefined;
const localSize = Form.useWatch('local_storage_size_gb', form) as
| number
const form = Form.useFormInstance<FormData>();
const volume = Form.useWatch(['spec', 'volume'], form) as
| InstanceVolume
| undefined;
const [overlayOpen, setOverlayOpen] = useState(false);
const { fetchData: createStorage } = useCreateStorage();
const { detailData: storageData, fetchData: fetchStorage } = useQueryStorage();
const namespace = initialState?.currentUser?.org_name || 'default';
useEffect(() => {
fetchStorage({});
}, [fetchStorage]);
}, []);
// const storageMode = useMemo(() => {
// if (volume?.ephemeral && !volume?.persistent) {
// return StorageModeValueMap.Temporary;
// }
// return StorageModeValueMap.Existing;
// }, [volume?.ephemeral, volume?.persistent]);
const storageOptions = useMemo(
() =>
@@ -49,29 +53,28 @@ const StorageVolume = () => {
[storageData]
);
useEffect(() => {
if (storageMode === StorageModeValueMap.Existing) {
const handleModeChange = (mode: string) => {
console.log('selected storage mode', mode);
if (mode === StorageModeValueMap.Existing) {
form.setFieldValue(['spec', 'volume'], { persistent: { name: '' } });
} else {
form.setFieldValue(['spec', 'volume'], {
persistent: { name: storageName || '' }
});
} else if (storageMode === StorageModeValueMap.Temporary) {
form.setFieldValue(['spec', 'volume'], {
ephemeral: {
capacity: localSize ? `${localSize}Gi` : ''
}
ephemeral: { capacity: `${DEFAULT_TEMP_CAPACITY_GB}Gi` }
});
}
}, [form, storageMode, storageName, localSize]);
setStorageMode(mode);
};
const handleCreateStorage = async (values: StorageFormData) => {
try {
await createStorage({ data: values });
await fetchStorage({});
form.setFieldValue('storage_name', values.metadata.name);
form.setFieldValue(['spec', 'volume'], {
persistent: { name: values.metadata.name }
});
setOverlayOpen(false);
message.success('操作成功');
} catch (error) {
message.error('操作失败');
// ignore
}
};
@@ -84,23 +87,15 @@ const StorageVolume = () => {
marginBottom: 6
}}
>
<Form.Item
name="storage_mode"
<Radio.Group
style={{ marginBottom: 12 }}
rules={[
{
required: true,
message: '请选择存储'
}
value={storageMode}
onChange={(e) => handleModeChange(e.target.value)}
options={[
{ label: '持久存储', value: StorageModeValueMap.Existing },
{ label: '临时存储', value: StorageModeValueMap.Temporary }
]}
>
<Radio.Group
options={[
{ label: '持久存储', value: StorageModeValueMap.Existing },
{ label: '临时存储', value: StorageModeValueMap.Temporary }
]}
/>
</Form.Item>
/>
<Button
type="link"
size="small"
@@ -113,7 +108,7 @@ const StorageVolume = () => {
{storageMode === StorageModeValueMap.Existing && (
<Form.Item
name="storage_name"
name={['spec', 'volume', 'persistent', 'name']}
rules={[
{
required: true,
@@ -127,7 +122,13 @@ const StorageVolume = () => {
{storageMode === StorageModeValueMap.Temporary && (
<Form.Item
name="local_storage_size_gb"
name={['spec', 'volume', 'ephemeral', 'capacity']}
getValueProps={(val) => ({
value: val
? Number(String(val).replace(/Gi$/i, '')) || undefined
: undefined
})}
normalize={(val) => (val ? `${val}Gi` : '')}
rules={[
{
required: true,
@@ -61,7 +61,6 @@ const TemplateSelector: React.FC<TemplateSelectorProps> = ({
hoverable
height={102}
active={value === item.id}
disabled={item.status !== 'enabled'}
onClick={() => onChange?.(item.id, item)}
>
<TemplateContent>
@@ -3,12 +3,7 @@ import { AutoTooltip, DropdownButtons, StatusTag } from '@gpustack/core-ui';
import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import { useMemo } from 'react';
import {
InstanceStatusLabelMap,
InstanceStatusValueMap,
rowActionList,
status
} from '../config';
import { InstanceStatusLabelMap, rowActionList, status } from '../config';
import { ListItem } from '../config/types';
interface ColumnsHookProps {
@@ -38,11 +33,10 @@ const useInstancesColumns = ({
},
{
title: '状态',
dataIndex: 'status',
dataIndex: ['status', 'phase'],
key: 'status',
sorter: tableSorter(2),
render: (statusValue: string) => {
const value = statusValue || InstanceStatusValueMap.Ready;
render: (value: string, record: ListItem) => {
return (
<StatusTag
statusValue={{
@@ -67,8 +61,8 @@ const useInstancesColumns = ({
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
dataIndex: ['metadata', 'creationTimestamp'],
key: 'creationTimestamp',
sorter: tableSorter(5),
ellipsis: {
showTitle: false
+49 -12
View File
@@ -1,7 +1,9 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
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 { ProviderValueMap } from '@/pages/cluster-management/config';
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
import { useModel } from '@@/plugin-model';
import {
@@ -14,8 +16,9 @@ import {
import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, Divider, Flex, message, Table } from 'antd';
import { useAtom } from 'jotai';
import _ from 'lodash';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { PageContainerInner } from '../../_components/page-box';
import {
deleteGPUServiceInstance,
@@ -31,10 +34,12 @@ import useUpdateInstance from './services/use-update-instance';
const GPUService: React.FC = () => {
const { initialState } = useModel('@@initialState');
const namespace = initialState?.currentUser?.org_name || 'default';
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
const clusterID = currentCluster?.id;
const deleteInstance = useCallback(
(id: number) => deleteGPUServiceInstance({ namespace, id }),
[namespace]
(id: number) => deleteGPUServiceInstance({ namespace, clusterID, id }),
[namespace, clusterID]
);
const fetchInstances = useCallback(
@@ -42,8 +47,19 @@ const GPUService: React.FC = () => {
params: any,
options?: any
): Promise<Global.PageResponse<ListItem>> => {
if (!clusterID) {
return {
items: [],
pagination: {
total: 0,
totalPage: 0,
page: 1,
perPage: params.perPage || 10
}
} as Global.PageResponse<ListItem>;
}
const res = await queryGPUServiceInstances(
{ ...params, namespace },
{ ...params, namespace, clusterID: params.cluster_id ?? clusterID },
options
);
const total = res.items?.length ?? 0;
@@ -58,7 +74,7 @@ const GPUService: React.FC = () => {
}
};
},
[namespace]
[namespace, clusterID]
);
const {
@@ -79,8 +95,8 @@ const GPUService: React.FC = () => {
key: PaginationKey.Instances,
fetchAPI: fetchInstances,
deleteAPI: deleteInstance,
watch: false,
API: GPU_SERVICE_INSTANCES_API(namespace),
watch: true,
API: GPU_SERVICE_INSTANCES_API({ namespace, clusterID }),
contentForDelete: 'GPU 实例'
});
@@ -93,6 +109,14 @@ const GPUService: React.FC = () => {
} = useQueryClusterList();
const intl = useIntl();
const k8sClusterList = useMemo(
() =>
clusterList.filter(
(item) => item.provider === ProviderValueMap.Kubernetes
),
[clusterList]
);
const [openAddModalStatus, setOpenAddModalStatus] = useState<{
action: PageActionType;
open: boolean;
@@ -107,9 +131,18 @@ const GPUService: React.FC = () => {
useEffect(() => {
fetchClusterList({ page: -1 }).then((clusters) => {
if (clusters.length > 0) {
const k8sClusters = clusters.filter(
(item: any) => item.provider === ProviderValueMap.Kubernetes
);
if (k8sClusters.length > 0) {
const firstCluster = k8sClusters[0];
setCurrentCluster({
...firstCluster,
label: firstCluster.name,
value: firstCluster.id
});
handleQueryChange({
cluster_id: clusters[0].id,
cluster_id: firstCluster.id,
page: 1
});
}
@@ -174,6 +207,10 @@ const GPUService: React.FC = () => {
});
const handleClusterChange = (value: number) => {
const cluster = k8sClusterList.find((item) => item.value === value);
if (cluster) {
setCurrentCluster(cluster);
}
handleQueryChange({
cluster_id: value,
page: 1
@@ -217,7 +254,7 @@ const GPUService: React.FC = () => {
<BaseSelect
variant="borderless"
value={queryParams.cluster_id}
options={clusterList}
options={k8sClusterList}
onChange={handleClusterChange}
style={{ minWidth: 120, fontWeight: 500 }}
></BaseSelect>
@@ -228,7 +265,7 @@ const GPUService: React.FC = () => {
marginBottom={22}
marginTop={30}
showSelect={false}
selectOptions={clusterList}
selectOptions={k8sClusterList}
select={{ showSearch: true }}
selectHolder="按集群过滤"
buttonText="添加 GPU 实例"
@@ -251,7 +288,7 @@ const GPUService: React.FC = () => {
}}
sortDirections={TABLE_SORT_DIRECTIONS}
showSorterTooltip={false}
rowKey="id"
rowKey={(record) => record.metadata.name}
onChange={handleTableChange}
pagination={{
size: 'middle',
@@ -1,5 +1,7 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { useModel } from '@@/plugin-model';
import { useQueryData } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react';
import { apiVersion, KindMapping } from '../../constants';
import { createGPUServiceInstance } from '../apis';
@@ -12,12 +14,15 @@ interface CreateInstanceParams {
export default function useCreateInstance() {
const { initialState } = useModel('@@initialState');
const namespace = initialState?.currentUser?.org_name || 'default';
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
const fetchDetail = useCallback(
(params: CreateInstanceParams, option?: any) =>
createGPUServiceInstance(
{
namespace,
clusterID,
data: {
apiVersion,
kind: KindMapping.instance,
@@ -30,7 +35,7 @@ export default function useCreateInstance() {
},
option
),
[namespace]
[namespace, clusterID]
);
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
@@ -1,17 +1,27 @@
import { useModel } from '@@/plugin-model';
import { currentClusterAtom } from '@/atoms/gpuservice';
import { useQueryData } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react';
import { queryGPUServiceInstanceTypes } from '../apis';
import { transformInstanceType } from '../config';
import { InstanceTypeItem } from '../config/types';
export default function useQueryInstanceTypes() {
const { initialState } = useModel('@@initialState');
const namespace = initialState?.currentUser?.org_name || 'default';
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
const fetchDetail = useCallback(
(params: Global.K8sSearchParams = {}, options?: any) =>
queryGPUServiceInstanceTypes({ ...params, namespace }, options),
[namespace]
async (params: Global.K8sSearchParams = {}, options?: any) => {
const res = await queryGPUServiceInstanceTypes(
{ ...params, clusterID },
options
);
return {
...res,
items: (res?.items || []).map(transformInstanceType)
};
},
[clusterID]
);
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
@@ -1,5 +1,7 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { useModel } from '@@/plugin-model';
import { useQueryData } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react';
import { apiVersion, KindMapping } from '../../constants';
import { updateGPUServiceInstance } from '../apis';
@@ -13,12 +15,15 @@ interface UpdateInstanceParams {
export default function useUpdateInstance() {
const { initialState } = useModel('@@initialState');
const namespace = initialState?.currentUser?.org_name || 'default';
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
const fetchDetail = useCallback(
(params: UpdateInstanceParams, option?: any) =>
updateGPUServiceInstance(
{
namespace,
clusterID,
id: params.id,
data: {
apiVersion,
@@ -32,7 +37,7 @@ export default function useUpdateInstance() {
},
option
),
[namespace]
[namespace, clusterID]
);
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
@@ -3,10 +3,7 @@ import { FormData, ListItem } from '../types';
export const GPU_SERVICE_PUBLIC_KEY_API = '/gpu-instance-ssh-public-keys/data';
export async function queryGPUServicePublicKeys(
params: Global.SearchParams,
options?: any
) {
export async function queryGPUServicePublicKeys(params: {}, options?: any) {
return request<ListItem>(GPU_SERVICE_PUBLIC_KEY_API, {
method: 'GET',
params,
+2 -2
View File
@@ -16,7 +16,7 @@ const GPUServicePublicKeys: React.FC = () => {
const { fetchData: updateSshkey, loading: updating } = useUpdateSshkey();
useEffect(() => {
fetchSshkey({ page: 1, perPage: 100 }).then((res) => {
fetchSshkey({}).then((res) => {
const data = res?.spec?.data || '';
form.setFieldsValue({
name: res?.name,
@@ -76,7 +76,7 @@ const GPUServicePublicKeys: React.FC = () => {
trim={false}
alwaysFocus
autoSize={{ minRows: 8, maxRows: 16 }}
style={{ width: INPUT_WIDTH.large }}
style={{ width: INPUT_WIDTH.large, minHeight: 186 }}
/>
</Form.Item>
<Button
@@ -5,8 +5,7 @@ import { ListItem } from '../types';
export default function useGetSshkey() {
const fetchDetail = useCallback(
(params: Global.SearchParams = { page: 1, perPage: 100 }, options?: any) =>
queryGPUServicePublicKeys(params, options),
(params: {}, options?: any) => queryGPUServicePublicKeys(params, options),
[]
);
+60 -14
View File
@@ -1,15 +1,27 @@
import { request } from '@umijs/max';
import { ListItem } from '../config/types';
import { ListItem, StorageClassItem } from '../config/types';
export const GPU_SERVICE_STORAGE_API = (namespace: string) =>
`/proxy/apis/worker.gpustack.ai/v1/namespaces/${namespace}/instancepersistentvolumes`;
export const GPU_SERVICE_STORAGE_API = (params: {
namespace: string;
clusterID?: number;
}) =>
`/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instancepersistentvolumes`;
export const STORAGE_CLASS_API = (params: { clusterID?: number }) =>
`/clusters/${params.clusterID}/proxy/apis/storage.k8s.io/v1/storageclasses`;
export async function queryGPUServiceStorage(
params: Global.K8sSearchParams,
params: Global.K8sSearchParams & {
namespace: string;
clusterID?: number;
},
options?: any
) {
return request<Global.K8sPageResponse<ListItem>>(
GPU_SERVICE_STORAGE_API(params.namespace || ''),
GPU_SERVICE_STORAGE_API({
namespace: params.namespace,
clusterID: params.clusterID
}),
{
method: 'GET',
params,
@@ -21,27 +33,38 @@ export async function queryGPUServiceStorage(
export async function createGPUServiceStorage(
params: {
namespace: string;
clusterID?: number;
data: Global.K8sCommonData;
},
option?: any
) {
return request<ListItem>(GPU_SERVICE_STORAGE_API(params.namespace), {
method: 'POST',
data: params.data,
cancelToken: option?.token
});
return request<ListItem>(
GPU_SERVICE_STORAGE_API({
namespace: params.namespace,
clusterID: params.clusterID
}),
{
method: 'POST',
data: params.data,
cancelToken: option?.token
}
);
}
export async function updateGPUServiceStorage(
params: {
namespace: string;
clusterID?: number;
id: number;
data: Global.K8sCommonData;
},
option?: any
) {
return request<ListItem>(
`${GPU_SERVICE_STORAGE_API(params.namespace)}/${params.id}`,
`${GPU_SERVICE_STORAGE_API({
namespace: params.namespace,
clusterID: params.clusterID
})}/${params.id}`,
{
method: 'PUT',
data: params.data,
@@ -52,9 +75,32 @@ export async function updateGPUServiceStorage(
export async function deleteGPUServiceStorage(params: {
namespace: string;
clusterID?: number;
id: number;
}) {
return request(`${GPU_SERVICE_STORAGE_API(params.namespace)}/${params.id}`, {
method: 'DELETE'
});
return request(
`${GPU_SERVICE_STORAGE_API({
namespace: params.namespace,
clusterID: params.clusterID
})}/${params.id}`,
{
method: 'DELETE'
}
);
}
export async function queryStorageClass(
params: Global.K8sSearchParams & {
clusterID?: number;
},
options?: any
) {
return request<Global.K8sPageResponse<StorageClassItem>>(
STORAGE_CLASS_API({ clusterID: params.clusterID }),
{
method: 'GET',
params,
cancelToken: options?.token
}
);
}
+28 -4
View File
@@ -11,14 +11,38 @@ export interface FormData {
namespace: string;
};
spec: {
accessMode: AccessModeType;
capacity: string;
type: string;
};
}
export interface ListItem extends FormData {
export interface ManagedField {
manager: string;
operation: string;
apiVersion: string;
time: string;
fieldsType: string;
fieldsV1: Record<string, any>;
}
export interface Metadata {
name: string;
namespace?: string;
uid: string;
resourceVersion: string;
creationTimestamp: string;
annotations?: Record<string, string>;
managedFields?: ManagedField[];
}
export interface ListItem extends Omit<FormData, 'metadata'> {
id: number;
created_at?: string;
updated_at?: string;
metadata: Metadata;
}
export interface StorageClassItem {
metadata: Metadata;
provisioner: string;
reclaimPolicy: 'Delete' | 'Retain';
volumeBindingMode: 'Immediate' | 'WaitForFirstConsumer';
}
+32 -27
View File
@@ -1,9 +1,24 @@
import { Input as CInput, Select as SealSelect } from '@gpustack/core-ui';
import { Form } from 'antd';
import { AccessModeOptions, StorageTypeOptions } from '../config';
import {
Input as CInput,
InputNumber,
Select as SealSelect,
useAppUtils
} from '@gpustack/core-ui';
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 { getRuleMessage } = useAppUtils();
const { storageClassList, fetchData, loading } = useQueryStorageClass();
useEffect(() => {
if (open) {
fetchData({});
}
}, [open]);
const Basic = () => {
return (
<>
<Form.Item<FormData>
@@ -11,59 +26,49 @@ const Basic = () => {
rules={[
{
required: true,
message: '请输入存储名称'
message: getRuleMessage('input', '名称')
}
]}
>
<CInput.Input label="名称" required />
</Form.Item>
<div style={{ display: 'flex', gap: 16 }}>
<Flex gap={16}>
<div style={{ flex: 1 }}>
<Form.Item<FormData>
name={['spec', 'type']}
rules={[
{
required: true,
message: '请选择存储类型'
message: getRuleMessage('select', '存储类型')
}
]}
>
<SealSelect
label="存储类型"
required
options={StorageTypeOptions}
></SealSelect>
loading={loading}
options={storageClassList}
/>
</Form.Item>
</div>
<div style={{ flex: 1 }}>
<Form.Item<FormData>
name={['spec', 'capacity']}
normalize={(value) => (value ? `${value}Gi` : undefined)}
getValueProps={(value) => ({
value: value ? String(value).replace(/Gi$/, '') : ''
})}
rules={[
{
required: true,
message: '请输入容量'
message: getRuleMessage('input', '容量')
}
]}
>
<CInput.Input label="容量" required placeholder="例如:10Gi" />
<InputNumber label="容量" required />
</Form.Item>
</div>
</div>
<Form.Item<FormData>
name={['spec', 'accessMode']}
rules={[
{
required: true,
message: '请选择访问模式'
}
]}
>
<SealSelect
label="存储卷访问方式"
required
options={AccessModeOptions}
></SealSelect>
</Form.Item>
</Flex>
</>
);
};
+3 -16
View File
@@ -2,7 +2,6 @@ import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import { Form } from 'antd';
import { forwardRef, useEffect, useImperativeHandle } from 'react';
import { AccessModeValueMap, StorageTypeValueMap } from '../config';
import { FormData, ListItem } from '../config/types';
import Basic from './basic';
@@ -39,25 +38,12 @@ const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
namespace: currentData.metadata?.namespace
},
spec: {
accessMode: currentData.spec?.accessMode,
capacity: currentData.spec?.capacity,
type: currentData.spec?.type
}
});
return;
}
form.setFieldsValue({
metadata: {
namespace
},
spec: {
accessMode:
AccessModeValueMap.ReadWriteOnce as FormData['spec']['accessMode'],
type: StorageTypeValueMap.Local
}
});
}, [action, currentData, form, open, namespace]);
}, [action, currentData, form, open]);
useImperativeHandle(ref, () => ({
submit: () => {
@@ -74,8 +60,9 @@ const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
form={form}
onFinish={onFinish}
preserve={false}
initialValues={{}}
>
<Basic />
<Basic open={open} />
</Form>
);
}
@@ -54,8 +54,8 @@ const useStorageColumns = ({
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
dataIndex: ['metadata', 'creationTimestamp'],
key: 'creationTimestamp',
sorter: tableSorter(5),
ellipsis: {
showTitle: false
+47 -10
View File
@@ -1,7 +1,9 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
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 { ProviderValueMap } from '@/pages/cluster-management/config';
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
import { useModel } from '@@/plugin-model';
import {
@@ -14,8 +16,9 @@ import {
import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, Divider, Flex, message, Table } from 'antd';
import { useAtom } from 'jotai';
import _ from 'lodash';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { PageContainerInner } from '../../_components/page-box';
import {
deleteGPUServiceStorage,
@@ -32,10 +35,12 @@ import useUpdateStorage from './services/use-update-storage';
const GPUServiceStorage: React.FC = () => {
const { initialState } = useModel('@@initialState');
const namespace = initialState?.currentUser?.org_name || 'default';
const [currentCluster, setCurrentCluster] = useAtom(currentClusterAtom);
const clusterID = currentCluster?.id;
const deleteStorage = useCallback(
(id: number) => deleteGPUServiceStorage({ namespace, id }),
[namespace]
(id: number) => deleteGPUServiceStorage({ namespace, clusterID, id }),
[namespace, clusterID]
);
const fetchStorage = useCallback(
@@ -43,8 +48,19 @@ const GPUServiceStorage: React.FC = () => {
params: any,
options?: any
): Promise<Global.PageResponse<ListItem>> => {
if (!clusterID) {
return {
items: [],
pagination: {
total: 0,
totalPage: 0,
page: 1,
perPage: params.perPage || 10
}
} as Global.PageResponse<ListItem>;
}
const res = await queryGPUServiceStorage(
{ ...params, namespace },
{ ...params, namespace, clusterID: params.cluster_id ?? clusterID },
options
);
const total = res.items?.length ?? 0;
@@ -59,7 +75,7 @@ const GPUServiceStorage: React.FC = () => {
}
};
},
[namespace]
[namespace, clusterID]
);
const {
@@ -81,7 +97,7 @@ const GPUServiceStorage: React.FC = () => {
fetchAPI: fetchStorage,
deleteAPI: deleteStorage,
watch: false,
API: GPU_SERVICE_STORAGE_API(namespace),
API: GPU_SERVICE_STORAGE_API({ namespace, clusterID }),
contentForDelete: '存储'
});
@@ -94,6 +110,14 @@ const GPUServiceStorage: React.FC = () => {
} = useQueryClusterList();
const intl = useIntl();
const k8sClusterList = useMemo(
() =>
clusterList.filter(
(item) => item.provider === ProviderValueMap.Kubernetes
),
[clusterList]
);
const [openAddModalStatus, setOpenAddModalStatus] = useState<{
action: PageActionType;
open: boolean;
@@ -108,9 +132,18 @@ const GPUServiceStorage: React.FC = () => {
useEffect(() => {
fetchClusterList({ page: -1 }).then((clusters) => {
if (clusters.length > 0) {
const k8sClusters = clusters.filter(
(item: any) => item.provider === ProviderValueMap.Kubernetes
);
if (k8sClusters.length > 0) {
const firstCluster = k8sClusters[0];
setCurrentCluster({
...firstCluster,
label: firstCluster.name,
value: firstCluster.id
});
handleQueryChange({
cluster_id: clusters[0].id,
cluster_id: firstCluster.id,
page: 1
});
}
@@ -175,6 +208,10 @@ const GPUServiceStorage: React.FC = () => {
});
const handleClusterChange = (value: number) => {
const cluster = k8sClusterList.find((item) => item.value === value);
if (cluster) {
setCurrentCluster(cluster);
}
handleQueryChange({
cluster_id: value,
page: 1
@@ -219,7 +256,7 @@ const GPUServiceStorage: React.FC = () => {
<BaseSelect
variant="borderless"
value={queryParams.cluster_id}
options={clusterList}
options={k8sClusterList}
style={{ minWidth: 120, fontWeight: 500 }}
onChange={handleClusterChange}
></BaseSelect>
@@ -230,7 +267,7 @@ const GPUServiceStorage: React.FC = () => {
marginBottom={22}
marginTop={30}
showSelect={false}
selectOptions={clusterList}
selectOptions={k8sClusterList}
select={{ showSearch: true }}
selectHolder="按集群过滤"
buttonText="添加存储"
@@ -1,5 +1,7 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { useModel } from '@@/plugin-model';
import { useQueryData } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react';
import { apiVersion, KindMapping } from '../../constants';
import { createGPUServiceStorage } from '../apis';
@@ -12,12 +14,15 @@ interface CreateStorageParams {
export default function useCreateStorage() {
const { initialState } = useModel('@@initialState');
const namespace = initialState?.currentUser?.org_name || 'default';
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
const fetchDetail = useCallback(
(params: CreateStorageParams, option?: any) =>
createGPUServiceStorage(
{
namespace,
clusterID,
data: {
apiVersion,
kind: KindMapping.instancePersistentVolume,
@@ -30,7 +35,7 @@ export default function useCreateStorage() {
},
option
),
[namespace]
[namespace, clusterID]
);
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
@@ -0,0 +1,34 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { useQueryDataList } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react';
import { queryStorageClass } from '../apis';
import { StorageClassItem } from '../config/types';
export default function useQueryStorageClass() {
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
const fetchList = useCallback(
(params: Global.K8sSearchParams = {}, options?: any) =>
queryStorageClass({ ...params, clusterID }, options),
[clusterID]
);
const { dataList, loading, cancelRequest, fetchData } = useQueryDataList<
StorageClassItem,
Global.K8sSearchParams
>({
key: 'storageClass',
fetchList,
getLabel: (item) => item.metadata.name,
getValue: (item) => item.metadata.name
});
return {
storageClassList: dataList as Array<{ label: string; value: string }>,
loading,
cancelRequest,
fetchData
};
}
@@ -1,5 +1,7 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { useModel } from '@@/plugin-model';
import { useQueryData } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react';
import { queryGPUServiceStorage } from '../apis';
import { ListItem } from '../config/types';
@@ -7,11 +9,13 @@ import { ListItem } from '../config/types';
export default function useQueryStorage() {
const { initialState } = useModel('@@initialState');
const namespace = initialState?.currentUser?.org_name || 'default';
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
const fetchDetail = useCallback(
(params: Global.K8sSearchParams = {}, options?: any) =>
queryGPUServiceStorage({ ...params, namespace }, options),
[namespace]
queryGPUServiceStorage({ ...params, namespace, clusterID }, options),
[namespace, clusterID]
);
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
@@ -1,5 +1,7 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { useModel } from '@@/plugin-model';
import { useQueryData } from '@gpustack/core-ui';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react';
import { apiVersion, KindMapping } from '../../constants';
import { updateGPUServiceStorage } from '../apis';
@@ -13,12 +15,15 @@ interface UpdateStorageParams {
export default function useUpdateStorage() {
const { initialState } = useModel('@@initialState');
const namespace = initialState?.currentUser?.org_name || 'default';
const currentCluster = useAtomValue(currentClusterAtom);
const clusterID = currentCluster?.id;
const fetchDetail = useCallback(
(params: UpdateStorageParams, option?: any) =>
updateGPUServiceStorage(
{
namespace,
clusterID,
id: params.id,
data: {
apiVersion,
@@ -32,7 +37,7 @@ export default function useUpdateStorage() {
},
option
),
[namespace]
[namespace, clusterID]
);
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
+19 -16
View File
@@ -50,23 +50,24 @@ const Basic: React.FC<BasicProps> = ({ page = 'template' }) => {
required
/>
</Form.Item>
<Form.Item<FormData>
name="manufacturer"
rules={[
{
required: true,
message: getRuleMessage('select', 'resources.table.vendor')
}
]}
>
<SealSelect
label={intl.formatMessage({ id: 'resources.table.vendor' })}
required
options={[...manufacturerOptions, { label: 'CPU', value: 'cpu' }]}
/>
</Form.Item>
</>
)}
<Form.Item<FormData>
name="manufacturer"
rules={[
{
required: true,
message: getRuleMessage('select', 'resources.table.vendor')
}
]}
>
<SealSelect
label={intl.formatMessage({ id: 'resources.table.vendor' })}
required
options={[...manufacturerOptions, { label: 'CPU', value: 'cpu' }]}
/>
</Form.Item>
<Form.Item<FormData>
name={['spec', 'image']}
rules={[
@@ -112,7 +113,9 @@ const Basic: React.FC<BasicProps> = ({ page = 'template' }) => {
<div style={{ flex: 1 }}>
<Form.Item<FormData> name={['spec', 'volumeMount']}>
<CInput.Input
label={intl.formatMessage({ id: 'gpuservice.template.mountPath' })}
label={intl.formatMessage({
id: 'gpuservice.template.mountPath'
})}
placeholder={intl.formatMessage({
id: 'clusters.volume.mountPath.format'
})}