fix: anti double submit in form

This commit is contained in:
jialin
2026-06-11 20:02:27 +08:00
committed by jialin
parent 1218c6d096
commit a7b33a928b
23 changed files with 246 additions and 93 deletions
+46
View File
@@ -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<boolean>(false);
const lockRef = useRef<boolean>(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<void>) => {
setLoading(true);
try {
await task();
} finally {
release();
}
});
return { loading, guard, run, release };
}
@@ -1,5 +1,6 @@
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { import {
AlertBlockInfo, AlertBlockInfo,
Input as CInput, Input as CInput,
@@ -45,7 +46,7 @@ const AddModal: React.FC<AddModalProps> = ({
const intl = useIntl(); const intl = useIntl();
const [showKey, setShowKey] = useState(false); const [showKey, setShowKey] = useState(false);
const [apikeyValue, setAPIKeyValue] = useState(''); const [apikeyValue, setAPIKeyValue] = useState('');
const [loading, setLoading] = useState(false); const { loading, guard, run, release } = useSubmitLock();
const [isChanged, setIsChanged] = useState(false); const [isChanged, setIsChanged] = useState(false);
const cacheFormRef = useRef<{ const cacheFormRef = useRef<{
allowed_type: string; allowed_type: string;
@@ -116,31 +117,31 @@ const AddModal: React.FC<AddModalProps> = ({
}; };
const handleOnOk = async (formdata: FormData) => { const handleOnOk = async (formdata: FormData) => {
try { await run(async () => {
setLoading(true); try {
const data = { const data = {
..._.omit(formdata, ['allowed_type']), ..._.omit(formdata, ['allowed_type']),
allowed_model_names: allowed_model_names:
formdata.allowed_type === 'all' || formdata.allowed_type === 'all' ||
!formdata.scope?.includes('inference') !formdata.scope?.includes('inference')
? [] ? []
: formdata.allowed_model_names || [] : formdata.allowed_model_names || []
}; };
if (action === PageAction.CREATE) { if (action === PageAction.CREATE) {
await createAPIKey(data); await createAPIKey(data);
} else if (action === PageAction.EDIT && currentData?.id) { } else if (action === PageAction.EDIT && currentData?.id) {
await updateAPIKey({ await updateAPIKey({
..._.omit(data, ['expires_in']) ..._.omit(data, ['expires_in'])
}); });
}
} catch (error) {
// handled in interceptor
} }
setLoading(false); });
} catch (error) {
setLoading(false);
}
}; };
const handleSumit = () => { const handleSumit = () => {
form.submit(); guard(() => form.submit());
}; };
const handleDone = () => { const handleDone = () => {
@@ -278,6 +279,7 @@ const AddModal: React.FC<AddModalProps> = ({
name="addAPIKey" name="addAPIKey"
form={form} form={form}
onFinish={handleOnOk} onFinish={handleOnOk}
onFinishFailed={release}
preserve={false} preserve={false}
initialValues={{ initialValues={{
allowed_type: 'all', allowed_type: 'all',
+28 -17
View File
@@ -1,5 +1,6 @@
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { import {
AlertBlockInfo, AlertBlockInfo,
GSDrawer, GSDrawer,
@@ -63,6 +64,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
const [yamlContent, setYamlContent] = useState<string>(''); const [yamlContent, setYamlContent] = useState<string>('');
const [formContent, setFormContent] = useState<FormData>({} as FormData); const [formContent, setFormContent] = useState<FormData>({} as FormData);
const alertRef = useRef<HTMLDivElement>(null); const alertRef = useRef<HTMLDivElement>(null);
const { loading, guard, run, release } = useSubmitLock();
const showVersionCustomSuffix = const showVersionCustomSuffix =
currentData?.backend_source === BackendSourceValueMap.BUILTIN || currentData?.backend_source === BackendSourceValueMap.BUILTIN ||
@@ -92,17 +94,22 @@ const AddModal: React.FC<AddModalProps> = (props) => {
}; };
const onOk = () => { const onOk = () => {
if (activeKey === 'yaml') { guard(() => {
const content = editorRef.current?.getContent(); if (activeKey === 'yaml') {
if (content) { const content = editorRef.current?.getContent();
onSubmitYaml({ content: content }); if (!content) {
// nothing to submit — drop the lock so the button stays usable
release();
return;
}
run(() => onSubmitYaml({ content: content }));
return;
} }
} else {
formRef.current?.submit(); formRef.current?.submit();
} });
}; };
const onFinish = (values: FormData) => { const onFinish = async (values: FormData) => {
const versionConfigs = values.version_configs?.reduce( const versionConfigs = values.version_configs?.reduce(
(acc: Record<string, any>, curr) => { (acc: Record<string, any>, curr) => {
if (curr.version_no) { if (curr.version_no) {
@@ -117,16 +124,18 @@ const AddModal: React.FC<AddModalProps> = (props) => {
const defaultVersion = values.version_configs?.find((v) => v.is_default); const defaultVersion = values.version_configs?.find((v) => v.is_default);
onSubmit({ await run(() =>
...values, onSubmit({
parameter_format: ...values,
values.parameter_format === 'auto' || !values.parameter_format parameter_format:
? null values.parameter_format === 'auto' || !values.parameter_format
: values.parameter_format, ? null
default_version: defaultVersion?.version_no || '', : values.parameter_format,
// @ts-ignore default_version: defaultVersion?.version_no || '',
version_configs: versionConfigs // @ts-ignore
}); version_configs: versionConfigs
})
);
}; };
useEffect(() => { useEffect(() => {
@@ -279,6 +288,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
<ModalFooter <ModalFooter
onCancel={onClose} onCancel={onClose}
onOk={onOk} onOk={onOk}
loading={loading}
style={ModalFooterStyle} style={ModalFooterStyle}
></ModalFooter> ></ModalFooter>
</> </>
@@ -295,6 +305,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
children: ( children: (
<BackendForm <BackendForm
onFinish={onFinish} onFinish={onFinish}
onFinishFailed={release}
action={action} action={action}
currentData={formContent as ListItem} currentData={formContent as ListItem}
ref={formRef} ref={formRef}
+5 -3
View File
@@ -13,10 +13,11 @@ type AddModalProps = {
action: PageActionType; action: PageActionType;
currentData?: ListItem; currentData?: ListItem;
onFinish: (values: FormData) => void; onFinish: (values: FormData) => void;
onFinishFailed?: (errorInfo: any) => void;
ref?: any; ref?: any;
}; };
const BackendForm: React.FC<AddModalProps> = forwardRef( const BackendForm: React.FC<AddModalProps> = forwardRef(
({ action, currentData, onFinish }, ref) => { ({ action, currentData, onFinish, onFinishFailed }, ref) => {
const intl = useIntl(); const intl = useIntl();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [activeKey, setActiveKey] = React.useState<string[]>([]); const [activeKey, setActiveKey] = React.useState<string[]>([]);
@@ -26,7 +27,7 @@ const BackendForm: React.FC<AddModalProps> = forwardRef(
currentData?.backend_source === BackendSourceValueMap.BUILTIN || currentData?.backend_source === BackendSourceValueMap.BUILTIN ||
currentData?.backend_source === BackendSourceValueMap.COMMUNITY; currentData?.backend_source === BackendSourceValueMap.COMMUNITY;
const onFinishFailed = (errorInfo: any) => { const handleOnFinishFailed = (errorInfo: any) => {
const errorFields = errorInfo.errorFields || []; const errorFields = errorInfo.errorFields || [];
if (errorFields.length > 0) { if (errorFields.length > 0) {
const versionError = errorFields.find((field: any) => const versionError = errorFields.find((field: any) =>
@@ -38,6 +39,7 @@ const BackendForm: React.FC<AddModalProps> = forwardRef(
setActiveKey([]); setActiveKey([]);
} }
} }
onFinishFailed?.(errorInfo);
}; };
const handleOnFinish = (values: FormData) => { const handleOnFinish = (values: FormData) => {
@@ -106,7 +108,7 @@ const BackendForm: React.FC<AddModalProps> = forwardRef(
preserve={false} preserve={false}
scrollToFirstError={true} scrollToFirstError={true}
initialValues={_.omit(currentData, ['version_configs'])} initialValues={_.omit(currentData, ['version_configs'])}
onFinishFailed={onFinishFailed} onFinishFailed={handleOnFinishFailed}
> >
<BasicForm></BasicForm> <BasicForm></BasicForm>
<VersionsForm <VersionsForm
@@ -1,4 +1,5 @@
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { FormDrawer } from '@gpustack/core-ui'; import { FormDrawer } from '@gpustack/core-ui';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { FormData, BenchmarkListItem as ListItem } from '../config/types'; import { FormData, BenchmarkListItem as ListItem } from '../config/types';
@@ -22,21 +23,24 @@ const AddBenchmark: React.FC<AddModalProps> = ({
open, open,
currentData, currentData,
clusterList, clusterList,
profilesOptions, profilesOptions = [],
datasetList, datasetList = [],
onOk, onOk,
onCancel onCancel
}) => { }) => {
const form = useRef<any>(null); const form = useRef<any>(null);
const { loading, guard, run, release } = useSubmitLock();
const handleSubmit = () => { const handleSubmit = () => {
form.current?.submit(); guard(() => form.current?.submit());
}; };
const handleOk = async (data: FormData) => { const handleOk = async (data: FormData) => {
onOk({ await run(() =>
...data onOk({
}); ...data
})
);
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -51,6 +55,7 @@ const AddBenchmark: React.FC<AddModalProps> = ({
onCancel={handleCancel} onCancel={handleCancel}
onSubmit={handleSubmit} onSubmit={handleSubmit}
width={600} width={600}
loading={loading}
> >
<BenchmarkForm <BenchmarkForm
ref={form} ref={form}
@@ -61,6 +66,7 @@ const AddBenchmark: React.FC<AddModalProps> = ({
profilesOptions={profilesOptions} profilesOptions={profilesOptions}
datasetList={datasetList} datasetList={datasetList}
onFinish={handleOk} onFinish={handleOk}
onFinishFailed={release}
/> />
</FormDrawer> </FormDrawer>
); );
+3
View File
@@ -29,6 +29,7 @@ interface ProviderFormProps {
datasetList: Global.BaseOption<number | string>[]; datasetList: Global.BaseOption<number | string>[];
profilesOptions: Global.BaseOption<string>[]; profilesOptions: Global.BaseOption<string>[];
onFinish: (values: FormData) => Promise<void>; onFinish: (values: FormData) => Promise<void>;
onFinishFailed?: (errorInfo: any) => void;
} }
const TABKeysMap = { const TABKeysMap = {
@@ -42,6 +43,7 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
action, action,
currentData, currentData,
onFinish, onFinish,
onFinishFailed,
open, open,
clusterList, clusterList,
profilesOptions, profilesOptions,
@@ -122,6 +124,7 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
<Form <Form
form={form} form={form}
onFinish={onFinish} onFinish={onFinish}
onFinishFailed={onFinishFailed}
initialValues={{ initialValues={{
dataset_input_tokens: null, dataset_input_tokens: null,
dataset_output_tokens: null, dataset_output_tokens: null,
@@ -1,5 +1,6 @@
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { ExclamationCircleFilled } from '@ant-design/icons'; import { ExclamationCircleFilled } from '@ant-design/icons';
import { AlertBlockInfo, FormDrawer, ModalFooter } from '@gpustack/core-ui'; import { AlertBlockInfo, FormDrawer, ModalFooter } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
@@ -39,20 +40,23 @@ const AddCluster: React.FC<AddModalProps> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const form = useRef<any>(null); const form = useRef<any>(null);
const { loading, guard, run, release } = useSubmitLock();
// Whether the user has changed any k8s_options field. Lifted from ClusterForm // 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 // so the "re-run registration" notice can sit in the drawer footer, above the
// Save/Cancel buttons (mirrors the model edit interaction). // Save/Cancel buttons (mirrors the model edit interaction).
const [k8sOptionsChanged, setK8sOptionsChanged] = useState<boolean>(false); const [k8sOptionsChanged, setK8sOptionsChanged] = useState<boolean>(false);
const handleSubmit = () => { const handleSubmit = () => {
form.current?.submit(); guard(() => form.current?.submit());
}; };
const handleOk = async (data: FormData) => { const handleOk = async (data: FormData) => {
onOk({ await run(() =>
...data, onOk({
provider ...data,
}); provider
})
);
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -84,6 +88,7 @@ const AddCluster: React.FC<AddModalProps> = ({
<ModalFooter <ModalFooter
onOk={handleSubmit} onOk={handleSubmit}
onCancel={handleCancel} onCancel={handleCancel}
loading={loading}
style={ModalFooterStyle} style={ModalFooterStyle}
></ModalFooter> ></ModalFooter>
</> </>
@@ -96,6 +101,7 @@ const AddCluster: React.FC<AddModalProps> = ({
action={action} action={action}
currentData={currentData} currentData={currentData}
onFinish={handleOk} onFinish={handleOk}
onFinishFailed={release}
onK8sOptionsChange={setK8sOptionsChanged} onK8sOptionsChange={setK8sOptionsChanged}
/> />
</FormDrawer> </FormDrawer>
@@ -36,6 +36,7 @@ type AddModalProps = {
provider: ProviderType; provider: ProviderType;
credentialList: Global.BaseOption<number>[]; credentialList: Global.BaseOption<number>[];
onFinish: (values: FormData) => void; onFinish: (values: FormData) => void;
onFinishFailed?: (errorInfo: any) => void;
// Reports whether the user has changed any k8s_options field, so the parent // Reports whether the user has changed any k8s_options field, so the parent
// can show the "re-run registration" notice in the footer. // can show the "re-run registration" notice in the footer.
onK8sOptionsChange?: (changed: boolean) => void; onK8sOptionsChange?: (changed: boolean) => void;
@@ -49,6 +50,7 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
currentData, currentData,
credentialList, credentialList,
onFinish, onFinish,
onFinishFailed,
onK8sOptionsChange onK8sOptionsChange
}, },
ref ref
@@ -212,8 +214,9 @@ const ClusterForm: React.FC<AddModalProps> = forwardRef(
} }
})); }));
const handleOnFinishFailed = () => { const handleOnFinishFailed = (errorInfo: any) => {
setSubmitAttempted(true); setSubmitAttempted(true);
onFinishFailed?.(errorInfo);
}; };
return ( return (
@@ -1,5 +1,6 @@
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list'; import { useQueryClusterList } from '@/pages/cluster-management/services/use-query-cluster-list';
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';
@@ -89,7 +90,7 @@ const AddModal: React.FC<AddModalProps> = ({
const [templateId, setTemplateId] = useState<number | undefined>(); const [templateId, setTemplateId] = useState<number | undefined>();
const [instanceKeyword, setInstanceKeyword] = useState(''); const [instanceKeyword, setInstanceKeyword] = useState('');
const [templateKeyword, setTemplateKeyword] = useState(''); const [templateKeyword, setTemplateKeyword] = useState('');
const [loading, setLoading] = useState(false); const { loading, guard, run, release } = useSubmitLock();
const initializedRef = useRef(false); const initializedRef = useRef(false);
const { const {
@@ -385,7 +386,7 @@ const AddModal: React.FC<AddModalProps> = ({
}); });
const handleSubmit = () => { const handleSubmit = () => {
form.current?.submit(); guard(() => form.current?.submit());
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -394,15 +395,12 @@ const AddModal: React.FC<AddModalProps> = ({
}; };
const onFinish = async (values: FormData) => { const onFinish = async (values: FormData) => {
setLoading(true); await run(async () => {
try {
await onOk({ await onOk({
...values ...values
}); });
console.log('submit form values', values); console.log('submit form values', values);
} finally { });
setLoading(false);
}
}; };
const handleInstanceTypeChange = (item: InstanceTypeItem) => { const handleInstanceTypeChange = (item: InstanceTypeItem) => {
@@ -576,6 +574,7 @@ const AddModal: React.FC<AddModalProps> = ({
currentData={data} currentData={data}
disabled={readonly} disabled={readonly}
onFinish={onFinish} onFinish={onFinish}
onFinishFailed={release}
onScopeChange={handleScopeChange} onScopeChange={handleScopeChange}
open={open} open={open}
instanceTypeList={ownedInstanceTypes} instanceTypeList={ownedInstanceTypes}
@@ -70,6 +70,7 @@ interface InstanceFormProps {
// tenant-scoped instance-type / template offerings. // tenant-scoped instance-type / template offerings.
onScopeChange?: (orgId: number | null | undefined) => void; onScopeChange?: (orgId: number | null | undefined) => void;
onFinish: (values: FormData) => Promise<void>; onFinish: (values: FormData) => Promise<void>;
onFinishFailed?: (errorInfo: any) => void;
} }
const TABKeysMap = { const TABKeysMap = {
@@ -113,7 +114,8 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
instanceTypeList = [], instanceTypeList = [],
noAvailableInstanceTypes, noAvailableInstanceTypes,
onScopeChange, onScopeChange,
onFinish onFinish,
onFinishFailed
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
const { getRuleMessage } = useAppUtils(); const { getRuleMessage } = useAppUtils();
@@ -370,6 +372,7 @@ const GPUServiceInstanceForm: React.FC<InstanceFormProps> = forwardRef(
] ]
})); }));
rawHandleOnFinishFailed({ ...errorInfo, errorFields }); rawHandleOnFinishFailed({ ...errorInfo, errorFields });
onFinishFailed?.(errorInfo);
}; };
const detectMode = (volume?: FormData['spec']['volume']) => { const detectMode = (volume?: FormData['spec']['volume']) => {
if (volume?.persistent?.name || volume?.persistentTemplate?.name) { if (volume?.persistent?.name || volume?.persistentTemplate?.name) {
@@ -1,4 +1,5 @@
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { ModalFooter } from '@gpustack/core-ui'; import { ModalFooter } from '@gpustack/core-ui';
import { useRef } from 'react'; import { useRef } from 'react';
import FormDrawer from '../../../_components/form-drawer'; import FormDrawer from '../../../_components/form-drawer';
@@ -23,9 +24,10 @@ const AddPublicKeyModal: React.FC<AddPublicKeyModalProps> = ({
onCancel onCancel
}) => { }) => {
const form = useRef<any>(null); const form = useRef<any>(null);
const { loading, guard, run, release } = useSubmitLock();
const handleSubmit = () => { const handleSubmit = () => {
form.current?.submit(); guard(() => form.current?.submit());
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -34,7 +36,7 @@ const AddPublicKeyModal: React.FC<AddPublicKeyModalProps> = ({
}; };
const onFinish = async (values: FormData) => { const onFinish = async (values: FormData) => {
onOk({ ...values }); await run(() => onOk({ ...values }));
}; };
return ( return (
@@ -48,6 +50,7 @@ const AddPublicKeyModal: React.FC<AddPublicKeyModalProps> = ({
<ModalFooter <ModalFooter
onOk={handleSubmit} onOk={handleSubmit}
onCancel={handleCancel} onCancel={handleCancel}
loading={loading}
style={{ style={{
padding: '16px 24px 8px', padding: '16px 24px 8px',
display: 'flex', display: 'flex',
@@ -61,6 +64,7 @@ const AddPublicKeyModal: React.FC<AddPublicKeyModalProps> = ({
action={action} action={action}
currentData={data} currentData={data}
onFinish={onFinish} onFinish={onFinish}
onFinishFailed={release}
open={open} open={open}
/> />
</FormDrawer> </FormDrawer>
@@ -11,11 +11,12 @@ interface PublicKeyFormProps {
action: PageActionType; action: PageActionType;
currentData?: ListItem | null; currentData?: ListItem | null;
onFinish: (values: FormData) => Promise<void>; onFinish: (values: FormData) => Promise<void>;
onFinishFailed?: (errorInfo: any) => void;
} }
const GPUServicePublicKeyForm: React.FC<PublicKeyFormProps> = forwardRef( const GPUServicePublicKeyForm: React.FC<PublicKeyFormProps> = forwardRef(
(props, ref) => { (props, ref) => {
const { action, currentData, open, onFinish } = props; const { action, currentData, open, onFinish, onFinishFailed } = props;
const [form] = Form.useForm<FormData>(); const [form] = Form.useForm<FormData>();
useEffect(() => { useEffect(() => {
@@ -50,6 +51,7 @@ const GPUServicePublicKeyForm: React.FC<PublicKeyFormProps> = forwardRef(
name="gpuServicePublicKeyForm" name="gpuServicePublicKeyForm"
form={form} form={form}
onFinish={onFinish} onFinish={onFinish}
onFinishFailed={onFinishFailed}
preserve={false} preserve={false}
initialValues={{}} initialValues={{}}
> >
@@ -1,4 +1,5 @@
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { ModalFooter } from '@gpustack/core-ui'; import { ModalFooter } from '@gpustack/core-ui';
import { useRef } from 'react'; import { useRef } from 'react';
import FormDrawer from '../../../_components/form-drawer'; import FormDrawer from '../../../_components/form-drawer';
@@ -23,9 +24,10 @@ const AddStorageTypeModal: React.FC<AddStorageTypeModalProps> = ({
onCancel onCancel
}) => { }) => {
const form = useRef<any>(null); const form = useRef<any>(null);
const { loading, guard, run, release } = useSubmitLock();
const handleSubmit = () => { const handleSubmit = () => {
form.current?.submit(); guard(() => form.current?.submit());
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -34,7 +36,7 @@ const AddStorageTypeModal: React.FC<AddStorageTypeModalProps> = ({
}; };
const onFinish = async (values: FormData) => { const onFinish = async (values: FormData) => {
onOk({ ...values }); await run(() => onOk({ ...values }));
}; };
return ( return (
@@ -48,6 +50,7 @@ const AddStorageTypeModal: React.FC<AddStorageTypeModalProps> = ({
<ModalFooter <ModalFooter
onOk={handleSubmit} onOk={handleSubmit}
onCancel={handleCancel} onCancel={handleCancel}
loading={loading}
style={{ style={{
padding: '16px 24px 8px', padding: '16px 24px 8px',
display: 'flex', display: 'flex',
@@ -61,6 +64,7 @@ const AddStorageTypeModal: React.FC<AddStorageTypeModalProps> = ({
action={action} action={action}
currentData={data} currentData={data}
onFinish={onFinish} onFinish={onFinish}
onFinishFailed={release}
open={open} open={open}
/> />
</FormDrawer> </FormDrawer>
@@ -18,6 +18,7 @@ interface StorageTypeFormProps {
action: PageActionType; action: PageActionType;
currentData?: ListItem | null; currentData?: ListItem | null;
onFinish: (values: FormData) => Promise<void>; onFinish: (values: FormData) => Promise<void>;
onFinishFailed?: (errorInfo: any) => void;
} }
const detectKind = (item?: ListItem | null): StorageTypeKind => { const detectKind = (item?: ListItem | null): StorageTypeKind => {
@@ -27,7 +28,7 @@ const detectKind = (item?: ListItem | null): StorageTypeKind => {
const GPUServiceStorageTypeForm: React.FC<StorageTypeFormProps> = forwardRef( const GPUServiceStorageTypeForm: React.FC<StorageTypeFormProps> = forwardRef(
(props, ref) => { (props, ref) => {
const { action, currentData, open, onFinish } = props; const { action, currentData, open, onFinish, onFinishFailed } = props;
const [form] = Form.useForm<FormData>(); const [form] = Form.useForm<FormData>();
const kind = Form.useWatch('type', form); const kind = Form.useWatch('type', form);
@@ -115,6 +116,7 @@ const GPUServiceStorageTypeForm: React.FC<StorageTypeFormProps> = forwardRef(
name="gpuServiceStorageTypeForm" name="gpuServiceStorageTypeForm"
form={form} form={form}
onFinish={handleFinish} onFinish={handleFinish}
onFinishFailed={onFinishFailed}
preserve={false} preserve={false}
initialValues={{}} initialValues={{}}
> >
@@ -1,4 +1,5 @@
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { ModalFooter } from '@gpustack/core-ui'; import { ModalFooter } from '@gpustack/core-ui';
import { useRef } from 'react'; import { useRef } from 'react';
import FormDrawer from '../../../_components/form-drawer'; import FormDrawer from '../../../_components/form-drawer';
@@ -26,9 +27,10 @@ const AddModal: React.FC<AddModalProps> = ({
storageClassList storageClassList
}) => { }) => {
const form = useRef<any>(null); const form = useRef<any>(null);
const { loading, guard, run, release } = useSubmitLock();
const handleSubmit = () => { const handleSubmit = () => {
form.current?.submit(); guard(() => form.current?.submit());
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -37,9 +39,11 @@ const AddModal: React.FC<AddModalProps> = ({
}; };
const onFinish = async (values: FormData) => { const onFinish = async (values: FormData) => {
onOk({ await run(() =>
...values onOk({
}); ...values
})
);
}; };
return ( return (
@@ -53,6 +57,7 @@ const AddModal: React.FC<AddModalProps> = ({
<ModalFooter <ModalFooter
onOk={handleSubmit} onOk={handleSubmit}
onCancel={handleCancel} onCancel={handleCancel}
loading={loading}
style={{ style={{
padding: '16px 24px 8px', padding: '16px 24px 8px',
display: 'flex', display: 'flex',
@@ -67,6 +72,7 @@ const AddModal: React.FC<AddModalProps> = ({
action={action} action={action}
currentData={data} currentData={data}
onFinish={onFinish} onFinish={onFinish}
onFinishFailed={release}
open={open} open={open}
/> />
</FormContext.Provider> </FormContext.Provider>
@@ -11,11 +11,12 @@ interface StorageFormProps {
action: PageActionType; action: PageActionType;
currentData?: ListItem | null; currentData?: ListItem | null;
onFinish: (values: FormData) => Promise<void>; onFinish: (values: FormData) => Promise<void>;
onFinishFailed?: (errorInfo: any) => void;
} }
const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef( const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
(props, ref) => { (props, ref) => {
const { action, currentData, open, onFinish } = props; const { action, currentData, open, onFinish, onFinishFailed } = props;
const [form] = Form.useForm<FormData>(); const [form] = Form.useForm<FormData>();
useEffect(() => { useEffect(() => {
@@ -51,6 +52,7 @@ const GPUServiceStorageForm: React.FC<StorageFormProps> = forwardRef(
name="gpuServiceStorageForm" name="gpuServiceStorageForm"
form={form} form={form}
onFinish={onFinish} onFinish={onFinish}
onFinishFailed={onFinishFailed}
preserve={false} preserve={false}
initialValues={{}} initialValues={{}}
> >
@@ -1,4 +1,5 @@
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { ModalFooter } from '@gpustack/core-ui'; import { ModalFooter } from '@gpustack/core-ui';
import { useRef } from 'react'; import { useRef } from 'react';
import FormDrawer from '../../../_components/form-drawer'; import FormDrawer from '../../../_components/form-drawer';
@@ -23,9 +24,10 @@ const AddModal: React.FC<AddModalProps> = ({
onCancel onCancel
}) => { }) => {
const form = useRef<any>(null); const form = useRef<any>(null);
const { loading, guard, run, release } = useSubmitLock();
const handleSubmit = () => { const handleSubmit = () => {
form.current?.submit(); guard(() => form.current?.submit());
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -34,9 +36,11 @@ const AddModal: React.FC<AddModalProps> = ({
}; };
const onFinish = async (values: FormData) => { const onFinish = async (values: FormData) => {
onOk({ await run(() =>
...values onOk({
}); ...values
})
);
}; };
return ( return (
@@ -50,6 +54,7 @@ const AddModal: React.FC<AddModalProps> = ({
<ModalFooter <ModalFooter
onOk={handleSubmit} onOk={handleSubmit}
onCancel={handleCancel} onCancel={handleCancel}
loading={loading}
style={{ style={{
padding: '16px 24px 8px', padding: '16px 24px 8px',
display: 'flex', display: 'flex',
@@ -63,6 +68,7 @@ const AddModal: React.FC<AddModalProps> = ({
action={action} action={action}
currentData={currentData} currentData={currentData}
onFinish={onFinish} onFinish={onFinish}
onFinishFailed={release}
open={open} open={open}
/> />
</FormDrawer> </FormDrawer>
@@ -12,11 +12,12 @@ interface TemplateFormProps {
action: PageActionType; action: PageActionType;
currentData?: ListItem | null; currentData?: ListItem | null;
onFinish: (values: FormData) => Promise<void>; onFinish: (values: FormData) => Promise<void>;
onFinishFailed?: (errorInfo: any) => void;
} }
const GPUServiceTemplateForm: React.FC<TemplateFormProps> = forwardRef( const GPUServiceTemplateForm: React.FC<TemplateFormProps> = forwardRef(
(props, ref) => { (props, ref) => {
const { action, currentData, open, onFinish } = props; const { action, currentData, open, onFinish, onFinishFailed } = props;
const [form] = Form.useForm<FormData>(); const [form] = Form.useForm<FormData>();
useEffect(() => { useEffect(() => {
@@ -58,6 +59,7 @@ const GPUServiceTemplateForm: React.FC<TemplateFormProps> = forwardRef(
name="gpuServiceTemplateForm" name="gpuServiceTemplateForm"
form={form} form={form}
onFinish={handleFinish} onFinish={handleFinish}
onFinishFailed={onFinishFailed}
preserve={false} preserve={false}
scrollToFirstError scrollToFirstError
initialValues={{ initialValues={{
@@ -1,4 +1,5 @@
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { FormDrawer } from '@gpustack/core-ui'; import { FormDrawer } from '@gpustack/core-ui';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { FormData, MaasProviderItem as ListItem } from '../config/types'; import { FormData, MaasProviderItem as ListItem } from '../config/types';
@@ -22,16 +23,19 @@ const AddProvider: React.FC<AddModalProps> = ({
onCancel onCancel
}) => { }) => {
const form = useRef<any>(null); const form = useRef<any>(null);
const { loading, guard, run, release } = useSubmitLock();
const handleSubmit = () => { const handleSubmit = () => {
form.current?.submit(); guard(() => form.current?.submit());
}; };
const handleOnFinish = async (data: FormData) => { const handleOnFinish = async (data: FormData) => {
console.log('handleOnFinish', data); console.log('handleOnFinish', data);
onOk({ await run(() =>
...data onOk({
}); ...data
})
);
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -46,12 +50,14 @@ const AddProvider: React.FC<AddModalProps> = ({
onCancel={handleCancel} onCancel={handleCancel}
onSubmit={handleSubmit} onSubmit={handleSubmit}
width={600} width={600}
loading={loading}
> >
<ProviderForm <ProviderForm
ref={form} ref={form}
action={action} action={action}
currentData={currentData} currentData={currentData}
onFinish={handleOnFinish} onFinish={handleOnFinish}
onFinishFailed={release}
/> />
</FormDrawer> </FormDrawer>
); );
+8 -2
View File
@@ -31,6 +31,7 @@ interface ProviderFormProps {
action: PageActionType; action: PageActionType;
currentData?: ListItem; // Used when action is EDIT currentData?: ListItem; // Used when action is EDIT
onFinish: (values: FormData) => Promise<void>; onFinish: (values: FormData) => Promise<void>;
onFinishFailed?: (errorInfo: any) => void;
} }
const TABKeysMap = { const TABKeysMap = {
@@ -51,7 +52,7 @@ const requiredFields = {
}; };
const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => { const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
const { action, currentData, onFinish } = props; const { action, currentData, onFinish, onFinishFailed } = props;
const intl = useIntl(); const intl = useIntl();
const providerRequiredFieldsMap = useProviderRequiredFields(); const providerRequiredFieldsMap = useProviderRequiredFields();
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -146,6 +147,11 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
updateActiveKey updateActiveKey
}); });
const handleFinishFailed = (errorInfo: any) => {
handleOnFinishFailed(errorInfo);
onFinishFailed?.(errorInfo);
};
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
submit: () => { submit: () => {
form.submit(); form.submit();
@@ -217,7 +223,7 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
<Form <Form
form={form} form={form}
onFinish={handleOnFinish} onFinish={handleOnFinish}
onFinishFailed={handleOnFinishFailed} onFinishFailed={handleFinishFailed}
initialValues={{ initialValues={{
proxy_enabled: false, proxy_enabled: false,
proxy_url: '', proxy_url: '',
@@ -1,4 +1,5 @@
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { AlertBlockInfo, FormDrawer, ModalFooter } from '@gpustack/core-ui'; import { AlertBlockInfo, FormDrawer, ModalFooter } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
@@ -26,15 +27,18 @@ const AddProvider: React.FC<AddModalProps> = ({
const intl = useIntl(); const intl = useIntl();
const [isChanged, setIsChanged] = React.useState(false); const [isChanged, setIsChanged] = React.useState(false);
const form = useRef<any>(null); const form = useRef<any>(null);
const { loading, guard, run, release } = useSubmitLock();
const handleSubmit = () => { const handleSubmit = () => {
form.current?.submit(); guard(() => form.current?.submit());
}; };
const onFinish = async (data: FormData) => { const onFinish = async (data: FormData) => {
onOk({ await run(() =>
...data onOk({
}); ...data
})
);
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -63,6 +67,7 @@ const AddProvider: React.FC<AddModalProps> = ({
<ModalFooter <ModalFooter
onOk={handleSubmit} onOk={handleSubmit}
onCancel={onCancel} onCancel={onCancel}
loading={loading}
styles={{ styles={{
wrapper: { wrapper: {
paddingTop: 16 paddingTop: 16
@@ -78,6 +83,7 @@ const AddProvider: React.FC<AddModalProps> = ({
realAction={realAction} realAction={realAction}
currentData={currentData} currentData={currentData}
onFinish={onFinish} onFinish={onFinish}
onFinishFailed={release}
open={open} open={open}
onFallbackChange={(changed: boolean) => { onFallbackChange={(changed: boolean) => {
setIsChanged(changed); setIsChanged(changed);
+16 -3
View File
@@ -65,6 +65,7 @@ interface ProviderFormProps {
routeTargets?: RouteTargetFormItem[]; routeTargets?: RouteTargetFormItem[];
}; // Used when action is EDIT }; // Used when action is EDIT
onFinish: (values: FormData) => Promise<void>; onFinish: (values: FormData) => Promise<void>;
onFinishFailed?: (errorInfo: any) => void;
onFallbackChange?: (changed: boolean) => void; onFallbackChange?: (changed: boolean) => void;
} }
@@ -86,8 +87,15 @@ const requiredFields = {
const AccessForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => { const AccessForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
const intl = useIntl(); const intl = useIntl();
const { action, realAction, currentData, open, onFinish, onFallbackChange } = const {
props; action,
realAction,
currentData,
open,
onFinish,
onFinishFailed,
onFallbackChange
} = props;
const { getScrollElementScrollableHeight } = useWrapperContext(); const { getScrollElementScrollableHeight } = useWrapperContext();
const [form] = Form.useForm(); const [form] = Form.useForm();
const scrollTabsRef = useRef<any>(null); const scrollTabsRef = useRef<any>(null);
@@ -238,6 +246,11 @@ const AccessForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
updateActiveKey updateActiveKey
}); });
const handleFinishFailed = (errorInfo: any) => {
handleOnFinishFailed(errorInfo);
onFinishFailed?.(errorInfo);
};
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
submit: () => { submit: () => {
form.submit(); form.submit();
@@ -266,7 +279,7 @@ const AccessForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
<Form <Form
form={form} form={form}
onFinish={handleOnFinish} onFinish={handleOnFinish}
onFinishFailed={handleOnFinishFailed} onFinishFailed={handleFinishFailed}
initialValues={{ initialValues={{
categories: [modelCategoriesMap.llm], categories: [modelCategoriesMap.llm],
meta: {} meta: {}
+15 -2
View File
@@ -1,5 +1,6 @@
import { PageAction, PasswordReg } from '@/config'; import { PageAction, PasswordReg } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import useSubmitLock from '@/hooks/use-submit-lock';
import { import {
Input as CInput, Input as CInput,
FormDrawer, FormDrawer,
@@ -32,6 +33,7 @@ const AddModal: React.FC<AddModalProps> = ({
const { initialState } = useModel('@@initialState') || {}; const { initialState } = useModel('@@initialState') || {};
const [form] = Form.useForm(); const [form] = Form.useForm();
const intl = useIntl(); const intl = useIntl();
const { loading, guard, run, release } = useSubmitLock();
const initFormValue = () => { const initFormValue = () => {
if (action === PageAction.EDIT && open) { if (action === PageAction.EDIT && open) {
@@ -49,7 +51,11 @@ const AddModal: React.FC<AddModalProps> = ({
}; };
const handleSubmit = () => { const handleSubmit = () => {
form.submit(); guard(() => form.submit());
};
const onFinish = async (values: FormData) => {
await run(() => onOk(values));
}; };
useEffect(() => { useEffect(() => {
@@ -62,8 +68,15 @@ const AddModal: React.FC<AddModalProps> = ({
open={open} open={open}
onCancel={onCancel} onCancel={onCancel}
onSubmit={handleSubmit} onSubmit={handleSubmit}
loading={loading}
> >
<Form name="addUserForm" form={form} onFinish={onOk} preserve={false}> <Form
name="addUserForm"
form={form}
onFinish={onFinish}
onFinishFailed={release}
preserve={false}
>
<Form.Item<FormData> <Form.Item<FormData>
name="username" name="username"
rules={[ rules={[