feat: add instance recreate action

This commit is contained in:
jialin
2026-05-15 16:22:07 +08:00
committed by jialin
parent c6e7075bf5
commit 87c8a3a64d
36 changed files with 616 additions and 106 deletions
@@ -1,3 +1,4 @@
import { validateLabelNameRegxFor63 } from '@/config';
import {
Input as CInput,
InputNumber,
@@ -26,10 +27,15 @@ export interface BasicResourceMax {
interface BasicProps {
page?: 'template' | 'instance';
disabled?: boolean;
onceMaxRequest?: BasicResourceMax;
}
const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
const Basic: React.FC<BasicProps> = ({
page = 'template',
onceMaxRequest,
disabled
}) => {
const { getRuleMessage } = useAppUtils();
const intl = useIntl();
@@ -68,10 +74,17 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
{
required: true,
message: getRuleMessage('input', 'common.table.name')
},
{
pattern: validateLabelNameRegxFor63,
message: intl.formatMessage({
id: 'gpuservice.form.rule.name'
})
}
]}
>
<CInput.Input
disabled={disabled}
label={intl.formatMessage({ id: 'common.table.name' })}
required
/>
@@ -105,6 +118,7 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
]}
>
<SealSelect
disabled={disabled}
label={intl.formatMessage({ id: 'resources.table.vendor' })}
required
options={[...manufacturerOptions, { label: 'CPU', value: 'cpu' }]}
@@ -123,12 +137,14 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
]}
>
<CInput.Input
disabled={disabled}
label={intl.formatMessage({ id: 'gpuservice.template.image' })}
required
/>
</Form.Item>
<Form.Item<FormData> name={['spec', 'imagePullPolicy']}>
<SealSelect
disabled={disabled}
label={intl.formatMessage({
id: 'gpuservice.template.imagePullPolicy'
})}
@@ -144,6 +160,7 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
getValueProps={(value) => ({ value: stringifyCommand(value) })}
>
<Textarea
disabled={disabled}
label={intl.formatMessage({ id: 'gpuservice.template.command' })}
placeholder={intl.formatMessage({
id: 'gpuservice.template.command.placeholder'
@@ -164,6 +181,7 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
placeholder={intl.formatMessage({
id: 'clusters.volume.mountPath.format'
})}
disabled={disabled}
/>
</Form.Item>
</div>
@@ -181,6 +199,7 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
onceMaxRequest?.localStorage
)}
max={onceMaxRequest?.localStorage ?? undefined}
disabled={disabled}
/>
</Form.Item>
</div>
@@ -195,6 +214,7 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
<InputNumber
label={renderMaxLabel('CPU', onceMaxRequest?.cpu)}
max={onceMaxRequest?.cpu ?? undefined}
disabled={disabled}
/>
</Form.Item>
</div>
@@ -212,13 +232,14 @@ const Basic: React.FC<BasicProps> = ({ page = 'template', onceMaxRequest }) => {
onceMaxRequest?.memory
)}
max={onceMaxRequest?.memory ?? undefined}
disabled={disabled}
/>
</Form.Item>
</div>
</Flex>
<Ports />
<Env />
<Ports disabled={disabled} />
<Env disabled={disabled} />
</>
);
};
+10 -2
View File
@@ -9,12 +9,17 @@ interface EnvItemProps {
onChange: (item: EnvItemType) => void;
}
const EnvItem: React.FC<EnvItemProps> = ({ item, onChange }) => {
const EnvItem: React.FC<EnvItemProps & { disabled?: boolean }> = ({
item,
onChange,
disabled = false
}) => {
const intl = useIntl();
return (
<div style={{ display: 'flex', gap: 12, width: '100%' }}>
<div style={{ flex: 1 }}>
<CInput.Input
disabled={disabled}
value={item.name}
placeholder={intl.formatMessage({
id: 'gpuservice.template.env.name'
@@ -30,6 +35,7 @@ const EnvItem: React.FC<EnvItemProps> = ({ item, onChange }) => {
</div>
<div style={{ flex: 1 }}>
<CInput.Input
disabled={disabled}
value={item.value}
placeholder={intl.formatMessage({
id: 'gpuservice.template.env.value'
@@ -47,7 +53,7 @@ const EnvItem: React.FC<EnvItemProps> = ({ item, onChange }) => {
);
};
const Env: React.FC = () => {
const Env: React.FC<{ disabled?: boolean }> = ({ disabled = false }) => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const env = Form.useWatch(['spec', 'env'], form) || [];
@@ -103,6 +109,7 @@ const Env: React.FC = () => {
]}
>
<MetadataList
disabled={disabled}
dataList={env}
btnText={intl.formatMessage({ id: 'gpuservice.template.env.add' })}
label={intl.formatMessage({ id: 'gpuservice.template.env' })}
@@ -111,6 +118,7 @@ const Env: React.FC = () => {
>
{(item, index) => (
<EnvItem
disabled={disabled}
key={index}
index={index}
item={item}
+51 -22
View File
@@ -6,12 +6,15 @@ import {
} from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import _ from 'lodash';
import { useEffect, useRef, useState } from 'react';
import { FormData, PortItem as PortItemType } from '../config/types';
type PortProtocol = PortItemType['protocol'];
const PORT_NAME_MAX = 16;
const PORT_MIN = 1;
const PORT_MAX = 65535;
const protocolOptions = [
{
@@ -25,9 +28,9 @@ const protocolOptions = [
];
const wellKnownTcpPortNames: Record<number, string> = {
22: 'SSH',
80: 'HTTP',
443: 'HTTPS'
22: 'ssh',
80: 'http',
443: 'https'
};
const getAutoFilledName = (
@@ -57,6 +60,7 @@ const getAutoFilledName = (
};
interface PortItemProps {
disabled?: boolean;
item: PortItemType;
index: number;
onChange: (item: PortItemType) => void;
@@ -68,10 +72,11 @@ interface PortNameInputProps {
placeholder: string;
}
const PortNameInput: React.FC<PortNameInputProps> = ({
const PortNameInput: React.FC<PortNameInputProps & { disabled?: boolean }> = ({
value,
onChange,
placeholder
placeholder,
disabled = false
}) => {
const [localValue, setLocalValue] = useState<string>(value ?? '');
const isComposingRef = useRef(false);
@@ -85,6 +90,7 @@ const PortNameInput: React.FC<PortNameInputProps> = ({
return (
<CInput.Input
showCount
disabled={disabled}
maxLength={PORT_NAME_MAX}
value={localValue}
placeholder={placeholder}
@@ -109,12 +115,13 @@ const PortNameInput: React.FC<PortNameInputProps> = ({
);
};
const PortItem: React.FC<PortItemProps> = ({ item, index, onChange }) => {
const PortItem: React.FC<PortItemProps & { disabled?: boolean }> = ({ item, index, disabled = false, onChange }) => {
const intl = useIntl();
return (
<div style={{ display: 'flex', gap: 12, width: '100%' }}>
<div style={{ width: 120 }}>
<SealSelect
disabled={disabled}
value={item.protocol}
options={protocolOptions}
onChange={(value) => {
@@ -136,8 +143,9 @@ const PortItem: React.FC<PortItemProps> = ({ item, index, onChange }) => {
</div>
<div style={{ width: 120 }}>
<CInputNumber
min={1}
max={65535}
min={PORT_MIN}
max={PORT_MAX}
disabled={disabled}
precision={0}
value={item.port}
onChange={(value) => {
@@ -160,6 +168,7 @@ const PortItem: React.FC<PortItemProps> = ({ item, index, onChange }) => {
</div>
<div style={{ flex: 1 }}>
<PortNameInput
disabled={disabled}
value={item.name}
placeholder={intl.formatMessage({
id: 'gpuservice.template.ports.name'
@@ -176,13 +185,15 @@ const PortItem: React.FC<PortItemProps> = ({ item, index, onChange }) => {
);
};
const Ports: React.FC = () => {
const Ports: React.FC<{ disabled?: boolean }> = ({ disabled = false }) => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const ports = Form.useWatch(['spec', 'ports'], form) || [];
const updatePorts = (list: PortItemType[]) => {
form.setFieldValue(['spec', 'ports'], list);
// setFieldValue doesn't run validators — re-run them so users see errors live.
form.validateFields([['spec', 'ports']]).catch(() => {});
};
const handleAdd = () => {
@@ -213,27 +224,31 @@ const Ports: React.FC = () => {
name={['spec', 'ports']}
rules={[
{
validator: async (_, value) => {
validator: async (_rule, value) => {
if (!value?.length) {
return Promise.resolve();
}
const hasInvalidPort = value.some(
(item: PortItemType) => !item.protocol || !item.port
);
if (hasInvalidPort) {
const items = value as PortItemType[];
if (_.some(items, (item) => !item.protocol || !item.port)) {
return Promise.reject(
new Error(
intl.formatMessage({
id: 'gpuservice.template.ports.invalid'
})
intl.formatMessage(
{ id: 'common.validate.value' },
{
name: intl.formatMessage({
id: 'gpuservice.template.ports'
})
}
)
)
);
}
const hasInvalidName = value.some(
(item: PortItemType) =>
item.name && item.name.length > PORT_NAME_MAX
);
if (hasInvalidName) {
const namedItems = items.filter((item) => !!item.name);
if (_.some(namedItems, (item) => item.name!.length > PORT_NAME_MAX)) {
return Promise.reject(
new Error(
intl.formatMessage({
@@ -242,12 +257,25 @@ const Ports: React.FC = () => {
)
);
}
const names = namedItems.map((item) => item.name);
if (_.uniq(names).length !== names.length) {
return Promise.reject(
new Error(
intl.formatMessage({
id: 'gpuservice.template.ports.name.duplicate'
})
)
);
}
return Promise.resolve();
}
}
]}
>
<MetadataList
disabled={disabled}
dataList={ports}
btnText={intl.formatMessage({ id: 'gpuservice.template.ports.add' })}
label={intl.formatMessage({ id: 'gpuservice.template.ports' })}
@@ -256,6 +284,7 @@ const Ports: React.FC = () => {
>
{(item, index) => (
<PortItem
disabled={disabled}
key={index}
index={index}
item={item}
+2 -2
View File
@@ -11,8 +11,8 @@ import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks';
import _ from 'lodash';
import { useMemo } from 'react';
import { GPUsConfigs } from '../../resources/config/gpu-driver';
import PageBox from '../../_components/page-box';
import { GPUsConfigs } from '../../resources/config/gpu-driver';
import {
createGPUServiceTemplate,
deleteGPUServiceTemplate,
@@ -55,7 +55,7 @@ const GPUServiceTemplates: React.FC = () => {
() => [
...Object.values(GPUsConfigs).map((item) => ({
label: item.label,
value: item.value
value: item.gpuVendor as string
})),
{ label: 'CPU', value: 'cpu' }
],