mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-08 05:29:51 +00:00
477 lines
19 KiB
TypeScript
477 lines
19 KiB
TypeScript
import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { FormOutlined as FormOutlinedIcon, PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
|
|
import { useControllableValue } from "ahooks";
|
|
import { AutoComplete, type AutoCompleteProps, Button, Divider, Form, type FormInstance, Input, Select, Space, Switch, Tooltip, Typography } from "antd";
|
|
import { createSchemaFieldRule } from "antd-zod";
|
|
import { z } from "zod";
|
|
|
|
import ModalForm from "@/components/ModalForm";
|
|
import MultipleInput from "@/components/MultipleInput";
|
|
import AccessEditModal from "@/components/access/AccessEditModal";
|
|
import AccessSelect from "@/components/access/AccessSelect";
|
|
import ApplyDNSProviderSelect from "@/components/provider/ApplyDNSProviderSelect";
|
|
import { ACCESS_PROVIDERS, ACCESS_USAGES, APPLY_DNS_PROVIDERS, accessProvidersMap, applyDNSProvidersMap } from "@/domain/provider";
|
|
import { type WorkflowNodeConfigForApply } from "@/domain/workflow";
|
|
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
|
|
import { useAccessesStore } from "@/stores/access";
|
|
import { useContactEmailsStore } from "@/stores/contact";
|
|
import { validDomainName, validIPv4Address, validIPv6Address } from "@/utils/validators";
|
|
|
|
import ApplyNodeConfigFormAWSRoute53Config from "./ApplyNodeConfigFormAWSRoute53Config";
|
|
import ApplyNodeConfigFormHuaweiCloudDNSConfig from "./ApplyNodeConfigFormHuaweiCloudDNSConfig";
|
|
|
|
type ApplyNodeConfigFormFieldValues = Partial<WorkflowNodeConfigForApply>;
|
|
|
|
export type ApplyNodeConfigFormProps = {
|
|
className?: string;
|
|
style?: React.CSSProperties;
|
|
disabled?: boolean;
|
|
initialValues?: ApplyNodeConfigFormFieldValues;
|
|
onValuesChange?: (values: ApplyNodeConfigFormFieldValues) => void;
|
|
};
|
|
|
|
export type ApplyNodeConfigFormInstance = {
|
|
getFieldsValue: () => ReturnType<FormInstance<ApplyNodeConfigFormFieldValues>["getFieldsValue"]>;
|
|
resetFields: FormInstance<ApplyNodeConfigFormFieldValues>["resetFields"];
|
|
validateFields: FormInstance<ApplyNodeConfigFormFieldValues>["validateFields"];
|
|
};
|
|
|
|
const MULTIPLE_INPUT_DELIMITER = ";";
|
|
|
|
const initFormModel = (): ApplyNodeConfigFormFieldValues => {
|
|
return {
|
|
keyAlgorithm: "RSA2048",
|
|
propagationTimeout: 60,
|
|
disableFollowCNAME: true,
|
|
};
|
|
};
|
|
|
|
const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeConfigFormProps>(
|
|
({ className, style, disabled, initialValues, onValuesChange }, ref) => {
|
|
const { t } = useTranslation();
|
|
|
|
const { accesses } = useAccessesStore(useZustandShallowSelector("accesses"));
|
|
|
|
const formSchema = z.object({
|
|
domains: z.string({ message: t("workflow_node.apply.form.domains.placeholder") }).refine((v) => {
|
|
return String(v)
|
|
.split(MULTIPLE_INPUT_DELIMITER)
|
|
.every((e) => validDomainName(e, { allowWildcard: true }));
|
|
}, t("common.errmsg.domain_invalid")),
|
|
contactEmail: z.string({ message: t("workflow_node.apply.form.contact_email.placeholder") }).email(t("common.errmsg.email_invalid")),
|
|
provider: z.string({ message: t("workflow_node.apply.form.provider.placeholder") }).nonempty(t("workflow_node.apply.form.provider.placeholder")),
|
|
providerAccessId: z
|
|
.string({ message: t("workflow_node.apply.form.provider_access.placeholder") })
|
|
.min(1, t("workflow_node.apply.form.provider_access.placeholder")),
|
|
providerConfig: z.any(),
|
|
keyAlgorithm: z
|
|
.string({ message: t("workflow_node.apply.form.key_algorithm.placeholder") })
|
|
.nonempty(t("workflow_node.apply.form.key_algorithm.placeholder")),
|
|
nameservers: z
|
|
.string()
|
|
.nullish()
|
|
.refine((v) => {
|
|
if (!v) return true;
|
|
return String(v)
|
|
.split(MULTIPLE_INPUT_DELIMITER)
|
|
.every((e) => validIPv4Address(e) || validIPv6Address(e) || validDomainName(e));
|
|
}, t("common.errmsg.host_invalid")),
|
|
propagationTimeout: z
|
|
.union([
|
|
z.number().int().gte(1, t("workflow_node.apply.form.propagation_timeout.placeholder")),
|
|
z.string().refine((v) => !v || /^[1-9]\d*$/.test(v), t("workflow_node.apply.form.propagation_timeout.placeholder")),
|
|
])
|
|
.nullish(),
|
|
disableFollowCNAME: z.boolean().nullish(),
|
|
});
|
|
const formRule = createSchemaFieldRule(formSchema);
|
|
const { form: formInst, formProps } = useAntdForm({
|
|
name: "workflowNodeApplyConfigForm",
|
|
initialValues: initialValues ?? initFormModel(),
|
|
});
|
|
|
|
const fieldProvider = Form.useWatch<string>("provider", { form: formInst, preserve: true });
|
|
const fieldProviderAccessId = Form.useWatch<string>("providerAccessId", formInst);
|
|
const fieldDomains = Form.useWatch<string>("domains", formInst);
|
|
const fieldNameservers = Form.useWatch<string>("nameservers", formInst);
|
|
|
|
const [nestedFormInst] = Form.useForm();
|
|
const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeApplyConfigFormProviderConfigForm" });
|
|
const nestedFormEl = useMemo(() => {
|
|
const nestedFormProps = {
|
|
form: nestedFormInst,
|
|
formName: nestedFormName,
|
|
disabled: disabled,
|
|
initialValues: initialValues?.providerConfig,
|
|
};
|
|
|
|
/*
|
|
注意:如果追加新的子组件,请保持以 ASCII 排序。
|
|
NOTICE: If you add new child component, please keep ASCII order.
|
|
*/
|
|
switch (fieldProvider) {
|
|
case ACCESS_PROVIDERS.AWS:
|
|
case APPLY_DNS_PROVIDERS.AWS_ROUTE53:
|
|
return <ApplyNodeConfigFormAWSRoute53Config {...nestedFormProps} />;
|
|
case ACCESS_PROVIDERS.HUAWEICLOUD:
|
|
case APPLY_DNS_PROVIDERS.HUAWEICLOUD_DNS:
|
|
return <ApplyNodeConfigFormHuaweiCloudDNSConfig {...nestedFormProps} />;
|
|
}
|
|
}, [disabled, initialValues?.providerConfig, fieldProvider, nestedFormInst, nestedFormName]);
|
|
|
|
const handleProviderSelect = (value: string) => {
|
|
if (fieldProvider === value) return;
|
|
|
|
if (initialValues?.provider === value) {
|
|
formInst.setFieldValue("providerAccessId", initialValues?.providerAccessId);
|
|
onValuesChange?.(formInst.getFieldsValue(true));
|
|
} else {
|
|
if (applyDNSProvidersMap.get(fieldProvider)?.provider !== applyDNSProvidersMap.get(value)?.provider) {
|
|
formInst.setFieldValue("providerAccessId", undefined);
|
|
onValuesChange?.(formInst.getFieldsValue(true));
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleProviderAccessSelect = (value: string) => {
|
|
if (fieldProviderAccessId === value) return;
|
|
|
|
// DNS 提供商和授权提供商目前一一对应,因此切换授权时,自动切换到相应提供商
|
|
const access = accesses.find((access) => access.id === value);
|
|
formInst.setFieldValue("provider", Array.from(applyDNSProvidersMap.values()).find((provider) => provider.provider === access?.provider)?.type);
|
|
onValuesChange?.(formInst.getFieldsValue(true));
|
|
};
|
|
|
|
const handleFormProviderChange = (name: string) => {
|
|
if (name === nestedFormName) {
|
|
formInst.setFieldValue("providerConfig", nestedFormInst.getFieldsValue());
|
|
onValuesChange?.(formInst.getFieldsValue(true));
|
|
}
|
|
};
|
|
|
|
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
|
|
onValuesChange?.(values as ApplyNodeConfigFormFieldValues);
|
|
};
|
|
|
|
useImperativeHandle(ref, () => {
|
|
return {
|
|
getFieldsValue: () => {
|
|
const values = formInst.getFieldsValue(true);
|
|
values.providerConfig = nestedFormInst.getFieldsValue();
|
|
return values;
|
|
},
|
|
resetFields: (fields) => {
|
|
formInst.resetFields(fields);
|
|
|
|
if (!!fields && fields.includes("providerConfig")) {
|
|
nestedFormInst.resetFields(fields);
|
|
}
|
|
},
|
|
validateFields: (nameList, config) => {
|
|
const t1 = formInst.validateFields(nameList, config);
|
|
const t2 = nestedFormInst.validateFields(undefined, config);
|
|
return Promise.all([t1, t2]).then(() => t1);
|
|
},
|
|
} as ApplyNodeConfigFormInstance;
|
|
});
|
|
|
|
return (
|
|
<Form.Provider onFormChange={handleFormProviderChange}>
|
|
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
|
|
<Form.Item
|
|
label={t("workflow_node.apply.form.domains.label")}
|
|
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.domains.tooltip") }}></span>}
|
|
>
|
|
<Space.Compact style={{ width: "100%" }}>
|
|
<Form.Item name="domains" noStyle rules={[formRule]}>
|
|
<Input placeholder={t("workflow_node.apply.form.domains.placeholder")} />
|
|
</Form.Item>
|
|
<DomainsModalInput
|
|
value={fieldDomains}
|
|
trigger={
|
|
<Button disabled={disabled}>
|
|
<FormOutlinedIcon />
|
|
</Button>
|
|
}
|
|
onChange={(v) => {
|
|
formInst.setFieldValue("domains", v);
|
|
}}
|
|
/>
|
|
</Space.Compact>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
name="contactEmail"
|
|
label={t("workflow_node.apply.form.contact_email.label")}
|
|
rules={[formRule]}
|
|
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.contact_email.tooltip") }}></span>}
|
|
>
|
|
<EmailInput placeholder={t("workflow_node.apply.form.contact_email.placeholder")} />
|
|
</Form.Item>
|
|
|
|
<Form.Item name="provider" label={t("workflow_node.apply.form.provider.label")} hidden rules={[formRule]}>
|
|
<ApplyDNSProviderSelect
|
|
allowClear
|
|
disabled
|
|
placeholder={t("workflow_node.apply.form.provider.placeholder")}
|
|
showSearch
|
|
onSelect={handleProviderSelect}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item className="mb-0">
|
|
<label className="mb-1 block">
|
|
<div className="flex w-full items-center justify-between gap-4">
|
|
<div className="max-w-full grow truncate">
|
|
<span>{t("workflow_node.apply.form.provider_access.label")}</span>
|
|
<Tooltip title={t("workflow_node.apply.form.provider_access.tooltip")}>
|
|
<Typography.Text className="ms-1" type="secondary">
|
|
<QuestionCircleOutlinedIcon />
|
|
</Typography.Text>
|
|
</Tooltip>
|
|
</div>
|
|
<div className="text-right">
|
|
<AccessEditModal
|
|
preset="add"
|
|
trigger={
|
|
<Button size="small" type="link">
|
|
<PlusOutlinedIcon />
|
|
{t("workflow_node.apply.form.provider_access.button")}
|
|
</Button>
|
|
}
|
|
afterSubmit={(record) => {
|
|
const provider = accessProvidersMap.get(record.provider);
|
|
if (ACCESS_USAGES.ALL === provider?.usage || ACCESS_USAGES.APPLY === provider?.usage) {
|
|
formInst.setFieldValue("providerAccessId", record.id);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</label>
|
|
<Form.Item name="providerAccessId" rules={[formRule]}>
|
|
<AccessSelect
|
|
placeholder={t("workflow_node.apply.form.provider_access.placeholder")}
|
|
filter={(record) => {
|
|
const provider = accessProvidersMap.get(record.provider);
|
|
return ACCESS_USAGES.ALL === provider?.usage || ACCESS_USAGES.APPLY === provider?.usage;
|
|
}}
|
|
onChange={handleProviderAccessSelect}
|
|
/>
|
|
</Form.Item>
|
|
</Form.Item>
|
|
</Form>
|
|
|
|
{nestedFormEl}
|
|
|
|
<Divider className="my-1">
|
|
<Typography.Text className="text-xs font-normal" type="secondary">
|
|
{t("workflow_node.apply.form.advanced_config.label")}
|
|
</Typography.Text>
|
|
</Divider>
|
|
|
|
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
|
|
<Form.Item name="keyAlgorithm" label={t("workflow_node.apply.form.key_algorithm.label")} rules={[formRule]}>
|
|
<Select
|
|
options={["RSA2048", "RSA3072", "RSA4096", "RSA8192", "EC256", "EC384"].map((e) => ({
|
|
label: e,
|
|
value: e,
|
|
}))}
|
|
placeholder={t("workflow_node.apply.form.key_algorithm.placeholder")}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label={t("workflow_node.apply.form.nameservers.label")}
|
|
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.nameservers.tooltip") }}></span>}
|
|
>
|
|
<Space.Compact style={{ width: "100%" }}>
|
|
<Form.Item name="nameservers" noStyle rules={[formRule]}>
|
|
<Input
|
|
allowClear
|
|
disabled={disabled}
|
|
value={fieldNameservers}
|
|
placeholder={t("workflow_node.apply.form.nameservers.placeholder")}
|
|
onChange={(e) => {
|
|
formInst.setFieldValue("nameservers", e.target.value);
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
<NameserversModalInput
|
|
value={fieldNameservers}
|
|
trigger={
|
|
<Button disabled={disabled}>
|
|
<FormOutlinedIcon />
|
|
</Button>
|
|
}
|
|
onChange={(value) => {
|
|
formInst.setFieldValue("nameservers", value);
|
|
}}
|
|
/>
|
|
</Space.Compact>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
name="propagationTimeout"
|
|
label={t("workflow_node.apply.form.propagation_timeout.label")}
|
|
rules={[formRule]}
|
|
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.propagation_timeout.tooltip") }}></span>}
|
|
>
|
|
<Input
|
|
type="number"
|
|
allowClear
|
|
min={0}
|
|
max={3600}
|
|
placeholder={t("workflow_node.apply.form.propagation_timeout.placeholder")}
|
|
addonAfter={t("workflow_node.apply.form.propagation_timeout.suffix")}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
name="disableFollowCNAME"
|
|
label={t("workflow_node.apply.form.disable_follow_cname.label")}
|
|
rules={[formRule]}
|
|
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.apply.form.disable_follow_cname.tooltip") }}></span>}
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
</Form>
|
|
</Form.Provider>
|
|
);
|
|
}
|
|
);
|
|
|
|
const EmailInput = memo(
|
|
({ disabled, placeholder, ...props }: { disabled?: boolean; placeholder?: string; value?: string; onChange?: (value: string) => void }) => {
|
|
const { emails, fetchEmails } = useContactEmailsStore();
|
|
const emailsToOptions = () => emails.map((email) => ({ label: email, value: email }));
|
|
useEffect(() => {
|
|
fetchEmails();
|
|
}, []);
|
|
|
|
const [value, setValue] = useControllableValue<string>(props, {
|
|
valuePropName: "value",
|
|
defaultValuePropName: "defaultValue",
|
|
trigger: "onChange",
|
|
});
|
|
|
|
const [options, setOptions] = useState<AutoCompleteProps["options"]>([]);
|
|
useEffect(() => {
|
|
setOptions(emailsToOptions());
|
|
}, [emails]);
|
|
|
|
const handleChange = (value: string) => {
|
|
setValue(value);
|
|
};
|
|
|
|
const handleSearch = (text: string) => {
|
|
const temp = emailsToOptions();
|
|
if (text?.trim()) {
|
|
if (temp.every((option) => option.label !== text)) {
|
|
temp.unshift({ label: text, value: text });
|
|
}
|
|
}
|
|
|
|
setOptions(temp);
|
|
};
|
|
|
|
return (
|
|
<AutoComplete
|
|
backfill
|
|
defaultValue={value}
|
|
disabled={disabled}
|
|
filterOption
|
|
options={options}
|
|
placeholder={placeholder}
|
|
showSearch
|
|
value={value}
|
|
onChange={handleChange}
|
|
onSearch={handleSearch}
|
|
/>
|
|
);
|
|
}
|
|
);
|
|
|
|
const DomainsModalInput = memo(({ value, trigger, onChange }: { value?: string; trigger?: React.ReactNode; onChange?: (value: string) => void }) => {
|
|
const { t } = useTranslation();
|
|
|
|
const formSchema = z.object({
|
|
domains: z.array(z.string()).refine((v) => {
|
|
return v.every((e) => !e?.trim() || validDomainName(e.trim(), { allowWildcard: true }));
|
|
}, t("common.errmsg.domain_invalid")),
|
|
});
|
|
const formRule = createSchemaFieldRule(formSchema);
|
|
const { form: formInst, formProps } = useAntdForm({
|
|
name: "workflowNodeApplyConfigFormDomainsModalInput",
|
|
initialValues: { domains: value?.split(MULTIPLE_INPUT_DELIMITER) },
|
|
onSubmit: (values) => {
|
|
onChange?.(
|
|
values.domains
|
|
.map((e) => e.trim())
|
|
.filter((e) => !!e)
|
|
.join(MULTIPLE_INPUT_DELIMITER)
|
|
);
|
|
},
|
|
});
|
|
|
|
return (
|
|
<ModalForm
|
|
{...formProps}
|
|
layout="vertical"
|
|
form={formInst}
|
|
modalProps={{ destroyOnClose: true }}
|
|
title={t("workflow_node.apply.form.domains.multiple_input_modal.title")}
|
|
trigger={trigger}
|
|
validateTrigger="onSubmit"
|
|
width={480}
|
|
>
|
|
<Form.Item name="domains" rules={[formRule]}>
|
|
<MultipleInput placeholder={t("workflow_node.apply.form.domains.multiple_input_modal.placeholder")} />
|
|
</Form.Item>
|
|
</ModalForm>
|
|
);
|
|
});
|
|
|
|
const NameserversModalInput = memo(({ trigger, value, onChange }: { trigger?: React.ReactNode; value?: string; onChange?: (value: string) => void }) => {
|
|
const { t } = useTranslation();
|
|
|
|
const formSchema = z.object({
|
|
nameservers: z.array(z.string()).refine((v) => {
|
|
return v.every((e) => !e?.trim() || validIPv4Address(e) || validIPv6Address(e) || validDomainName(e));
|
|
}, t("common.errmsg.domain_invalid")),
|
|
});
|
|
const formRule = createSchemaFieldRule(formSchema);
|
|
const { form: formInst, formProps } = useAntdForm({
|
|
name: "workflowNodeApplyConfigFormNameserversModalInput",
|
|
initialValues: { nameservers: value?.split(MULTIPLE_INPUT_DELIMITER) },
|
|
onSubmit: (values) => {
|
|
onChange?.(
|
|
values.nameservers
|
|
.map((e) => e.trim())
|
|
.filter((e) => !!e)
|
|
.join(MULTIPLE_INPUT_DELIMITER)
|
|
);
|
|
},
|
|
});
|
|
|
|
return (
|
|
<ModalForm
|
|
{...formProps}
|
|
layout="vertical"
|
|
form={formInst}
|
|
modalProps={{ destroyOnClose: true }}
|
|
title={t("workflow_node.apply.form.nameservers.multiple_input_modal.title")}
|
|
trigger={trigger}
|
|
validateTrigger="onSubmit"
|
|
width={480}
|
|
>
|
|
<Form.Item name="nameservers" rules={[formRule]}>
|
|
<MultipleInput placeholder={t("workflow_node.apply.form.nameservers.multiple_input_modal.placeholder")} />
|
|
</Form.Item>
|
|
</ModalForm>
|
|
);
|
|
});
|
|
|
|
export default memo(ApplyNodeConfigForm);
|