fix: recreate action

This commit is contained in:
jialin
2026-05-25 22:42:02 +08:00
committed by jialin
parent a0b408e8cb
commit 2ebb8f44b7
16 changed files with 457 additions and 180 deletions
+4
View File
@@ -92,6 +92,10 @@ export default {
'gpuservice.storage.persistent': 'Persistent',
'gpuservice.storage.temporary': 'Temporary',
'gpuservice.storage.persistentVolume': 'Persistent Volume',
'gpuservice.storage.temporary.tips':
'Data is cleared when the instance stops.',
'gpuservice.storage.persistentVolume.tips':
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.',
'gpuservice.storage.persistentVolume.required':
'Please select a persistent volume',
'gpuservice.storage.tempCapacity': 'Capacity (GB)',
+5 -1
View File
@@ -96,5 +96,9 @@ export default {
'gpuservice.storage.tempCapacity.required':
'一時ストレージ容量を入力してください',
'gpuservice.form.rule.name':
"小文字、数字、'-' のみ使用可能。文字または数字で始まり、文字または数字で終わる必要があり、連続する '-' は不可、最大 63 文字。"
"小文字、数字、'-' のみ使用可能。文字または数字で始まり、文字または数字で終わる必要があり、連続する '-' は不可、最大 63 文字。",
'gpuservice.storage.temporary.tips':
'Data is cleared when the instance stops.',
'gpuservice.storage.persistentVolume.tips':
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.'
};
+5 -1
View File
@@ -96,5 +96,9 @@ export default {
'gpuservice.storage.tempCapacity.required':
'Введите объём временного хранилища',
'gpuservice.form.rule.name':
"Строчные буквы, цифры и '-'. Должно начинаться и заканчиваться буквой или цифрой, без подряд идущих '-', максимум 63 символа."
"Строчные буквы, цифры и '-'. Должно начинаться и заканчиваться буквой или цифрой, без подряд идущих '-', максимум 63 символа.",
'gpuservice.storage.temporary.tips':
'Data is cleared when the instance stops.',
'gpuservice.storage.persistentVolume.tips':
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.'
};
+5 -1
View File
@@ -97,5 +97,9 @@ export default {
'gpuservice.storage.tempCapacity.required':
'Lütfen geçici depolama kapasitesini girin',
'gpuservice.form.rule.name':
"Küçük harfler, rakamlar ve '-'. Harf veya rakamla başlamalı ve bitmeli, ardışık '-' içermemeli, en fazla 63 karakter."
"Küçük harfler, rakamlar ve '-'. Harf veya rakamla başlamalı ve bitmeli, ardışık '-' içermemeli, en fazla 63 karakter.",
'gpuservice.storage.temporary.tips':
'Data is cleared when the instance stops.',
'gpuservice.storage.persistentVolume.tips':
'Data persists across restarts and is deleted only when the instance is terminated. Cannot be shared with other instances.'
};
+3
View File
@@ -85,6 +85,9 @@ export default {
'gpuservice.storage.persistent': '持久',
'gpuservice.storage.temporary': '临时',
'gpuservice.storage.persistentVolume': '持久卷',
'gpuservice.storage.temporary.tips': '实例停止后,数据将被清除。',
'gpuservice.storage.persistentVolume.tips':
'数据在实例重启后仍会保留,仅在实例被终止时删除。无法被其它实例共享。',
'gpuservice.storage.persistentVolume.required': '请选择持久卷',
'gpuservice.storage.tempCapacity': '容量(GB',
'gpuservice.storage.tempCapacity.required': '请输入临时存储容量',
@@ -37,6 +37,14 @@ export const GPU_SERVICE_INSTANCES_EVENTS_API = (params: {
return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instances/${params.name}/events`;
};
export const GPU_SERVICE_INSTANCE_PV_EVENTS_API = (params: {
namespace: string;
name: string;
clusterID?: number;
}) => {
return `/clusters/${params.clusterID}/proxy/apis/worker.gpustack.ai/v1/namespaces/${params.namespace}/instancepersistentvolumes/${params.name}/events`;
};
// =========== Instances ===========
export async function queryGPUServiceInstances(
@@ -2,7 +2,12 @@ import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import Separator from '@/pages/llmodels/components/separator';
import { SearchOutlined } from '@ant-design/icons';
import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui';
import {
AlertBlockInfo,
ColumnWrapper,
GSDrawer,
ModalFooter
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Empty, Input, Typography } from 'antd';
import { useEffect, useRef, useState } from 'react';
@@ -19,6 +24,8 @@ type AddModalProps = {
title: string;
action: PageActionType;
open: boolean;
width?: number | string;
realAction?: string;
onOk: (values: FormData) => void;
data?: ListItem | null;
onCancel: () => void;
@@ -71,7 +78,9 @@ const AddModal: React.FC<AddModalProps> = ({
open,
onOk,
data,
onCancel
onCancel,
width,
realAction
}) => {
const intl = useIntl();
const form = useRef<any>(null);
@@ -92,6 +101,10 @@ const AddModal: React.FC<AddModalProps> = ({
const instanceTypeList = detailData?.items || [];
const templateList = templatesData?.items || [];
const readonly = action === PageAction.VIEW;
const isRecreate = realAction === PageAction.CREATE;
const showResourceSelectors = action === PageAction.CREATE || isRecreate;
const shouldAutoSelectResource = action === PageAction.CREATE && !isRecreate;
const findTemplateByManufacturer = (
manufacturer: string | undefined,
@@ -143,7 +156,7 @@ const AddModal: React.FC<AddModalProps> = ({
instanceTypes: InstanceTypeItem[],
templates: TemplateItem[]
) => {
if (action !== PageAction.CREATE) return;
if (!shouldAutoSelectResource) return;
if (!instanceTypes.length) return;
const first = instanceTypes[0];
@@ -172,7 +185,7 @@ const AddModal: React.FC<AddModalProps> = ({
applyAutoSelection(instanceRes?.items || [], templatesRes?.items || []);
}
);
}, [open]);
}, [open, shouldAutoSelectResource]);
// filter instance types
const filteredInstanceTypes = instanceTypeList.filter((item) =>
@@ -238,102 +251,122 @@ const AddModal: React.FC<AddModalProps> = ({
}}
keyboard={false}
styles={{
wrapper: { width: 'calc(100vw - 220px)' },
wrapper: { width: width || 'calc(100vw - 220px)' },
body: { overflowY: 'hidden' }
}}
footer={false}
>
<div className={styles.container}>
<div className={styles.colWrapper}>
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
<div className={styles.panelBody}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 16,
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: 'var(--ant-color-bg-elevated)'
}}
>
<ColTitle style={{ paddingBottom: 0 }}>
{intl.formatMessage({ id: 'gpuservice.instance.types' })}
</ColTitle>
<Input
allowClear
prefix={<SearchOutlined className="text-tertiary" />}
placeholder={intl.formatMessage({
id: 'gpuservice.instance.search.type.placeholder'
})}
value={instanceKeyword}
onChange={(e) => setInstanceKeyword(e.target.value)}
/>
</div>
<InstanceTypeList
value={instanceTypeSelection.instanceType}
dataList={filteredInstanceTypes}
loading={instanceTypesLoading}
onChange={handleInstanceTypeChange}
/>
{showResourceSelectors && (
<>
<div className={styles.colWrapper}>
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
<div className={styles.panelBody}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 16,
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: 'var(--ant-color-bg-elevated)'
}}
>
<ColTitle style={{ paddingBottom: 0 }}>
{intl.formatMessage({ id: 'gpuservice.instance.types' })}
</ColTitle>
<Input
allowClear
prefix={<SearchOutlined className="text-tertiary" />}
placeholder={intl.formatMessage({
id: 'gpuservice.instance.search.type.placeholder'
})}
value={instanceKeyword}
onChange={(e) => setInstanceKeyword(e.target.value)}
/>
</div>
<InstanceTypeList
value={instanceTypeSelection.instanceType}
dataList={filteredInstanceTypes}
loading={instanceTypesLoading}
onChange={handleInstanceTypeChange}
/>
</div>
</ColumnWrapper>
<Separator></Separator>
</div>
</ColumnWrapper>
<Separator></Separator>
</div>
<div className={styles.colWrapper}>
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
<div className={styles.panelBody}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 16,
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: 'var(--ant-color-bg-elevated)'
}}
>
<ColTitle style={{ paddingBottom: 0 }}>
{intl.formatMessage({ id: 'gpuservice.instance.templates' })}
</ColTitle>
<Input
allowClear
prefix={<SearchOutlined className="text-tertiary" />}
placeholder={intl.formatMessage({
id: 'gpuservice.instance.search.template.placeholder'
})}
value={templateKeyword}
onChange={(e) => setTemplateKeyword(e.target.value)}
/>
</div>
{filteredTemplates.length > 0 ? (
<TemplateSelector
value={templateId}
dataList={filteredTemplates}
onChange={handleTemplateChange}
/>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
<div className={styles.colWrapper}>
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
<div className={styles.panelBody}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 16,
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: 'var(--ant-color-bg-elevated)'
}}
>
<ColTitle style={{ paddingBottom: 0 }}>
{intl.formatMessage({
id: 'gpuservice.instance.templates'
})}
</ColTitle>
<Input
allowClear
prefix={<SearchOutlined className="text-tertiary" />}
placeholder={intl.formatMessage({
id: 'gpuservice.instance.search.template.placeholder'
})}
value={templateKeyword}
onChange={(e) => setTemplateKeyword(e.target.value)}
/>
</div>
{filteredTemplates.length > 0 ? (
<TemplateSelector
value={templateId}
dataList={filteredTemplates}
onChange={handleTemplateChange}
/>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</div>
</ColumnWrapper>
<Separator></Separator>
</div>
</ColumnWrapper>
<Separator></Separator>
</div>
</>
)}
<div className={styles.formWrapper}>
<ColumnWrapper
styles={{ container: { paddingBlock: 0 } }}
footer={
<ModalFooter
onOk={handleSubmit}
onCancel={handleCancel}
style={{
padding: '16px 24px 8px',
display: 'flex',
justifyContent: 'flex-end'
}}
/>
<>
{isRecreate && open && (
<div style={{ marginInline: 24, paddingTop: 8 }}>
<AlertBlockInfo
type="warning"
contentStyle={{ paddingInline: 0 }}
message={intl.formatMessage({
id: 'gpuservice.instance.recreate.confirm.content'
})}
/>
</div>
)}
<ModalFooter
onOk={handleSubmit}
onCancel={handleCancel}
showOkBtn={!readonly}
style={{
padding: '16px 24px 8px',
display: 'flex',
justifyContent: 'flex-end'
}}
/>
</>
}
>
<>
@@ -343,7 +376,9 @@ const AddModal: React.FC<AddModalProps> = ({
<GPUServiceInstanceForm
ref={form}
action={action}
realAction={realAction}
currentData={data}
disabled={readonly}
onFinish={onFinish}
open={open}
instanceTypeList={instanceTypeList}
@@ -46,6 +46,12 @@ export const status: Record<string, StatusType> = {
};
export const rowActionList = [
{
label: 'common.button.view',
key: 'view',
locale: true,
icon: icons.DetailInfo
},
{
label: 'common.button.viewlog',
key: 'viewlog',
@@ -58,12 +64,12 @@ export const rowActionList = [
locale: true,
icon: icons.ProfileOutlined
},
{
label: 'common.button.edit',
key: 'edit',
locale: true,
icon: icons.EditOutlined
},
// {
// label: 'common.button.edit',
// key: 'edit',
// locale: true,
// icon: icons.EditOutlined
// },
{
label: 'common.button.recreate',
key: 'recreate',
@@ -5,7 +5,13 @@ import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { FormData } from '../config/types';
const Basic = ({ action }: { action: PageActionType }) => {
const Basic = ({
action,
disabled
}: {
action: PageActionType;
disabled?: boolean;
}) => {
const intl = useIntl();
return (
<>
@@ -26,13 +32,14 @@ const Basic = ({ action }: { action: PageActionType }) => {
]}
>
<CInput.Input
disabled={action === PageAction.EDIT}
disabled={disabled || action === PageAction.EDIT}
label={intl.formatMessage({ id: 'gpuservice.instance.name' })}
required
/>
</Form.Item>
<Form.Item<FormData> name={['spec', 'description']}>
<CInput.TextArea
disabled={disabled}
label={intl.formatMessage({ id: 'common.table.description' })}
scaleSize
/>
+60 -14
View File
@@ -1,10 +1,11 @@
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import {
Input as CInput,
CheckboxField,
CollapsePanel,
IconFont,
ScrollSpyTabs,
Select,
useFinishFailed,
useScrollActiveChange,
useWrapperContext
@@ -39,9 +40,11 @@ interface InstanceFormProps {
ref?: any;
open: boolean;
action: PageActionType;
realAction?: PageActionType | string;
currentData?: ListItem | null;
namespace?: string;
instanceTypeList?: InstanceTypeItem[];
disabled?: boolean;
onFinish: (values: FormData) => Promise<void>;
}
@@ -83,7 +86,9 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
(props, ref) => {
const {
action,
realAction,
currentData,
disabled,
open,
namespace = 'default',
instanceTypeList = [],
@@ -92,6 +97,9 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
const intl = useIntl();
const [form] = Form.useForm<InstanceFormValues>();
const scrollTabsRef = useRef<any>(null);
const formAction =
realAction === PageAction.CREATE ? PageAction.CREATE : action;
const sshEnabled = Form.useWatch('enable_ssh', form);
const { detailData: sshKeyData, fetchData: fetchSSHData } = useGetSshkey();
const { getScrollElementScrollableHeight } = useWrapperContext();
const {
@@ -186,9 +194,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
updateActiveKey
});
// useFinishFailed only matches errorFields[].name[0] against `fields`,
// but our form fields are nested (e.g. ['spec','type']), so collapse each
// error path into a single dotted string before delegating.
const handleOnFinishFailed = (errorInfo: any) => {
const errorFields = (errorInfo?.errorFields || []).map((field: any) => ({
...field,
@@ -205,7 +210,12 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
return;
}
if (action === PageAction.EDIT && currentData) {
if (
currentData &&
(action === PageAction.EDIT ||
action === PageAction.VIEW ||
realAction === PageAction.CREATE)
) {
form.setFieldsValue({
metadata: {
name: currentData.metadata?.name,
@@ -220,7 +230,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
});
return;
}
}, [action, currentData, form, open, namespace]);
}, [action, currentData, form, open, namespace, realAction]);
const handleFinish = async (values: InstanceFormValues) => {
await onFinish({
@@ -261,6 +271,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
<Form
name="gpuServiceInstanceForm"
form={form}
disabled={disabled}
onFinish={handleFinish}
onFinishFailed={handleOnFinishFailed}
preserve={false}
@@ -292,7 +303,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
enable_ssh: false
}}
>
<Basic action={action} />
<Basic action={formAction} disabled={disabled} />
<CollapsePanel
activeKey={collapseKeys}
accordion={false}
@@ -304,7 +315,12 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
id: 'gpuservice.instance.section.type'
}),
forceRender: true,
children: <InstanceTypeFormItem action={action} />
children: (
<InstanceTypeFormItem
action={formAction}
disabled={disabled}
/>
)
},
{
key: TABKeysMap.TEMPLATE,
@@ -315,7 +331,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
children: (
<TemplateBasicForm
page="instance"
disabled={action === PageAction.EDIT}
disabled={disabled || formAction === PageAction.EDIT}
onceMaxRequest={onceMaxRequest}
/>
)
@@ -328,16 +344,46 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
forceRender: true,
children: (
<StorageVolume
disabled={action === PageAction.EDIT}
action={action}
disabled={disabled || formAction === PageAction.EDIT}
action={formAction}
/>
)
}
]}
/>
{hasSshPort && (
<Form.Item<FormData> hidden name={['spec', 'sshPublicKey', 'name']}>
<CInput.Input />
<Form.Item<FormData>
name="enable_ssh"
valuePropName="checked"
style={{ marginBottom: 8 }}
>
<CheckboxField
disabled={disabled}
label={'启用 SSH 访问'}
></CheckboxField>
</Form.Item>
{sshEnabled && (
<Form.Item<FormData> name={['spec', 'sshPublicKey', 'name']}>
<Select
disabled={disabled}
mode="multiple"
maxTagCount={1}
label={'SSH 公钥'}
options={[
{
label: 'SSH-key-1',
value: 'gpustack-ssh-public-key'
},
{
label: 'SSH-key-2',
value: 'gpustack-ssh-public-key-2'
},
{
label: 'SSH-key-3',
value: 'gpustack-ssh-public-key-3'
}
]}
></Select>
</Form.Item>
)}
</Form>
@@ -49,10 +49,12 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
interface InstanceTypeFormItemProps {
action: PageActionType;
disabled?: boolean;
}
const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
action
action,
disabled
}) => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
@@ -116,7 +118,7 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
<InstanceTypePicker dataList={dataList} value={typeName} />
</Form.Item>
</FieldBlock>
{!selectedInstanceType?.spec?.acceleratable && (
{selectedInstanceType?.spec?.acceleratable && (
<Form.Item
name={['spec', 'resources', 'accelerator']}
rules={[
@@ -156,7 +158,8 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
min={1}
max={maxGpuCount}
step={1}
disabled={action === PageAction.EDIT}
required
disabled={disabled || action === PageAction.EDIT}
label={intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })}
/>
</Form.Item>
@@ -1,7 +1,11 @@
import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { PageAction } from '@/config';
import { InputNumber as CInputNumber, Select } from '@gpustack/core-ui';
import {
InputNumber as CInputNumber,
LabelInfo,
Select
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Button, Flex, Form, Radio } from 'antd';
import { useAtomValue } from 'jotai';
@@ -95,13 +99,37 @@ const StorageVolume = ({
onChange={(e) => handleModeChange(e.target.value)}
options={[
{
label: intl.formatMessage({ id: 'gpuservice.storage.temporary' }),
label: (
<LabelInfo
description={intl.formatMessage({
id: 'gpuservice.storage.temporary.tips'
})}
label={
<span className="text-primary">
{intl.formatMessage({
id: 'gpuservice.storage.temporary'
})}
</span>
}
/>
),
value: StorageModeValueMap.Temporary
},
{
label: intl.formatMessage({
id: 'gpuservice.storage.persistent'
}),
label: (
<LabelInfo
description={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.tips'
})}
label={
<span className="text-primary">
{intl.formatMessage({
id: 'gpuservice.storage.persistentVolume'
})}
</span>
}
/>
),
value: StorageModeValueMap.Existing
}
]}
@@ -0,0 +1,105 @@
import { PageAction } from '@/config';
import type { PageActionType } from '@/config/types';
import useBodyScroll from '@/hooks/use-body-scroll';
import { useIntl } from '@umijs/max';
import { useState } from 'react';
import type { ListItem } from '../config/types';
const useCreateInstance = () => {
const intl = useIntl();
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [openModalStatus, setOpenModalStatus] = useState<{
action: PageActionType;
open: boolean;
title: string;
currentData?: ListItem | null;
width?: number | string;
realAction?: string;
}>({
action: PageAction.CREATE,
title: '',
open: false,
width: undefined,
currentData: null,
realAction: undefined
});
const openModal = (
action: PageActionType,
title: string,
currentData?: ListItem | null,
width?: number | string,
realAction?: string
) => {
setOpenModalStatus({
action,
title,
open: true,
currentData,
width,
realAction
});
saveScrollHeight();
};
const openCreateInstanceModal = () => {
openModal(
PageAction.CREATE,
intl.formatMessage({ id: 'gpuservice.instance.add' }),
null,
'calc(100vw - 220px)'
);
};
const openEditInstanceModal = (row: ListItem) => {
openModal(
PageAction.EDIT,
intl.formatMessage({ id: 'gpuservice.instance.edit' }),
row,
600
);
};
const openViewInstanceModal = (row: ListItem) => {
openModal(
PageAction.VIEW,
intl.formatMessage({ id: 'common.button.view' }),
row,
600
);
};
const openRecreateInstanceModal = (row: ListItem) => {
openModal(
PageAction.EDIT,
intl.formatMessage({ id: 'common.button.recreate' }),
row,
'calc(100vw - 220px)',
PageAction.CREATE
);
};
const closeModal = () => {
setOpenModalStatus({
...openModalStatus,
title: '',
open: false,
currentData: null,
realAction: undefined
});
restoreScrollHeight();
};
return {
openInstanceModalStatus: openModalStatus,
setOpenInstanceModalStatus: setOpenModalStatus,
openInstanceModal: openModal,
openCreateInstanceModal,
openEditInstanceModal,
openViewInstanceModal,
openRecreateInstanceModal,
closeInstanceModal: closeModal
};
};
export default useCreateInstance;
@@ -6,7 +6,7 @@ import {
StatusTag
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import { Button, Flex } from 'antd';
import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs';
import _ from 'lodash';
@@ -190,6 +190,44 @@ const useInstancesColumns = ({
);
}
},
{
title: 'Instance Type',
dataIndex: ['spec', 'type'],
key: 'type',
sorter: false,
ellipsis: {
showTitle: false
},
width: 230,
render: (text: string) => (
<AutoTooltip
ghost
style={{ maxWidth: 360, color: 'var(--ant-color-text) !important' }}
>
<Flex align="center">
<span
style={{
fontWeight: 400
}}
>
NVIDIA A100 (x2)
</span>
<span
className="text-secondary"
style={{
display: 'flex',
width: 4,
height: 4,
borderRadius: '50%',
backgroundColor: 'var(--ant-color-text-secondary)',
margin: '0 8px'
}}
></span>
<span className="text-tertiary">4C / 16GB</span>
</Flex>
</AutoTooltip>
)
},
{
title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: ['metadata', 'creationTimestamp'],
+39 -56
View File
@@ -2,7 +2,6 @@ import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user';
import { PageAction } from '@/config';
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
import type { PageActionType } from '@/config/types';
import useTableFetch from '@/hooks/use-table-fetch';
import { ProviderValueMap } from '@/pages/cluster-management/config';
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
@@ -18,7 +17,7 @@ import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, Divider, Flex, message, Modal, Table } from 'antd';
import { useAtom } from 'jotai';
import _ from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { PageContainerInner } from '../../_components/page-box';
import {
deleteGPUServiceInstance,
@@ -29,10 +28,11 @@ import AddModal from './components/add-modal';
import ViewEventsModal from './components/view-events-modal';
import ViewLogsModal from './components/view-logs-modal';
import { FormData, ListItem } from './config/types';
import useCreateInstance from './hooks/use-create-instance';
import useInstancesColumns from './hooks/use-instances-columns';
import useViewEvents from './hooks/use-view-events';
import useViewLogs from './hooks/use-view-logs';
import useCreateInstance from './services/use-create-instance';
import useCreateInstanceRequest from './services/use-create-instance';
import useUpdateInstance from './services/use-update-instance';
const GPUService: React.FC = () => {
@@ -108,8 +108,16 @@ const GPUService: React.FC = () => {
contentForDelete: intl.formatMessage({ id: 'gpuservice.instance' })
});
const { fetchData: createInstance } = useCreateInstance();
const { fetchData: createInstance } = useCreateInstanceRequest();
const { fetchData: updateInstance } = useUpdateInstance();
const {
openInstanceModalStatus,
openCreateInstanceModal,
openEditInstanceModal,
openViewInstanceModal,
openRecreateInstanceModal,
closeInstanceModal
} = useCreateInstance();
const { openViewLogsModal, closeViewLogsModal, openViewLogsModalStatus } =
useViewLogs();
const {
@@ -131,18 +139,6 @@ const GPUService: React.FC = () => {
[clusterList]
);
const [openAddModalStatus, setOpenAddModalStatus] = useState<{
action: PageActionType;
open: boolean;
title: string;
currentData?: ListItem | null;
}>({
action: PageAction.CREATE,
title: '',
open: false,
currentData: null
});
useEffect(() => {
fetchClusterList({ page: -1 }).then((clusters) => {
const k8sClusters = clusters.filter(
@@ -172,38 +168,19 @@ const GPUService: React.FC = () => {
};
}, []);
const handleAddInstance = () => {
setOpenAddModalStatus({
action: PageAction.CREATE,
title: intl.formatMessage({ id: 'gpuservice.instance.add' }),
open: true,
currentData: null
});
};
const handleEditInstance = (row: ListItem) => {
setOpenAddModalStatus({
action: PageAction.EDIT,
title: intl.formatMessage({ id: 'gpuservice.instance.edit' }),
open: true,
currentData: row
});
};
const closeModal = () => {
setOpenAddModalStatus({
action: PageAction.CREATE,
title: '',
open: false,
currentData: null
});
};
const handleModalOk = async (data: FormData) => {
try {
if (openAddModalStatus.action === PageAction.EDIT) {
if (openInstanceModalStatus.realAction === PageAction.CREATE) {
await deleteInstance(
openInstanceModalStatus.currentData!.metadata?.name as any
);
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
await createInstance({ data });
} else if (openInstanceModalStatus.action === PageAction.EDIT) {
await updateInstance({
id: openAddModalStatus.currentData!.id,
id: openInstanceModalStatus.currentData!.id,
data
});
} else {
@@ -211,7 +188,7 @@ const GPUService: React.FC = () => {
}
fetchData();
closeModal();
closeInstanceModal();
message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) {
message.error(intl.formatMessage({ id: 'common.message.fail' }));
@@ -249,8 +226,10 @@ const GPUService: React.FC = () => {
});
const handleSelect = useMemoizedFn((val: string, row: ListItem) => {
if (val === 'edit') {
handleEditInstance(row);
if (val === 'view') {
openViewInstanceModal(row);
} else if (val === 'edit') {
openEditInstanceModal(row);
} else if (val === 'delete') {
handleDelete({
...row,
@@ -258,7 +237,9 @@ const GPUService: React.FC = () => {
id: row.metadata?.name as any
});
} else if (val === 'recreate') {
handleRecreate(row);
// Keep handleRecreate above for the old confirm-modal flow. The current
// UX opens the editable form and recreates after submit.
openRecreateInstanceModal(row);
} else if (val === 'viewlog') {
openViewLogsModal(row);
} else if (val === 'viewevent') {
@@ -295,7 +276,7 @@ const GPUService: React.FC = () => {
subTitle={intl.formatMessage({
id: 'noresult.gpuservice.instance.subTitle'
})}
onClick={handleAddInstance}
onClick={openCreateInstanceModal}
buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
/>
);
@@ -340,7 +321,7 @@ const GPUService: React.FC = () => {
handleSearch={handleSearch}
handleSelectChange={handleClusterChange}
handleDeleteByBatch={handleDeleteBatch}
handleClickPrimary={handleAddInstance}
handleClickPrimary={openCreateInstanceModal}
handleInputChange={handleNameChange}
rowSelection={rowSelection}
widths={{ input: 300 }}
@@ -362,11 +343,13 @@ const GPUService: React.FC = () => {
/>
</ConfigProvider>
<AddModal
open={openAddModalStatus.open}
action={openAddModalStatus.action}
title={openAddModalStatus.title}
data={openAddModalStatus.currentData}
onCancel={closeModal}
open={openInstanceModalStatus.open}
action={openInstanceModalStatus.action}
title={openInstanceModalStatus.title}
data={openInstanceModalStatus.currentData}
width={openInstanceModalStatus.width}
realAction={openInstanceModalStatus.realAction}
onCancel={closeInstanceModal}
onOk={handleModalOk}
/>
<ViewLogsModal
@@ -22,6 +22,5 @@
.formWrapper {
display: flex;
flex: 1;
max-width: 34%;
min-height: 0;
}