import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router"; import { FormOutlined as FormOutlinedIcon, PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon, RightOutlined as RightOutlinedIcon, } from "@ant-design/icons"; import { useControllableValue } from "ahooks"; import { AutoComplete, type AutoCompleteProps, Button, Divider, Flex, Form, type FormInstance, Input, InputNumber, Select, Space, Switch, Tooltip, Typography, } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import AccessEditModal from "@/components/access/AccessEditModal"; import AccessSelect from "@/components/access/AccessSelect"; import ModalForm from "@/components/ModalForm"; import MultipleInput from "@/components/MultipleInput"; import ACMEDns01ProviderSelect from "@/components/provider/ACMEDns01ProviderSelect"; import CAProviderSelect from "@/components/provider/CAProviderSelect"; import Show from "@/components/Show"; import { ACCESS_USAGES, ACME_DNS01_PROVIDERS, accessProvidersMap, acmeDns01ProvidersMap, caProvidersMap } 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"; import ApplyNodeConfigFormJDCloudDNSConfig from "./ApplyNodeConfigFormJDCloudDNSConfig"; import ApplyNodeConfigFormTencentCloudEOConfig from "./ApplyNodeConfigFormTencentCloudEOConfig"; 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 { challengeType: "dns-01", keyAlgorithm: "RSA2048", skipBeforeExpiryDays: 30, }; }; 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) => { if (!v) return false; 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")), challengeType: z.string().nullish(), 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().nullish(), caProvider: z.string({ message: t("workflow_node.apply.form.ca_provider.placeholder") }).nullish(), caProviderAccessId: z .string({ message: t("workflow_node.apply.form.ca_provider_access.placeholder") }) .nullish() .refine((v) => { if (!fieldCAProvider) return true; const provider = caProvidersMap.get(fieldCAProvider); return !!provider?.builtin || !!v; }, t("workflow_node.apply.form.ca_provider_access.placeholder")), caProviderConfig: z.any().nullish(), 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")), dnsPropagationWait: z.preprocess( (v) => (v == null || v === "" ? undefined : Number(v)), z .number() .int(t("workflow_node.apply.form.dns_propagation_wait.placeholder")) .gte(0, t("workflow_node.apply.form.dns_propagation_wait.placeholder")) .nullish() ), dnsPropagationTimeout: z.preprocess( (v) => (v == null || v === "" ? undefined : Number(v)), z .number() .int(t("workflow_node.apply.form.dns_propagation_timeout.placeholder")) .gte(1, t("workflow_node.apply.form.dns_propagation_timeout.placeholder")) .nullish() ), dnsTTL: z.preprocess( (v) => (v == null || v === "" ? undefined : Number(v)), z.number().int(t("workflow_node.apply.form.dns_ttl.placeholder")).gte(1, t("workflow_node.apply.form.dns_ttl.placeholder")).nullish() ), disableFollowCNAME: z.boolean().nullish(), disableARI: z.boolean().nullish(), skipBeforeExpiryDays: z.preprocess( (v) => Number(v), z .number() .int(t("workflow_node.apply.form.skip_before_expiry_days.placeholder")) .gte(1, t("workflow_node.apply.form.skip_before_expiry_days.placeholder")) ), }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formProps } = useAntdForm({ name: "workflowNodeApplyConfigForm", initialValues: initialValues ?? initFormModel(), }); const fieldDomains = Form.useWatch("domains", formInst); const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true }); const fieldProviderAccessId = Form.useWatch("providerAccessId", formInst); const fieldCAProvider = Form.useWatch("caProvider", formInst); const fieldNameservers = Form.useWatch("nameservers", formInst); const [showProvider, setShowProvider] = useState(false); useEffect(() => { // 通常情况下每个授权信息只对应一个 DNS 提供商,此时无需显示 DNS 提供商字段; // 如果对应多个(如 AWS 的 Route53、Lightsail,腾讯云的 DNS、EdgeOne 等),则显示。 if (fieldProviderAccessId) { const access = accesses.find((e) => e.id === fieldProviderAccessId); const providers = Array.from(acmeDns01ProvidersMap.values()).filter((e) => e.provider === access?.provider); setShowProvider(providers.length > 1); } else { setShowProvider(false); } }, [accesses, fieldProviderAccessId]); const [showCAProviderAccess, setShowCAProviderAccess] = useState(false); useEffect(() => { // 内置的 CA 提供商(如 Let's Encrypt)无需显示授权信息字段 if (fieldCAProvider) { const provider = caProvidersMap.get(fieldCAProvider); setShowCAProviderAccess(!provider?.builtin); } else { setShowCAProviderAccess(false); } }, [fieldCAProvider]); 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 ACME_DNS01_PROVIDERS.AWS: case ACME_DNS01_PROVIDERS.AWS_ROUTE53: return ; case ACME_DNS01_PROVIDERS.HUAWEICLOUD: case ACME_DNS01_PROVIDERS.HUAWEICLOUD_DNS: return ; case ACME_DNS01_PROVIDERS.JDCLOUD: case ACME_DNS01_PROVIDERS.JDCLOUD_DNS: return ; case ACME_DNS01_PROVIDERS.TENCENTCLOUD_EO: return ; } }, [disabled, initialValues?.providerConfig, fieldProvider, nestedFormInst, nestedFormName]); const handleProviderSelect = (value: string) => { if (fieldProvider === value) return; // 切换 DNS 提供商时联动授权信息 if (initialValues?.provider === value) { formInst.setFieldValue("providerAccessId", initialValues?.providerAccessId); onValuesChange?.(formInst.getFieldsValue(true)); } else { if (acmeDns01ProvidersMap.get(fieldProvider)?.provider !== acmeDns01ProvidersMap.get(value)?.provider) { formInst.setFieldValue("providerAccessId", undefined); onValuesChange?.(formInst.getFieldsValue(true)); } } }; const handleProviderAccessSelect = (value: string) => { // 切换授权信息时联动 DNS 提供商 const access = accesses.find((access) => access.id === value); const provider = Array.from(acmeDns01ProvidersMap.values()).find((provider) => provider.provider === access?.provider); if (fieldProvider !== provider?.type) { formInst.setFieldValue("provider", provider?.type); onValuesChange?.(formInst.getFieldsValue(true)); } }; const handleCAProviderSelect = (value?: string | undefined) => { // 切换 CA 提供商时联动授权信息 if (value === "") { setTimeout(() => { formInst.setFieldValue("caProvider", undefined); formInst.setFieldValue("caProviderAccessId", undefined); onValuesChange?.(formInst.getFieldsValue(true)); }, 1); } else if (initialValues?.caProvider === value) { formInst.setFieldValue("caProviderAccessId", initialValues?.caProviderAccessId); onValuesChange?.(formInst.getFieldsValue(true)); } else { if (caProvidersMap.get(fieldCAProvider)?.provider !== caProvidersMap.get(value!)?.provider) { formInst.setFieldValue("caProviderAccessId", undefined); 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); }} /> } >
{t("workflow_node.apply.form.advanced_config.label")}
} > { formInst.setFieldValue("nameservers", e.target.value); }} onClear={() => { formInst.setFieldValue("nameservers", undefined); }} /> } onChange={(value) => { formInst.setFieldValue("nameservers", value); }} /> } > } > } > } > } >
{t("workflow_node.apply.form.strategy_config.label")}
} >
{t("workflow_node.apply.form.skip_before_expiry_days.prefix")}
{t("workflow_node.apply.form.skip_before_expiry_days.suffix")}
); } ); 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);