import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; import { Input as CInput, CollapsePanel, IconFont, ScrollSpyTabs, useFinishFailed, useScrollActiveChange, useWrapperContext } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react'; import useGetSshkey from '../../public-keys/services/use-get-sshkey'; import { DefaultImagePullPolicy } from '../../templates/config'; import TemplateBasicForm, { BasicResourceMax } from '../../templates/forms/basic'; import { FormData, InstanceTypeItem, ListItem } from '../config/types'; import Basic from './basic'; import InstanceTypeFormItem from './instance-type'; import StorageVolume from './storage-volume'; const SSH_PORT = 22; type InstanceFormValues = FormData & { // ssh holder enable_ssh?: boolean; }; interface InstanceFormProps { ref?: any; open: boolean; action: PageActionType; currentData?: ListItem | null; namespace?: string; instanceTypeList?: InstanceTypeItem[]; onFinish: (values: FormData) => Promise; } const parseQuantityToNumber = (value?: string): number | null => { if (!value) return null; const match = /^(-?\d+(?:\.\d+)?)/.exec(String(value)); if (!match) return null; const num = Number(match[1]); return Number.isFinite(num) && num > 0 ? num : null; }; const TABKeysMap = { BASIC: 'basic', INSTANCE_TYPE: 'instanceType', TEMPLATE: 'template', STORAGE: 'storage' }; const requiredFields = { [TABKeysMap.BASIC]: { sort: 1, fields: ['metadata.name'] }, [TABKeysMap.INSTANCE_TYPE]: { sort: 2, fields: ['spec.type', 'spec.resources.accelerator'] }, [TABKeysMap.TEMPLATE]: { sort: 3, fields: ['spec.image', 'spec.ports', 'spec.env'] }, [TABKeysMap.STORAGE]: { sort: 4, fields: ['spec.volume.persistent.name', 'spec.volume.ephemeral.capacity'] } }; const GPUServiceInstanceForm: React.FC = forwardRef( (props, ref) => { const { action, currentData, open, namespace = 'default', instanceTypeList = [], onFinish } = props; const intl = useIntl(); const [form] = Form.useForm(); const scrollTabsRef = useRef(null); const { detailData, fetchData } = useGetSshkey(); const { getScrollElementScrollableHeight } = useWrapperContext(); const { activeKey, collapseKeys, handleActiveChange, handleOnCollapseChange, updateActiveKey } = useScrollActiveChange({ initalActiveKeys: [TABKeysMap.BASIC], initialCollapseKeys: [ TABKeysMap.INSTANCE_TYPE, TABKeysMap.TEMPLATE, TABKeysMap.STORAGE ] }); useEffect(() => { if (open) { fetchData({}); } }, [open]); const segmentOptions = useMemo( () => [ { value: TABKeysMap.BASIC, label: intl.formatMessage({ id: 'common.title.basicInfo' }), icon: , field: 'name' }, { value: TABKeysMap.INSTANCE_TYPE, label: intl.formatMessage({ id: 'gpuservice.instance.section.type' }), icon: , field: 'instanceType' }, { value: TABKeysMap.TEMPLATE, label: intl.formatMessage({ id: 'gpuservice.instance.section.template' }), icon: , field: 'template' }, { value: TABKeysMap.STORAGE, label: intl.formatMessage({ id: 'gpuservice.instance.section.storage' }), icon: , field: 'storage' } ], [intl] ); const ports = Form.useWatch(['spec', 'ports'], form) || []; const hasSshPort = useMemo( () => ports.some((p) => Number(p?.port) === SSH_PORT && p.protocol === 'TCP'), [ports] ); const selectedTypeName = Form.useWatch(['spec', 'type'], form) as | string | undefined; const onceMaxRequest = useMemo(() => { const selected = instanceTypeList.find( (item) => (item.metadata?.name || '') === selectedTypeName ); const status = selected?.status; return { cpu: parseQuantityToNumber(status?.cpu?.onceMaxRequest), memory: parseQuantityToNumber(status?.ram?.onceMaxRequest), localStorage: parseQuantityToNumber( status?.localStorage?.onceMaxRequest ) }; }, [instanceTypeList, selectedTypeName]); const onTargetChange = (key: string) => { scrollTabsRef.current?.handleTargetChange(key); }; const { handleOnFinishFailed: rawHandleOnFinishFailed } = useFinishFailed({ requiredFields, onTargetChange, 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, name: [ Array.isArray(field.name) ? field.name.join('.') : String(field.name) ] })); rawHandleOnFinishFailed({ ...errorInfo, errorFields }); }; useEffect(() => { if (!open) { form.resetFields(); return; } if (action === PageAction.EDIT && currentData) { form.setFieldsValue({ metadata: { name: currentData.metadata?.name, namespace: currentData.metadata?.namespace }, spec: { ...currentData.spec, imagePullPolicy: currentData.spec?.imagePullPolicy || DefaultImagePullPolicy }, enable_ssh: !!currentData.spec?.sshPublicKey?.name }); return; } }, [action, currentData, form, open, namespace]); const handleFinish = async (values: InstanceFormValues) => { await onFinish(values); }; useImperativeHandle(ref, () => ({ submit: () => { form.submit(); }, resetFields: () => { form.resetFields(); }, setFieldsValue: (values: Partial) => { form.setFieldsValue(values as any); }, getFieldsValue: () => form.getFieldsValue() })); return (
}, { key: TABKeysMap.TEMPLATE, label: intl.formatMessage({ id: 'gpuservice.instance.section.template' }), forceRender: true, children: ( ) }, { key: TABKeysMap.STORAGE, label: intl.formatMessage({ id: 'gpuservice.instance.section.storage' }), forceRender: true, children: } ]} /> {hasSshPort && ( hidden name={['spec', 'sshPublicKey', 'name']}> )}
); } ); export default GPUServiceInstanceForm;