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 { 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>(
|
||||||
method: 'POST',
|
GPU_SERVICE_INSTANCES_API({
|
||||||
data: params.data,
|
namespace: params.namespace,
|
||||||
cancelToken: option?.token
|
clusterID: params.clusterID
|
||||||
});
|
}),
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
data: params.data,
|
||||||
|
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({
|
||||||
type: name,
|
spec: {
|
||||||
spec: { resources: { accelerator: '1' } }
|
type: name,
|
||||||
|
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>
|
||||||
<Tag
|
<Flex gap={8} align="center">
|
||||||
style={{
|
{item.status?.phase && (
|
||||||
fontWeight: 400,
|
<StatusTag
|
||||||
color: 'var(--ant-color-text-tertiary)'
|
statusValue={{
|
||||||
}}
|
status:
|
||||||
>
|
InstanceTypePhaseStatus[item.status.phase] ??
|
||||||
<span>库存 {item.status?.accelerator?.remaining ?? '-'}</span>
|
'inactive',
|
||||||
</Tag>
|
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>
|
</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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
<Form.Item<FormData> hidden name={['spec', 'sshPublicKey', 'name']}>
|
||||||
name="enable_ssh"
|
<CInput.Input />
|
||||||
valuePropName="checked"
|
|
||||||
style={{ marginBottom: 8 }}
|
|
||||||
>
|
|
||||||
<CheckboxField label="SSH Terminal Access" />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
<Form.Item<FormData> hidden name={['spec', 'sshPublicKey', 'name']}>
|
|
||||||
<CInput.Input />
|
|
||||||
</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>
|
||||||
<span>显存 {selected?.spec?.memory ?? '-'}</span>
|
{selected?.spec?.acceleratable && (
|
||||||
|
<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,54 +143,40 @@ const InstanceTypeFormItem = () => {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InstanceTypePicker dataList={dataList} />
|
<InstanceTypePicker dataList={dataList} value={typeName} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
<Form.Item
|
{selectedInstanceType?.spec?.acceleratable && (
|
||||||
name={['spec', 'resources', 'accelerator']}
|
<Form.Item
|
||||||
rules={[
|
name={['spec', 'resources', 'accelerator']}
|
||||||
{
|
rules={[
|
||||||
required: true,
|
{
|
||||||
message: '请输入 GPU 数量'
|
required: true,
|
||||||
},
|
message: '请输入 GPU 数量'
|
||||||
{
|
},
|
||||||
validator: (_, value) => {
|
{
|
||||||
if (value > maxGpuCount) {
|
validator: (_, value) => {
|
||||||
return Promise.reject(
|
if (value > maxGpuCount) {
|
||||||
new Error(`当前实例类型最多支持 ${maxGpuCount} 个 GPU`)
|
return Promise.reject(
|
||||||
);
|
new Error(`当前实例类型最多支持 ${maxGpuCount} 个 GPU`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
}
|
]}
|
||||||
]}
|
>
|
||||||
>
|
<CInputNumber
|
||||||
<CInputNumber
|
min={1}
|
||||||
min={1}
|
max={maxGpuCount}
|
||||||
max={maxGpuCount}
|
precision={0}
|
||||||
precision={0}
|
label="GPU 数量"
|
||||||
label="GPU 数量"
|
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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,23 +87,15 @@ const StorageVolume = () => {
|
|||||||
marginBottom: 6
|
marginBottom: 6
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Radio.Group
|
||||||
name="storage_mode"
|
|
||||||
style={{ marginBottom: 12 }}
|
style={{ marginBottom: 12 }}
|
||||||
rules={[
|
value={storageMode}
|
||||||
{
|
onChange={(e) => handleModeChange(e.target.value)}
|
||||||
required: true,
|
options={[
|
||||||
message: '请选择存储卷'
|
{ label: '持久存储', value: StorageModeValueMap.Existing },
|
||||||
}
|
{ label: '临时存储', value: StorageModeValueMap.Temporary }
|
||||||
]}
|
]}
|
||||||
>
|
/>
|
||||||
<Radio.Group
|
|
||||||
options={[
|
|
||||||
{ label: '持久存储', value: StorageModeValueMap.Existing },
|
|
||||||
{ 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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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>(
|
||||||
method: 'POST',
|
GPU_SERVICE_STORAGE_API({
|
||||||
data: params.data,
|
namespace: params.namespace,
|
||||||
cancelToken: option?.token
|
clusterID: params.clusterID
|
||||||
});
|
}),
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
data: params.data,
|
||||||
|
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(
|
||||||
method: 'DELETE'
|
`${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;
|
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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,23 +50,24 @@ const Basic: React.FC<BasicProps> = ({ page = 'template' }) => {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</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>
|
<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'
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user