feat: support configuring independent ca in workflows

This commit is contained in:
Fu Diwei
2025-03-30 13:09:18 +08:00
parent deb3b2f412
commit 6ad0d8e42f
23 changed files with 496 additions and 186 deletions

View File

@@ -0,0 +1,83 @@
import { memo, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Avatar, Select, type SelectProps, Space, Typography } from "antd";
import { type ApplyCAProvider, applyCAProvidersMap } from "@/domain/provider";
export type CAProviderSelectProps = Omit<
SelectProps,
"filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"
> & {
filter?: (record: ApplyCAProvider) => boolean;
};
const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => {
const { t } = useTranslation();
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: ApplyCAProvider }>>([]);
useEffect(() => {
const allItems = Array.from(applyCAProvidersMap.values());
const filteredItems = filter != null ? allItems.filter(filter) : allItems;
setOptions([
{
key: "",
value: "",
label: "provider.default_ca_provider.label",
data: {} as ApplyCAProvider,
},
...filteredItems.map((item) => ({
key: item.type,
value: item.type,
label: t(item.name),
data: item,
})),
]);
}, [filter]);
const renderOption = (key: string) => {
if (key === "") {
return (
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
<Typography.Text className="italic leading-loose" type="secondary" ellipsis italic>
{t("provider.default_ca_provider.label")}
</Typography.Text>
</Space>
);
}
const provider = applyCAProvidersMap.get(key);
return (
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
<Avatar src={provider?.icon} size="small" />
<Typography.Text className="leading-loose" ellipsis>
{t(provider?.name ?? "")}
</Typography.Text>
</Space>
);
};
return (
<Select
{...props}
filterOption={(inputValue, option) => {
if (!option) return false;
const value = inputValue.toLowerCase();
return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value);
}}
labelRender={({ label, value }) => {
if (!label) {
return <Typography.Text type="secondary">{props.placeholder || t("provider.default_ca_provider.label")}</Typography.Text>;
}
return renderOption(value as string);
}}
options={options}
optionFilterProp={undefined}
optionLabelProp={undefined}
optionRender={(option) => renderOption(option.data.value)}
/>
);
};
export default memo(CAProviderSelect);

View File

@@ -5,7 +5,7 @@ import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Typography }
import Show from "@/components/Show";
import { applyDNSProvidersMap } from "@/domain/provider";
export type ApplyDNSProviderPickerProps = {
export type DNSProviderPickerProps = {
className?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
@@ -13,7 +13,7 @@ export type ApplyDNSProviderPickerProps = {
onSelect?: (value: string) => void;
};
const ApplyDNSProviderPicker = ({ className, style, autoFocus, placeholder, onSelect }: ApplyDNSProviderPickerProps) => {
const DNSProviderPicker = ({ className, style, autoFocus, placeholder, onSelect }: DNSProviderPickerProps) => {
const { t } = useTranslation();
const [keyword, setKeyword] = useState<string>();
@@ -71,4 +71,4 @@ const ApplyDNSProviderPicker = ({ className, style, autoFocus, placeholder, onSe
);
};
export default memo(ApplyDNSProviderPicker);
export default memo(DNSProviderPicker);

View File

@@ -4,14 +4,14 @@ import { Avatar, Select, type SelectProps, Space, Typography } from "antd";
import { type ApplyDNSProvider, applyDNSProvidersMap } from "@/domain/provider";
export type ApplyDNSProviderSelectProps = Omit<
export type DNSProviderSelectProps = Omit<
SelectProps,
"filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"
> & {
filter?: (record: ApplyDNSProvider) => boolean;
};
const ApplyDNSProviderSelect = ({ filter, ...props }: ApplyDNSProviderSelectProps) => {
const DNSProviderSelect = ({ filter, ...props }: DNSProviderSelectProps) => {
const { t } = useTranslation();
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: ApplyDNSProvider }>>([]);
@@ -64,4 +64,4 @@ const ApplyDNSProviderSelect = ({ filter, ...props }: ApplyDNSProviderSelectProp
);
};
export default memo(ApplyDNSProviderSelect);
export default memo(DNSProviderSelect);

View File

@@ -5,7 +5,7 @@ import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Tabs, Toolti
import Show from "@/components/Show";
import { DEPLOY_CATEGORIES, deployProvidersMap } from "@/domain/provider";
export type DeployProviderPickerProps = {
export type HostingProviderPickerProps = {
className?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
@@ -13,7 +13,7 @@ export type DeployProviderPickerProps = {
onSelect?: (value: string) => void;
};
const DeployProviderPicker = ({ className, style, autoFocus, placeholder, onSelect }: DeployProviderPickerProps) => {
const HostingProviderPicker = ({ className, style, autoFocus, placeholder, onSelect }: HostingProviderPickerProps) => {
const { t } = useTranslation();
const [keyword, setKeyword] = useState<string>();
@@ -110,4 +110,4 @@ const DeployProviderPicker = ({ className, style, autoFocus, placeholder, onSele
);
};
export default memo(DeployProviderPicker);
export default memo(HostingProviderPicker);

View File

@@ -4,14 +4,14 @@ import { Avatar, Select, type SelectProps, Space, Typography } from "antd";
import { type DeployProvider, deployProvidersMap } from "@/domain/provider";
export type DeployProviderSelectProps = Omit<
export type HostingProviderSelectProps = Omit<
SelectProps,
"filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"
> & {
filter?: (record: DeployProvider) => boolean;
};
const DeployProviderSelect = ({ filter, ...props }: DeployProviderSelectProps) => {
const HostingProviderSelect = ({ filter, ...props }: HostingProviderSelectProps) => {
const { t } = useTranslation();
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: DeployProvider }>>([]);
@@ -64,4 +64,4 @@ const DeployProviderSelect = ({ filter, ...props }: DeployProviderSelectProps) =
);
};
export default memo(DeployProviderSelect);
export default memo(HostingProviderSelect);

View File

@@ -1,6 +1,12 @@
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 { 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,
@@ -25,8 +31,9 @@ import AccessEditModal from "@/components/access/AccessEditModal";
import AccessSelect from "@/components/access/AccessSelect";
import ModalForm from "@/components/ModalForm";
import MultipleInput from "@/components/MultipleInput";
import ApplyDNSProviderSelect from "@/components/provider/ApplyDNSProviderSelect";
import { ACCESS_USAGES, APPLY_DNS_PROVIDERS, accessProvidersMap, applyDNSProvidersMap } from "@/domain/provider";
import CAProviderSelect from "@/components/provider/CAProviderSelect";
import DNSProviderSelect from "@/components/provider/DNSProviderSelect";
import { ACCESS_USAGES, APPLY_DNS_PROVIDERS, accessProvidersMap, applyCAProvidersMap, applyDNSProvidersMap } from "@/domain/provider";
import { type WorkflowNodeConfigForApply } from "@/domain/workflow";
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
import { useAccessesStore } from "@/stores/access";
@@ -60,7 +67,7 @@ const initFormModel = (): ApplyNodeConfigFormFieldValues => {
return {
challengeType: "dns-01",
keyAlgorithm: "RSA2048",
skipBeforeExpiryDays: 20,
skipBeforeExpiryDays: 30,
};
};
@@ -83,7 +90,16 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
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(),
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;
return !!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")),
@@ -121,9 +137,10 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
initialValues: initialValues ?? initFormModel(),
});
const fieldDomains = Form.useWatch<string>("domains", formInst);
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 fieldCAProvider = Form.useWatch<string>("caProvider", formInst);
const fieldNameservers = Form.useWatch<string>("nameservers", formInst);
const [showProvider, setShowProvider] = useState(false);
@@ -139,6 +156,15 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
}
}, [accesses, fieldProviderAccessId]);
const [showCAProviderAccess, setShowCAProviderAccess] = useState(false);
useEffect(() => {
if (fieldCAProvider) {
setShowCAProviderAccess(true);
} else {
setShowCAProviderAccess(false);
}
}, [fieldCAProvider]);
const [nestedFormInst] = Form.useForm();
const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeApplyConfigFormProviderConfigForm" });
const nestedFormEl = useMemo(() => {
@@ -195,6 +221,27 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
}
};
const handleCAProviderSelect = (value?: string | undefined) => {
if (fieldCAProvider === value) return;
// 切换 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 (applyCAProvidersMap.get(fieldCAProvider)?.provider !== applyCAProvidersMap.get(value!)?.provider) {
formInst.setFieldValue("caProviderAccessId", undefined);
onValuesChange?.(formInst.getFieldsValue(true));
}
}
};
const handleFormProviderChange = (name: string) => {
if (name === nestedFormName) {
formInst.setFieldValue("providerConfig", nestedFormInst.getFieldsValue());
@@ -273,7 +320,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
</Form.Item>
<Form.Item name="provider" label={t("workflow_node.apply.form.provider.label")} hidden={!showProvider} rules={[formRule]}>
<ApplyDNSProviderSelect
<DNSProviderSelect
disabled={!showProvider}
filter={(record) => {
if (fieldProviderAccessId) {
@@ -304,13 +351,13 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
preset="add"
trigger={
<Button size="small" type="link">
<PlusOutlinedIcon />
{t("workflow_node.apply.form.provider_access.button")}
<PlusOutlinedIcon className="text-xs" />
</Button>
}
afterSubmit={(record) => {
const provider = accessProvidersMap.get(record.provider);
if (provider?.usages?.includes(ACCESS_USAGES.APPLY)) {
if (provider?.usages?.includes(ACCESS_USAGES.DNS)) {
formInst.setFieldValue("providerAccessId", record.id);
}
}}
@@ -322,7 +369,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
<AccessSelect
filter={(record) => {
const provider = accessProvidersMap.get(record.provider);
return !!provider?.usages?.includes(ACCESS_USAGES.APPLY);
return !!provider?.usages?.includes(ACCESS_USAGES.DNS);
}}
placeholder={t("workflow_node.apply.form.provider_access.placeholder")}
onChange={handleProviderAccessSelect}
@@ -340,6 +387,71 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
</Divider>
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
<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">{t("workflow_node.apply.form.ca_provider.label")}</div>
<div className="text-right">
<Link className="ant-typography" to="/settings/ssl-provider" target="_blank">
<Button size="small" type="link">
{t("workflow_node.apply.form.ca_provider.button")}
<RightOutlinedIcon className="text-xs" />
</Button>
</Link>
</div>
</div>
</label>
<Form.Item name="caProvider" rules={[formRule]}>
<CAProviderSelect
allowClear
placeholder={t("workflow_node.apply.form.ca_provider.placeholder")}
showSearch
onSelect={handleCAProviderSelect}
onClear={handleCAProviderSelect}
/>
</Form.Item>
</Form.Item>
<Form.Item className="mb-0" hidden={!showCAProviderAccess}>
<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.ca_provider_access.label")}</span>
</div>
<div className="text-right">
<AccessEditModal
preset="add"
trigger={
<Button size="small" type="link">
{t("workflow_node.apply.form.ca_provider_access.button")}
<PlusOutlinedIcon className="text-xs" />
</Button>
}
afterSubmit={(record) => {
const provider = accessProvidersMap.get(record.provider);
if (provider?.usages?.includes(ACCESS_USAGES.CA)) {
formInst.setFieldValue("caProviderAccessId", record.id);
}
}}
/>
</div>
</div>
</label>
<Form.Item name="caProviderAccessId" rules={[formRule]}>
<AccessSelect
filter={(record) => {
if (fieldCAProvider) {
return applyCAProvidersMap.get(fieldCAProvider)?.provider === record.provider;
}
const provider = accessProvidersMap.get(record.provider);
return !!provider?.usages?.includes(ACCESS_USAGES.CA);
}}
placeholder={t("workflow_node.apply.form.ca_provider_access.placeholder")}
/>
</Form.Item>
</Form.Item>
<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) => ({
@@ -364,6 +476,9 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
onChange={(e) => {
formInst.setFieldValue("nameservers", e.target.value);
}}
onClear={() => {
formInst.setFieldValue("nameservers", undefined);
}}
/>
</Form.Item>
<NameserversModalInput

View File

@@ -7,8 +7,8 @@ import { z } from "zod";
import AccessEditModal from "@/components/access/AccessEditModal";
import AccessSelect from "@/components/access/AccessSelect";
import DeployProviderPicker from "@/components/provider/DeployProviderPicker";
import DeployProviderSelect from "@/components/provider/DeployProviderSelect";
import HostingProviderPicker from "@/components/provider/HostingProviderPicker.tsx";
import HostingProviderSelect from "@/components/provider/HostingProviderSelect.tsx";
import Show from "@/components/Show";
import { ACCESS_USAGES, DEPLOY_PROVIDERS, accessProvidersMap, deployProvidersMap } from "@/domain/provider";
import { type WorkflowNode, type WorkflowNodeConfigForDeploy } from "@/domain/workflow";
@@ -292,7 +292,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
onValuesChange?.(formInst.getFieldsValue(true));
};
const handleProviderSelect = (value: string) => {
const handleProviderSelect = (value?: string | undefined) => {
if (fieldProvider === value) return;
// 切换部署目标时重置表单,避免其他部署目标的配置字段影响当前部署目标
@@ -310,7 +310,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
}
formInst.setFieldsValue(newValues);
if (deployProvidersMap.get(fieldProvider)?.provider !== deployProvidersMap.get(value)?.provider) {
if (deployProvidersMap.get(fieldProvider)?.provider !== deployProvidersMap.get(value!)?.provider) {
formInst.setFieldValue("providerAccessId", undefined);
onValuesChange?.(formInst.getFieldsValue(true));
}
@@ -355,15 +355,16 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
<Show
when={!!fieldProvider}
fallback={<DeployProviderPicker autoFocus placeholder={t("workflow_node.deploy.search.provider.placeholder")} onSelect={handleProviderPick} />}
fallback={<HostingProviderPicker autoFocus placeholder={t("workflow_node.deploy.search.provider.placeholder")} onSelect={handleProviderPick} />}
>
<Form.Item name="provider" label={t("workflow_node.deploy.form.provider.label")} rules={[formRule]}>
<DeployProviderSelect
<HostingProviderSelect
allowClear
disabled={!!initialValues?.provider}
placeholder={t("workflow_node.deploy.form.provider.placeholder")}
showSearch
onSelect={handleProviderSelect}
onClear={handleProviderSelect}
/>
</Form.Item>
@@ -384,13 +385,13 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
preset="add"
trigger={
<Button size="small" type="link">
<PlusOutlinedIcon />
{t("workflow_node.deploy.form.provider_access.button")}
<PlusOutlinedIcon className="text-xs" />
</Button>
}
afterSubmit={(record) => {
const provider = accessProvidersMap.get(record.provider);
if (provider?.usages?.includes(ACCESS_USAGES.DEPLOY)) {
if (provider?.usages?.includes(ACCESS_USAGES.HOSTING)) {
formInst.setFieldValue("providerAccessId", record.id);
}
}}
@@ -406,7 +407,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
}
const provider = accessProvidersMap.get(record.provider);
return !!provider?.usages?.includes(ACCESS_USAGES.DEPLOY);
return !!provider?.usages?.includes(ACCESS_USAGES.HOSTING);
}}
placeholder={t("workflow_node.deploy.form.provider_access.placeholder")}
/>

View File

@@ -100,6 +100,9 @@ const DeployNodeConfigFormAliyunCASDeployConfig = ({
onChange={(e) => {
formInst.setFieldValue("resourceIds", e.target.value);
}}
onClear={() => {
formInst.setFieldValue("resourceIds", "");
}}
/>
</Form.Item>
<ResourceIdsModalInput
@@ -130,6 +133,9 @@ const DeployNodeConfigFormAliyunCASDeployConfig = ({
onChange={(e) => {
formInst.setFieldValue("contactIds", e.target.value);
}}
onClear={() => {
formInst.setFieldValue("contactIds", "");
}}
/>
</Form.Item>
<ContactIdsModalInput

View File

@@ -123,6 +123,9 @@ const DeployNodeConfigFormBaotaPanelSiteConfig = ({
onChange={(e) => {
formInst.setFieldValue("siteNames", e.target.value);
}}
onClear={() => {
formInst.setFieldValue("siteNames", "");
}}
/>
</Form.Item>
<SiteNamesModalInput

View File

@@ -107,6 +107,9 @@ const DeployNodeConfigFormTencentCloudSSLDeployConfig = ({
onChange={(e) => {
formInst.setFieldValue("resourceIds", e.target.value);
}}
onClear={() => {
formInst.setFieldValue("resourceIds", "");
}}
/>
</Form.Item>
<ResourceIdsModalInput