diff --git a/ui/src/components/provider/ACMEDns01ProviderSelect.tsx b/ui/src/components/provider/ACMEDns01ProviderSelect.tsx index 4d137e484..0f5179c31 100644 --- a/ui/src/components/provider/ACMEDns01ProviderSelect.tsx +++ b/ui/src/components/provider/ACMEDns01ProviderSelect.tsx @@ -4,31 +4,44 @@ import { Avatar, Select, Typography, theme } from "antd"; import { type ACMEDns01Provider, acmeDns01ProvidersMap } from "@/domain/provider"; -import { type SharedSelectProps } from "./_shared"; +import { type SharedSelectProps, useSelectDataSource } from "./_shared"; -export interface ACMEDns01ProviderSelectProps extends SharedSelectProps {} +export interface ACMEDns01ProviderSelectProps extends SharedSelectProps { + showAvailability?: boolean; +} -const ACMEDns01ProviderSelect = ({ onFilter, ...props }: ACMEDns01ProviderSelectProps) => { +const ACMEDns01ProviderSelect = ({ showAvailability, onFilter, ...props }: ACMEDns01ProviderSelectProps) => { const { t } = useTranslation(); const { token: themeToken } = theme.useToken(); - const options = useMemo>(() => { - return Array.from(acmeDns01ProvidersMap.values()) - .filter((provider) => { - if (onFilter) { - return onFilter(provider.type, provider); - } - - return true; - }) - .map((provider) => ({ + const dataSources = useSelectDataSource({ + dataSource: Array.from(acmeDns01ProvidersMap.values()), + filters: [onFilter!], + }); + const options = useMemo(() => { + const convert = (providers: ACMEDns01Provider[]): Array<{ key: string; value: string; label: string; data: ACMEDns01Provider }> => { + return providers.map((provider) => ({ key: provider.type, value: provider.type, label: t(provider.name), data: provider, })); - }, [onFilter]); + }; + + return showAvailability + ? [ + { + label: t("provider.text.available_group"), + options: convert(dataSources.available), + }, + { + label: t("provider.text.unavailable_group"), + options: convert(dataSources.unavailable), + }, + ].filter((group) => group.options.length > 0) + : convert(dataSources.filtered); + }, [showAvailability, dataSources]); const renderOption = (key: string) => { const provider = acmeDns01ProvidersMap.get(key); @@ -45,9 +58,11 @@ const ACMEDns01ProviderSelect = ({ onFilter, ...props }: ACMEDns01ProviderSelect {...props} filterOption={(inputValue, option) => { if (!option) return false; + if (!option.label) return false; + if (!option.value) return false; const value = inputValue.toLowerCase(); - return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); + return String(option.value).toLowerCase().includes(value) || String(option.label).toLowerCase().includes(value); }} labelRender={({ value }) => { if (value != null) { @@ -59,7 +74,7 @@ const ACMEDns01ProviderSelect = ({ onFilter, ...props }: ACMEDns01ProviderSelect options={options} optionFilterProp={void 0} optionLabelProp={void 0} - optionRender={(option) => renderOption(option.data.value)} + optionRender={(option) => renderOption(option.data.value as string)} /> ); }; diff --git a/ui/src/components/provider/ACMEHttp01ProviderSelect.tsx b/ui/src/components/provider/ACMEHttp01ProviderSelect.tsx new file mode 100644 index 000000000..a91c8c5a2 --- /dev/null +++ b/ui/src/components/provider/ACMEHttp01ProviderSelect.tsx @@ -0,0 +1,82 @@ +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Avatar, Select, Typography, theme } from "antd"; + +import { type ACMEHttp01Provider, acmeHttp01ProvidersMap } from "@/domain/provider"; + +import { type SharedSelectProps, useSelectDataSource } from "./_shared"; + +export interface ACMEHttp01ProviderSelectProps extends SharedSelectProps { + showAvailability?: boolean; +} + +const ACMEHttp01ProviderSelect = ({ showAvailability, onFilter, ...props }: ACMEHttp01ProviderSelectProps) => { + const { t } = useTranslation(); + + const { token: themeToken } = theme.useToken(); + + const dataSources = useSelectDataSource({ + dataSource: Array.from(acmeHttp01ProvidersMap.values()), + filters: [onFilter!], + }); + const options = useMemo(() => { + const convert = (providers: ACMEHttp01Provider[]): Array<{ key: string; value: string; label: string; data: ACMEHttp01Provider }> => { + return providers.map((provider) => ({ + key: provider.type, + value: provider.type, + label: t(provider.name), + data: provider, + })); + }; + + return showAvailability + ? [ + { + label: t("provider.text.available_group"), + options: convert(dataSources.available), + }, + { + label: t("provider.text.unavailable_group"), + options: convert(dataSources.unavailable), + }, + ].filter((group) => group.options.length > 0) + : convert(dataSources.filtered); + }, [showAvailability, dataSources]); + + const renderOption = (key: string) => { + const provider = acmeHttp01ProvidersMap.get(key); + return ( +
+ + {t(provider?.name ?? "")} +
+ ); + }; + + return ( + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + webRootPath: "/var/www/html/", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { + const { t } = i18n; + + return z.object({ + webRootPath: z.string().nonempty(t("workflow_node.apply.form.local_webroot_path.placeholder")), + }); +}; + +const _default = Object.assign(BizApplyNodeConfigFieldsProviderLocal, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderSSH.tsx b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderSSH.tsx new file mode 100644 index 000000000..cad4fa024 --- /dev/null +++ b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigFieldsProviderSSH.tsx @@ -0,0 +1,52 @@ +import { getI18n, useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { useFormNestedFieldsContext } from "./_context"; + +const BizApplyNodeConfigFieldsProviderSSH = () => { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const initialValues = getInitialValues(); + + return ( + <> + } + > + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + webRootPath: "/var/www/html/", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { + const { t } = i18n; + + return z.object({ + webRootPath: z.string().nonempty(t("workflow_node.apply.form.ssh_webroot_path.placeholder")), + }); +}; + +const _default = Object.assign(BizApplyNodeConfigFieldsProviderSSH, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx index 888bf7635..5ab9cb563 100644 --- a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx +++ b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx @@ -27,9 +27,11 @@ import AccessEditDrawer from "@/components/access/AccessEditDrawer"; import AccessSelect from "@/components/access/AccessSelect"; import MultipleSplitValueInput from "@/components/MultipleSplitValueInput"; import ACMEDns01ProviderSelect from "@/components/provider/ACMEDns01ProviderSelect"; +import ACMEHttp01ProviderSelect from "@/components/provider/ACMEHttp01ProviderSelect"; 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 AccessModel } from "@/domain/access"; +import { ACME_DNS01_PROVIDERS, ACME_HTTP01_PROVIDERS, acmeDns01ProvidersMap, acmeHttp01ProvidersMap, caProvidersMap } from "@/domain/provider"; import { type WorkflowNodeConfigForBizApply, defaultNodeConfigForBizApply } from "@/domain/workflow"; import { useAntdForm, useZustandShallowSelector } from "@/hooks"; import { useAccessesStore } from "@/stores/access"; @@ -41,6 +43,8 @@ import BizApplyNodeConfigFieldsProviderAliyunESA from "./BizApplyNodeConfigField import BizApplyNodeConfigFieldsProviderAWSRoute53 from "./BizApplyNodeConfigFieldsProviderAWSRoute53"; import BizApplyNodeConfigFieldsProviderHuaweiCloudDNS from "./BizApplyNodeConfigFieldsProviderHuaweiCloudDNS"; import BizApplyNodeConfigFieldsProviderJDCloudDNS from "./BizApplyNodeConfigFieldsProviderJDCloudDNS"; +import BizApplyNodeConfigFieldsProviderLocal from "./BizApplyNodeConfigFieldsProviderLocal"; +import BizApplyNodeConfigFieldsProviderSSH from "./BizApplyNodeConfigFieldsProviderSSH"; import BizApplyNodeConfigFieldsProviderTencentCloudEO from "./BizApplyNodeConfigFieldsProviderTencentCloudEO"; import { NodeType } from "../nodes/typings"; @@ -62,6 +66,16 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps) const { i18n, t } = useTranslation(); const { accesses } = useAccessesStore(useZustandShallowSelector("accesses")); + const accessOptionFilter = (_: string, option: AccessModel) => { + if (option.reserve) return false; + if (fieldChallengeType === CHALLENGE_TYPE_DNS01) return acmeDns01ProvidersMap.get(fieldProvider)?.provider === option.provider; + if (fieldChallengeType === CHALLENGE_TYPE_HTTP01) return acmeHttp01ProvidersMap.get(fieldProvider)?.provider === option.provider; + return false; + }; + const accessOptionFilterForCA = (_: string, option: AccessModel) => { + if (option.reserve !== "ca") return false; + return caProvidersMap.get(fieldCAProvider)?.provider === option.provider; + }; const initialValues = useMemo(() => { return getNodeForm(node)?.getValueIn("config") as WorkflowNodeConfigForBizApply | undefined; @@ -79,46 +93,85 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps) const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true }); const fieldProviderAccessId = Form.useWatch("providerAccessId", { form: formInst, preserve: true }); const fieldCAProvider = Form.useWatch("caProvider", { form: formInst, preserve: true }); + const fieldCAProviderAccessId = Form.useWatch("caProviderAccessId", { form: formInst, preserve: true }); const NestedProviderConfigFields = useMemo(() => { /* 注意:如果追加新的子组件,请保持以 ASCII 排序。 NOTICE: If you add new child component, please keep ASCII order. */ - switch (fieldProvider) { - case ACME_DNS01_PROVIDERS.ALIYUN_ESA: { - return BizApplyNodeConfigFieldsProviderAliyunESA; - } - case ACME_DNS01_PROVIDERS.AWS: - case ACME_DNS01_PROVIDERS.AWS_ROUTE53: { - return BizApplyNodeConfigFieldsProviderAWSRoute53; - } - case ACME_DNS01_PROVIDERS.HUAWEICLOUD: - case ACME_DNS01_PROVIDERS.HUAWEICLOUD_DNS: { - return BizApplyNodeConfigFieldsProviderHuaweiCloudDNS; - } - case ACME_DNS01_PROVIDERS.JDCLOUD: - case ACME_DNS01_PROVIDERS.JDCLOUD_DNS: { - return BizApplyNodeConfigFieldsProviderJDCloudDNS; - } - case ACME_DNS01_PROVIDERS.TENCENTCLOUD_EO: { - return BizApplyNodeConfigFieldsProviderTencentCloudEO; - } - } - }, [fieldProvider]); + switch (fieldChallengeType) { + case CHALLENGE_TYPE_DNS01: + { + switch (fieldProvider) { + case ACME_DNS01_PROVIDERS.ALIYUN_ESA: { + return BizApplyNodeConfigFieldsProviderAliyunESA; + } + case ACME_DNS01_PROVIDERS.AWS: + case ACME_DNS01_PROVIDERS.AWS_ROUTE53: { + return BizApplyNodeConfigFieldsProviderAWSRoute53; + } + case ACME_DNS01_PROVIDERS.HUAWEICLOUD: + case ACME_DNS01_PROVIDERS.HUAWEICLOUD_DNS: { + return BizApplyNodeConfigFieldsProviderHuaweiCloudDNS; + } + case ACME_DNS01_PROVIDERS.JDCLOUD: + case ACME_DNS01_PROVIDERS.JDCLOUD_DNS: { + return BizApplyNodeConfigFieldsProviderJDCloudDNS; + } + case ACME_DNS01_PROVIDERS.TENCENTCLOUD_EO: { + return BizApplyNodeConfigFieldsProviderTencentCloudEO; + } + } + } + break; - const [showProvider, setShowProvider] = useState(false); - useEffect(() => { - // 通常情况下每个授权信息只对应一个 DNS 提供商,此时无需显示 DNS 提供商字段; - // 如果对应多个(如 AWS 的 Route53、Lightsail,阿里云的 DNS、ESA,腾讯云的 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); + case CHALLENGE_TYPE_HTTP01: + switch (fieldProvider) { + case ACME_HTTP01_PROVIDERS.LOCAL: { + return BizApplyNodeConfigFieldsProviderLocal; + } + case ACME_HTTP01_PROVIDERS.SSH: { + return BizApplyNodeConfigFieldsProviderSSH; + } + } + break; } - }, [accesses, fieldProviderAccessId]); + }, [fieldChallengeType, fieldProvider]); + + const [showProviderAccess, setShowProviderAccess] = useState(false); + useEffect(() => { + // 内置的质询提供商(如本地主机)无需显示授权信息字段 + switch (fieldChallengeType) { + case CHALLENGE_TYPE_DNS01: + { + if (fieldProvider) { + const provider = acmeDns01ProvidersMap.get(fieldProvider); + setShowProviderAccess(!provider?.builtin); + } else { + setShowProviderAccess(false); + } + } + break; + + case CHALLENGE_TYPE_HTTP01: + { + if (fieldProvider) { + const provider = acmeHttp01ProvidersMap.get(fieldProvider); + setShowProviderAccess(!provider?.builtin); + } else { + setShowProviderAccess(false); + } + } + break; + + default: + { + setShowProviderAccess(false); + } + break; + } + }, [fieldChallengeType, fieldProvider]); const [showCAProviderAccess, setShowCAProviderAccess] = useState(false); useEffect(() => { @@ -131,7 +184,55 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps) } }, [fieldCAProvider]); + useEffect(() => { + // 如果未选择质询提供商,则清空授权信息 + if (!fieldProvider && fieldProviderAccessId) { + formInst.setFieldValue("providerAccessId", void 0); + return; + } + + // 如果已选择质询提供商只有一个授权信息,则自动选择该授权信息 + if (fieldProvider && !fieldProviderAccessId) { + const availableAccesses = accesses + .filter((access) => accessOptionFilter(access.provider, access)) + .filter((access) => { + if (fieldChallengeType === CHALLENGE_TYPE_DNS01) return acmeDns01ProvidersMap.get(fieldProvider)?.provider === access.provider; + if (fieldChallengeType === CHALLENGE_TYPE_HTTP01) return acmeHttp01ProvidersMap.get(fieldProvider)?.provider === access.provider; + return false; + }); + if (availableAccesses.length === 1) { + formInst.setFieldValue("providerAccessId", availableAccesses[0].id); + } + } + }, [fieldChallengeType, fieldProvider, fieldProviderAccessId]); + + useEffect(() => { + // 如果未选择 CA 提供商,则清空授权信息 + if (!fieldCAProvider && fieldCAProviderAccessId) { + formInst.setFieldValue("caProviderAccessId", void 0); + return; + } + + // 如果已选择 CA 提供商只有一个授权信息,则自动选择该授权信息 + if (fieldCAProvider && !fieldCAProviderAccessId) { + const availableAccesses = accesses + .filter((access) => accessOptionFilterForCA(access.provider, access)) + .filter((access) => caProvidersMap.get(fieldCAProvider)?.provider === access.provider); + if (availableAccesses.length === 1) { + formInst.setFieldValue("caProviderAccessId", availableAccesses[0].id); + } + } + }, [fieldCAProvider, fieldCAProviderAccessId]); + const handleChallengeTypeChange = (value: string) => { + const resetFieldIfInvalid = (field: keyof z.infer) => { + const fieldSchame = formSchema.pick({ [field]: true }); + const fieldValue = formInst.getFieldValue(field); + if (!fieldSchame.safeParse({ [field]: fieldValue }).success) { + formInst.setFieldValue(field, void 0); + } + }; + switch (value) { case CHALLENGE_TYPE_DNS01: { @@ -146,33 +247,23 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps) formInst.setFieldValue("provider", void 0); formInst.setFieldValue("providerAccessId", void 0); formInst.setFieldValue("providerConfig", void 0); - formInst.setFieldValue("dnsPropagationWait", void 0); - formInst.setFieldValue("dnsPropagationTimeout", void 0); - formInst.setFieldValue("dnsTTL", void 0); + + resetFieldIfInvalid("dnsPropagationWait"); + resetFieldIfInvalid("dnsPropagationTimeout"); + resetFieldIfInvalid("dnsTTL"); } break; } }; - const handleProviderSelect = (value: string) => { - if (fieldProvider === value) return; - - // 切换 DNS 提供商时联动授权信息 + const handleProviderSelect = (value?: string | undefined) => { + // 切换质询提供商时重置表单,避免其他提供商的配置字段影响当前提供商 if (initialValues?.provider === value) { - formInst.setFieldValue("providerAccessId", initialValues?.providerAccessId); + formInst.setFieldValue("providerAccessId", void 0); + formInst.resetFields(["providerConfig"]); } else { - if (acmeDns01ProvidersMap.get(fieldProvider)?.provider !== acmeDns01ProvidersMap.get(value)?.provider) { - formInst.setFieldValue("providerAccessId", void 0); - } - } - }; - - 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); + formInst.setFieldValue("providerAccessId", void 0); + formInst.setFieldValue("providerConfig", void 0); } }; @@ -196,7 +287,21 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps)
- + + } + rules={[formRule]} + > +
- +
+ + + {t("workflow_node.apply.form_anchor.challenge.title")} + + + + } + > handleChallengeTypeChange(e.target.value)}> DNS-01 HTTP-01 -