fix: recreate action
This commit is contained in:
@@ -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)',
|
||||
|
||||
@@ -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.'
|
||||
};
|
||||
|
||||
@@ -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.'
|
||||
};
|
||||
|
||||
@@ -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.'
|
||||
};
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user