fix: api test
This commit is contained in:
@@ -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);
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
[]
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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'
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user