feat: support overwriting the default config of notifiers

This commit is contained in:
Fu Diwei 2025-04-25 11:51:19 +08:00
parent 11b413d0dc
commit 3c2fbd720f
12 changed files with 288 additions and 25 deletions

View File

@ -86,7 +86,7 @@ const AccessFormEmailConfig = ({ form: formInst, formName, disabled, initialValu
onValuesChange={handleFormChange}
>
<div className="flex space-x-2">
<div className="w-2/5">
<div className="w-3/5">
<Form.Item name="smtpHost" label={t("access.form.email_smtp_host.label")} rules={[formRule]}>
<Input placeholder={t("access.form.email_smtp_host.placeholder")} />
</Form.Item>
@ -97,13 +97,11 @@ const AccessFormEmailConfig = ({ form: formInst, formName, disabled, initialValu
<InputNumber className="w-full" placeholder={t("access.form.email_smtp_port.placeholder")} min={1} max={65535} />
</Form.Item>
</div>
</div>
<div className="w-1/5">
<Form.Item name="smtpTls" label={t("access.form.email_smtp_tls.label")} rules={[formRule]}>
<Switch onChange={handleTlsSwitchChange} />
</Form.Item>
</div>
</div>
<Form.Item name="username" label={t("access.form.email_username.label")} rules={[formRule]}>
<Input autoComplete="new-password" placeholder={t("access.form.email_username.placeholder")} />

View File

@ -31,7 +31,7 @@ const AccessFormTelegramConfig = ({ form: formInst, formName, disabled, initialV
.max(256, t("common.errmsg.string_max", { max: 256 })),
defaultChatId: z
.preprocess(
(v) => Number(v),
(v) => (v == null || v === "" ? undefined : Number(v)),
z
.number()
.nullish()

View File

@ -34,6 +34,9 @@ export type NotifyChannelEditFormInstance = {
validateFields: FormInstance<NotifyChannelEditFormFieldValues>["validateFields"];
};
/**
* @deprecated
*/
const NotifyChannelEditForm = forwardRef<NotifyChannelEditFormInstance, NotifyChannelEditFormProps>(
({ className, style, channel, disabled, initialValues, onValuesChange }, ref) => {
const { form: formInst, formProps } = useAntdForm({

View File

@ -218,8 +218,6 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
};
const handleProviderAccessSelect = (value: string) => {
if (fieldProviderAccessId === value) return;
// 切换授权信息时联动 DNS 提供商
const access = accesses.find((access) => access.id === value);
const provider = Array.from(acmeDns01ProvidersMap.values()).find((provider) => provider.provider === access?.provider);
@ -230,8 +228,6 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
};
const handleCAProviderSelect = (value?: string | undefined) => {
if (fieldCAProvider === value) return;
// 切换 CA 提供商时联动授权信息
if (value === "") {
setTimeout(() => {
@ -368,6 +364,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
const provider = accessProvidersMap.get(record.provider);
if (provider?.usages?.includes(ACCESS_USAGES.DNS)) {
formInst.setFieldValue("providerAccessId", record.id);
handleProviderAccessSelect(record.id);
}
}}
/>

View File

@ -322,8 +322,6 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
};
const handleProviderSelect = (value?: string | undefined) => {
if (fieldProvider === value) return;
// 切换部署目标时重置表单,避免其他部署目标的配置字段影响当前部署目标
if (initialValues?.provider === value) {
formInst.resetFields();

View File

@ -1,4 +1,4 @@
import { forwardRef, memo, useEffect, useImperativeHandle, useState } from "react";
import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { PlusOutlined as PlusOutlinedIcon, RightOutlined as RightOutlinedIcon } from "@ant-design/icons";
@ -9,13 +9,17 @@ import { z } from "zod";
import AccessEditModal from "@/components/access/AccessEditModal";
import AccessSelect from "@/components/access/AccessSelect";
import NotificationProviderSelect from "@/components/provider/NotificationProviderSelect";
import { ACCESS_USAGES, accessProvidersMap, notificationProvidersMap } from "@/domain/provider";
import { ACCESS_USAGES, NOTIFICATION_PROVIDERS, accessProvidersMap, notificationProvidersMap } from "@/domain/provider";
import { notifyChannelsMap } from "@/domain/settings";
import { type WorkflowNodeConfigForNotify } from "@/domain/workflow";
import { useAntdForm, useZustandShallowSelector } from "@/hooks";
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
import { useAccessesStore } from "@/stores/access";
import { useNotifyChannelsStore } from "@/stores/notify";
import NotifyNodeConfigFormEmailConfig from "./NotifyNodeConfigFormEmailConfig";
import NotifyNodeConfigFormMattermostConfig from "./NotifyNodeConfigFormMattermostConfig";
import NotifyNodeConfigFormTelegramConfig from "./NotifyNodeConfigFormTelegramConfig";
type NotifyNodeConfigFormFieldValues = Partial<WorkflowNodeConfigForNotify>;
export type NotifyNodeConfigFormProps = {
@ -65,6 +69,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
providerAccessId: z
.string({ message: t("workflow_node.notify.form.provider_access.placeholder") })
.nonempty(t("workflow_node.notify.form.provider_access.placeholder")),
providerConfig: z.any().nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
const { form: formInst, formProps } = useAntdForm({
@ -88,9 +93,31 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
}
}, [accesses, fieldProviderAccessId]);
const handleProviderSelect = (value: string) => {
if (fieldProvider === value) return;
const [nestedFormInst] = Form.useForm();
const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeNotifyConfigFormProviderConfigForm" });
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 NOTIFICATION_PROVIDERS.EMAIL:
return <NotifyNodeConfigFormEmailConfig {...nestedFormProps} />;
case NOTIFICATION_PROVIDERS.MATTERMOST:
return <NotifyNodeConfigFormMattermostConfig {...nestedFormProps} />;
case NOTIFICATION_PROVIDERS.TELEGRAM:
return <NotifyNodeConfigFormTelegramConfig {...nestedFormProps} />;
}
}, [disabled, initialValues?.providerConfig, fieldProvider, nestedFormInst, nestedFormName]);
const handleProviderSelect = (value: string) => {
// 切换消息通知提供商时联动授权信息
if (initialValues?.provider === value) {
formInst.setFieldValue("providerAccessId", initialValues?.providerAccessId);
@ -104,8 +131,6 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
};
const handleProviderAccessSelect = (value: string) => {
if (fieldProviderAccessId === value) return;
// 切换授权信息时联动消息通知提供商
const access = accesses.find((access) => access.id === value);
const provider = Array.from(notificationProvidersMap.values()).find((provider) => provider.provider === access?.provider);
@ -122,13 +147,21 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
useImperativeHandle(ref, () => {
return {
getFieldsValue: () => {
return formInst.getFieldsValue(true);
const values = formInst.getFieldsValue(true);
values.providerConfig = nestedFormInst.getFieldsValue();
return values;
},
resetFields: (fields) => {
return formInst.resetFields(fields as (keyof NotifyNodeConfigFormFieldValues)[]);
formInst.resetFields(fields);
if (!!fields && fields.includes("providerConfig")) {
nestedFormInst.resetFields(fields);
}
},
validateFields: (nameList, config) => {
return formInst.validateFields(nameList, config);
const t1 = formInst.validateFields(nameList, config);
const t2 = nestedFormInst.validateFields(undefined, config);
return Promise.all([t1, t2]).then(() => t1);
},
} as NotifyNodeConfigFormInstance;
});
@ -207,6 +240,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
const provider = accessProvidersMap.get(record.provider);
if (provider?.usages?.includes(ACCESS_USAGES.NOTIFICATION)) {
formInst.setFieldValue("providerAccessId", record.id);
handleProviderAccessSelect(record.id);
}
}}
/>
@ -224,6 +258,8 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
/>
</Form.Item>
</Form.Item>
{nestedFormEl}
</Form>
);
}

View File

@ -0,0 +1,80 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validEmailAddress } from "@/utils/validators";
type NotifyNodeConfigFormEmailConfigFieldValues = Nullish<{
senderAddress?: string;
receiverAddress?: string;
}>;
export type NotifyNodeConfigFormEmailConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: NotifyNodeConfigFormEmailConfigFieldValues;
onValuesChange?: (values: NotifyNodeConfigFormEmailConfigFieldValues) => void;
};
const initFormModel = (): NotifyNodeConfigFormEmailConfigFieldValues => {
return {};
};
const NotifyNodeConfigFormEmailConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: NotifyNodeConfigFormEmailConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
senderAddress: z
.string()
.nullish()
.refine((v) => {
if (!v) return true;
return validEmailAddress(v);
}, t("common.errmsg.email_invalid")),
receiverAddress: z
.string()
.nullish()
.refine((v) => {
if (!v) return true;
return validEmailAddress(v);
}, t("common.errmsg.email_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="senderAddress"
label={t("workflow_node.notify.form.email_sender_address.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.notify.form.email_sender_address.tooltip") }}></span>}
>
<Input type="email" placeholder={t("workflow_node.notify.form.email_sender_address.placeholder")} />
</Form.Item>
<Form.Item
name="receiverAddress"
label={t("workflow_node.notify.form.email_receiver_address.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.notify.form.email_receiver_address.tooltip") }}></span>}
>
<Input type="email" placeholder={t("workflow_node.notify.form.email_receiver_address.placeholder")} />
</Form.Item>
</Form>
);
};
export default NotifyNodeConfigFormEmailConfig;

View File

@ -0,0 +1,61 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
type NotifyNodeConfigFormMattermostConfigFieldValues = Nullish<{
channelId?: string;
}>;
export type NotifyNodeConfigFormMattermostConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: NotifyNodeConfigFormMattermostConfigFieldValues;
onValuesChange?: (values: NotifyNodeConfigFormMattermostConfigFieldValues) => void;
};
const initFormModel = (): NotifyNodeConfigFormMattermostConfigFieldValues => {
return {};
};
const NotifyNodeConfigFormMattermostConfig = ({
form: formInst,
formName,
disabled,
initialValues,
onValuesChange,
}: NotifyNodeConfigFormMattermostConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
channelId: z.string().nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="channelId"
label={t("workflow_node.notify.form.mattermost_channel_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.notify.form.mattermost_channel_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.notify.form.mattermost_channel_id.placeholder")} />
</Form.Item>
</Form>
);
};
export default NotifyNodeConfigFormMattermostConfig;

View File

@ -0,0 +1,66 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
type NotifyNodeConfigFormTelegramConfigFieldValues = Nullish<{
chatId?: string | number;
}>;
export type NotifyNodeConfigFormTelegramConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: NotifyNodeConfigFormTelegramConfigFieldValues;
onValuesChange?: (values: NotifyNodeConfigFormTelegramConfigFieldValues) => void;
};
const initFormModel = (): NotifyNodeConfigFormTelegramConfigFieldValues => {
return {};
};
const NotifyNodeConfigFormTelegramConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: NotifyNodeConfigFormTelegramConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
chatId: z
.preprocess(
(v) => (v == null || v === "" ? undefined : Number(v)),
z
.number()
.nullish()
.refine((v) => {
if (v == null || v + "" === "") return true;
return /^\d+$/.test(v + "") && +v! > 0;
}, t("workflow_node.notify.form.telegram_chat_id.placeholder"))
)
.nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="chatId"
label={t("workflow_node.notify.form.telegram_chat_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.notify.form.telegram_chat_id.tooltip") }}></span>}
>
<Input type="number" placeholder={t("workflow_node.notify.form.telegram_chat_id.placeholder")} />
</Form.Item>
</Form>
);
};
export default NotifyNodeConfigFormTelegramConfig;

View File

@ -222,7 +222,7 @@
"access.form.mattermost_username.placeholder": "Please enter Mattermost username",
"access.form.mattermost_password.label": "Mattermost password",
"access.form.mattermost_password.placeholder": "Please enter Mattermost password",
"access.form.mattermost_default_channel_id.label": "Default Mattermost channel ID",
"access.form.mattermost_default_channel_id.label": "Default Mattermost channel ID (Optional)",
"access.form.mattermost_default_channel_id.placeholder": "Please enter default Mattermost channel ID",
"access.form.mattermost_default_channel_id.tooltip": "How to get the channel ID? Select the target channel from the left sidebar, click on the channel name at the top, and choose ”Channel Details.” You can directly see the channel ID on the pop-up page.",
"access.form.namecheap_username.label": "Namecheap username",

View File

@ -720,6 +720,18 @@
"workflow_node.notify.form.provider_access.label": "Notification provider authorization",
"workflow_node.notify.form.provider_access.placeholder": "Please select an authorization of notification provider",
"workflow_node.notify.form.provider_access.button": "Create",
"workflow_node.notify.form.email_sender_address.label": "Sender email address (Optional)",
"workflow_node.notify.form.email_sender_address.placeholder": "Please enter sender email address",
"workflow_node.notify.form.email_sender_address.tooltip": "Leave it blank to use the default sender email address provided by the authorization.",
"workflow_node.notify.form.email_receiver_address.label": "Receiver email address (Optional)",
"workflow_node.notify.form.email_receiver_address.placeholder": "Please enter receiver email address",
"workflow_node.notify.form.email_receiver_address.tooltip": "Leave it blank to use the default receiver email address provided by the selected authorization.",
"workflow_node.notify.form.mattermost_channel_id.label": "Mattermost channel ID (Optional)",
"workflow_node.notify.form.mattermost_channel_id.placeholder": "Please enter Mattermost channel ID",
"workflow_node.notify.form.mattermost_channel_id.tooltip": "Leave it blank to use the default channel ID provided by the authorization.",
"workflow_node.notify.form.telegram_chat_id.label": "Telegram chat ID (Optional)",
"workflow_node.notify.form.telegram_chat_id.placeholder": "Please enter Telegram chat ID",
"workflow_node.notify.form.telegram_chat_id.tooltip": "Leave it blank to use the default chat ID provided by the selected authorization.",
"workflow_node.end.label": "End",

View File

@ -719,6 +719,18 @@
"workflow_node.notify.form.provider_access.label": "通知渠道授权",
"workflow_node.notify.form.provider_access.placeholder": "请选择通知渠道授权",
"workflow_node.notify.form.provider_access.button": "新建",
"workflow_node.notify.form.email_sender_address.label": "发送邮箱地址(可选)",
"workflow_node.notify.form.email_sender_address.placeholder": "请输入发送邮箱地址",
"workflow_node.notify.form.email_sender_address.tooltip": "不填写时,将使用所选通知渠道授权的默认发送邮箱地址。",
"workflow_node.notify.form.email_receiver_address.label": "接收邮箱地址(可选)",
"workflow_node.notify.form.email_receiver_address.placeholder": "请输入接收邮箱地址",
"workflow_node.notify.form.email_receiver_address.tooltip": "不填写时,将使用所选通知渠道授权的默认接收邮箱地址。",
"workflow_node.notify.form.mattermost_channel_id.label": "Mattermost 频道 ID可选",
"workflow_node.notify.form.mattermost_channel_id.placeholder": "请输入 Mattermost 频道 ID",
"workflow_node.notify.form.mattermost_channel_id.tooltip": "不填写时,将使用所选通知渠道授权的默认频道 ID。",
"workflow_node.notify.form.telegram_chat_id.label": "Telegram 会话 ID可选",
"workflow_node.notify.form.telegram_chat_id.placeholder": "请输入 Telegram 会话 ID",
"workflow_node.notify.form.telegram_chat_id.tooltip": "不填写时,将使用所选通知渠道授权的默认会话 ID。",
"workflow_node.end.label": "结束",