From 2ebb8f44b71a3e80620839f5321db468131e08f9 Mon Sep 17 00:00:00 2001 From: jialin Date: Mon, 18 May 2026 16:23:34 +0800 Subject: [PATCH] fix: recreate action --- src/locales/en-US/gpuservice.ts | 4 + src/locales/ja-JP/gpuservice.ts | 6 +- src/locales/ru-RU/gpuservice.ts | 6 +- src/locales/tr-TR/gpuservice.ts | 6 +- src/locales/zh-CN/gpuservice.ts | 3 + src/pages/gpu-service/instances/apis/index.ts | 8 + .../instances/components/add-modal.tsx | 213 ++++++++++-------- .../gpu-service/instances/config/index.ts | 18 +- .../gpu-service/instances/forms/basic.tsx | 11 +- .../gpu-service/instances/forms/index.tsx | 74 ++++-- .../instances/forms/instance-type.tsx | 9 +- .../instances/forms/storage-volume.tsx | 38 +++- .../instances/hooks/use-create-instance.ts | 105 +++++++++ .../instances/hooks/use-instances-columns.tsx | 40 +++- src/pages/gpu-service/instances/index.tsx | 95 ++++---- .../instances/styles/instances.module.less | 1 - 16 files changed, 457 insertions(+), 180 deletions(-) create mode 100644 src/pages/gpu-service/instances/hooks/use-create-instance.ts diff --git a/src/locales/en-US/gpuservice.ts b/src/locales/en-US/gpuservice.ts index e2521f3d..938aa806 100644 --- a/src/locales/en-US/gpuservice.ts +++ b/src/locales/en-US/gpuservice.ts @@ -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)', diff --git a/src/locales/ja-JP/gpuservice.ts b/src/locales/ja-JP/gpuservice.ts index 0ecf0f35..e9ce2b3e 100644 --- a/src/locales/ja-JP/gpuservice.ts +++ b/src/locales/ja-JP/gpuservice.ts @@ -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.' }; diff --git a/src/locales/ru-RU/gpuservice.ts b/src/locales/ru-RU/gpuservice.ts index 4d0606e8..79fbff5b 100644 --- a/src/locales/ru-RU/gpuservice.ts +++ b/src/locales/ru-RU/gpuservice.ts @@ -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.' }; diff --git a/src/locales/tr-TR/gpuservice.ts b/src/locales/tr-TR/gpuservice.ts index fa1d2c47..066ece9c 100644 --- a/src/locales/tr-TR/gpuservice.ts +++ b/src/locales/tr-TR/gpuservice.ts @@ -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.' }; diff --git a/src/locales/zh-CN/gpuservice.ts b/src/locales/zh-CN/gpuservice.ts index f29301ba..c589e2fe 100644 --- a/src/locales/zh-CN/gpuservice.ts +++ b/src/locales/zh-CN/gpuservice.ts @@ -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': '请输入临时存储容量', diff --git a/src/pages/gpu-service/instances/apis/index.ts b/src/pages/gpu-service/instances/apis/index.ts index 08dfe9c4..c7099a69 100644 --- a/src/pages/gpu-service/instances/apis/index.ts +++ b/src/pages/gpu-service/instances/apis/index.ts @@ -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( diff --git a/src/pages/gpu-service/instances/components/add-modal.tsx b/src/pages/gpu-service/instances/components/add-modal.tsx index f9c0de6a..139270eb 100644 --- a/src/pages/gpu-service/instances/components/add-modal.tsx +++ b/src/pages/gpu-service/instances/components/add-modal.tsx @@ -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 = ({ open, onOk, data, - onCancel + onCancel, + width, + realAction }) => { const intl = useIntl(); const form = useRef(null); @@ -92,6 +101,10 @@ const AddModal: React.FC = ({ 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 = ({ 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 = ({ applyAutoSelection(instanceRes?.items || [], templatesRes?.items || []); } ); - }, [open]); + }, [open, shouldAutoSelectResource]); // filter instance types const filteredInstanceTypes = instanceTypeList.filter((item) => @@ -238,102 +251,122 @@ const AddModal: React.FC = ({ }} keyboard={false} styles={{ - wrapper: { width: 'calc(100vw - 220px)' }, + wrapper: { width: width || 'calc(100vw - 220px)' }, body: { overflowY: 'hidden' } }} footer={false} >
-
- -
-
- - {intl.formatMessage({ id: 'gpuservice.instance.types' })} - - } - placeholder={intl.formatMessage({ - id: 'gpuservice.instance.search.type.placeholder' - })} - value={instanceKeyword} - onChange={(e) => setInstanceKeyword(e.target.value)} - /> -
- + {showResourceSelectors && ( + <> +
+ +
+
+ + {intl.formatMessage({ id: 'gpuservice.instance.types' })} + + } + placeholder={intl.formatMessage({ + id: 'gpuservice.instance.search.type.placeholder' + })} + value={instanceKeyword} + onChange={(e) => setInstanceKeyword(e.target.value)} + /> +
+ +
+
+
- - -
-
- -
-
- - {intl.formatMessage({ id: 'gpuservice.instance.templates' })} - - } - placeholder={intl.formatMessage({ - id: 'gpuservice.instance.search.template.placeholder' - })} - value={templateKeyword} - onChange={(e) => setTemplateKeyword(e.target.value)} - /> -
- {filteredTemplates.length > 0 ? ( - - ) : ( - - )} +
+ +
+
+ + {intl.formatMessage({ + id: 'gpuservice.instance.templates' + })} + + } + placeholder={intl.formatMessage({ + id: 'gpuservice.instance.search.template.placeholder' + })} + value={templateKeyword} + onChange={(e) => setTemplateKeyword(e.target.value)} + /> +
+ {filteredTemplates.length > 0 ? ( + + ) : ( + + )} +
+
+
- - -
+ + )}
+ <> + {isRecreate && open && ( +
+ +
+ )} + + } > <> @@ -343,7 +376,9 @@ const AddModal: React.FC = ({ = { }; 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', diff --git a/src/pages/gpu-service/instances/forms/basic.tsx b/src/pages/gpu-service/instances/forms/basic.tsx index b98225ec..011f2448 100644 --- a/src/pages/gpu-service/instances/forms/basic.tsx +++ b/src/pages/gpu-service/instances/forms/basic.tsx @@ -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 }) => { ]} > name={['spec', 'description']}> diff --git a/src/pages/gpu-service/instances/forms/index.tsx b/src/pages/gpu-service/instances/forms/index.tsx index 0b9e433a..41c3df4a 100644 --- a/src/pages/gpu-service/instances/forms/index.tsx +++ b/src/pages/gpu-service/instances/forms/index.tsx @@ -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; } @@ -83,7 +86,9 @@ const GPUServiceInstanceForm: React.FC = forwardRef( (props, ref) => { const { action, + realAction, currentData, + disabled, open, namespace = 'default', instanceTypeList = [], @@ -92,6 +97,9 @@ const GPUServiceInstanceForm: React.FC = forwardRef( const intl = useIntl(); const [form] = Form.useForm(); const scrollTabsRef = useRef(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 = 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 = 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 = 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 = forwardRef(
= forwardRef( enable_ssh: false }} > - + = forwardRef( id: 'gpuservice.instance.section.type' }), forceRender: true, - children: + children: ( + + ) }, { key: TABKeysMap.TEMPLATE, @@ -315,7 +331,7 @@ const GPUServiceInstanceForm: React.FC = forwardRef( children: ( ) @@ -328,16 +344,46 @@ const GPUServiceInstanceForm: React.FC = forwardRef( forceRender: true, children: ( ) } ]} /> - {hasSshPort && ( - hidden name={['spec', 'sshPublicKey', 'name']}> - + + + name="enable_ssh" + valuePropName="checked" + style={{ marginBottom: 8 }} + > + + + {sshEnabled && ( + name={['spec', 'sshPublicKey', 'name']}> + )} diff --git a/src/pages/gpu-service/instances/forms/instance-type.tsx b/src/pages/gpu-service/instances/forms/instance-type.tsx index fbb1f2ad..149a374a 100644 --- a/src/pages/gpu-service/instances/forms/instance-type.tsx +++ b/src/pages/gpu-service/instances/forms/instance-type.tsx @@ -49,10 +49,12 @@ const InstanceTypePicker: React.FC = ({ interface InstanceTypeFormItemProps { action: PageActionType; + disabled?: boolean; } const InstanceTypeFormItem: React.FC = ({ - action + action, + disabled }) => { const intl = useIntl(); const form = Form.useFormInstance(); @@ -116,7 +118,7 @@ const InstanceTypeFormItem: React.FC = ({ - {!selectedInstanceType?.spec?.acceleratable && ( + {selectedInstanceType?.spec?.acceleratable && ( = ({ min={1} max={maxGpuCount} step={1} - disabled={action === PageAction.EDIT} + required + disabled={disabled || action === PageAction.EDIT} label={intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })} /> diff --git a/src/pages/gpu-service/instances/forms/storage-volume.tsx b/src/pages/gpu-service/instances/forms/storage-volume.tsx index 2f315b19..b1e4b56d 100644 --- a/src/pages/gpu-service/instances/forms/storage-volume.tsx +++ b/src/pages/gpu-service/instances/forms/storage-volume.tsx @@ -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: ( + + {intl.formatMessage({ + id: 'gpuservice.storage.temporary' + })} + + } + /> + ), value: StorageModeValueMap.Temporary }, { - label: intl.formatMessage({ - id: 'gpuservice.storage.persistent' - }), + label: ( + + {intl.formatMessage({ + id: 'gpuservice.storage.persistentVolume' + })} + + } + /> + ), value: StorageModeValueMap.Existing } ]} diff --git a/src/pages/gpu-service/instances/hooks/use-create-instance.ts b/src/pages/gpu-service/instances/hooks/use-create-instance.ts new file mode 100644 index 00000000..525f07cb --- /dev/null +++ b/src/pages/gpu-service/instances/hooks/use-create-instance.ts @@ -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; diff --git a/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx b/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx index 8a650b16..7ae08db2 100644 --- a/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx +++ b/src/pages/gpu-service/instances/hooks/use-instances-columns.tsx @@ -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) => ( + + + + NVIDIA A100 (x2) + + + 4C / 16GB + + + ) + }, { title: intl.formatMessage({ id: 'common.table.createTime' }), dataIndex: ['metadata', 'creationTimestamp'], diff --git a/src/pages/gpu-service/instances/index.tsx b/src/pages/gpu-service/instances/index.tsx index f9c3c254..691812eb 100644 --- a/src/pages/gpu-service/instances/index.tsx +++ b/src/pages/gpu-service/instances/index.tsx @@ -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 = () => { />