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