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.persistent': 'Persistent',
'gpuservice.storage.temporary': 'Temporary', 'gpuservice.storage.temporary': 'Temporary',
'gpuservice.storage.persistentVolume': 'Persistent Volume', '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': 'gpuservice.storage.persistentVolume.required':
'Please select a persistent volume', 'Please select a persistent volume',
'gpuservice.storage.tempCapacity': 'Capacity (GB)', 'gpuservice.storage.tempCapacity': 'Capacity (GB)',
+5 -1
View File
@@ -96,5 +96,9 @@ export default {
'gpuservice.storage.tempCapacity.required': 'gpuservice.storage.tempCapacity.required':
'一時ストレージ容量を入力してください', '一時ストレージ容量を入力してください',
'gpuservice.form.rule.name': '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.storage.tempCapacity.required':
'Введите объём временного хранилища', 'Введите объём временного хранилища',
'gpuservice.form.rule.name': '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': 'gpuservice.storage.tempCapacity.required':
'Lütfen geçici depolama kapasitesini girin', 'Lütfen geçici depolama kapasitesini girin',
'gpuservice.form.rule.name': '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.persistent': '持久',
'gpuservice.storage.temporary': '临时', 'gpuservice.storage.temporary': '临时',
'gpuservice.storage.persistentVolume': '持久卷', 'gpuservice.storage.persistentVolume': '持久卷',
'gpuservice.storage.temporary.tips': '实例停止后,数据将被清除。',
'gpuservice.storage.persistentVolume.tips':
'数据在实例重启后仍会保留,仅在实例被终止时删除。无法被其它实例共享。',
'gpuservice.storage.persistentVolume.required': '请选择持久卷', 'gpuservice.storage.persistentVolume.required': '请选择持久卷',
'gpuservice.storage.tempCapacity': '容量(GB', 'gpuservice.storage.tempCapacity': '容量(GB',
'gpuservice.storage.tempCapacity.required': '请输入临时存储容量', '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`; 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 =========== // =========== Instances ===========
export async function queryGPUServiceInstances( export async function queryGPUServiceInstances(
@@ -2,7 +2,12 @@ import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import Separator from '@/pages/llmodels/components/separator'; import Separator from '@/pages/llmodels/components/separator';
import { SearchOutlined } from '@ant-design/icons'; 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 { useIntl } from '@umijs/max';
import { Empty, Input, Typography } from 'antd'; import { Empty, Input, Typography } from 'antd';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
@@ -19,6 +24,8 @@ type AddModalProps = {
title: string; title: string;
action: PageActionType; action: PageActionType;
open: boolean; open: boolean;
width?: number | string;
realAction?: string;
onOk: (values: FormData) => void; onOk: (values: FormData) => void;
data?: ListItem | null; data?: ListItem | null;
onCancel: () => void; onCancel: () => void;
@@ -71,7 +78,9 @@ const AddModal: React.FC<AddModalProps> = ({
open, open,
onOk, onOk,
data, data,
onCancel onCancel,
width,
realAction
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const form = useRef<any>(null); const form = useRef<any>(null);
@@ -92,6 +101,10 @@ const AddModal: React.FC<AddModalProps> = ({
const instanceTypeList = detailData?.items || []; const instanceTypeList = detailData?.items || [];
const templateList = templatesData?.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 = ( const findTemplateByManufacturer = (
manufacturer: string | undefined, manufacturer: string | undefined,
@@ -143,7 +156,7 @@ const AddModal: React.FC<AddModalProps> = ({
instanceTypes: InstanceTypeItem[], instanceTypes: InstanceTypeItem[],
templates: TemplateItem[] templates: TemplateItem[]
) => { ) => {
if (action !== PageAction.CREATE) return; if (!shouldAutoSelectResource) return;
if (!instanceTypes.length) return; if (!instanceTypes.length) return;
const first = instanceTypes[0]; const first = instanceTypes[0];
@@ -172,7 +185,7 @@ const AddModal: React.FC<AddModalProps> = ({
applyAutoSelection(instanceRes?.items || [], templatesRes?.items || []); applyAutoSelection(instanceRes?.items || [], templatesRes?.items || []);
} }
); );
}, [open]); }, [open, shouldAutoSelectResource]);
// filter instance types // filter instance types
const filteredInstanceTypes = instanceTypeList.filter((item) => const filteredInstanceTypes = instanceTypeList.filter((item) =>
@@ -238,102 +251,122 @@ const AddModal: React.FC<AddModalProps> = ({
}} }}
keyboard={false} keyboard={false}
styles={{ styles={{
wrapper: { width: 'calc(100vw - 220px)' }, wrapper: { width: width || 'calc(100vw - 220px)' },
body: { overflowY: 'hidden' } body: { overflowY: 'hidden' }
}} }}
footer={false} footer={false}
> >
<div className={styles.container}> <div className={styles.container}>
<div className={styles.colWrapper}> {showResourceSelectors && (
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}> <>
<div className={styles.panelBody}> <div className={styles.colWrapper}>
<div <ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
style={{ <div className={styles.panelBody}>
display: 'flex', <div
flexDirection: 'column', style={{
gap: 16, display: 'flex',
position: 'sticky', flexDirection: 'column',
top: 0, gap: 16,
zIndex: 10, position: 'sticky',
backgroundColor: 'var(--ant-color-bg-elevated)' top: 0,
}} zIndex: 10,
> backgroundColor: 'var(--ant-color-bg-elevated)'
<ColTitle style={{ paddingBottom: 0 }}> }}
{intl.formatMessage({ id: 'gpuservice.instance.types' })} >
</ColTitle> <ColTitle style={{ paddingBottom: 0 }}>
<Input {intl.formatMessage({ id: 'gpuservice.instance.types' })}
allowClear </ColTitle>
prefix={<SearchOutlined className="text-tertiary" />} <Input
placeholder={intl.formatMessage({ allowClear
id: 'gpuservice.instance.search.type.placeholder' prefix={<SearchOutlined className="text-tertiary" />}
})} placeholder={intl.formatMessage({
value={instanceKeyword} id: 'gpuservice.instance.search.type.placeholder'
onChange={(e) => setInstanceKeyword(e.target.value)} })}
/> value={instanceKeyword}
</div> onChange={(e) => setInstanceKeyword(e.target.value)}
<InstanceTypeList />
value={instanceTypeSelection.instanceType} </div>
dataList={filteredInstanceTypes} <InstanceTypeList
loading={instanceTypesLoading} value={instanceTypeSelection.instanceType}
onChange={handleInstanceTypeChange} dataList={filteredInstanceTypes}
/> loading={instanceTypesLoading}
onChange={handleInstanceTypeChange}
/>
</div>
</ColumnWrapper>
<Separator></Separator>
</div> </div>
</ColumnWrapper> <div className={styles.colWrapper}>
<Separator></Separator> <ColumnWrapper styles={{ container: { paddingBlock: 0 } }}>
</div> <div className={styles.panelBody}>
<div className={styles.colWrapper}> <div
<ColumnWrapper styles={{ container: { paddingBlock: 0 } }}> style={{
<div className={styles.panelBody}> display: 'flex',
<div flexDirection: 'column',
style={{ gap: 16,
display: 'flex', position: 'sticky',
flexDirection: 'column', top: 0,
gap: 16, zIndex: 10,
position: 'sticky', backgroundColor: 'var(--ant-color-bg-elevated)'
top: 0, }}
zIndex: 10, >
backgroundColor: 'var(--ant-color-bg-elevated)' <ColTitle style={{ paddingBottom: 0 }}>
}} {intl.formatMessage({
> id: 'gpuservice.instance.templates'
<ColTitle style={{ paddingBottom: 0 }}> })}
{intl.formatMessage({ id: 'gpuservice.instance.templates' })} </ColTitle>
</ColTitle> <Input
<Input allowClear
allowClear prefix={<SearchOutlined className="text-tertiary" />}
prefix={<SearchOutlined className="text-tertiary" />} placeholder={intl.formatMessage({
placeholder={intl.formatMessage({ id: 'gpuservice.instance.search.template.placeholder'
id: 'gpuservice.instance.search.template.placeholder' })}
})} value={templateKeyword}
value={templateKeyword} onChange={(e) => setTemplateKeyword(e.target.value)}
onChange={(e) => setTemplateKeyword(e.target.value)} />
/> </div>
</div> {filteredTemplates.length > 0 ? (
{filteredTemplates.length > 0 ? ( <TemplateSelector
<TemplateSelector value={templateId}
value={templateId} dataList={filteredTemplates}
dataList={filteredTemplates} onChange={handleTemplateChange}
onChange={handleTemplateChange} />
/> ) : (
) : ( <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /> )}
)} </div>
</ColumnWrapper>
<Separator></Separator>
</div> </div>
</ColumnWrapper> </>
<Separator></Separator> )}
</div>
<div className={styles.formWrapper}> <div className={styles.formWrapper}>
<ColumnWrapper <ColumnWrapper
styles={{ container: { paddingBlock: 0 } }} styles={{ container: { paddingBlock: 0 } }}
footer={ footer={
<ModalFooter <>
onOk={handleSubmit} {isRecreate && open && (
onCancel={handleCancel} <div style={{ marginInline: 24, paddingTop: 8 }}>
style={{ <AlertBlockInfo
padding: '16px 24px 8px', type="warning"
display: 'flex', contentStyle={{ paddingInline: 0 }}
justifyContent: 'flex-end' 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 <GPUServiceInstanceForm
ref={form} ref={form}
action={action} action={action}
realAction={realAction}
currentData={data} currentData={data}
disabled={readonly}
onFinish={onFinish} onFinish={onFinish}
open={open} open={open}
instanceTypeList={instanceTypeList} instanceTypeList={instanceTypeList}
@@ -46,6 +46,12 @@ export const status: Record<string, StatusType> = {
}; };
export const rowActionList = [ export const rowActionList = [
{
label: 'common.button.view',
key: 'view',
locale: true,
icon: icons.DetailInfo
},
{ {
label: 'common.button.viewlog', label: 'common.button.viewlog',
key: 'viewlog', key: 'viewlog',
@@ -58,12 +64,12 @@ export const rowActionList = [
locale: true, locale: true,
icon: icons.ProfileOutlined icon: icons.ProfileOutlined
}, },
{ // {
label: 'common.button.edit', // label: 'common.button.edit',
key: 'edit', // key: 'edit',
locale: true, // locale: true,
icon: icons.EditOutlined // icon: icons.EditOutlined
}, // },
{ {
label: 'common.button.recreate', label: 'common.button.recreate',
key: 'recreate', key: 'recreate',
@@ -5,7 +5,13 @@ import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
const Basic = ({ action }: { action: PageActionType }) => { const Basic = ({
action,
disabled
}: {
action: PageActionType;
disabled?: boolean;
}) => {
const intl = useIntl(); const intl = useIntl();
return ( return (
<> <>
@@ -26,13 +32,14 @@ const Basic = ({ action }: { action: PageActionType }) => {
]} ]}
> >
<CInput.Input <CInput.Input
disabled={action === PageAction.EDIT} disabled={disabled || action === PageAction.EDIT}
label={intl.formatMessage({ id: 'gpuservice.instance.name' })} label={intl.formatMessage({ id: 'gpuservice.instance.name' })}
required required
/> />
</Form.Item> </Form.Item>
<Form.Item<FormData> name={['spec', 'description']}> <Form.Item<FormData> name={['spec', 'description']}>
<CInput.TextArea <CInput.TextArea
disabled={disabled}
label={intl.formatMessage({ id: 'common.table.description' })} label={intl.formatMessage({ id: 'common.table.description' })}
scaleSize scaleSize
/> />
+60 -14
View File
@@ -1,10 +1,11 @@
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { import {
Input as CInput, CheckboxField,
CollapsePanel, CollapsePanel,
IconFont, IconFont,
ScrollSpyTabs, ScrollSpyTabs,
Select,
useFinishFailed, useFinishFailed,
useScrollActiveChange, useScrollActiveChange,
useWrapperContext useWrapperContext
@@ -39,9 +40,11 @@ interface InstanceFormProps {
ref?: any; ref?: any;
open: boolean; open: boolean;
action: PageActionType; action: PageActionType;
realAction?: PageActionType | string;
currentData?: ListItem | null; currentData?: ListItem | null;
namespace?: string; namespace?: string;
instanceTypeList?: InstanceTypeItem[]; instanceTypeList?: InstanceTypeItem[];
disabled?: boolean;
onFinish: (values: FormData) => Promise<void>; onFinish: (values: FormData) => Promise<void>;
} }
@@ -83,7 +86,9 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
(props, ref) => { (props, ref) => {
const { const {
action, action,
realAction,
currentData, currentData,
disabled,
open, open,
namespace = 'default', namespace = 'default',
instanceTypeList = [], instanceTypeList = [],
@@ -92,6 +97,9 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
const intl = useIntl(); const intl = useIntl();
const [form] = Form.useForm<InstanceFormValues>(); const [form] = Form.useForm<InstanceFormValues>();
const scrollTabsRef = useRef<any>(null); 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 { detailData: sshKeyData, fetchData: fetchSSHData } = useGetSshkey();
const { getScrollElementScrollableHeight } = useWrapperContext(); const { getScrollElementScrollableHeight } = useWrapperContext();
const { const {
@@ -186,9 +194,6 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
updateActiveKey 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 handleOnFinishFailed = (errorInfo: any) => {
const errorFields = (errorInfo?.errorFields || []).map((field: any) => ({ const errorFields = (errorInfo?.errorFields || []).map((field: any) => ({
...field, ...field,
@@ -205,7 +210,12 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
return; return;
} }
if (action === PageAction.EDIT && currentData) { if (
currentData &&
(action === PageAction.EDIT ||
action === PageAction.VIEW ||
realAction === PageAction.CREATE)
) {
form.setFieldsValue({ form.setFieldsValue({
metadata: { metadata: {
name: currentData.metadata?.name, name: currentData.metadata?.name,
@@ -220,7 +230,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
}); });
return; return;
} }
}, [action, currentData, form, open, namespace]); }, [action, currentData, form, open, namespace, realAction]);
const handleFinish = async (values: InstanceFormValues) => { const handleFinish = async (values: InstanceFormValues) => {
await onFinish({ await onFinish({
@@ -261,6 +271,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
<Form <Form
name="gpuServiceInstanceForm" name="gpuServiceInstanceForm"
form={form} form={form}
disabled={disabled}
onFinish={handleFinish} onFinish={handleFinish}
onFinishFailed={handleOnFinishFailed} onFinishFailed={handleOnFinishFailed}
preserve={false} preserve={false}
@@ -292,7 +303,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
enable_ssh: false enable_ssh: false
}} }}
> >
<Basic action={action} /> <Basic action={formAction} disabled={disabled} />
<CollapsePanel <CollapsePanel
activeKey={collapseKeys} activeKey={collapseKeys}
accordion={false} accordion={false}
@@ -304,7 +315,12 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
id: 'gpuservice.instance.section.type' id: 'gpuservice.instance.section.type'
}), }),
forceRender: true, forceRender: true,
children: <InstanceTypeFormItem action={action} /> children: (
<InstanceTypeFormItem
action={formAction}
disabled={disabled}
/>
)
}, },
{ {
key: TABKeysMap.TEMPLATE, key: TABKeysMap.TEMPLATE,
@@ -315,7 +331,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
children: ( children: (
<TemplateBasicForm <TemplateBasicForm
page="instance" page="instance"
disabled={action === PageAction.EDIT} disabled={disabled || formAction === PageAction.EDIT}
onceMaxRequest={onceMaxRequest} onceMaxRequest={onceMaxRequest}
/> />
) )
@@ -328,16 +344,46 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
forceRender: true, forceRender: true,
children: ( children: (
<StorageVolume <StorageVolume
disabled={action === PageAction.EDIT} disabled={disabled || formAction === PageAction.EDIT}
action={action} action={formAction}
/> />
) )
} }
]} ]}
/> />
{hasSshPort && (
<Form.Item<FormData> hidden name={['spec', 'sshPublicKey', 'name']}> <Form.Item<FormData>
<CInput.Input /> 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.Item>
)} )}
</Form> </Form>
@@ -49,10 +49,12 @@ const InstanceTypePicker: React.FC<InstanceTypePickerProps> = ({
interface InstanceTypeFormItemProps { interface InstanceTypeFormItemProps {
action: PageActionType; action: PageActionType;
disabled?: boolean;
} }
const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
action action,
disabled
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const form = Form.useFormInstance<FormData>(); const form = Form.useFormInstance<FormData>();
@@ -116,7 +118,7 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
<InstanceTypePicker dataList={dataList} value={typeName} /> <InstanceTypePicker dataList={dataList} value={typeName} />
</Form.Item> </Form.Item>
</FieldBlock> </FieldBlock>
{!selectedInstanceType?.spec?.acceleratable && ( {selectedInstanceType?.spec?.acceleratable && (
<Form.Item <Form.Item
name={['spec', 'resources', 'accelerator']} name={['spec', 'resources', 'accelerator']}
rules={[ rules={[
@@ -156,7 +158,8 @@ const InstanceTypeFormItem: React.FC<InstanceTypeFormItemProps> = ({
min={1} min={1}
max={maxGpuCount} max={maxGpuCount}
step={1} step={1}
disabled={action === PageAction.EDIT} required
disabled={disabled || action === PageAction.EDIT}
label={intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })} label={intl.formatMessage({ id: 'gpuservice.instance.gpuCount' })}
/> />
</Form.Item> </Form.Item>
@@ -1,7 +1,11 @@
import { currentClusterAtom } from '@/atoms/gpuservice'; import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user'; import { getCurrentOrgNamespace } from '@/atoms/user';
import { PageAction } from '@/config'; 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 { useIntl } from '@umijs/max';
import { Button, Flex, Form, Radio } from 'antd'; import { Button, Flex, Form, Radio } from 'antd';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
@@ -95,13 +99,37 @@ const StorageVolume = ({
onChange={(e) => handleModeChange(e.target.value)} onChange={(e) => handleModeChange(e.target.value)}
options={[ 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 value: StorageModeValueMap.Temporary
}, },
{ {
label: intl.formatMessage({ label: (
id: 'gpuservice.storage.persistent' <LabelInfo
}), description={intl.formatMessage({
id: 'gpuservice.storage.persistentVolume.tips'
})}
label={
<span className="text-primary">
{intl.formatMessage({
id: 'gpuservice.storage.persistentVolume'
})}
</span>
}
/>
),
value: StorageModeValueMap.Existing 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 StatusTag
} from '@gpustack/core-ui'; } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button } from 'antd'; import { Button, Flex } from 'antd';
import type { ColumnsType } from 'antd/lib/table'; import type { ColumnsType } from 'antd/lib/table';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import _ from 'lodash'; 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' }), title: intl.formatMessage({ id: 'common.table.createTime' }),
dataIndex: ['metadata', 'creationTimestamp'], dataIndex: ['metadata', 'creationTimestamp'],
+39 -56
View File
@@ -2,7 +2,6 @@ import { currentClusterAtom } from '@/atoms/gpuservice';
import { getCurrentOrgNamespace } from '@/atoms/user'; import { getCurrentOrgNamespace } from '@/atoms/user';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings'; import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
import type { PageActionType } from '@/config/types';
import useTableFetch from '@/hooks/use-table-fetch'; import useTableFetch from '@/hooks/use-table-fetch';
import { ProviderValueMap } from '@/pages/cluster-management/config'; import { ProviderValueMap } from '@/pages/cluster-management/config';
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list'; import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
@@ -18,7 +17,7 @@ import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, Divider, Flex, message, Modal, Table } from 'antd'; import { ConfigProvider, Divider, Flex, message, Modal, Table } from 'antd';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import _ from 'lodash'; import _ from 'lodash';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { PageContainerInner } from '../../_components/page-box'; import { PageContainerInner } from '../../_components/page-box';
import { import {
deleteGPUServiceInstance, deleteGPUServiceInstance,
@@ -29,10 +28,11 @@ import AddModal from './components/add-modal';
import ViewEventsModal from './components/view-events-modal'; import ViewEventsModal from './components/view-events-modal';
import ViewLogsModal from './components/view-logs-modal'; import ViewLogsModal from './components/view-logs-modal';
import { FormData, ListItem } from './config/types'; import { FormData, ListItem } from './config/types';
import useCreateInstance from './hooks/use-create-instance';
import useInstancesColumns from './hooks/use-instances-columns'; import useInstancesColumns from './hooks/use-instances-columns';
import useViewEvents from './hooks/use-view-events'; import useViewEvents from './hooks/use-view-events';
import useViewLogs from './hooks/use-view-logs'; 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'; import useUpdateInstance from './services/use-update-instance';
const GPUService: React.FC = () => { const GPUService: React.FC = () => {
@@ -108,8 +108,16 @@ const GPUService: React.FC = () => {
contentForDelete: intl.formatMessage({ id: 'gpuservice.instance' }) contentForDelete: intl.formatMessage({ id: 'gpuservice.instance' })
}); });
const { fetchData: createInstance } = useCreateInstance(); const { fetchData: createInstance } = useCreateInstanceRequest();
const { fetchData: updateInstance } = useUpdateInstance(); const { fetchData: updateInstance } = useUpdateInstance();
const {
openInstanceModalStatus,
openCreateInstanceModal,
openEditInstanceModal,
openViewInstanceModal,
openRecreateInstanceModal,
closeInstanceModal
} = useCreateInstance();
const { openViewLogsModal, closeViewLogsModal, openViewLogsModalStatus } = const { openViewLogsModal, closeViewLogsModal, openViewLogsModalStatus } =
useViewLogs(); useViewLogs();
const { const {
@@ -131,18 +139,6 @@ const GPUService: React.FC = () => {
[clusterList] [clusterList]
); );
const [openAddModalStatus, setOpenAddModalStatus] = useState<{
action: PageActionType;
open: boolean;
title: string;
currentData?: ListItem | null;
}>({
action: PageAction.CREATE,
title: '',
open: false,
currentData: null
});
useEffect(() => { useEffect(() => {
fetchClusterList({ page: -1 }).then((clusters) => { fetchClusterList({ page: -1 }).then((clusters) => {
const k8sClusters = clusters.filter( 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) => { const handleModalOk = async (data: FormData) => {
try { 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({ await updateInstance({
id: openAddModalStatus.currentData!.id, id: openInstanceModalStatus.currentData!.id,
data data
}); });
} else { } else {
@@ -211,7 +188,7 @@ const GPUService: React.FC = () => {
} }
fetchData(); fetchData();
closeModal(); closeInstanceModal();
message.success(intl.formatMessage({ id: 'common.message.success' })); message.success(intl.formatMessage({ id: 'common.message.success' }));
} catch (error) { } catch (error) {
message.error(intl.formatMessage({ id: 'common.message.fail' })); message.error(intl.formatMessage({ id: 'common.message.fail' }));
@@ -249,8 +226,10 @@ const GPUService: React.FC = () => {
}); });
const handleSelect = useMemoizedFn((val: string, row: ListItem) => { const handleSelect = useMemoizedFn((val: string, row: ListItem) => {
if (val === 'edit') { if (val === 'view') {
handleEditInstance(row); openViewInstanceModal(row);
} else if (val === 'edit') {
openEditInstanceModal(row);
} else if (val === 'delete') { } else if (val === 'delete') {
handleDelete({ handleDelete({
...row, ...row,
@@ -258,7 +237,9 @@ const GPUService: React.FC = () => {
id: row.metadata?.name as any id: row.metadata?.name as any
}); });
} else if (val === 'recreate') { } 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') { } else if (val === 'viewlog') {
openViewLogsModal(row); openViewLogsModal(row);
} else if (val === 'viewevent') { } else if (val === 'viewevent') {
@@ -295,7 +276,7 @@ const GPUService: React.FC = () => {
subTitle={intl.formatMessage({ subTitle={intl.formatMessage({
id: 'noresult.gpuservice.instance.subTitle' id: 'noresult.gpuservice.instance.subTitle'
})} })}
onClick={handleAddInstance} onClick={openCreateInstanceModal}
buttonText={intl.formatMessage({ id: 'noresult.button.add' })} buttonText={intl.formatMessage({ id: 'noresult.button.add' })}
/> />
); );
@@ -340,7 +321,7 @@ const GPUService: React.FC = () => {
handleSearch={handleSearch} handleSearch={handleSearch}
handleSelectChange={handleClusterChange} handleSelectChange={handleClusterChange}
handleDeleteByBatch={handleDeleteBatch} handleDeleteByBatch={handleDeleteBatch}
handleClickPrimary={handleAddInstance} handleClickPrimary={openCreateInstanceModal}
handleInputChange={handleNameChange} handleInputChange={handleNameChange}
rowSelection={rowSelection} rowSelection={rowSelection}
widths={{ input: 300 }} widths={{ input: 300 }}
@@ -362,11 +343,13 @@ const GPUService: React.FC = () => {
/> />
</ConfigProvider> </ConfigProvider>
<AddModal <AddModal
open={openAddModalStatus.open} open={openInstanceModalStatus.open}
action={openAddModalStatus.action} action={openInstanceModalStatus.action}
title={openAddModalStatus.title} title={openInstanceModalStatus.title}
data={openAddModalStatus.currentData} data={openInstanceModalStatus.currentData}
onCancel={closeModal} width={openInstanceModalStatus.width}
realAction={openInstanceModalStatus.realAction}
onCancel={closeInstanceModal}
onOk={handleModalOk} onOk={handleModalOk}
/> />
<ViewLogsModal <ViewLogsModal
@@ -22,6 +22,5 @@
.formWrapper { .formWrapper {
display: flex; display: flex;
flex: 1; flex: 1;
max-width: 34%;
min-height: 0; min-height: 0;
} }