fix: storage api alignment
This commit is contained in:
+13
-3
@@ -192,9 +192,9 @@ const baseRoutes = [
|
||||
name: 'instances',
|
||||
path: '/gpu-service/instances',
|
||||
key: 'gpuServiceList',
|
||||
icon: 'icon-instances-outlined',
|
||||
selectedIcon: 'icon-instances-filled',
|
||||
defaultIcon: 'icon-instances-outlined',
|
||||
icon: 'icon-cloud-outlined',
|
||||
selectedIcon: 'icon-cloud-filled',
|
||||
defaultIcon: 'icon-cloud-outlined',
|
||||
access: 'canSeeAdmin',
|
||||
component: './gpu-service/instances'
|
||||
},
|
||||
@@ -217,6 +217,16 @@ const baseRoutes = [
|
||||
defaultIcon: 'icon-storage-outlined',
|
||||
access: 'canSeeAdmin',
|
||||
component: './gpu-service/storage'
|
||||
},
|
||||
{
|
||||
name: 'publicKeys',
|
||||
path: '/gpu-service/public-keys',
|
||||
key: 'gpuServicePublicKeys',
|
||||
icon: 'icon-ssh-outlined',
|
||||
selectedIcon: 'icon-ssh-filled',
|
||||
defaultIcon: 'icon-ssh-outlined',
|
||||
access: 'canSeeAdmin',
|
||||
component: './gpu-service/public-keys'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Vendored
+38
@@ -16,6 +16,43 @@ declare namespace Global {
|
||||
};
|
||||
}
|
||||
|
||||
interface K8sPageResponse<T> {
|
||||
apiVersion: string;
|
||||
items: T[];
|
||||
kind: string;
|
||||
metadata: {
|
||||
continue?: string;
|
||||
resourceVersion: string;
|
||||
selfLink?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
interface K8sCommonData {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
metadata: {
|
||||
name: string;
|
||||
namespace: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface K8sSearchParams {
|
||||
allowWatchBookmarks?: boolean;
|
||||
continue?: string;
|
||||
fieldSelector?: string;
|
||||
labelSelector?: string;
|
||||
limit?: number;
|
||||
resourceVersion?: string;
|
||||
resourceVersionMatch?: string;
|
||||
sendInitialEvents?: boolean;
|
||||
timeoutSeconds?: number;
|
||||
watch?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
username: string;
|
||||
is_admin: boolean;
|
||||
@@ -24,6 +61,7 @@ declare namespace Global {
|
||||
id: number;
|
||||
source: string;
|
||||
avatar_url: string;
|
||||
org_name?: string;
|
||||
}
|
||||
type EmptyObject = Record<never, never>;
|
||||
|
||||
|
||||
@@ -72,3 +72,16 @@ export const lengthReg = /^.{6,64}$/;
|
||||
*/
|
||||
export const modelNameReg =
|
||||
/^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,61}[A-Za-z0-9])?$/g;
|
||||
|
||||
/**
|
||||
* Label name rules:
|
||||
* 1. no more than 63 characters
|
||||
* 2. contain only lowercase alphanumeric characters and '-'
|
||||
* 3. start and end with an alphanumeric character
|
||||
* 4. must not contain consecutive '-' characters
|
||||
* 5. must not start with a digit
|
||||
* 6. must not end with a '-'
|
||||
*/
|
||||
|
||||
export const validateLabelNameRegxFor63 =
|
||||
/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
||||
|
||||
@@ -356,7 +356,7 @@ export default (props: any) => {
|
||||
config={{
|
||||
apiBaseUrl: GPUSTACK_API_BASE_URL,
|
||||
theme: userSettings.theme,
|
||||
iconUrl: '//at.alicdn.com/t/c/font_4613488_tzcsatubq4f.js',
|
||||
iconUrl: '//at.alicdn.com/t/c/font_4613488_ieeghfm8o3o.js',
|
||||
isDarkTheme: userSettings.isDarkTheme,
|
||||
defaultColorPrimary: COLOR_PRIMARY
|
||||
}}
|
||||
|
||||
@@ -44,5 +44,6 @@ export default {
|
||||
'menu.gpuService': 'GPU Service',
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
'menu.gpuService.storage': 'Storage'
|
||||
'menu.gpuService.storage': 'Storage',
|
||||
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
||||
};
|
||||
|
||||
@@ -44,7 +44,8 @@ export default {
|
||||
'menu.gpuService': 'GPU Service',
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
'menu.gpuService.storage': 'Storage'
|
||||
'menu.gpuService.storage': 'Storage',
|
||||
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
|
||||
@@ -44,7 +44,8 @@ export default {
|
||||
'menu.gpuService': 'GPU Service',
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
'menu.gpuService.storage': 'Storage'
|
||||
'menu.gpuService.storage': 'Storage',
|
||||
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
|
||||
@@ -44,7 +44,8 @@ export default {
|
||||
'menu.gpuService': 'GPU Service',
|
||||
'menu.gpuService.instances': 'GPU Instances',
|
||||
'menu.gpuService.templates': 'Instance Templates',
|
||||
'menu.gpuService.storage': 'Storage'
|
||||
'menu.gpuService.storage': 'Storage',
|
||||
'menu.gpuService.publicKeys': 'SSH Public Keys'
|
||||
};
|
||||
|
||||
// ========== To-Do: Translate Keys (Remove After Translation) ==========
|
||||
|
||||
@@ -44,5 +44,6 @@ export default {
|
||||
'menu.gpuService': 'GPU 服务',
|
||||
'menu.gpuService.instances': 'GPU 实例',
|
||||
'menu.gpuService.templates': '实例模板',
|
||||
'menu.gpuService.storage': '存储'
|
||||
'menu.gpuService.storage': '存储',
|
||||
'menu.gpuService.publicKeys': 'SSH 公钥'
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const apiVersion = 'worker.gpustack.ai/v1';
|
||||
|
||||
export const KindMapping = {
|
||||
sshPublicKey: 'InstanceSSHPublicKey',
|
||||
instance: 'Instance',
|
||||
instancePersistentVolume: 'InstancePersistentVolume',
|
||||
instanceTemplate: 'InstanceTemplate'
|
||||
};
|
||||
@@ -1,11 +1,15 @@
|
||||
import { request } from '@umijs/max';
|
||||
import { mockInstanceData } from '../config/mock-data';
|
||||
import { FormData, ListItem } from '../config/types';
|
||||
import { FormData, InstanceTypeItem, ListItem } from '../config/types';
|
||||
|
||||
export const GPU_SERVICE_INSTANCES_API = '/gpu-service-instances';
|
||||
export const GPU_SERVICE_INSTANCES_API = (namespace: string) =>
|
||||
`/proxy/apis/worker.gpustack.ai/v1/namespaces/${namespace}/instances`;
|
||||
|
||||
export const GPU_SERVICE_INSTANCES_TYPE_API =
|
||||
'/proxy/apis/worker.gpustack.ai/v1/instancetypes';
|
||||
|
||||
export async function queryGPUServiceInstances(
|
||||
params: Global.SearchParams,
|
||||
params: Global.K8sSearchParams,
|
||||
options?: any
|
||||
) {
|
||||
// return request<Global.PageResponse<ListItem>>(GPU_SERVICE_INSTANCES_API, {
|
||||
@@ -35,7 +39,7 @@ export async function queryGPUServiceInstances(
|
||||
page,
|
||||
perPage
|
||||
}
|
||||
} as Global.PageResponse<ListItem>;
|
||||
} as Global.K8sPageResponse<ListItem>;
|
||||
}
|
||||
|
||||
export async function createGPUServiceInstance(params: { data: FormData }) {
|
||||
@@ -62,3 +66,35 @@ export async function deleteGPUServiceInstance(id: number) {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
// =========== Instance Types ===========
|
||||
|
||||
export async function queryGPUServiceInstanceTypes(
|
||||
params: Global.K8sSearchParams,
|
||||
options?: any
|
||||
) {
|
||||
return request<Global.K8sPageResponse<InstanceTypeItem>>(
|
||||
GPU_SERVICE_INSTANCES_TYPE_API,
|
||||
{
|
||||
method: 'GET',
|
||||
params,
|
||||
cancelToken: options?.token
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function queryGPUServiceInstanceTypeItems(
|
||||
params: {
|
||||
name: string;
|
||||
},
|
||||
options?: any
|
||||
) {
|
||||
return request<InstanceTypeItem>(
|
||||
`${GPU_SERVICE_INSTANCES_TYPE_API}/${params.name}`,
|
||||
{
|
||||
method: 'GET',
|
||||
params,
|
||||
cancelToken: options?.token
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,57 @@
|
||||
export interface FormData {
|
||||
export type ImagePullPolicy = 'Always' | 'IfNotPresent' | 'Never';
|
||||
|
||||
export interface InstanceEnvVar {
|
||||
name: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
instance_type?: string;
|
||||
instance_type_id?: number;
|
||||
template_id?: number;
|
||||
gpu_count?: number;
|
||||
replicas?: number;
|
||||
storage_mode?: string;
|
||||
storage_id?: number;
|
||||
local_storage_size_gb?: number;
|
||||
cluster_id?: number;
|
||||
mount_path?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface InstancePort {
|
||||
port: number;
|
||||
protocol?: string;
|
||||
}
|
||||
|
||||
export interface InstanceResources {
|
||||
cpu?: string;
|
||||
ram?: string;
|
||||
localStorage?: string;
|
||||
accelerator?: string;
|
||||
}
|
||||
|
||||
export interface InstanceVolume {
|
||||
ephemeral?: {
|
||||
capacity?: string;
|
||||
};
|
||||
persistent?: {
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
// instance form data
|
||||
export interface FormData {
|
||||
metadata: {
|
||||
name: string;
|
||||
namespace: string;
|
||||
};
|
||||
spec: {
|
||||
image: string;
|
||||
displayName: string;
|
||||
command: string[];
|
||||
ports: InstancePort[];
|
||||
env: InstanceEnvVar[];
|
||||
volumeMount: string;
|
||||
resources: InstanceResources;
|
||||
description: string;
|
||||
volume: InstanceVolume;
|
||||
sshPublicKey: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// instance list item
|
||||
export interface ListItem extends FormData {
|
||||
id: number;
|
||||
status?: string;
|
||||
endpoint?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
@@ -31,3 +65,42 @@ export interface InstanceItem {
|
||||
gpu_count: number;
|
||||
status: string; // available, unavailable
|
||||
}
|
||||
|
||||
export interface InstanceTypeSpec {
|
||||
acceleratable: boolean;
|
||||
computeCapability?: string;
|
||||
family?: string;
|
||||
group: string;
|
||||
manufacturer?: string;
|
||||
memory?: string;
|
||||
product?: string;
|
||||
sliced?: number;
|
||||
}
|
||||
|
||||
export interface InstanceTypeResource {
|
||||
capacity?: string;
|
||||
onceMaxRequest?: string;
|
||||
remaining?: string;
|
||||
}
|
||||
|
||||
export interface InstanceTypeStatus {
|
||||
accelerator: InstanceTypeResource;
|
||||
cpu: InstanceTypeResource;
|
||||
localStorage: InstanceTypeResource;
|
||||
phase: string;
|
||||
phaseMessage?: string;
|
||||
ram: InstanceTypeResource;
|
||||
}
|
||||
|
||||
export interface InstanceTypeItem {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
metadata: {
|
||||
name: string;
|
||||
namespace: string;
|
||||
};
|
||||
id: number;
|
||||
name: string;
|
||||
spec: InstanceTypeSpec;
|
||||
status: InstanceTypeStatus;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import {
|
||||
CheckboxField,
|
||||
Input as CInput,
|
||||
CollapsePanel,
|
||||
IconFont,
|
||||
ScrollSpyTabs,
|
||||
@@ -206,6 +208,20 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<Form.Item<FormData>
|
||||
name="enable_ssh"
|
||||
valuePropName="checked"
|
||||
style={{ marginBottom: 8 }}
|
||||
>
|
||||
<CheckboxField label={'SSH Terminal Access'}></CheckboxField>
|
||||
</Form.Item>
|
||||
<Form.Item<FormData>
|
||||
data-field="name"
|
||||
hidden
|
||||
name={['spec', 'sshPublicKey', 'name']}
|
||||
>
|
||||
<CInput.Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</ScrollSpyTabs>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { InputNumber as CInputNumber, Input, Select } from '@gpustack/core-ui';
|
||||
import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui';
|
||||
import { Button, Flex, Form, Radio } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { StorageStatusValueMap } from '../../storage/config';
|
||||
import { mockStorageData } from '../../storage/config/mock-data';
|
||||
import { StorageModeValueMap } from '../config';
|
||||
import { FormData } from '../config/types';
|
||||
@@ -57,29 +56,24 @@ const StorageVolume = () => {
|
||||
</Flex>
|
||||
|
||||
{storageMode === StorageModeValueMap.Existing && (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Form.Item<FormData>
|
||||
name="storage_id"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择预存储卷'
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
label="持久卷"
|
||||
required
|
||||
options={mockStorageData.map((item) => ({
|
||||
label: `${item.name} / ${item.capacity_gb ?? '-'} GB`,
|
||||
value: item.id,
|
||||
disabled: item.status !== StorageStatusValueMap.Available
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item<FormData>
|
||||
name="storage_id"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择预存储卷'
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
label="持久卷"
|
||||
required
|
||||
options={mockStorageData.map((item) => ({
|
||||
label: `${item.metadata?.name} / ${item.spec?.capacity ?? '-'}`,
|
||||
value: item.id
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{storageMode === StorageModeValueMap.Temporary && (
|
||||
@@ -95,22 +89,6 @@ const StorageVolume = () => {
|
||||
<CInputNumber min={1} precision={0} label="存储容量 (GB)" required />
|
||||
</Form.Item>
|
||||
)}
|
||||
{/* 挂载路径 */}
|
||||
<Form.Item<FormData>
|
||||
name="mount_path"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入存储挂载路径'
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input.Input
|
||||
label="挂载路径"
|
||||
required
|
||||
placeholder="挂载路径须以 / 开头"
|
||||
/>
|
||||
</Form.Item>
|
||||
</FieldBlock>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { request } from '@umijs/max';
|
||||
import { FormData, ListItem } from '../types';
|
||||
|
||||
export const GPU_SERVICE_PUBLIC_KEY_API = (namespace: string) =>
|
||||
`/proxy/apis/worker.gpustack.ai/v1/namespaces/${namespace}/instancesshpublickeys`;
|
||||
|
||||
export async function queryGPUServicePublicKeys(
|
||||
params: Global.K8sSearchParams,
|
||||
options?: any
|
||||
) {
|
||||
return request<Global.K8sPageResponse<ListItem>>(
|
||||
GPU_SERVICE_PUBLIC_KEY_API(params.namespace || ''),
|
||||
{
|
||||
method: 'GET',
|
||||
params,
|
||||
cancelToken: options?.token
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function createGPUServicePublicKey(params: {
|
||||
namespace: string;
|
||||
data: FormData;
|
||||
}) {
|
||||
return request<ListItem>(GPU_SERVICE_PUBLIC_KEY_API(params.namespace), {
|
||||
method: 'POST',
|
||||
data: params.data
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateGPUServicePublicKey(params: {
|
||||
namespace: string;
|
||||
id: number;
|
||||
data: FormData;
|
||||
}) {
|
||||
return request<ListItem>(
|
||||
`${GPU_SERVICE_PUBLIC_KEY_API(params.namespace)}/${params.id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
data: params.data
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteGPUServicePublicKey(params: {
|
||||
namespace: string;
|
||||
id: number;
|
||||
}) {
|
||||
return request(
|
||||
`${GPU_SERVICE_PUBLIC_KEY_API(params.namespace)}/${params.id}`,
|
||||
{
|
||||
method: 'DELETE'
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { INPUT_WIDTH } from '@/constants';
|
||||
import { Textarea } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Form, message } from 'antd';
|
||||
import React from 'react';
|
||||
import PageBox from '../../_components/page-box';
|
||||
|
||||
interface PublicKeyFormData {
|
||||
public_key?: string;
|
||||
}
|
||||
|
||||
const PLACEHOLDER = `以 ssh-rsa 或 ssh-ed25519 开头,每个 Public Key 单独一行
|
||||
|
||||
查看 Public Key:
|
||||
- RSA
|
||||
cat ~/.ssh/id_rsa.pub
|
||||
- Ed25519
|
||||
cat ~/.ssh/id_ed25519.pub`;
|
||||
|
||||
const GPUServicePublicKeys: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const [form] = Form.useForm<PublicKeyFormData>();
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await form.validateFields();
|
||||
message.success('操作成功');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageBox>
|
||||
<Form
|
||||
style={{ width: INPUT_WIDTH.large }}
|
||||
name="gpuServicePublicKeyForm"
|
||||
form={form}
|
||||
>
|
||||
<Form.Item<PublicKeyFormData> name="public_key">
|
||||
<Textarea
|
||||
required
|
||||
label="SSH Public Key"
|
||||
placeholder={PLACEHOLDER}
|
||||
trim={false}
|
||||
alwaysFocus
|
||||
autoSize={{ minRows: 8, maxRows: 16 }}
|
||||
style={{ width: INPUT_WIDTH.large }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ width: 120, marginTop: 24 }}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.save' })}
|
||||
</Button>
|
||||
</Form>
|
||||
</PageBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default GPUServicePublicKeys;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useCallback } from 'react';
|
||||
import { apiVersion, KindMapping } from '../../constants';
|
||||
import { createGPUServicePublicKey } from '../apis';
|
||||
import { ListItem } from '../types';
|
||||
|
||||
interface CreateSshkeyParams {
|
||||
name: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export default function useCreateSshkey() {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
||||
|
||||
const fetchDetail = useCallback(
|
||||
(params: CreateSshkeyParams) =>
|
||||
createGPUServicePublicKey({
|
||||
namespace,
|
||||
data: {
|
||||
apiVersion,
|
||||
kind: KindMapping.sshPublicKey,
|
||||
metadata: {
|
||||
name: params.name,
|
||||
namespace
|
||||
},
|
||||
data: params.data
|
||||
}
|
||||
}),
|
||||
[namespace]
|
||||
);
|
||||
|
||||
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
|
||||
ListItem,
|
||||
CreateSshkeyParams
|
||||
>({
|
||||
fetchDetail,
|
||||
key: 'createSshkey'
|
||||
});
|
||||
|
||||
return {
|
||||
detailData,
|
||||
loading,
|
||||
cancelRequest,
|
||||
fetchData
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useCallback } from 'react';
|
||||
import { queryGPUServicePublicKeys } from '../apis';
|
||||
import { ListItem } from '../types';
|
||||
|
||||
export default function useGetSshkey() {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
||||
|
||||
const fetchDetail = useCallback(
|
||||
(params: Global.K8sSearchParams = {}, options?: any) =>
|
||||
queryGPUServicePublicKeys({ ...params, namespace }, options),
|
||||
[namespace]
|
||||
);
|
||||
|
||||
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
|
||||
Global.K8sPageResponse<ListItem>,
|
||||
Global.K8sSearchParams
|
||||
>({
|
||||
fetchDetail,
|
||||
key: 'sshkey'
|
||||
});
|
||||
|
||||
return {
|
||||
detailData,
|
||||
loading,
|
||||
cancelRequest,
|
||||
fetchData
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useCallback } from 'react';
|
||||
import { apiVersion, KindMapping } from '../../constants';
|
||||
import { updateGPUServicePublicKey } from '../apis';
|
||||
import { ListItem } from '../types';
|
||||
|
||||
interface UpdateSshkeyParams {
|
||||
id: number;
|
||||
name: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export default function useUpdateSshkey() {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
||||
|
||||
const fetchDetail = useCallback(
|
||||
(params: UpdateSshkeyParams) =>
|
||||
updateGPUServicePublicKey({
|
||||
namespace,
|
||||
id: params.id,
|
||||
data: {
|
||||
apiVersion,
|
||||
kind: KindMapping.sshPublicKey,
|
||||
metadata: {
|
||||
name: params.name,
|
||||
namespace
|
||||
},
|
||||
data: params.data
|
||||
}
|
||||
}),
|
||||
[namespace]
|
||||
);
|
||||
|
||||
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
|
||||
ListItem,
|
||||
UpdateSshkeyParams
|
||||
>({
|
||||
fetchDetail,
|
||||
key: 'updateSshkey'
|
||||
});
|
||||
|
||||
return {
|
||||
detailData,
|
||||
loading,
|
||||
cancelRequest,
|
||||
fetchData
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface FormData extends Global.K8sCommonData {
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface ListItem extends FormData {
|
||||
id: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
@@ -1,64 +1,60 @@
|
||||
import { request } from '@umijs/max';
|
||||
import { mockStorageData } from '../config/mock-data';
|
||||
import { FormData, ListItem } from '../config/types';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
export const GPU_SERVICE_STORAGE_API = '/gpu-service-storage';
|
||||
export const GPU_SERVICE_STORAGE_API = (namespace: string) =>
|
||||
`/proxy/apis/worker.gpustack.ai/v1/namespaces/${namespace}/instancepersistentvolumes`;
|
||||
|
||||
export async function queryGPUServiceStorage(
|
||||
params: Global.SearchParams,
|
||||
params: Global.K8sSearchParams,
|
||||
options?: any
|
||||
) {
|
||||
// return request<Global.PageResponse<ListItem>>(GPU_SERVICE_STORAGE_API, {
|
||||
// method: 'GET',
|
||||
// params,
|
||||
// cancelToken: options?.token
|
||||
// });
|
||||
const page = params.page || 1;
|
||||
const perPage = params.perPage || 10;
|
||||
const search = params.search?.toLowerCase();
|
||||
const clusterId = params.cluster_id;
|
||||
const filteredData = mockStorageData.filter((item) => {
|
||||
const matchSearch = search
|
||||
? item.name.toLowerCase().includes(search)
|
||||
: true;
|
||||
const matchCluster = clusterId ? item.cluster_id === clusterId : true;
|
||||
return matchSearch && matchCluster;
|
||||
});
|
||||
const start = (page - 1) * perPage;
|
||||
const items = filteredData.slice(start, start + perPage);
|
||||
|
||||
return {
|
||||
items,
|
||||
pagination: {
|
||||
total: filteredData.length,
|
||||
totalPage: Math.ceil(filteredData.length / perPage),
|
||||
page,
|
||||
perPage
|
||||
return request<Global.K8sPageResponse<ListItem>>(
|
||||
GPU_SERVICE_STORAGE_API(params.namespace || ''),
|
||||
{
|
||||
method: 'GET',
|
||||
params,
|
||||
cancelToken: options?.token
|
||||
}
|
||||
} as Global.PageResponse<ListItem>;
|
||||
);
|
||||
}
|
||||
|
||||
export async function createGPUServiceStorage(params: { data: FormData }) {
|
||||
// return request<ListItem>(GPU_SERVICE_STORAGE_API, {
|
||||
// method: 'POST',
|
||||
// data: params.data
|
||||
// });
|
||||
return true;
|
||||
export async function createGPUServiceStorage(
|
||||
params: {
|
||||
namespace: string;
|
||||
data: Global.K8sCommonData;
|
||||
},
|
||||
option?: any
|
||||
) {
|
||||
return request<ListItem>(GPU_SERVICE_STORAGE_API(params.namespace), {
|
||||
method: 'POST',
|
||||
data: params.data,
|
||||
cancelToken: option?.token
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateGPUServiceStorage(params: {
|
||||
export async function updateGPUServiceStorage(
|
||||
params: {
|
||||
namespace: string;
|
||||
id: number;
|
||||
data: Global.K8sCommonData;
|
||||
},
|
||||
option?: any
|
||||
) {
|
||||
return request<ListItem>(
|
||||
`${GPU_SERVICE_STORAGE_API(params.namespace)}/${params.id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
data: params.data,
|
||||
cancelToken: option?.token
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteGPUServiceStorage(params: {
|
||||
namespace: string;
|
||||
id: number;
|
||||
data: FormData;
|
||||
}) {
|
||||
// return request<ListItem>(`${GPU_SERVICE_STORAGE_API}/${params.id}`, {
|
||||
// method: 'PUT',
|
||||
// data: params.data
|
||||
// });
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function deleteGPUServiceStorage(id: number) {
|
||||
return request(`${GPU_SERVICE_STORAGE_API}/${id}`, {
|
||||
return request(`${GPU_SERVICE_STORAGE_API(params.namespace)}/${params.id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { StatusMaps } from '@/config';
|
||||
import { StatusType } from '@/config/types';
|
||||
import { icons } from '@gpustack/core-ui';
|
||||
|
||||
export const StorageTypeValueMap = {
|
||||
@@ -9,52 +7,72 @@ export const StorageTypeValueMap = {
|
||||
};
|
||||
|
||||
export const StorageTypeLabelMap: Record<string, string> = {
|
||||
[StorageTypeValueMap.Local]: '默认存储',
|
||||
[StorageTypeValueMap.Shared]: '默认存储',
|
||||
[StorageTypeValueMap.Local]: '本地存储',
|
||||
[StorageTypeValueMap.Shared]: '共享存储',
|
||||
[StorageTypeValueMap.Object]: '对象存储'
|
||||
};
|
||||
|
||||
export const StorageAccessModeValueMap = {
|
||||
ReadWriteOnce: 'ReadWriteOnce',
|
||||
ReadOnlyMany: 'ReadOnlyMany',
|
||||
ReadWriteMany: 'ReadWriteMany'
|
||||
};
|
||||
|
||||
export const StorageAccessModeOptions = [
|
||||
export const StorageTypeOptions = [
|
||||
{
|
||||
label: 'ReadWriteOnce',
|
||||
value: StorageAccessModeValueMap.ReadWriteOnce
|
||||
label: StorageTypeLabelMap[StorageTypeValueMap.Local],
|
||||
value: StorageTypeValueMap.Local
|
||||
},
|
||||
{
|
||||
label: 'ReadWriteMany',
|
||||
value: StorageAccessModeValueMap.ReadWriteMany
|
||||
label: StorageTypeLabelMap[StorageTypeValueMap.Shared],
|
||||
value: StorageTypeValueMap.Shared
|
||||
},
|
||||
{
|
||||
label: 'ReadOnlyMany',
|
||||
value: StorageAccessModeValueMap.ReadOnlyMany
|
||||
label: StorageTypeLabelMap[StorageTypeValueMap.Object],
|
||||
value: StorageTypeValueMap.Object
|
||||
}
|
||||
];
|
||||
|
||||
export const StorageStatusValueMap = {
|
||||
Available: 'Available',
|
||||
Bound: 'Bound',
|
||||
Released: 'Released',
|
||||
Failed: 'Failed'
|
||||
export const AccessModeValueMap = {
|
||||
ReadWriteOnce: 'ReadWriteOnce',
|
||||
ReadOnlyMany: 'ReadOnlyMany',
|
||||
ReadWriteMany: 'ReadWriteMany',
|
||||
ReadWriteOncePod: 'ReadWriteOncePod'
|
||||
};
|
||||
|
||||
export const StorageStatusLabelMap: Record<string, string> = {
|
||||
[StorageStatusValueMap.Available]: 'Available',
|
||||
[StorageStatusValueMap.Bound]: 'Bound',
|
||||
[StorageStatusValueMap.Released]: 'Released',
|
||||
[StorageStatusValueMap.Failed]: 'Failed'
|
||||
export const AccessModeOptions = [
|
||||
{
|
||||
label: 'ReadWriteOnce',
|
||||
value: AccessModeValueMap.ReadWriteOnce
|
||||
},
|
||||
{
|
||||
label: 'ReadWriteMany',
|
||||
value: AccessModeValueMap.ReadWriteMany
|
||||
},
|
||||
{
|
||||
label: 'ReadOnlyMany',
|
||||
value: AccessModeValueMap.ReadOnlyMany
|
||||
},
|
||||
{
|
||||
label: 'ReadWriteOncePod',
|
||||
value: AccessModeValueMap.ReadWriteOncePod
|
||||
}
|
||||
];
|
||||
|
||||
export const ReClaimPolicyValueMap = {
|
||||
Retain: 'Retain',
|
||||
Delete: 'Delete',
|
||||
Recycle: 'Recycle'
|
||||
};
|
||||
|
||||
export const status: Record<string, StatusType> = {
|
||||
[StorageStatusValueMap.Available]: StatusMaps.success,
|
||||
[StorageStatusValueMap.Bound]: StatusMaps.transitioning,
|
||||
[StorageStatusValueMap.Released]: StatusMaps.inactive,
|
||||
[StorageStatusValueMap.Failed]: StatusMaps.error
|
||||
};
|
||||
export const ReClaimPolicyOptions = [
|
||||
{
|
||||
label: 'Retain',
|
||||
value: ReClaimPolicyValueMap.Retain
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
value: ReClaimPolicyValueMap.Delete
|
||||
},
|
||||
{
|
||||
label: 'Recycle',
|
||||
value: ReClaimPolicyValueMap.Recycle
|
||||
}
|
||||
];
|
||||
|
||||
export const rowActionList = [
|
||||
{
|
||||
|
||||
@@ -1,68 +1,64 @@
|
||||
import { StorageStatusValueMap, StorageTypeValueMap } from '.';
|
||||
import { StorageTypeValueMap } from '.';
|
||||
import { ListItem } from './types';
|
||||
|
||||
export const mockStorageData: ListItem[] = [
|
||||
export const mockStorageData: (ListItem & { cluster_id: number })[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'local-nvme-cache',
|
||||
type: StorageTypeValueMap.Local,
|
||||
capacity_gb: 500,
|
||||
mount_path: '/mnt/nvme',
|
||||
access_modes: ['ReadWriteOnce'],
|
||||
parameters: {
|
||||
path: '/mnt/nvme'
|
||||
},
|
||||
status: StorageStatusValueMap.Available,
|
||||
cluster_id: 1,
|
||||
description: 'Local NVMe cache for hot model files',
|
||||
metadata: {
|
||||
name: 'local-nvme-cache',
|
||||
namespace: 'default'
|
||||
},
|
||||
spec: {
|
||||
accessMode: 'ReadWriteOnce',
|
||||
capacity: '500Gi',
|
||||
type: StorageTypeValueMap.Local
|
||||
},
|
||||
created_at: '2026-04-01T10:00:00Z',
|
||||
updated_at: '2026-04-10T10:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'shared-model-store',
|
||||
type: StorageTypeValueMap.Shared,
|
||||
capacity_gb: 2048,
|
||||
mount_path: '/models',
|
||||
access_modes: ['ReadWriteMany'],
|
||||
parameters: {
|
||||
storageClassName: 'nfs-client'
|
||||
},
|
||||
status: StorageStatusValueMap.Bound,
|
||||
cluster_id: 1,
|
||||
description: 'Shared storage for model weights',
|
||||
metadata: {
|
||||
name: 'shared-model-store',
|
||||
namespace: 'default'
|
||||
},
|
||||
spec: {
|
||||
accessMode: 'ReadWriteMany',
|
||||
capacity: '2Ti',
|
||||
type: StorageTypeValueMap.Shared
|
||||
},
|
||||
created_at: '2026-04-02T10:00:00Z',
|
||||
updated_at: '2026-04-11T10:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'object-dataset-bucket',
|
||||
type: StorageTypeValueMap.Object,
|
||||
capacity_gb: 10240,
|
||||
mount_path: '/datasets',
|
||||
access_modes: ['ReadOnlyMany'],
|
||||
parameters: {
|
||||
bucket: 'datasets'
|
||||
},
|
||||
status: StorageStatusValueMap.Released,
|
||||
cluster_id: 2,
|
||||
description: 'Object storage mounted for datasets',
|
||||
metadata: {
|
||||
name: 'object-dataset-bucket',
|
||||
namespace: 'default'
|
||||
},
|
||||
spec: {
|
||||
accessMode: 'ReadOnlyMany',
|
||||
capacity: '10Ti',
|
||||
type: StorageTypeValueMap.Object
|
||||
},
|
||||
created_at: '2026-04-03T10:00:00Z',
|
||||
updated_at: '2026-04-12T10:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'failed-shared-cache',
|
||||
type: StorageTypeValueMap.Shared,
|
||||
capacity_gb: 1024,
|
||||
mount_path: '/failed-cache',
|
||||
access_modes: ['ReadWriteMany'],
|
||||
parameters: {
|
||||
storageClassName: 'nfs-client'
|
||||
},
|
||||
status: StorageStatusValueMap.Failed,
|
||||
cluster_id: 2,
|
||||
description: 'Shared cache waiting for storage backend recovery',
|
||||
metadata: {
|
||||
name: 'failed-shared-cache',
|
||||
namespace: 'default'
|
||||
},
|
||||
spec: {
|
||||
accessMode: 'ReadWriteMany',
|
||||
capacity: '1Ti',
|
||||
type: StorageTypeValueMap.Shared
|
||||
},
|
||||
created_at: '2026-04-04T10:00:00Z',
|
||||
updated_at: '2026-04-13T10:00:00Z'
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
export type AccessModeType =
|
||||
| 'ReadOnlyMany'
|
||||
| 'ReadWriteMany'
|
||||
| 'ReadWriteOnce'
|
||||
| 'ReadWriteOncePod';
|
||||
|
||||
export type ReClaimPolicyType = 'Retain' | 'Delete' | 'Recycle';
|
||||
export interface FormData {
|
||||
name: string;
|
||||
description?: string;
|
||||
type?: string;
|
||||
capacity_gb?: number;
|
||||
mount_path?: string;
|
||||
access_modes?: string[];
|
||||
parameters?: Record<string, any>;
|
||||
status?: string;
|
||||
cluster_id?: number;
|
||||
metadata: {
|
||||
name: string;
|
||||
namespace: string;
|
||||
};
|
||||
spec: {
|
||||
accessMode: AccessModeType;
|
||||
capacity: string;
|
||||
type: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ListItem extends FormData {
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
import {
|
||||
Input as CInput,
|
||||
InputNumber as CInputNumber,
|
||||
Select as SealSelect
|
||||
} from '@gpustack/core-ui';
|
||||
import { Input as CInput, Select as SealSelect } from '@gpustack/core-ui';
|
||||
import { Form } from 'antd';
|
||||
import { StorageAccessModeOptions } from '../config';
|
||||
import { AccessModeOptions, StorageTypeOptions } from '../config';
|
||||
import { FormData } from '../config/types';
|
||||
|
||||
const Basic = () => {
|
||||
const form = Form.useFormInstance<FormData>();
|
||||
const parameters = Form.useWatch('parameters', form);
|
||||
|
||||
const handleParametersChange = (value: Record<string, any>) => {
|
||||
form.setFieldValue('parameters', value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item<FormData>
|
||||
name="name"
|
||||
name={['metadata', 'name']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -31,7 +20,7 @@ const Basic = () => {
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Form.Item<FormData>
|
||||
name="type"
|
||||
name={['spec', 'type']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -39,12 +28,16 @@ const Basic = () => {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealSelect label="存储类型" required options={[]}></SealSelect>
|
||||
<SealSelect
|
||||
label="存储类型"
|
||||
required
|
||||
options={StorageTypeOptions}
|
||||
></SealSelect>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Form.Item<FormData>
|
||||
name="capacity_gb"
|
||||
name={['spec', 'capacity']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -52,28 +45,25 @@ const Basic = () => {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CInputNumber min={1} precision={0} label="容量 (GB)" required />
|
||||
<CInput.Input label="容量" required placeholder="例如:10Gi" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item<FormData> name="access_modes">
|
||||
<Form.Item<FormData>
|
||||
name={['spec', 'accessMode']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择访问模式'
|
||||
}
|
||||
]}
|
||||
>
|
||||
<SealSelect
|
||||
label="存储卷访问方式"
|
||||
allowClear
|
||||
options={StorageAccessModeOptions}
|
||||
required
|
||||
options={AccessModeOptions}
|
||||
></SealSelect>
|
||||
</Form.Item>
|
||||
{/* <Form.Item<FormData> name="parameters">
|
||||
<LabelSelector
|
||||
label="存储卷配置参数"
|
||||
labels={parameters || {}}
|
||||
btnText="添加参数"
|
||||
onChange={handleParametersChange}
|
||||
/>
|
||||
</Form.Item> */}
|
||||
<Form.Item<FormData> name="description">
|
||||
<CInput.TextArea label="描述" scaleSize />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PageAction } from '@/config';
|
||||
import { PageActionType } from '@/config/types';
|
||||
import { Form } from 'antd';
|
||||
import { forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||
import { StorageStatusValueMap } from '../config';
|
||||
import { AccessModeValueMap, StorageTypeValueMap } from '../config';
|
||||
import { FormData, ListItem } from '../config/types';
|
||||
import Basic from './basic';
|
||||
|
||||
@@ -11,12 +11,19 @@ interface StorageFormProps {
|
||||
open: boolean;
|
||||
action: PageActionType;
|
||||
currentData?: ListItem | null;
|
||||
namespace?: string;
|
||||
onFinish: (values: FormData) => Promise<void>;
|
||||
}
|
||||
|
||||
const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
|
||||
(props, ref) => {
|
||||
const { action, currentData, open, onFinish } = props;
|
||||
const {
|
||||
action,
|
||||
currentData,
|
||||
open,
|
||||
namespace = 'default',
|
||||
onFinish
|
||||
} = props;
|
||||
const [form] = Form.useForm<FormData>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -27,24 +34,30 @@ const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
|
||||
|
||||
if (action === PageAction.EDIT && currentData) {
|
||||
form.setFieldsValue({
|
||||
name: currentData.name,
|
||||
description: currentData.description,
|
||||
type: currentData.type,
|
||||
capacity_gb: currentData.capacity_gb,
|
||||
mount_path: currentData.mount_path,
|
||||
access_modes: currentData.access_modes,
|
||||
parameters: currentData.parameters,
|
||||
status: currentData.status
|
||||
metadata: {
|
||||
name: currentData.metadata?.name,
|
||||
namespace: currentData.metadata?.namespace
|
||||
},
|
||||
spec: {
|
||||
accessMode: currentData.spec?.accessMode,
|
||||
capacity: currentData.spec?.capacity,
|
||||
type: currentData.spec?.type
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
type: '默认存储',
|
||||
parameters: {},
|
||||
status: StorageStatusValueMap.Available
|
||||
metadata: {
|
||||
namespace
|
||||
},
|
||||
spec: {
|
||||
accessMode:
|
||||
AccessModeValueMap.ReadWriteOnce as FormData['spec']['accessMode'],
|
||||
type: StorageTypeValueMap.Local
|
||||
}
|
||||
});
|
||||
}, [action, currentData, form, open]);
|
||||
}, [action, currentData, form, open, namespace]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
submit: () => {
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { tableSorter } from '@/config/settings';
|
||||
import { AutoTooltip, DropdownButtons, StatusTag } from '@gpustack/core-ui';
|
||||
import { AutoTooltip, DropdownButtons } from '@gpustack/core-ui';
|
||||
import type { ColumnsType } from 'antd/lib/table';
|
||||
import dayjs from 'dayjs';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
rowActionList,
|
||||
status,
|
||||
StorageStatusLabelMap,
|
||||
StorageStatusValueMap,
|
||||
StorageTypeLabelMap
|
||||
} from '../config';
|
||||
import { rowActionList, StorageTypeLabelMap } from '../config';
|
||||
import { ListItem } from '../config/types';
|
||||
|
||||
interface ColumnsHookProps {
|
||||
@@ -25,7 +19,7 @@ const useStorageColumns = ({
|
||||
return [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
dataIndex: ['metadata', 'name'],
|
||||
key: 'name',
|
||||
sorter: tableSorter(1),
|
||||
ellipsis: {
|
||||
@@ -39,48 +33,25 @@ const useStorageColumns = ({
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
dataIndex: ['spec', 'type'],
|
||||
key: 'type',
|
||||
sorter: tableSorter(2),
|
||||
render: (value: string) => StorageTypeLabelMap[value] || value || '-'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
title: '容量',
|
||||
dataIndex: ['spec', 'capacity'],
|
||||
key: 'capacity',
|
||||
sorter: tableSorter(3),
|
||||
render: (statusValue: string) => {
|
||||
const value = statusValue || StorageStatusValueMap.Available;
|
||||
return (
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
status: status[value],
|
||||
text: StorageStatusLabelMap[value] || value
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
render: (value: string) => value ?? '-'
|
||||
},
|
||||
{
|
||||
title: '容量 (GB)',
|
||||
dataIndex: 'capacity_gb',
|
||||
key: 'capacity_gb',
|
||||
title: '访问模式',
|
||||
dataIndex: ['spec', 'accessMode'],
|
||||
key: 'accessMode',
|
||||
sorter: tableSorter(4),
|
||||
render: (value: number) => value ?? '-'
|
||||
render: (value: string) => value || '-'
|
||||
},
|
||||
// {
|
||||
// title: '挂载路径',
|
||||
// dataIndex: 'mount_path',
|
||||
// key: 'mount_path',
|
||||
// ellipsis: {
|
||||
// showTitle: false
|
||||
// },
|
||||
// render: (text: string) => (
|
||||
// <AutoTooltip ghost minWidth={20}>
|
||||
// {text || '-'}
|
||||
// </AutoTooltip>
|
||||
// )
|
||||
// },
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
||||
import type { PageActionType } from '@/config/types';
|
||||
import useTableFetch from '@/hooks/use-table-fetch';
|
||||
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import {
|
||||
BaseSelect,
|
||||
DeleteModal,
|
||||
@@ -14,20 +15,53 @@ import { useIntl } from '@umijs/max';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { ConfigProvider, Divider, Flex, message, Table } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { PageContainerInner } from '../../_components/page-box';
|
||||
import {
|
||||
createGPUServiceStorage,
|
||||
deleteGPUServiceStorage,
|
||||
GPU_SERVICE_STORAGE_API,
|
||||
queryGPUServiceStorage,
|
||||
updateGPUServiceStorage
|
||||
queryGPUServiceStorage
|
||||
} from './apis';
|
||||
|
||||
import AddModal from './components/add-modal';
|
||||
import { FormData, ListItem } from './config/types';
|
||||
import useStorageColumns from './hooks/use-storage-columns';
|
||||
import useCreateStorage from './services/use-create-storage';
|
||||
import useUpdateStorage from './services/use-update-storage';
|
||||
|
||||
const GPUServiceStorage: React.FC = () => {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
||||
|
||||
const deleteStorage = useCallback(
|
||||
(id: number) => deleteGPUServiceStorage({ namespace, id }),
|
||||
[namespace]
|
||||
);
|
||||
|
||||
const fetchStorage = useCallback(
|
||||
async (
|
||||
params: any,
|
||||
options?: any
|
||||
): Promise<Global.PageResponse<ListItem>> => {
|
||||
const res = await queryGPUServiceStorage(
|
||||
{ ...params, namespace },
|
||||
options
|
||||
);
|
||||
const total = res.items?.length ?? 0;
|
||||
const perPage = params.perPage || 10;
|
||||
return {
|
||||
items: res.items ?? [],
|
||||
pagination: {
|
||||
total,
|
||||
totalPage: Math.ceil(total / perPage),
|
||||
page: params.page || 1,
|
||||
perPage
|
||||
}
|
||||
};
|
||||
},
|
||||
[namespace]
|
||||
);
|
||||
|
||||
const {
|
||||
dataSource,
|
||||
rowSelection,
|
||||
@@ -44,12 +78,15 @@ const GPUServiceStorage: React.FC = () => {
|
||||
handleNameChange
|
||||
} = useTableFetch<ListItem>({
|
||||
key: PaginationKey.Storage,
|
||||
fetchAPI: queryGPUServiceStorage,
|
||||
deleteAPI: deleteGPUServiceStorage,
|
||||
fetchAPI: fetchStorage,
|
||||
deleteAPI: deleteStorage,
|
||||
watch: false,
|
||||
API: GPU_SERVICE_STORAGE_API,
|
||||
API: GPU_SERVICE_STORAGE_API(namespace),
|
||||
contentForDelete: '存储'
|
||||
});
|
||||
|
||||
const { fetchData: createStorage } = useCreateStorage();
|
||||
const { fetchData: updateStorage } = useUpdateStorage();
|
||||
const {
|
||||
fetchClusterList,
|
||||
cancelRequest: cancelClusterRequest,
|
||||
@@ -113,12 +150,12 @@ const GPUServiceStorage: React.FC = () => {
|
||||
const handleModalOk = async (data: FormData) => {
|
||||
try {
|
||||
if (openAddModalStatus.action === PageAction.EDIT) {
|
||||
await updateGPUServiceStorage({
|
||||
await updateStorage({
|
||||
id: openAddModalStatus.currentData!.id,
|
||||
data
|
||||
});
|
||||
} else {
|
||||
await createGPUServiceStorage({ data });
|
||||
await createStorage({ data });
|
||||
}
|
||||
|
||||
fetchData();
|
||||
@@ -133,7 +170,7 @@ const GPUServiceStorage: React.FC = () => {
|
||||
if (val === 'edit') {
|
||||
handleEditStorage(row);
|
||||
} else if (val === 'delete') {
|
||||
handleDelete({ ...row, name: row.name });
|
||||
handleDelete({ ...row, name: row.metadata?.name });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useCallback } from 'react';
|
||||
import { apiVersion, KindMapping } from '../../constants';
|
||||
import { createGPUServiceStorage } from '../apis';
|
||||
import { FormData, ListItem } from '../config/types';
|
||||
|
||||
interface CreateStorageParams {
|
||||
data: FormData;
|
||||
}
|
||||
|
||||
export default function useCreateStorage() {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
||||
|
||||
const fetchDetail = useCallback(
|
||||
(params: CreateStorageParams, option?: any) =>
|
||||
createGPUServiceStorage(
|
||||
{
|
||||
namespace,
|
||||
data: {
|
||||
apiVersion,
|
||||
kind: KindMapping.instancePersistentVolume,
|
||||
metadata: {
|
||||
name: params.data.metadata.name,
|
||||
namespace
|
||||
},
|
||||
spec: params.data.spec
|
||||
}
|
||||
},
|
||||
option
|
||||
),
|
||||
[namespace]
|
||||
);
|
||||
|
||||
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
|
||||
ListItem,
|
||||
CreateStorageParams
|
||||
>({
|
||||
fetchDetail,
|
||||
key: 'createStorage'
|
||||
});
|
||||
|
||||
return {
|
||||
detailData,
|
||||
loading,
|
||||
cancelRequest,
|
||||
fetchData
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { useQueryData } from '@gpustack/core-ui';
|
||||
import { useCallback } from 'react';
|
||||
import { apiVersion, KindMapping } from '../../constants';
|
||||
import { updateGPUServiceStorage } from '../apis';
|
||||
import { FormData, ListItem } from '../config/types';
|
||||
|
||||
interface UpdateStorageParams {
|
||||
id: number;
|
||||
data: FormData;
|
||||
}
|
||||
|
||||
export default function useUpdateStorage() {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const namespace = initialState?.currentUser?.org_name || 'default';
|
||||
|
||||
const fetchDetail = useCallback(
|
||||
(params: UpdateStorageParams, option?: any) =>
|
||||
updateGPUServiceStorage(
|
||||
{
|
||||
namespace,
|
||||
id: params.id,
|
||||
data: {
|
||||
apiVersion,
|
||||
kind: KindMapping.instancePersistentVolume,
|
||||
metadata: {
|
||||
name: params.data.metadata.name,
|
||||
namespace
|
||||
},
|
||||
spec: params.data.spec
|
||||
}
|
||||
},
|
||||
option
|
||||
),
|
||||
[namespace]
|
||||
);
|
||||
|
||||
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
|
||||
ListItem,
|
||||
UpdateStorageParams
|
||||
>({
|
||||
fetchDetail,
|
||||
key: 'updateStorage'
|
||||
});
|
||||
|
||||
return {
|
||||
detailData,
|
||||
loading,
|
||||
cancelRequest,
|
||||
fetchData
|
||||
};
|
||||
}
|
||||
@@ -1,26 +1,32 @@
|
||||
export interface PortItem {
|
||||
protocol: 'udp' | 'tcp';
|
||||
value?: number;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface EnvItem {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface FormData {
|
||||
name: string;
|
||||
image?: string;
|
||||
vendor?: string;
|
||||
image_pull_policy?: string;
|
||||
run_command?: string;
|
||||
boot_disk_size_gb?: number;
|
||||
volume_size_gb?: number;
|
||||
volume_mount_path?: string;
|
||||
ports?: PortItem[];
|
||||
env?: Record<string, any>;
|
||||
image: string;
|
||||
imagePullPolicy: string;
|
||||
imagePullSecret: {
|
||||
name: string;
|
||||
};
|
||||
command: string[];
|
||||
privileged: boolean;
|
||||
ports: PortItem[];
|
||||
env: EnvItem[];
|
||||
volumeMount: string;
|
||||
resources: {
|
||||
cpu: string;
|
||||
ram: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ListItem extends FormData {
|
||||
id: number;
|
||||
description?: string;
|
||||
gpu_count?: number;
|
||||
replicas?: number;
|
||||
status?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { FormData } from '../config/types';
|
||||
import Ports from './ports';
|
||||
|
||||
interface BasicProps {
|
||||
page: 'template' | 'instance';
|
||||
page?: 'template' | 'instance';
|
||||
}
|
||||
|
||||
const Basic: React.FC<BasicProps> = ({ page = 'template' }) => {
|
||||
|
||||
@@ -45,7 +45,6 @@ const PortItem: React.FC<PortItemProps> = ({ item, index, onChange }) => {
|
||||
<CInputNumber
|
||||
min={1}
|
||||
max={65535}
|
||||
disabled={index === 0}
|
||||
precision={0}
|
||||
value={item.value}
|
||||
onChange={(value) => {
|
||||
|
||||
@@ -6,7 +6,6 @@ import styled from 'styled-components';
|
||||
import PageBox from '../_components/page-box';
|
||||
import Appearance from './components/appearance';
|
||||
import ModifyPasswordn from './components/modify-password';
|
||||
import PublicKey from './components/public-key';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.ant-page-header-heading {
|
||||
@@ -27,11 +26,6 @@ const Profile: React.FC = () => {
|
||||
const items: TabsProps['items'] = useMemo(() => {
|
||||
if (initialState?.currentUser?.source !== 'Local') {
|
||||
return [
|
||||
{
|
||||
key: 'public-key',
|
||||
label: 'SSH Public Key',
|
||||
children: <PublicKey />
|
||||
},
|
||||
{
|
||||
key: 'appearance',
|
||||
label: intl.formatMessage({ id: 'common.appearance' }),
|
||||
@@ -45,11 +39,6 @@ const Profile: React.FC = () => {
|
||||
label: intl.formatMessage({ id: 'users.form.updatepassword' }),
|
||||
children: <ModifyPasswordn />
|
||||
},
|
||||
{
|
||||
key: 'public-key',
|
||||
label: 'SSH 公钥',
|
||||
children: <PublicKey />
|
||||
},
|
||||
{
|
||||
key: 'appearance',
|
||||
label: intl.formatMessage({ id: 'common.appearance' }),
|
||||
|
||||
Reference in New Issue
Block a user