From a7b33a928b88356959bc215780991874d9836b90 Mon Sep 17 00:00:00 2001 From: jialin Date: Thu, 11 Jun 2026 17:40:40 +0800 Subject: [PATCH] fix: anti double submit in form --- src/hooks/use-submit-lock.ts | 46 +++++++++++++++++++ .../components/add-apikey-modal/index.tsx | 46 ++++++++++--------- src/pages/backends/components/add-modal.tsx | 45 +++++++++++------- src/pages/backends/forms/index.tsx | 8 ++-- .../components/add-benchmark-modal.tsx | 18 +++++--- src/pages/benchmark/forms/index.tsx | 3 ++ .../components/add-cluster.tsx | 16 +++++-- .../components/cluster-form.tsx | 5 +- .../instances/components/add-modal.tsx | 13 +++--- .../gpu-service/instances/forms/index.tsx | 5 +- .../components/add-public-key-modal.tsx | 8 +++- .../gpu-service/public-keys/forms/index.tsx | 4 +- .../components/add-storage-type-modal.tsx | 8 +++- .../gpu-service/storage-types/forms/index.tsx | 4 +- .../storage/components/add-modal.tsx | 14 ++++-- src/pages/gpu-service/storage/forms/index.tsx | 4 +- .../templates/components/add-modal.tsx | 14 ++++-- .../gpu-service/templates/forms/index.tsx | 4 +- .../components/add-provider-modal.tsx | 14 ++++-- src/pages/maas-provider/forms/index.tsx | 10 +++- .../components/add-route-modal.tsx | 14 ++++-- src/pages/model-routes/forms/index.tsx | 19 ++++++-- src/pages/users/components/add-modal.tsx | 17 ++++++- 23 files changed, 246 insertions(+), 93 deletions(-) create mode 100644 src/hooks/use-submit-lock.ts diff --git a/src/hooks/use-submit-lock.ts b/src/hooks/use-submit-lock.ts new file mode 100644 index 00000000..262f83be --- /dev/null +++ b/src/hooks/use-submit-lock.ts @@ -0,0 +1,46 @@ +import { useMemoizedFn } from 'ahooks'; +import { useRef, useState } from 'react'; + +/** + * Centralizes the anti-double-submit + anti-deadlock logic shared by the + * "add/edit" modals. + * + * The lock has to straddle antd's async field validation, whose result comes + * back through two separate callbacks (`onFinish` / `onFinishFailed`). So the + * three wiring points below are intentional and map 1:1 to those anchors: + * + * - `guard` wrap the submit trigger (button / ModalFooter onOk). Blocks + * re-entry while a submit is already in flight, then fires submit. + * - `run` wrap the `onFinish` handler. Holds the lock + loading until the + * `onOk` request settles (success or error). + * - `release` pass as the form's `onFinishFailed`. Releases the lock when + * validation fails, otherwise the button would dead-lock. + */ +export default function useSubmitLock() { + const [loading, setLoading] = useState(false); + const lockRef = useRef(false); + + const release = useMemoizedFn(() => { + setLoading(false); + lockRef.current = false; + }); + + const guard = useMemoizedFn((submit: () => void) => { + if (lockRef.current) { + return; + } + lockRef.current = true; + submit(); + }); + + const run = useMemoizedFn(async (task: () => void | Promise) => { + setLoading(true); + try { + await task(); + } finally { + release(); + } + }); + + return { loading, guard, run, release }; +} diff --git a/src/pages/api-keys/components/add-apikey-modal/index.tsx b/src/pages/api-keys/components/add-apikey-modal/index.tsx index 8238bcf3..be3f6c86 100644 --- a/src/pages/api-keys/components/add-apikey-modal/index.tsx +++ b/src/pages/api-keys/components/add-apikey-modal/index.tsx @@ -1,5 +1,6 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; +import useSubmitLock from '@/hooks/use-submit-lock'; import { AlertBlockInfo, Input as CInput, @@ -45,7 +46,7 @@ const AddModal: React.FC = ({ const intl = useIntl(); const [showKey, setShowKey] = useState(false); const [apikeyValue, setAPIKeyValue] = useState(''); - const [loading, setLoading] = useState(false); + const { loading, guard, run, release } = useSubmitLock(); const [isChanged, setIsChanged] = useState(false); const cacheFormRef = useRef<{ allowed_type: string; @@ -116,31 +117,31 @@ const AddModal: React.FC = ({ }; const handleOnOk = async (formdata: FormData) => { - try { - setLoading(true); - const data = { - ..._.omit(formdata, ['allowed_type']), - allowed_model_names: - formdata.allowed_type === 'all' || - !formdata.scope?.includes('inference') - ? [] - : formdata.allowed_model_names || [] - }; - if (action === PageAction.CREATE) { - await createAPIKey(data); - } else if (action === PageAction.EDIT && currentData?.id) { - await updateAPIKey({ - ..._.omit(data, ['expires_in']) - }); + await run(async () => { + try { + const data = { + ..._.omit(formdata, ['allowed_type']), + allowed_model_names: + formdata.allowed_type === 'all' || + !formdata.scope?.includes('inference') + ? [] + : formdata.allowed_model_names || [] + }; + if (action === PageAction.CREATE) { + await createAPIKey(data); + } else if (action === PageAction.EDIT && currentData?.id) { + await updateAPIKey({ + ..._.omit(data, ['expires_in']) + }); + } + } catch (error) { + // handled in interceptor } - setLoading(false); - } catch (error) { - setLoading(false); - } + }); }; const handleSumit = () => { - form.submit(); + guard(() => form.submit()); }; const handleDone = () => { @@ -278,6 +279,7 @@ const AddModal: React.FC = ({ name="addAPIKey" form={form} onFinish={handleOnOk} + onFinishFailed={release} preserve={false} initialValues={{ allowed_type: 'all', diff --git a/src/pages/backends/components/add-modal.tsx b/src/pages/backends/components/add-modal.tsx index 62b29bed..3820eb15 100644 --- a/src/pages/backends/components/add-modal.tsx +++ b/src/pages/backends/components/add-modal.tsx @@ -1,5 +1,6 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; +import useSubmitLock from '@/hooks/use-submit-lock'; import { AlertBlockInfo, GSDrawer, @@ -63,6 +64,7 @@ const AddModal: React.FC = (props) => { const [yamlContent, setYamlContent] = useState(''); const [formContent, setFormContent] = useState({} as FormData); const alertRef = useRef(null); + const { loading, guard, run, release } = useSubmitLock(); const showVersionCustomSuffix = currentData?.backend_source === BackendSourceValueMap.BUILTIN || @@ -92,17 +94,22 @@ const AddModal: React.FC = (props) => { }; const onOk = () => { - if (activeKey === 'yaml') { - const content = editorRef.current?.getContent(); - if (content) { - onSubmitYaml({ content: content }); + guard(() => { + if (activeKey === 'yaml') { + const content = editorRef.current?.getContent(); + if (!content) { + // nothing to submit — drop the lock so the button stays usable + release(); + return; + } + run(() => onSubmitYaml({ content: content })); + return; } - } else { formRef.current?.submit(); - } + }); }; - const onFinish = (values: FormData) => { + const onFinish = async (values: FormData) => { const versionConfigs = values.version_configs?.reduce( (acc: Record, curr) => { if (curr.version_no) { @@ -117,16 +124,18 @@ const AddModal: React.FC = (props) => { const defaultVersion = values.version_configs?.find((v) => v.is_default); - onSubmit({ - ...values, - parameter_format: - values.parameter_format === 'auto' || !values.parameter_format - ? null - : values.parameter_format, - default_version: defaultVersion?.version_no || '', - // @ts-ignore - version_configs: versionConfigs - }); + await run(() => + onSubmit({ + ...values, + parameter_format: + values.parameter_format === 'auto' || !values.parameter_format + ? null + : values.parameter_format, + default_version: defaultVersion?.version_no || '', + // @ts-ignore + version_configs: versionConfigs + }) + ); }; useEffect(() => { @@ -279,6 +288,7 @@ const AddModal: React.FC = (props) => { @@ -295,6 +305,7 @@ const AddModal: React.FC = (props) => { children: ( void; + onFinishFailed?: (errorInfo: any) => void; ref?: any; }; const BackendForm: React.FC = forwardRef( - ({ action, currentData, onFinish }, ref) => { + ({ action, currentData, onFinish, onFinishFailed }, ref) => { const intl = useIntl(); const [form] = Form.useForm(); const [activeKey, setActiveKey] = React.useState([]); @@ -26,7 +27,7 @@ const BackendForm: React.FC = forwardRef( currentData?.backend_source === BackendSourceValueMap.BUILTIN || currentData?.backend_source === BackendSourceValueMap.COMMUNITY; - const onFinishFailed = (errorInfo: any) => { + const handleOnFinishFailed = (errorInfo: any) => { const errorFields = errorInfo.errorFields || []; if (errorFields.length > 0) { const versionError = errorFields.find((field: any) => @@ -38,6 +39,7 @@ const BackendForm: React.FC = forwardRef( setActiveKey([]); } } + onFinishFailed?.(errorInfo); }; const handleOnFinish = (values: FormData) => { @@ -106,7 +108,7 @@ const BackendForm: React.FC = forwardRef( preserve={false} scrollToFirstError={true} initialValues={_.omit(currentData, ['version_configs'])} - onFinishFailed={onFinishFailed} + onFinishFailed={handleOnFinishFailed} > = ({ open, currentData, clusterList, - profilesOptions, - datasetList, + profilesOptions = [], + datasetList = [], onOk, onCancel }) => { const form = useRef(null); + const { loading, guard, run, release } = useSubmitLock(); const handleSubmit = () => { - form.current?.submit(); + guard(() => form.current?.submit()); }; const handleOk = async (data: FormData) => { - onOk({ - ...data - }); + await run(() => + onOk({ + ...data + }) + ); }; const handleCancel = () => { @@ -51,6 +55,7 @@ const AddBenchmark: React.FC = ({ onCancel={handleCancel} onSubmit={handleSubmit} width={600} + loading={loading} > = ({ profilesOptions={profilesOptions} datasetList={datasetList} onFinish={handleOk} + onFinishFailed={release} /> ); diff --git a/src/pages/benchmark/forms/index.tsx b/src/pages/benchmark/forms/index.tsx index 186d3f8e..b7de7105 100644 --- a/src/pages/benchmark/forms/index.tsx +++ b/src/pages/benchmark/forms/index.tsx @@ -29,6 +29,7 @@ interface ProviderFormProps { datasetList: Global.BaseOption[]; profilesOptions: Global.BaseOption[]; onFinish: (values: FormData) => Promise; + onFinishFailed?: (errorInfo: any) => void; } const TABKeysMap = { @@ -42,6 +43,7 @@ const ProviderForm: React.FC = forwardRef((props, ref) => { action, currentData, onFinish, + onFinishFailed, open, clusterList, profilesOptions, @@ -122,6 +124,7 @@ const ProviderForm: React.FC = forwardRef((props, ref) => {
= ({ }) => { const intl = useIntl(); const form = useRef(null); + const { loading, guard, run, release } = useSubmitLock(); // Whether the user has changed any k8s_options field. Lifted from ClusterForm // so the "re-run registration" notice can sit in the drawer footer, above the // Save/Cancel buttons (mirrors the model edit interaction). const [k8sOptionsChanged, setK8sOptionsChanged] = useState(false); const handleSubmit = () => { - form.current?.submit(); + guard(() => form.current?.submit()); }; const handleOk = async (data: FormData) => { - onOk({ - ...data, - provider - }); + await run(() => + onOk({ + ...data, + provider + }) + ); }; const handleCancel = () => { @@ -84,6 +88,7 @@ const AddCluster: React.FC = ({ @@ -96,6 +101,7 @@ const AddCluster: React.FC = ({ action={action} currentData={currentData} onFinish={handleOk} + onFinishFailed={release} onK8sOptionsChange={setK8sOptionsChanged} /> diff --git a/src/pages/cluster-management/components/cluster-form.tsx b/src/pages/cluster-management/components/cluster-form.tsx index 84781d68..1d9b6014 100644 --- a/src/pages/cluster-management/components/cluster-form.tsx +++ b/src/pages/cluster-management/components/cluster-form.tsx @@ -36,6 +36,7 @@ type AddModalProps = { provider: ProviderType; credentialList: Global.BaseOption[]; onFinish: (values: FormData) => void; + onFinishFailed?: (errorInfo: any) => void; // Reports whether the user has changed any k8s_options field, so the parent // can show the "re-run registration" notice in the footer. onK8sOptionsChange?: (changed: boolean) => void; @@ -49,6 +50,7 @@ const ClusterForm: React.FC = forwardRef( currentData, credentialList, onFinish, + onFinishFailed, onK8sOptionsChange }, ref @@ -212,8 +214,9 @@ const ClusterForm: React.FC = forwardRef( } })); - const handleOnFinishFailed = () => { + const handleOnFinishFailed = (errorInfo: any) => { setSubmitAttempted(true); + onFinishFailed?.(errorInfo); }; return ( diff --git a/src/pages/gpu-service/instances/components/add-modal.tsx b/src/pages/gpu-service/instances/components/add-modal.tsx index a92b978a..990adb30 100644 --- a/src/pages/gpu-service/instances/components/add-modal.tsx +++ b/src/pages/gpu-service/instances/components/add-modal.tsx @@ -1,5 +1,6 @@ import { PageAction } from '@/config'; import { PageActionType } from '@/config/types'; +import useSubmitLock from '@/hooks/use-submit-lock'; import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list'; import Separator from '@/pages/llmodels/components/separator'; import { SearchOutlined } from '@ant-design/icons'; @@ -89,7 +90,7 @@ const AddModal: React.FC = ({ const [templateId, setTemplateId] = useState(); const [instanceKeyword, setInstanceKeyword] = useState(''); const [templateKeyword, setTemplateKeyword] = useState(''); - const [loading, setLoading] = useState(false); + const { loading, guard, run, release } = useSubmitLock(); const initializedRef = useRef(false); const { @@ -385,7 +386,7 @@ const AddModal: React.FC = ({ }); const handleSubmit = () => { - form.current?.submit(); + guard(() => form.current?.submit()); }; const handleCancel = () => { @@ -394,15 +395,12 @@ const AddModal: React.FC = ({ }; const onFinish = async (values: FormData) => { - setLoading(true); - try { + await run(async () => { await onOk({ ...values }); console.log('submit form values', values); - } finally { - setLoading(false); - } + }); }; const handleInstanceTypeChange = (item: InstanceTypeItem) => { @@ -576,6 +574,7 @@ const AddModal: React.FC = ({ currentData={data} disabled={readonly} onFinish={onFinish} + onFinishFailed={release} onScopeChange={handleScopeChange} open={open} instanceTypeList={ownedInstanceTypes} diff --git a/src/pages/gpu-service/instances/forms/index.tsx b/src/pages/gpu-service/instances/forms/index.tsx index 3ecab9e4..e51d69a9 100644 --- a/src/pages/gpu-service/instances/forms/index.tsx +++ b/src/pages/gpu-service/instances/forms/index.tsx @@ -70,6 +70,7 @@ interface InstanceFormProps { // tenant-scoped instance-type / template offerings. onScopeChange?: (orgId: number | null | undefined) => void; onFinish: (values: FormData) => Promise; + onFinishFailed?: (errorInfo: any) => void; } const TABKeysMap = { @@ -113,7 +114,8 @@ const GPUServiceInstanceForm: React.FC = forwardRef( instanceTypeList = [], noAvailableInstanceTypes, onScopeChange, - onFinish + onFinish, + onFinishFailed } = props; const intl = useIntl(); const { getRuleMessage } = useAppUtils(); @@ -370,6 +372,7 @@ const GPUServiceInstanceForm: React.FC = forwardRef( ] })); rawHandleOnFinishFailed({ ...errorInfo, errorFields }); + onFinishFailed?.(errorInfo); }; const detectMode = (volume?: FormData['spec']['volume']) => { if (volume?.persistent?.name || volume?.persistentTemplate?.name) { diff --git a/src/pages/gpu-service/public-keys/components/add-public-key-modal.tsx b/src/pages/gpu-service/public-keys/components/add-public-key-modal.tsx index 602683ad..c64c0590 100644 --- a/src/pages/gpu-service/public-keys/components/add-public-key-modal.tsx +++ b/src/pages/gpu-service/public-keys/components/add-public-key-modal.tsx @@ -1,4 +1,5 @@ import { PageActionType } from '@/config/types'; +import useSubmitLock from '@/hooks/use-submit-lock'; import { ModalFooter } from '@gpustack/core-ui'; import { useRef } from 'react'; import FormDrawer from '../../../_components/form-drawer'; @@ -23,9 +24,10 @@ const AddPublicKeyModal: React.FC = ({ onCancel }) => { const form = useRef(null); + const { loading, guard, run, release } = useSubmitLock(); const handleSubmit = () => { - form.current?.submit(); + guard(() => form.current?.submit()); }; const handleCancel = () => { @@ -34,7 +36,7 @@ const AddPublicKeyModal: React.FC = ({ }; const onFinish = async (values: FormData) => { - onOk({ ...values }); + await run(() => onOk({ ...values })); }; return ( @@ -48,6 +50,7 @@ const AddPublicKeyModal: React.FC = ({ = ({ action={action} currentData={data} onFinish={onFinish} + onFinishFailed={release} open={open} /> diff --git a/src/pages/gpu-service/public-keys/forms/index.tsx b/src/pages/gpu-service/public-keys/forms/index.tsx index 77615f4b..94a6cb2a 100644 --- a/src/pages/gpu-service/public-keys/forms/index.tsx +++ b/src/pages/gpu-service/public-keys/forms/index.tsx @@ -11,11 +11,12 @@ interface PublicKeyFormProps { action: PageActionType; currentData?: ListItem | null; onFinish: (values: FormData) => Promise; + onFinishFailed?: (errorInfo: any) => void; } const GPUServicePublicKeyForm: React.FC = forwardRef( (props, ref) => { - const { action, currentData, open, onFinish } = props; + const { action, currentData, open, onFinish, onFinishFailed } = props; const [form] = Form.useForm(); useEffect(() => { @@ -50,6 +51,7 @@ const GPUServicePublicKeyForm: React.FC = forwardRef( name="gpuServicePublicKeyForm" form={form} onFinish={onFinish} + onFinishFailed={onFinishFailed} preserve={false} initialValues={{}} > diff --git a/src/pages/gpu-service/storage-types/components/add-storage-type-modal.tsx b/src/pages/gpu-service/storage-types/components/add-storage-type-modal.tsx index e80ad12b..ee9bbad5 100644 --- a/src/pages/gpu-service/storage-types/components/add-storage-type-modal.tsx +++ b/src/pages/gpu-service/storage-types/components/add-storage-type-modal.tsx @@ -1,4 +1,5 @@ import { PageActionType } from '@/config/types'; +import useSubmitLock from '@/hooks/use-submit-lock'; import { ModalFooter } from '@gpustack/core-ui'; import { useRef } from 'react'; import FormDrawer from '../../../_components/form-drawer'; @@ -23,9 +24,10 @@ const AddStorageTypeModal: React.FC = ({ onCancel }) => { const form = useRef(null); + const { loading, guard, run, release } = useSubmitLock(); const handleSubmit = () => { - form.current?.submit(); + guard(() => form.current?.submit()); }; const handleCancel = () => { @@ -34,7 +36,7 @@ const AddStorageTypeModal: React.FC = ({ }; const onFinish = async (values: FormData) => { - onOk({ ...values }); + await run(() => onOk({ ...values })); }; return ( @@ -48,6 +50,7 @@ const AddStorageTypeModal: React.FC = ({ = ({ action={action} currentData={data} onFinish={onFinish} + onFinishFailed={release} open={open} /> diff --git a/src/pages/gpu-service/storage-types/forms/index.tsx b/src/pages/gpu-service/storage-types/forms/index.tsx index b7a3072f..5734d208 100644 --- a/src/pages/gpu-service/storage-types/forms/index.tsx +++ b/src/pages/gpu-service/storage-types/forms/index.tsx @@ -18,6 +18,7 @@ interface StorageTypeFormProps { action: PageActionType; currentData?: ListItem | null; onFinish: (values: FormData) => Promise; + onFinishFailed?: (errorInfo: any) => void; } const detectKind = (item?: ListItem | null): StorageTypeKind => { @@ -27,7 +28,7 @@ const detectKind = (item?: ListItem | null): StorageTypeKind => { const GPUServiceStorageTypeForm: React.FC = forwardRef( (props, ref) => { - const { action, currentData, open, onFinish } = props; + const { action, currentData, open, onFinish, onFinishFailed } = props; const [form] = Form.useForm(); const kind = Form.useWatch('type', form); @@ -115,6 +116,7 @@ const GPUServiceStorageTypeForm: React.FC = forwardRef( name="gpuServiceStorageTypeForm" form={form} onFinish={handleFinish} + onFinishFailed={onFinishFailed} preserve={false} initialValues={{}} > diff --git a/src/pages/gpu-service/storage/components/add-modal.tsx b/src/pages/gpu-service/storage/components/add-modal.tsx index 970244b8..17cca11d 100644 --- a/src/pages/gpu-service/storage/components/add-modal.tsx +++ b/src/pages/gpu-service/storage/components/add-modal.tsx @@ -1,4 +1,5 @@ import { PageActionType } from '@/config/types'; +import useSubmitLock from '@/hooks/use-submit-lock'; import { ModalFooter } from '@gpustack/core-ui'; import { useRef } from 'react'; import FormDrawer from '../../../_components/form-drawer'; @@ -26,9 +27,10 @@ const AddModal: React.FC = ({ storageClassList }) => { const form = useRef(null); + const { loading, guard, run, release } = useSubmitLock(); const handleSubmit = () => { - form.current?.submit(); + guard(() => form.current?.submit()); }; const handleCancel = () => { @@ -37,9 +39,11 @@ const AddModal: React.FC = ({ }; const onFinish = async (values: FormData) => { - onOk({ - ...values - }); + await run(() => + onOk({ + ...values + }) + ); }; return ( @@ -53,6 +57,7 @@ const AddModal: React.FC = ({ = ({ action={action} currentData={data} onFinish={onFinish} + onFinishFailed={release} open={open} /> diff --git a/src/pages/gpu-service/storage/forms/index.tsx b/src/pages/gpu-service/storage/forms/index.tsx index 7fff3c8c..890f2311 100644 --- a/src/pages/gpu-service/storage/forms/index.tsx +++ b/src/pages/gpu-service/storage/forms/index.tsx @@ -11,11 +11,12 @@ interface StorageFormProps { action: PageActionType; currentData?: ListItem | null; onFinish: (values: FormData) => Promise; + onFinishFailed?: (errorInfo: any) => void; } const GPUServiceStorageForm: React.FC = forwardRef( (props, ref) => { - const { action, currentData, open, onFinish } = props; + const { action, currentData, open, onFinish, onFinishFailed } = props; const [form] = Form.useForm(); useEffect(() => { @@ -51,6 +52,7 @@ const GPUServiceStorageForm: React.FC = forwardRef( name="gpuServiceStorageForm" form={form} onFinish={onFinish} + onFinishFailed={onFinishFailed} preserve={false} initialValues={{}} > diff --git a/src/pages/gpu-service/templates/components/add-modal.tsx b/src/pages/gpu-service/templates/components/add-modal.tsx index f99bc157..0499c15e 100644 --- a/src/pages/gpu-service/templates/components/add-modal.tsx +++ b/src/pages/gpu-service/templates/components/add-modal.tsx @@ -1,4 +1,5 @@ import { PageActionType } from '@/config/types'; +import useSubmitLock from '@/hooks/use-submit-lock'; import { ModalFooter } from '@gpustack/core-ui'; import { useRef } from 'react'; import FormDrawer from '../../../_components/form-drawer'; @@ -23,9 +24,10 @@ const AddModal: React.FC = ({ onCancel }) => { const form = useRef(null); + const { loading, guard, run, release } = useSubmitLock(); const handleSubmit = () => { - form.current?.submit(); + guard(() => form.current?.submit()); }; const handleCancel = () => { @@ -34,9 +36,11 @@ const AddModal: React.FC = ({ }; const onFinish = async (values: FormData) => { - onOk({ - ...values - }); + await run(() => + onOk({ + ...values + }) + ); }; return ( @@ -50,6 +54,7 @@ const AddModal: React.FC = ({ = ({ action={action} currentData={currentData} onFinish={onFinish} + onFinishFailed={release} open={open} /> diff --git a/src/pages/gpu-service/templates/forms/index.tsx b/src/pages/gpu-service/templates/forms/index.tsx index d53485ad..ee0e9c5f 100644 --- a/src/pages/gpu-service/templates/forms/index.tsx +++ b/src/pages/gpu-service/templates/forms/index.tsx @@ -12,11 +12,12 @@ interface TemplateFormProps { action: PageActionType; currentData?: ListItem | null; onFinish: (values: FormData) => Promise; + onFinishFailed?: (errorInfo: any) => void; } const GPUServiceTemplateForm: React.FC = forwardRef( (props, ref) => { - const { action, currentData, open, onFinish } = props; + const { action, currentData, open, onFinish, onFinishFailed } = props; const [form] = Form.useForm(); useEffect(() => { @@ -58,6 +59,7 @@ const GPUServiceTemplateForm: React.FC = forwardRef( name="gpuServiceTemplateForm" form={form} onFinish={handleFinish} + onFinishFailed={onFinishFailed} preserve={false} scrollToFirstError initialValues={{ diff --git a/src/pages/maas-provider/components/add-provider-modal.tsx b/src/pages/maas-provider/components/add-provider-modal.tsx index 6678d177..bddfd113 100644 --- a/src/pages/maas-provider/components/add-provider-modal.tsx +++ b/src/pages/maas-provider/components/add-provider-modal.tsx @@ -1,4 +1,5 @@ import { PageActionType } from '@/config/types'; +import useSubmitLock from '@/hooks/use-submit-lock'; import { FormDrawer } from '@gpustack/core-ui'; import React, { useRef } from 'react'; import { FormData, MaasProviderItem as ListItem } from '../config/types'; @@ -22,16 +23,19 @@ const AddProvider: React.FC = ({ onCancel }) => { const form = useRef(null); + const { loading, guard, run, release } = useSubmitLock(); const handleSubmit = () => { - form.current?.submit(); + guard(() => form.current?.submit()); }; const handleOnFinish = async (data: FormData) => { console.log('handleOnFinish', data); - onOk({ - ...data - }); + await run(() => + onOk({ + ...data + }) + ); }; const handleCancel = () => { @@ -46,12 +50,14 @@ const AddProvider: React.FC = ({ onCancel={handleCancel} onSubmit={handleSubmit} width={600} + loading={loading} > ); diff --git a/src/pages/maas-provider/forms/index.tsx b/src/pages/maas-provider/forms/index.tsx index 9e4c9ec6..4c2df459 100644 --- a/src/pages/maas-provider/forms/index.tsx +++ b/src/pages/maas-provider/forms/index.tsx @@ -31,6 +31,7 @@ interface ProviderFormProps { action: PageActionType; currentData?: ListItem; // Used when action is EDIT onFinish: (values: FormData) => Promise; + onFinishFailed?: (errorInfo: any) => void; } const TABKeysMap = { @@ -51,7 +52,7 @@ const requiredFields = { }; const ProviderForm: React.FC = forwardRef((props, ref) => { - const { action, currentData, onFinish } = props; + const { action, currentData, onFinish, onFinishFailed } = props; const intl = useIntl(); const providerRequiredFieldsMap = useProviderRequiredFields(); const [form] = Form.useForm(); @@ -146,6 +147,11 @@ const ProviderForm: React.FC = forwardRef((props, ref) => { updateActiveKey }); + const handleFinishFailed = (errorInfo: any) => { + handleOnFinishFailed(errorInfo); + onFinishFailed?.(errorInfo); + }; + useImperativeHandle(ref, () => ({ submit: () => { form.submit(); @@ -217,7 +223,7 @@ const ProviderForm: React.FC = forwardRef((props, ref) => { = ({ const intl = useIntl(); const [isChanged, setIsChanged] = React.useState(false); const form = useRef(null); + const { loading, guard, run, release } = useSubmitLock(); const handleSubmit = () => { - form.current?.submit(); + guard(() => form.current?.submit()); }; const onFinish = async (data: FormData) => { - onOk({ - ...data - }); + await run(() => + onOk({ + ...data + }) + ); }; const handleCancel = () => { @@ -63,6 +67,7 @@ const AddProvider: React.FC = ({ = ({ realAction={realAction} currentData={currentData} onFinish={onFinish} + onFinishFailed={release} open={open} onFallbackChange={(changed: boolean) => { setIsChanged(changed); diff --git a/src/pages/model-routes/forms/index.tsx b/src/pages/model-routes/forms/index.tsx index 39289008..5a3a8b5e 100644 --- a/src/pages/model-routes/forms/index.tsx +++ b/src/pages/model-routes/forms/index.tsx @@ -65,6 +65,7 @@ interface ProviderFormProps { routeTargets?: RouteTargetFormItem[]; }; // Used when action is EDIT onFinish: (values: FormData) => Promise; + onFinishFailed?: (errorInfo: any) => void; onFallbackChange?: (changed: boolean) => void; } @@ -86,8 +87,15 @@ const requiredFields = { const AccessForm: React.FC = forwardRef((props, ref) => { const intl = useIntl(); - const { action, realAction, currentData, open, onFinish, onFallbackChange } = - props; + const { + action, + realAction, + currentData, + open, + onFinish, + onFinishFailed, + onFallbackChange + } = props; const { getScrollElementScrollableHeight } = useWrapperContext(); const [form] = Form.useForm(); const scrollTabsRef = useRef(null); @@ -238,6 +246,11 @@ const AccessForm: React.FC = forwardRef((props, ref) => { updateActiveKey }); + const handleFinishFailed = (errorInfo: any) => { + handleOnFinishFailed(errorInfo); + onFinishFailed?.(errorInfo); + }; + useImperativeHandle(ref, () => ({ submit: () => { form.submit(); @@ -266,7 +279,7 @@ const AccessForm: React.FC = forwardRef((props, ref) => { = ({ const { initialState } = useModel('@@initialState') || {}; const [form] = Form.useForm(); const intl = useIntl(); + const { loading, guard, run, release } = useSubmitLock(); const initFormValue = () => { if (action === PageAction.EDIT && open) { @@ -49,7 +51,11 @@ const AddModal: React.FC = ({ }; const handleSubmit = () => { - form.submit(); + guard(() => form.submit()); + }; + + const onFinish = async (values: FormData) => { + await run(() => onOk(values)); }; useEffect(() => { @@ -62,8 +68,15 @@ const AddModal: React.FC = ({ open={open} onCancel={onCancel} onSubmit={handleSubmit} + loading={loading} > - + name="username" rules={[