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; export type ApplyNodeConfigFormProps = { className?: string; style?: React.CSSProperties; disabled?: boolean; initialValues?: ApplyNodeConfigFormFieldValues; onValuesChange?: (values: ApplyNodeConfigFormFieldValues) => void; }; export type ApplyNodeConfigFormInstance = { getFieldsValue: () => ReturnType["getFieldsValue"]>; resetFields: FormInstance["resetFields"]; validateFields: FormInstance["validateFields"]; }; const MULTIPLE_INPUT_DELIMITER = ";"; const initFormModel = (): ApplyNodeConfigFormFieldValues => { return { keyAlgorithm: "RSA2048", propagationTimeout: 60, disableFollowCNAME: true, }; }; const ApplyNodeConfigForm = forwardRef( ({ 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("provider", { form: formInst, preserve: true }); const fieldProviderAccessId = Form.useWatch("providerAccessId", formInst); const fieldDomains = Form.useWatch("domains", formInst); const fieldNameservers = Form.useWatch("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 ; case ACCESS_PROVIDERS.HUAWEICLOUD: case APPLY_DNS_PROVIDERS.HUAWEICLOUD_DNS: return ; } }, [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) => { 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 (
} > } onChange={(v) => { formInst.setFieldValue("domains", v); }} /> } > { const provider = accessProvidersMap.get(record.provider); return ACCESS_USAGES.ALL === provider?.usage || ACCESS_USAGES.APPLY === provider?.usage; }} onChange={handleProviderAccessSelect} />
{nestedFormEl} {t("workflow_node.apply.form.advanced_config.label")}
{ formInst.setFieldValue("nameservers", e.target.value); }} /> } onChange={(value) => { formInst.setFieldValue("nameservers", value); }} /> } > } >
); } ); 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(props, { valuePropName: "value", defaultValuePropName: "defaultValue", trigger: "onChange", }); const [options, setOptions] = useState([]); 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 ( ); } ); 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 ( ); }); 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 ( ); }); export default memo(ApplyNodeConfigForm);