diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index 33eb8779..0f58364c 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -250,7 +250,7 @@ type SSLProviderEab struct { } func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, error) { - record, _ := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='ssl-provider'") + record, _ := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='sslProvider'") sslProvider := &SSLProviderConfig{ Config: SSLProviderConfigContent{}, diff --git a/internal/certificate/service.go b/internal/certificate/service.go index 48c07928..8fdf060b 100644 --- a/internal/certificate/service.go +++ b/internal/certificate/service.go @@ -61,7 +61,7 @@ func buildMsg(records []domain.Certificate) *domain.NotifyMessage { // 查询模板信息 settingRepo := repository.NewSettingRepository() - setting, err := settingRepo.GetByName(context.Background(), "templates") + setting, err := settingRepo.GetByName(context.Background(), "notifyTemplates") subject := defaultExpireSubject message := defaultExpireMessage diff --git a/ui/src/components/access/AccessEditForm.tsx b/ui/src/components/access/AccessEditForm.tsx index b7dbdb8c..af10775e 100644 --- a/ui/src/components/access/AccessEditForm.tsx +++ b/ui/src/components/access/AccessEditForm.tsx @@ -50,7 +50,7 @@ const AccessEditForm = forwardRef(( const formSchema = z.object({ name: z - .string() + .string({ message: t("access.form.name.placeholder") }) .trim() .min(1, t("access.form.name.placeholder")) .max(64, t("common.errmsg.string_max", { max: 64 })), diff --git a/ui/src/components/access/AccessEditFormACMEHttpReqConfig.tsx b/ui/src/components/access/AccessEditFormACMEHttpReqConfig.tsx index a3d10c1c..d52e67c2 100644 --- a/ui/src/components/access/AccessEditFormACMEHttpReqConfig.tsx +++ b/ui/src/components/access/AccessEditFormACMEHttpReqConfig.tsx @@ -22,6 +22,8 @@ const initModel = () => { return { endpoint: "https://example.com/api/", mode: "", + username: "", + password: "", } as AccessEditFormACMEHttpReqConfigModelType; }; diff --git a/ui/src/components/access/AccessEditFormAWSConfig.tsx b/ui/src/components/access/AccessEditFormAWSConfig.tsx index 816ad264..e2973cce 100644 --- a/ui/src/components/access/AccessEditFormAWSConfig.tsx +++ b/ui/src/components/access/AccessEditFormAWSConfig.tsx @@ -20,7 +20,10 @@ export type AccessEditFormAWSConfigProps = { const initModel = () => { return { + accessKeyId: "", + secretAccessKey: "", region: "us-east-1", + hostedZoneId: "", } as AccessEditFormAWSConfigModelType; }; diff --git a/ui/src/components/access/AccessEditFormAliyunConfig.tsx b/ui/src/components/access/AccessEditFormAliyunConfig.tsx index 036ea33b..229f2f30 100644 --- a/ui/src/components/access/AccessEditFormAliyunConfig.tsx +++ b/ui/src/components/access/AccessEditFormAliyunConfig.tsx @@ -19,7 +19,10 @@ export type AccessEditFormAliyunConfigProps = { }; const initModel = () => { - return {} as AccessEditFormAliyunConfigModelType; + return { + accessKeyId: "", + accessKeySecret: "", + } as AccessEditFormAliyunConfigModelType; }; const AccessEditFormAliyunConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormAliyunConfigProps) => { diff --git a/ui/src/components/access/AccessEditFormBaiduCloudConfig.tsx b/ui/src/components/access/AccessEditFormBaiduCloudConfig.tsx index 572c19d9..d4be8c31 100644 --- a/ui/src/components/access/AccessEditFormBaiduCloudConfig.tsx +++ b/ui/src/components/access/AccessEditFormBaiduCloudConfig.tsx @@ -19,7 +19,10 @@ export type AccessEditFormBaiduCloudConfigProps = { }; const initModel = () => { - return {} as AccessEditFormBaiduCloudConfigModelType; + return { + accessKeyId: "", + secretAccessKey: "", + } as AccessEditFormBaiduCloudConfigModelType; }; const AccessEditFormBaiduCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormBaiduCloudConfigProps) => { diff --git a/ui/src/components/access/AccessEditFormBytePlusConfig.tsx b/ui/src/components/access/AccessEditFormBytePlusConfig.tsx index 2d287b5f..a8b08635 100644 --- a/ui/src/components/access/AccessEditFormBytePlusConfig.tsx +++ b/ui/src/components/access/AccessEditFormBytePlusConfig.tsx @@ -19,7 +19,10 @@ export type AccessEditFormBytePlusConfigProps = { }; const initModel = () => { - return {} as AccessEditFormBytePlusConfigModelType; + return { + accessKey: "", + secretKey: "", + } as AccessEditFormBytePlusConfigModelType; }; const AccessEditFormBytePlusConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormBytePlusConfigProps) => { diff --git a/ui/src/components/access/AccessEditFormCloudflareConfig.tsx b/ui/src/components/access/AccessEditFormCloudflareConfig.tsx index f08a7a2d..b6cb2e32 100644 --- a/ui/src/components/access/AccessEditFormCloudflareConfig.tsx +++ b/ui/src/components/access/AccessEditFormCloudflareConfig.tsx @@ -19,7 +19,9 @@ export type AccessEditFormCloudflareConfigProps = { }; const initModel = () => { - return {} as AccessEditFormCloudflareConfigModelType; + return { + dnsApiToken: "", + } as AccessEditFormCloudflareConfigModelType; }; const AccessEditFormCloudflareConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormCloudflareConfigProps) => { diff --git a/ui/src/components/access/AccessEditFormDogeCloudConfig.tsx b/ui/src/components/access/AccessEditFormDogeCloudConfig.tsx index 7de01aad..c5f3502b 100644 --- a/ui/src/components/access/AccessEditFormDogeCloudConfig.tsx +++ b/ui/src/components/access/AccessEditFormDogeCloudConfig.tsx @@ -19,7 +19,10 @@ export type AccessEditFormDogeCloudConfigProps = { }; const initModel = () => { - return {} as AccessEditFormDogeCloudConfigModelType; + return { + accessKey: "", + secretKey: "", + } as AccessEditFormDogeCloudConfigModelType; }; const AccessEditFormDogeCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormDogeCloudConfigProps) => { diff --git a/ui/src/components/access/AccessEditFormGoDaddyConfig.tsx b/ui/src/components/access/AccessEditFormGoDaddyConfig.tsx index 92fe5128..349ce453 100644 --- a/ui/src/components/access/AccessEditFormGoDaddyConfig.tsx +++ b/ui/src/components/access/AccessEditFormGoDaddyConfig.tsx @@ -19,7 +19,10 @@ export type AccessEditFormGoDaddyConfigProps = { }; const initModel = () => { - return {} as AccessEditFormGoDaddyConfigModelType; + return { + apiKey: "", + apiSecret: "", + } as AccessEditFormGoDaddyConfigModelType; }; const AccessEditFormGoDaddyConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormGoDaddyConfigProps) => { diff --git a/ui/src/components/access/AccessEditFormHuaweiCloudConfig.tsx b/ui/src/components/access/AccessEditFormHuaweiCloudConfig.tsx index 69e6e2e6..0e4556e4 100644 --- a/ui/src/components/access/AccessEditFormHuaweiCloudConfig.tsx +++ b/ui/src/components/access/AccessEditFormHuaweiCloudConfig.tsx @@ -20,6 +20,8 @@ export type AccessEditFormHuaweiCloudConfigProps = { const initModel = () => { return { + accessKeyId: "", + secretAccessKey: "", region: "cn-north-1", } as AccessEditFormHuaweiCloudConfigModelType; }; diff --git a/ui/src/components/access/AccessEditFormKubernetesConfig.tsx b/ui/src/components/access/AccessEditFormKubernetesConfig.tsx index 32d19fc2..9042b9ff 100644 --- a/ui/src/components/access/AccessEditFormKubernetesConfig.tsx +++ b/ui/src/components/access/AccessEditFormKubernetesConfig.tsx @@ -59,7 +59,7 @@ const AccessEditFormKubernetesConfig = ({ form, formName, disabled, loading, mod setKubeFileList([]); } - flushSync(() => onModelChange?.(form.getFieldsValue())); + flushSync(() => onModelChange?.(form.getFieldsValue(true))); }; return ( diff --git a/ui/src/components/access/AccessEditFormNameSiloConfig.tsx b/ui/src/components/access/AccessEditFormNameSiloConfig.tsx index 822bf477..7ac252bd 100644 --- a/ui/src/components/access/AccessEditFormNameSiloConfig.tsx +++ b/ui/src/components/access/AccessEditFormNameSiloConfig.tsx @@ -19,7 +19,9 @@ export type AccessEditFormNameSiloConfigProps = { }; const initModel = () => { - return {} as AccessEditFormNameSiloConfigModelType; + return { + apiKey: "", + } as AccessEditFormNameSiloConfigModelType; }; const AccessEditFormNameSiloConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormNameSiloConfigProps) => { diff --git a/ui/src/components/access/AccessEditFormPowerDNSConfig.tsx b/ui/src/components/access/AccessEditFormPowerDNSConfig.tsx index bbd74c9a..5677c781 100644 --- a/ui/src/components/access/AccessEditFormPowerDNSConfig.tsx +++ b/ui/src/components/access/AccessEditFormPowerDNSConfig.tsx @@ -19,7 +19,10 @@ export type AccessEditFormPowerDNSConfigProps = { }; const initModel = () => { - return {} as AccessEditFormPowerDNSConfigModelType; + return { + apiUrl: "", + apiKey: "", + } as AccessEditFormPowerDNSConfigModelType; }; const AccessEditFormPowerDNSConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormPowerDNSConfigProps) => { diff --git a/ui/src/components/access/AccessEditFormQiniuConfig.tsx b/ui/src/components/access/AccessEditFormQiniuConfig.tsx index 7fa736a3..043af052 100644 --- a/ui/src/components/access/AccessEditFormQiniuConfig.tsx +++ b/ui/src/components/access/AccessEditFormQiniuConfig.tsx @@ -19,7 +19,10 @@ export type AccessEditFormQiniuConfigProps = { }; const initModel = () => { - return {} as AccessEditFormQiniuConfigModelType; + return { + accessKey: "", + secretKey: "", + } as AccessEditFormQiniuConfigModelType; }; const AccessEditFormQiniuConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormQiniuConfigProps) => { diff --git a/ui/src/components/access/AccessEditFormSSHConfig.tsx b/ui/src/components/access/AccessEditFormSSHConfig.tsx index ca312927..e4843f9b 100644 --- a/ui/src/components/access/AccessEditFormSSHConfig.tsx +++ b/ui/src/components/access/AccessEditFormSSHConfig.tsx @@ -94,7 +94,7 @@ const AccessEditFormSSHConfig = ({ form, formName, disabled, loading, model, onM setKeyFileList([]); } - flushSync(() => onModelChange?.(form.getFieldsValue())); + flushSync(() => onModelChange?.(form.getFieldsValue(true))); }; return ( diff --git a/ui/src/components/access/AccessEditFormTencentCloudConfig.tsx b/ui/src/components/access/AccessEditFormTencentCloudConfig.tsx index 90d46844..78c311e1 100644 --- a/ui/src/components/access/AccessEditFormTencentCloudConfig.tsx +++ b/ui/src/components/access/AccessEditFormTencentCloudConfig.tsx @@ -19,7 +19,10 @@ export type AccessEditFormTencentCloudConfigProps = { }; const initModel = () => { - return {} as AccessEditFormTencentCloudConfigModelType; + return { + secretId: "", + secretKey: "", + } as AccessEditFormTencentCloudConfigModelType; }; const AccessEditFormTencentCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormTencentCloudConfigProps) => { diff --git a/ui/src/components/access/AccessEditFormVolcEngineConfig.tsx b/ui/src/components/access/AccessEditFormVolcEngineConfig.tsx index f30f7d5f..b00e82c4 100644 --- a/ui/src/components/access/AccessEditFormVolcEngineConfig.tsx +++ b/ui/src/components/access/AccessEditFormVolcEngineConfig.tsx @@ -19,7 +19,10 @@ export type AccessEditFormVolcEngineConfigProps = { }; const initModel = () => { - return {} as AccessEditFormVolcEngineConfigModelType; + return { + accessKeyId: "", + secretAccessKey: "", + } as AccessEditFormVolcEngineConfigModelType; }; const AccessEditFormVolcEngineConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormVolcEngineConfigProps) => { diff --git a/ui/src/components/access/AccessEditFormWebhookConfig.tsx b/ui/src/components/access/AccessEditFormWebhookConfig.tsx index c4ee09e1..546dfc3b 100644 --- a/ui/src/components/access/AccessEditFormWebhookConfig.tsx +++ b/ui/src/components/access/AccessEditFormWebhookConfig.tsx @@ -18,7 +18,9 @@ export type AccessEditFormWebhookConfigProps = { }; const initModel = () => { - return {} as AccessEditFormWebhookConfigModelType; + return { + url: "", + } as AccessEditFormWebhookConfigModelType; }; const AccessEditFormWebhookConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormWebhookConfigProps) => { diff --git a/ui/src/components/notification/NotifyChannelEditForm.tsx b/ui/src/components/notification/NotifyChannelEditForm.tsx new file mode 100644 index 00000000..fc53e413 --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditForm.tsx @@ -0,0 +1,97 @@ +import { forwardRef, useImperativeHandle, useMemo, useState } from "react"; +import { useCreation, useDeepCompareEffect } from "ahooks"; +import { Form } from "antd"; + +import { type NotifyChannelsSettingsContent } from "@/domain/settings"; +import NotifyChannelEditFormBarkFields from "./NotifyChannelEditFormBarkFields"; +import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalkFields"; +import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields"; +import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields"; +import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields"; +import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields"; +import NotifyChannelEditFormWebhookFields from "./NotifyChannelEditFormWebhookFields"; + +type NotifyChannelEditFormModelType = NotifyChannelsSettingsContent[keyof NotifyChannelsSettingsContent]; + +export type NotifyChannelEditFormProps = { + className?: string; + style?: React.CSSProperties; + channel: keyof NotifyChannelsSettingsContent; + disabled?: boolean; + loading?: boolean; + model?: NotifyChannelEditFormModelType; + onModelChange?: (model: NotifyChannelEditFormModelType) => void; +}; + +export type NotifyChannelEditFormInstance = { + getFieldsValue: () => NotifyChannelEditFormModelType; + resetFields: () => void; + validateFields: () => Promise; +}; + +const NotifyChannelEditForm = forwardRef( + ({ className, style, channel, disabled, loading, model, onModelChange }, ref) => { + const [form] = Form.useForm(); + const formName = useCreation(() => `notifyChannelEditForm_${Math.random().toString(36).substring(2, 10)}${new Date().getTime()}`, []); + const formFieldsComponent = useMemo(() => { + /* + 注意:如果追加新的子组件,请保持以 ASCII 排序。 + NOTICE: If you add new child component, please keep ASCII order. + */ + switch (channel) { + case "bark": + return ; + case "dingtalk": + return ; + case "email": + return ; + case "lark": + return ; + case "serverchan": + return ; + case "telegram": + return ; + case "webhook": + return ; + } + }, [channel]); + + const [initialValues, setInitialValues] = useState(model); + useDeepCompareEffect(() => { + setInitialValues(model); + }, [model]); + + const handleFormChange = (_: unknown, fields: NotifyChannelEditFormModelType) => { + onModelChange?.(fields); + }; + + useImperativeHandle(ref, () => ({ + getFieldsValue: () => { + return form.getFieldsValue(true); + }, + resetFields: () => { + return form.resetFields(); + }, + validateFields: () => { + return form.validateFields(); + }, + })); + + return ( +
+ {formFieldsComponent} +
+ ); + } +); + +export default NotifyChannelEditForm; diff --git a/ui/src/components/notification/NotifyChannelEditFormBarkFields.tsx b/ui/src/components/notification/NotifyChannelEditFormBarkFields.tsx new file mode 100644 index 00000000..dd7ae6c5 --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormBarkFields.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormBarkFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + serverUrl: z + .string({ message: t("settings.notification.channel.form.bark_server_url.placeholder") }) + .url({ message: t("common.errmsg.url_invalid") }) + .nullish(), + deviceKey: z + .string({ message: t("settings.notification.channel.form.bark_device_key.placeholder") }) + .min(1, t("settings.notification.channel.form.bark_device_key.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + + } + > + + + + ); +}; + +export default NotifyChannelEditFormBarkFields; diff --git a/ui/src/components/notification/NotifyChannelEditFormDingTalkFields.tsx b/ui/src/components/notification/NotifyChannelEditFormDingTalkFields.tsx new file mode 100644 index 00000000..9543f142 --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormDingTalkFields.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormDingTalkFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessToken: z + .string({ message: t("settings.notification.channel.form.dingtalk_access_token.placeholder") }) + .min(1, t("settings.notification.channel.form.dingtalk_access_token.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + secret: z + .string({ message: t("settings.notification.channel.form.dingtalk_secret.placeholder") }) + .min(1, t("settings.notification.channel.form.dingtalk_secret.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + + } + > + + + + ); +}; + +export default NotifyChannelEditFormDingTalkFields; diff --git a/ui/src/components/notification/NotifyChannelEditFormEmailFields.tsx b/ui/src/components/notification/NotifyChannelEditFormEmailFields.tsx new file mode 100644 index 00000000..524d84ae --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormEmailFields.tsx @@ -0,0 +1,96 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input, InputNumber, Switch } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormEmailFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + smtpHost: z + .string({ message: t("settings.notification.channel.form.email_smtp_host.placeholder") }) + .min(1, t("settings.notification.channel.form.email_smtp_host.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + smtpPort: z + .number({ message: t("settings.notification.channel.form.email_smtp_port.placeholder") }) + .int() + .gte(1, t("common.errmsg.port_invalid")) + .lte(65535, t("common.errmsg.port_invalid")) + .transform((v) => +v), + smtpTLS: z.boolean().nullish(), + username: z + .string({ message: t("settings.notification.channel.form.email_username.placeholder") }) + .min(1, t("settings.notification.channel.form.email_username.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + password: z + .string({ message: t("settings.notification.channel.form.email_password.placeholder") }) + .min(1, t("settings.notification.channel.form.email_password.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + senderAddress: z + .string({ message: t("settings.notification.channel.form.email_sender_address.placeholder") }) + .min(1, t("settings.notification.channel.form.email_sender_address.placeholder")) + .email({ message: t("common.errmsg.email_invalid") }), + receiverAddress: z + .string({ message: t("settings.notification.channel.form.email_receiver_address.placeholder") }) + .min(1, t("settings.notification.channel.form.email_receiver_address.placeholder")) + .email({ message: t("common.errmsg.email_invalid") }), + }); + const formRule = createSchemaFieldRule(formSchema); + const form = Form.useFormInstance(); + + const handleTLSSwitchChange = (checked: boolean) => { + const oldPort = form.getFieldValue("smtpPort"); + const newPort = checked && (oldPort == null || oldPort === 25) ? 465 : !checked && (oldPort == null || oldPort === 465) ? 25 : oldPort; + if (newPort !== oldPort) { + form.setFieldValue("smtpPort", newPort); + } + }; + + return ( + <> +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ +
+
+ + + +
+ +
+ + + +
+
+ + + + + + + + + + ); +}; + +export default NotifyChannelEditFormEmailFields; diff --git a/ui/src/components/notification/NotifyChannelEditFormLarkFields.tsx b/ui/src/components/notification/NotifyChannelEditFormLarkFields.tsx new file mode 100644 index 00000000..1feaccce --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormLarkFields.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormLarkFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + webhookUrl: z + .string({ message: t("settings.notification.channel.form.lark_webhook_url.placeholder") }) + .min(1, t("settings.notification.channel.form.lark_webhook_url.placeholder")) + .url({ message: t("common.errmsg.url_invalid") }), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + + ); +}; + +export default NotifyChannelEditFormLarkFields; diff --git a/ui/src/components/notification/NotifyChannelEditFormServerChanFields.tsx b/ui/src/components/notification/NotifyChannelEditFormServerChanFields.tsx new file mode 100644 index 00000000..bf6b49b9 --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormServerChanFields.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormServerChanFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + url: z + .string({ message: t("settings.notification.channel.form.serverchan_url.placeholder") }) + .min(1, t("settings.notification.channel.form.serverchan_url.placeholder")) + .url({ message: t("common.errmsg.url_invalid") }), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + + ); +}; + +export default NotifyChannelEditFormServerChanFields; diff --git a/ui/src/components/notification/NotifyChannelEditFormTelegramFields.tsx b/ui/src/components/notification/NotifyChannelEditFormTelegramFields.tsx new file mode 100644 index 00000000..4d6b31f4 --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormTelegramFields.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormTelegramFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiToken: z + .string({ message: t("settings.notification.channel.form.telegram_api_token.placeholder") }) + .min(1, t("settings.notification.channel.form.telegram_api_token.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + chatId: z + .string({ message: t("settings.notification.channel.form.telegram_chat_id.placeholder") }) + .min(1, t("settings.notification.channel.form.telegram_chat_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + + } + > + + + + ); +}; + +export default NotifyChannelEditFormTelegramFields; diff --git a/ui/src/components/notification/NotifyChannelEditFormWebhookFields.tsx b/ui/src/components/notification/NotifyChannelEditFormWebhookFields.tsx new file mode 100644 index 00000000..322781cb --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormWebhookFields.tsx @@ -0,0 +1,26 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormWebhookFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + url: z + .string({ message: t("settings.notification.channel.form.webhook_url.placeholder") }) + .min(1, t("settings.notification.channel.form.webhook_url.placeholder")) + .url({ message: t("common.errmsg.url_invalid") }), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( +
+ + + +
+ ); +}; + +export default NotifyChannelEditFormWebhookFields; diff --git a/ui/src/components/notification/NotifyChannels.tsx b/ui/src/components/notification/NotifyChannels.tsx new file mode 100644 index 00000000..b9a3a22a --- /dev/null +++ b/ui/src/components/notification/NotifyChannels.tsx @@ -0,0 +1,110 @@ +import { useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { useDeepCompareMemo } from "@ant-design/pro-components"; +import { Button, Collapse, message, notification, Skeleton, Space, Switch, Tooltip, type CollapseProps } from "antd"; + +import NotifyChannelEditForm, { type NotifyChannelEditFormInstance } from "./NotifyChannelEditForm"; +import NotifyTestButton from "./NotifyTestButton"; +import { notifyChannelsMap } from "@/domain/settings"; +import { useNotifyChannelStore } from "@/stores/notify"; +import { getErrMsg } from "@/utils/error"; + +type NotifyChannelsSemanticDOM = "collapse" | "form"; + +export type NotifyChannelsProps = { + className?: string; + classNames?: Partial>; + style?: React.CSSProperties; + styles?: Partial>; +}; + +const NotifyChannels = ({ className, classNames, style, styles }: NotifyChannelsProps) => { + const { t, i18n } = useTranslation(); + + const [messageApi, MessageContextHolder] = message.useMessage(); + const [notificationApi, NotificationContextHolder] = notification.useNotification(); + + const { initialized, channels, setChannel, fetchChannels } = useNotifyChannelStore(); + useEffect(() => { + fetchChannels(); + }, [fetchChannels]); + + const channelFormRefs = useRef>([]); + const channelCollapseItems: CollapseProps["items"] = useDeepCompareMemo( + () => + Array.from(notifyChannelsMap.values()).map((channel, index) => { + return { + key: `channel-${channel.type}`, + label: <>{t(channel.name)}, + children: ( +
+ (channelFormRefs.current[index] = ref)} model={channels[channel.type]} channel={channel.type} /> + + + + + {channels[channel.type] ? ( + + <> + + + + ) : null} + +
+ ), + extra: ( +
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} onMouseUp={(e) => e.stopPropagation()}> + handleSwitchChange(channel.type, checked)} + /> +
+ ), + forceRender: true, + }; + }), + [i18n.language, channels] + ); + + const handleSwitchChange = (channel: string, enabled: boolean) => { + setChannel(channel, { enabled }); + }; + + const handleClickSubmit = async (channel: string, index: number) => { + const form = channelFormRefs.current[index]; + if (!form) { + return; + } + + await form.validateFields(); + + try { + setChannel(channel, form.getFieldsValue()); + + messageApi.success(t("common.text.operation_succeeded")); + } catch (err) { + notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + } + }; + + return ( +
+ {MessageContextHolder} + {NotificationContextHolder} + + {!initialized ? ( + + ) : ( + + )} +
+ ); +}; + +export default NotifyChannels; diff --git a/ui/src/components/notification/NotifyTemplate.tsx b/ui/src/components/notification/NotifyTemplate.tsx new file mode 100644 index 00000000..21a29cd3 --- /dev/null +++ b/ui/src/components/notification/NotifyTemplate.tsx @@ -0,0 +1,128 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useRequest } from "ahooks"; +import { Button, Form, Input, message, notification, Skeleton } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; +import { ClientResponseError } from "pocketbase"; + +import { defaultNotifyTemplate, SETTINGS_NAMES, type NotifyTemplatesSettingsContent } from "@/domain/settings"; +import { get as getSettings, save as saveSettings } from "@/repository/settings"; +import { getErrMsg } from "@/utils/error"; + +export type NotifyTemplateFormProps = { + className?: string; + style?: React.CSSProperties; +}; + +const NotifyTemplateForm = ({ className, style }: NotifyTemplateFormProps) => { + const { t } = useTranslation(); + + const [messageApi, MessageContextHolder] = message.useMessage(); + const [notificationApi, NotificationContextHolder] = notification.useNotification(); + + const formSchema = z.object({ + subject: z + .string() + .trim() + .min(1, t("settings.notification.template.form.subject.placeholder")) + .max(1000, t("common.errmsg.string_max", { max: 1000 })), + message: z + .string() + .trim() + .min(1, t("settings.notification.template.form.message.placeholder")) + .max(1000, t("common.errmsg.string_max", { max: 1000 })), + }); + const formRule = createSchemaFieldRule(formSchema); + const [form] = Form.useForm>(); + const [formPending, setFormPending] = useState(false); + + const [initialValues, setInitialValues] = useState>>(); + const [initialChanged, setInitialChanged] = useState(false); + + const { loading } = useRequest( + () => { + return getSettings(SETTINGS_NAMES.NOTIFY_TEMPLATES); + }, + { + onError: (err) => { + if (err instanceof ClientResponseError && err.isAbort) { + return; + } + + console.error(err); + }, + onFinally: (_, resp) => { + const template = resp?.content?.notifyTemplates?.[0] ?? defaultNotifyTemplate; + setInitialValues({ ...template }); + }, + } + ); + + const handleInputChange = () => { + setInitialChanged(true); + }; + + const handleFormFinish = async (fields: z.infer) => { + setFormPending(true); + + try { + const settings = await getSettings(SETTINGS_NAMES.NOTIFY_TEMPLATES); + await saveSettings({ + ...settings, + content: { + notifyTemplates: [fields], + }, + }); + + messageApi.success(t("common.text.operation_succeeded")); + } catch (err) { + notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + } finally { + setFormPending(false); + } + }; + + return ( +
+ {MessageContextHolder} + {NotificationContextHolder} + + {loading ? ( + + ) : ( +
+ + + + + + + + + + + +
+ )} +
+ ); +}; + +export default NotifyTemplateForm; diff --git a/ui/src/components/notification/NotifyTestButton.tsx b/ui/src/components/notification/NotifyTestButton.tsx new file mode 100644 index 00000000..f6667aa8 --- /dev/null +++ b/ui/src/components/notification/NotifyTestButton.tsx @@ -0,0 +1,54 @@ +import { useRequest } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { Button, message, notification, type ButtonProps } from "antd"; + +import { notifyTest } from "@/api/notify"; +import { getErrMsg } from "@/utils/error"; + +export type NotifyTestButtonProps = { + className?: string; + style?: React.CSSProperties; + channel: string; + disabled?: boolean; + size?: ButtonProps["size"]; +}; + +const NotifyTestButton = ({ className, style, channel, disabled, size }: NotifyTestButtonProps) => { + const { t } = useTranslation(); + + const [messageApi, MessageContextHolder] = message.useMessage(); + const [notificationApi, NotificationContextHolder] = notification.useNotification(); + + const { loading, run: executeNotifyTest } = useRequest( + () => { + return notifyTest(channel); + }, + { + refreshDeps: [channel], + manual: true, + onSuccess: () => { + messageApi.success(t("settings.notification.push_test.pushed")); + }, + onError: (err) => { + notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); + }, + } + ); + + const handleClick = () => { + executeNotifyTest(); + }; + + return ( + <> + {MessageContextHolder} + {NotificationContextHolder} + + + + ); +}; + +export default NotifyTestButton; diff --git a/ui/src/components/notify/Bark.tsx b/ui/src/components/notify/Bark.tsx deleted file mode 100644 index 4c2a5370..00000000 --- a/ui/src/components/notify/Bark.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import { useToast } from "@/components/ui/use-toast"; -import { getErrMsg } from "@/utils/error"; -import { NotifyChannels, NotifyChannelBark } from "@/domain/settings"; -import { save } from "@/repository/settings"; -import { useNotifyContext } from "@/providers/notify"; -import { notifyTest } from "@/api/notify"; -import Show from "@/components/Show"; - -type BarkSetting = { - id: string; - name: string; - data: NotifyChannelBark; -}; - -const Bark = () => { - const { config, setChannels } = useNotifyContext(); - const { t } = useTranslation(); - - const [changed, setChanged] = useState(false); - - const [bark, setBark] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - serverUrl: "", - deviceKey: "", - enabled: false, - }, - }); - - const [originBark, setOriginBark] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - serverUrl: "", - deviceKey: "", - enabled: false, - }, - }); - - useEffect(() => { - setChanged(false); - }, [config]); - - useEffect(() => { - const data = getDetailBark(); - setOriginBark({ - id: config.id ?? "", - name: "common.notifier.bark", - data, - }); - }, [config]); - - useEffect(() => { - const data = getDetailBark(); - setBark({ - id: config.id ?? "", - name: "common.notifier.bark", - data, - }); - }, [config]); - - const { toast } = useToast(); - - const checkChanged = (data: NotifyChannelBark) => { - if (data.serverUrl !== originBark.data.serverUrl || data.deviceKey !== originBark.data.deviceKey) { - setChanged(true); - } else { - setChanged(false); - } - }; - - const getDetailBark = () => { - const df: NotifyChannelBark = { - serverUrl: "", - deviceKey: "", - enabled: false, - }; - if (!config.content) { - return df; - } - const chanels = config.content as NotifyChannels; - if (!chanels.bark) { - return df; - } - - return chanels.bark as NotifyChannelBark; - }; - - const handleSaveClick = async () => { - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - bark: { - ...bark.data, - }, - }, - }); - - setChannels(resp); - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.config.saved.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - const [testing, setTesting] = useState(false); - const handlePushTestClick = async () => { - if (testing) return; - - try { - setTesting(true); - - await notifyTest("bark"); - - toast({ - title: t("settings.notification.push_test_message.succeeded.message"), - description: t("settings.notification.push_test_message.succeeded.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("settings.notification.push_test_message.failed.message"), - description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`, - variant: "destructive", - }); - } finally { - setTesting(false); - } - }; - - const handleSwitchChange = async () => { - const newData = { - ...bark, - data: { - ...bark.data, - enabled: !bark.data.enabled, - }, - }; - setBark(newData); - - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - bark: { - ...newData.data, - }, - }, - }); - - setChannels(resp); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - return ( -
-
- - { - const newData = { - ...bark, - data: { - ...bark.data, - serverUrl: e.target.value, - }, - }; - - checkChanged(newData.data); - setBark(newData); - }} - /> -
- -
- - { - const newData = { - ...bark, - data: { - ...bark.data, - deviceKey: e.target.value, - }, - }; - - checkChanged(newData.data); - setBark(newData); - }} - /> -
- -
-
- - -
- -
- - - - - - - -
-
-
- ); -}; - -export default Bark; diff --git a/ui/src/components/notify/DingTalk.tsx b/ui/src/components/notify/DingTalk.tsx deleted file mode 100644 index 04e5b577..00000000 --- a/ui/src/components/notify/DingTalk.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import { useToast } from "@/components/ui/use-toast"; -import { getErrMsg } from "@/utils/error"; -import { NotifyChannelDingTalk, NotifyChannels } from "@/domain/settings"; -import { useNotifyContext } from "@/providers/notify"; -import { save } from "@/repository/settings"; -import Show from "@/components/Show"; -import { notifyTest } from "@/api/notify"; - -type DingTalkSetting = { - id: string; - name: string; - data: NotifyChannelDingTalk; -}; - -const DingTalk = () => { - const { config, setChannels } = useNotifyContext(); - const { t } = useTranslation(); - - const [changed, setChanged] = useState(false); - - const [dingtalk, setDingtalk] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - accessToken: "", - secret: "", - enabled: false, - }, - }); - - const [originDingtalk, setOriginDingtalk] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - accessToken: "", - secret: "", - enabled: false, - }, - }); - - useEffect(() => { - setChanged(false); - }, [config]); - - useEffect(() => { - const data = getDetailDingTalk(); - setOriginDingtalk({ - id: config.id ?? "", - name: "dingtalk", - data, - }); - }, [config]); - - useEffect(() => { - const data = getDetailDingTalk(); - setDingtalk({ - id: config.id ?? "", - name: "dingtalk", - data, - }); - }, [config]); - - const { toast } = useToast(); - - const getDetailDingTalk = () => { - const df: NotifyChannelDingTalk = { - accessToken: "", - secret: "", - enabled: false, - }; - if (!config.content) { - return df; - } - const chanels = config.content as NotifyChannels; - if (!chanels.dingtalk) { - return df; - } - - return chanels.dingtalk as NotifyChannelDingTalk; - }; - - const checkChanged = (data: NotifyChannelDingTalk) => { - if (data.accessToken !== originDingtalk.data.accessToken || data.secret !== originDingtalk.data.secret) { - setChanged(true); - } else { - setChanged(false); - } - }; - - const handleSaveClick = async () => { - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - dingtalk: { - ...dingtalk.data, - }, - }, - }); - - setChannels(resp); - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.config.saved.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } finally { - setTesting(false); - } - }; - - const [testing, setTesting] = useState(false); - const handlePushTestClick = async () => { - if (testing) return; - - try { - setTesting(true); - - await notifyTest("dingtalk"); - - toast({ - title: t("settings.notification.push_test_message.succeeded.message"), - description: t("settings.notification.push_test_message.succeeded.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("settings.notification.push_test_message.failed.message"), - description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - const handleSwitchChange = async () => { - const newData = { - ...dingtalk, - data: { - ...dingtalk.data, - enabled: !dingtalk.data.enabled, - }, - }; - setDingtalk(newData); - - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - dingtalk: { - ...newData.data, - }, - }, - }); - - setChannels(resp); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - return ( -
-
- - { - const newData = { - ...dingtalk, - data: { - ...dingtalk.data, - accessToken: e.target.value, - }, - }; - checkChanged(newData.data); - setDingtalk(newData); - }} - /> -
- -
- - { - const newData = { - ...dingtalk, - data: { - ...dingtalk.data, - secret: e.target.value, - }, - }; - checkChanged(newData.data); - setDingtalk(newData); - }} - /> -
- -
-
- - -
- -
- - - - - - - -
-
-
- ); -}; - -export default DingTalk; diff --git a/ui/src/components/notify/Email.tsx b/ui/src/components/notify/Email.tsx deleted file mode 100644 index 3defc2f3..00000000 --- a/ui/src/components/notify/Email.tsx +++ /dev/null @@ -1,384 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import { useToast } from "@/components/ui/use-toast"; -import { getErrMsg } from "@/utils/error"; -import { NotifyChannelEmail, NotifyChannels } from "@/domain/settings"; -import { useNotifyContext } from "@/providers/notify"; -import { save } from "@/repository/settings"; -import Show from "@/components/Show"; -import { notifyTest } from "@/api/notify"; - -type EmailSetting = { - id: string; - name: string; - data: NotifyChannelEmail; -}; - -const Mail = () => { - const { config, setChannels } = useNotifyContext(); - const { t } = useTranslation(); - - const [changed, setChanged] = useState(false); - - const [mail, setMail] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - smtpHost: "", - smtpPort: 465, - smtpTLS: true, - username: "", - password: "", - senderAddress: "", - receiverAddress: "", - enabled: false, - }, - }); - - const [originMail, setOriginMail] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - smtpHost: "", - smtpPort: 465, - smtpTLS: true, - username: "", - password: "", - senderAddress: "", - receiverAddress: "", - enabled: false, - }, - }); - - useEffect(() => { - setChanged(false); - }, [config]); - - useEffect(() => { - const data = getDetailMail(); - setOriginMail({ - id: config.id ?? "", - name: "email", - data, - }); - }, [config]); - - useEffect(() => { - const data = getDetailMail(); - setMail({ - id: config.id ?? "", - name: "email", - data, - }); - }, [config]); - - const { toast } = useToast(); - - const getDetailMail = () => { - const df: NotifyChannelEmail = { - smtpHost: "smtp.example.com", - smtpPort: 465, - smtpTLS: true, - username: "", - password: "", - senderAddress: "", - receiverAddress: "", - enabled: false, - }; - if (!config.content) { - return df; - } - const chanels = config.content as NotifyChannels; - if (!chanels.email) { - return df; - } - - return chanels.email as NotifyChannelEmail; - }; - - const checkChanged = (data: NotifyChannelEmail) => { - if ( - data.smtpHost !== originMail.data.smtpHost || - data.smtpPort !== originMail.data.smtpPort || - data.smtpTLS !== originMail.data.smtpTLS || - data.username !== originMail.data.username || - data.password !== originMail.data.password || - data.senderAddress !== originMail.data.senderAddress || - data.receiverAddress !== originMail.data.receiverAddress - ) { - setChanged(true); - } else { - setChanged(false); - } - }; - - const handleSaveClick = async () => { - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - email: { - ...mail.data, - }, - }, - }); - - setChannels(resp); - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.config.saved.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - const [testing, setTesting] = useState(false); - const handlePushTestClick = async () => { - if (testing) return; - - try { - setTesting(true); - - await notifyTest("email"); - - toast({ - title: t("settings.notification.push_test_message.succeeded.message"), - description: t("settings.notification.push_test_message.succeeded.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("settings.notification.push_test_message.failed.message"), - description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`, - variant: "destructive", - }); - } finally { - setTesting(false); - } - }; - - const handleSwitchChange = async () => { - const newData = { - ...mail, - data: { - ...mail.data, - enabled: !mail.data.enabled, - }, - }; - setMail(newData); - - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - email: { - ...newData.data, - }, - }, - }); - - setChannels(resp); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - return ( -
-
-
- - { - const newData = { - ...mail, - data: { - ...mail.data, - smtpHost: e.target.value, - }, - }; - checkChanged(newData.data); - setMail(newData); - }} - /> -
- -
- - { - const newData = { - ...mail, - data: { - ...mail.data, - smtpPort: +e.target.value || 0, - }, - }; - checkChanged(newData.data); - setMail(newData); - }} - /> -
- -
- - { - const newData = { - ...mail, - data: { - ...mail.data, - smtpPort: e && mail.data.smtpPort === 25 ? 465 : !e && mail.data.smtpPort === 465 ? 25 : mail.data.smtpPort, - smtpTLS: e, - }, - }; - checkChanged(newData.data); - setMail(newData); - }} - /> -
-
- -
-
- - { - const newData = { - ...mail, - data: { - ...mail.data, - username: e.target.value, - }, - }; - checkChanged(newData.data); - setMail(newData); - }} - /> -
- -
- - { - const newData = { - ...mail, - data: { - ...mail.data, - password: e.target.value, - }, - }; - checkChanged(newData.data); - setMail(newData); - }} - /> -
-
- -
- - { - const newData = { - ...mail, - data: { - ...mail.data, - senderAddress: e.target.value, - }, - }; - checkChanged(newData.data); - setMail(newData); - }} - /> -
- -
- - { - const newData = { - ...mail, - data: { - ...mail.data, - receiverAddress: e.target.value, - }, - }; - checkChanged(newData.data); - setMail(newData); - }} - /> -
- -
-
- - -
- -
- - - - - - - -
-
-
- ); -}; - -export default Mail; diff --git a/ui/src/components/notify/Lark.tsx b/ui/src/components/notify/Lark.tsx deleted file mode 100644 index e70278e9..00000000 --- a/ui/src/components/notify/Lark.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { Switch } from "@/components/ui/switch"; -import { Label } from "@/components/ui/label"; -import { useNotifyContext } from "@/providers/notify"; -import { NotifyChannelLark, NotifyChannels } from "@/domain/settings"; -import { useEffect, useState } from "react"; -import { save } from "@/repository/settings"; -import { getErrMsg } from "@/utils/error"; -import { useToast } from "@/components/ui/use-toast"; -import { useTranslation } from "react-i18next"; -import { notifyTest } from "@/api/notify"; -import Show from "@/components/Show"; - -type LarkSetting = { - id: string; - name: string; - data: NotifyChannelLark; -}; - -const Lark = () => { - const { config, setChannels } = useNotifyContext(); - const { t } = useTranslation(); - - const [changed, setChanged] = useState(false); - - const [lark, setLark] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - webhookUrl: "", - enabled: false, - }, - }); - - const [originLark, setOriginLark] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - webhookUrl: "", - enabled: false, - }, - }); - - useEffect(() => { - setChanged(false); - }, [config]); - - useEffect(() => { - const data = getDetailLark(); - setOriginLark({ - id: config.id ?? "", - name: "lark", - data, - }); - }, [config]); - - useEffect(() => { - const data = getDetailLark(); - setLark({ - id: config.id ?? "", - name: "lark", - data, - }); - }, [config]); - - const { toast } = useToast(); - - const checkChanged = (data: NotifyChannelLark) => { - if (data.webhookUrl !== originLark.data.webhookUrl) { - setChanged(true); - } else { - setChanged(false); - } - }; - - const getDetailLark = () => { - const df: NotifyChannelLark = { - webhookUrl: "", - enabled: false, - }; - if (!config.content) { - return df; - } - const chanels = config.content as NotifyChannels; - if (!chanels.lark) { - return df; - } - - return chanels.lark as NotifyChannelLark; - }; - - const handleSaveClick = async () => { - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - lark: { - ...lark.data, - }, - }, - }); - - setChannels(resp); - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.config.saved.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } finally { - setTesting(false); - } - }; - - const [testing, setTesting] = useState(false); - const handlePushTestClick = async () => { - if (testing) return; - - try { - setTesting(true); - - await notifyTest("lark"); - - toast({ - title: t("settings.notification.push_test_message.succeeded.message"), - description: t("settings.notification.push_test_message.succeeded.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("settings.notification.push_test_message.failed.message"), - description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - const handleSwitchChange = async () => { - const newData = { - ...lark, - data: { - ...lark.data, - enabled: !lark.data.enabled, - }, - }; - setLark(newData); - - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - lark: { - ...newData.data, - }, - }, - }); - - setChannels(resp); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - return ( -
-
- - { - const newData = { - ...lark, - data: { - ...lark.data, - webhookUrl: e.target.value, - }, - }; - - checkChanged(newData.data); - setLark(newData); - }} - /> -
- -
-
- - -
- -
- - - - - - - -
-
-
- ); -}; - -export default Lark; diff --git a/ui/src/components/notify/NotifyTemplate.tsx b/ui/src/components/notify/NotifyTemplate.tsx deleted file mode 100644 index 03d3a433..00000000 --- a/ui/src/components/notify/NotifyTemplate.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { useToast } from "@/components/ui/use-toast"; -import { defaultNotifyTemplate, NotifyTemplates, NotifyTemplate as NotifyTemplateT } from "@/domain/settings"; -import { get, save } from "@/repository/settings"; - -const NotifyTemplate = () => { - const [id, setId] = useState(""); - const [templates, setTemplates] = useState([defaultNotifyTemplate]); - - const { toast } = useToast(); - const { t } = useTranslation(); - - useEffect(() => { - const featchData = async () => { - const resp = await get("templates"); - - if (resp.content) { - setTemplates((resp.content as NotifyTemplates).notifyTemplates); - setId(resp.id ? resp.id : ""); - } - }; - featchData(); - }, []); - - const handleTitleChange = (val: string) => { - const template = templates?.[0] ?? {}; - - setTemplates([ - { - ...template, - title: val, - }, - ]); - }; - - const handleContentChange = (val: string) => { - const template = templates?.[0] ?? {}; - - setTemplates([ - { - ...template, - content: val, - }, - ]); - }; - - const handleSaveClick = async () => { - const resp = await save({ - id: id, - content: { - notifyTemplates: templates, - }, - name: "templates", - }); - - if (resp.id) { - setId(resp.id); - } - - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.template.saved.message"), - }); - }; - - return ( -
- { - handleTitleChange(e.target.value); - }} - /> - -
{t("settings.notification.template.variables.tips.title")}
- - -
{t("settings.notification.template.variables.tips.content")}
-
- -
-
- ); -}; - -export default NotifyTemplate; diff --git a/ui/src/components/notify/ServerChan.tsx b/ui/src/components/notify/ServerChan.tsx deleted file mode 100644 index cb2ff1a6..00000000 --- a/ui/src/components/notify/ServerChan.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import { useToast } from "@/components/ui/use-toast"; -import { getErrMsg } from "@/utils/error"; -import { isValidURL } from "@/utils/url"; -import { NotifyChannels, NotifyChannelServerChan } from "@/domain/settings"; -import { save } from "@/repository/settings"; -import { useNotifyContext } from "@/providers/notify"; -import { notifyTest } from "@/api/notify"; -import Show from "@/components/Show"; - -type ServerChanSetting = { - id: string; - name: string; - data: NotifyChannelServerChan; -}; - -const ServerChan = () => { - const { config, setChannels } = useNotifyContext(); - const { t } = useTranslation(); - const [changed, setChanged] = useState(false); - - const [serverchan, setServerChan] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - url: "", - enabled: false, - }, - }); - - const [originServerChan, setOriginServerChan] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - url: "", - enabled: false, - }, - }); - - useEffect(() => { - setChanged(false); - }, [config]); - - useEffect(() => { - const data = getDetailServerChan(); - setOriginServerChan({ - id: config.id ?? "", - name: "serverchan", - data, - }); - }, [config]); - - useEffect(() => { - const data = getDetailServerChan(); - setServerChan({ - id: config.id ?? "", - name: "serverchan", - data, - }); - }, [config]); - - const { toast } = useToast(); - - const checkChanged = (data: NotifyChannelServerChan) => { - if (data.url !== originServerChan.data.url) { - setChanged(true); - } else { - setChanged(false); - } - }; - - const getDetailServerChan = () => { - const df: NotifyChannelServerChan = { - url: "", - enabled: false, - }; - if (!config.content) { - return df; - } - const chanels = config.content as NotifyChannels; - if (!chanels.serverchan) { - return df; - } - - return chanels.serverchan as NotifyChannelServerChan; - }; - - const handleSaveClick = async () => { - try { - serverchan.data.url = serverchan.data.url.trim(); - if (!isValidURL(serverchan.data.url)) { - toast({ - title: t("common.text.operation_failed"), - description: t("common.errmsg.url_invalid"), - variant: "destructive", - }); - return; - } - - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - serverchan: { - ...serverchan.data, - }, - }, - }); - - setChannels(resp); - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.config.saved.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - const [testing, setTesting] = useState(false); - const handlePushTestClick = async () => { - if (testing) return; - - try { - setTesting(true); - - await notifyTest("serverchan"); - - toast({ - title: t("settings.notification.push_test_message.succeeded.message"), - description: t("settings.notification.push_test_message.succeeded.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("settings.notification.push_test_message.failed.message"), - description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`, - variant: "destructive", - }); - } finally { - setTesting(false); - } - }; - - const handleSwitchChange = async () => { - const newData = { - ...serverchan, - data: { - ...serverchan.data, - enabled: !serverchan.data.enabled, - }, - }; - setServerChan(newData); - - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - serverchan: { - ...newData.data, - }, - }, - }); - - setChannels(resp); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - return ( -
-
- - { - const newData = { - ...serverchan, - data: { - ...serverchan.data, - url: e.target.value, - }, - }; - - checkChanged(newData.data); - setServerChan(newData); - }} - /> -
- -
-
- - -
- -
- - - - - - - -
-
-
- ); -}; - -export default ServerChan; diff --git a/ui/src/components/notify/Telegram.tsx b/ui/src/components/notify/Telegram.tsx deleted file mode 100644 index febf0905..00000000 --- a/ui/src/components/notify/Telegram.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import { useToast } from "@/components/ui/use-toast"; -import { getErrMsg } from "@/utils/error"; -import { NotifyChannels, NotifyChannelTelegram } from "@/domain/settings"; -import { save } from "@/repository/settings"; -import { useNotifyContext } from "@/providers/notify"; -import { notifyTest } from "@/api/notify"; -import Show from "@/components/Show"; - -type TelegramSetting = { - id: string; - name: string; - data: NotifyChannelTelegram; -}; - -const Telegram = () => { - const { config, setChannels } = useNotifyContext(); - const { t } = useTranslation(); - - const [changed, setChanged] = useState(false); - - const [telegram, setTelegram] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - apiToken: "", - chatId: "", - enabled: false, - }, - }); - - const [originTelegram, setOriginTelegram] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - apiToken: "", - chatId: "", - enabled: false, - }, - }); - - useEffect(() => { - setChanged(false); - }, [config]); - - useEffect(() => { - const data = getDetailTelegram(); - setOriginTelegram({ - id: config.id ?? "", - name: "common.notifier.telegram", - data, - }); - }, [config]); - - useEffect(() => { - const data = getDetailTelegram(); - setTelegram({ - id: config.id ?? "", - name: "common.notifier.telegram", - data, - }); - }, [config]); - - const { toast } = useToast(); - - const checkChanged = (data: NotifyChannelTelegram) => { - if (data.apiToken !== originTelegram.data.apiToken || data.chatId !== originTelegram.data.chatId) { - setChanged(true); - } else { - setChanged(false); - } - }; - - const getDetailTelegram = () => { - const df: NotifyChannelTelegram = { - apiToken: "", - chatId: "", - enabled: false, - }; - if (!config.content) { - return df; - } - const chanels = config.content as NotifyChannels; - if (!chanels.telegram) { - return df; - } - - return chanels.telegram as NotifyChannelTelegram; - }; - - const handleSaveClick = async () => { - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - telegram: { - ...telegram.data, - }, - }, - }); - - setChannels(resp); - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.config.saved.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - const [testing, setTesting] = useState(false); - const handlePushTestClick = async () => { - if (testing) return; - - try { - setTesting(true); - - await notifyTest("telegram"); - - toast({ - title: t("settings.notification.push_test_message.succeeded.message"), - description: t("settings.notification.push_test_message.succeeded.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("settings.notification.push_test_message.failed.message"), - description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`, - variant: "destructive", - }); - } finally { - setTesting(false); - } - }; - - const handleSwitchChange = async () => { - const newData = { - ...telegram, - data: { - ...telegram.data, - enabled: !telegram.data.enabled, - }, - }; - setTelegram(newData); - - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - telegram: { - ...newData.data, - }, - }, - }); - - setChannels(resp); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - return ( -
-
- - { - const newData = { - ...telegram, - data: { - ...telegram.data, - apiToken: e.target.value, - }, - }; - - checkChanged(newData.data); - setTelegram(newData); - }} - /> -
- -
- - { - const newData = { - ...telegram, - data: { - ...telegram.data, - chatId: e.target.value, - }, - }; - - checkChanged(newData.data); - setTelegram(newData); - }} - /> -
- -
-
- - -
- -
- - - - - - - -
-
-
- ); -}; - -export default Telegram; diff --git a/ui/src/components/notify/Webhook.tsx b/ui/src/components/notify/Webhook.tsx deleted file mode 100644 index 13145f21..00000000 --- a/ui/src/components/notify/Webhook.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import { useToast } from "@/components/ui/use-toast"; -import { getErrMsg } from "@/utils/error"; -import { isValidURL } from "@/utils/url"; -import { NotifyChannels, NotifyChannelWebhook } from "@/domain/settings"; -import { save } from "@/repository/settings"; -import { useNotifyContext } from "@/providers/notify"; -import { notifyTest } from "@/api/notify"; -import Show from "@/components/Show"; - -type WebhookSetting = { - id: string; - name: string; - data: NotifyChannelWebhook; -}; - -const Webhook = () => { - const { config, setChannels } = useNotifyContext(); - const { t } = useTranslation(); - const [changed, setChanged] = useState(false); - - const [webhook, setWebhook] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - url: "", - enabled: false, - }, - }); - - const [originWebhook, setOriginWebhook] = useState({ - id: config.id ?? "", - name: "notifyChannels", - data: { - url: "", - enabled: false, - }, - }); - - useEffect(() => { - setChanged(false); - }, [config]); - - useEffect(() => { - const data = getDetailWebhook(); - setOriginWebhook({ - id: config.id ?? "", - name: "webhook", - data, - }); - }, [config]); - - useEffect(() => { - const data = getDetailWebhook(); - setWebhook({ - id: config.id ?? "", - name: "webhook", - data, - }); - }, [config]); - - const { toast } = useToast(); - - const checkChanged = (data: NotifyChannelWebhook) => { - if (data.url !== originWebhook.data.url) { - setChanged(true); - } else { - setChanged(false); - } - }; - - const getDetailWebhook = () => { - const df: NotifyChannelWebhook = { - url: "", - enabled: false, - }; - if (!config.content) { - return df; - } - const chanels = config.content as NotifyChannels; - if (!chanels.webhook) { - return df; - } - - return chanels.webhook as NotifyChannelWebhook; - }; - - const handleSaveClick = async () => { - try { - webhook.data.url = webhook.data.url.trim(); - if (!isValidURL(webhook.data.url)) { - toast({ - title: t("common.text.operation_failed"), - description: t("common.errmsg.url_invalid"), - variant: "destructive", - }); - return; - } - - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - webhook: { - ...webhook.data, - }, - }, - }); - - setChannels(resp); - toast({ - title: t("common.text.operation_succeeded"), - description: t("settings.notification.config.saved.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - const [testing, setTesting] = useState(false); - const handlePushTestClick = async () => { - if (testing) return; - - try { - setTesting(true); - - await notifyTest("webhook"); - - toast({ - title: t("settings.notification.push_test_message.succeeded.message"), - description: t("settings.notification.push_test_message.succeeded.message"), - }); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("settings.notification.push_test_message.failed.message"), - description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`, - variant: "destructive", - }); - } finally { - setTesting(false); - } - }; - - const handleSwitchChange = async () => { - const newData = { - ...webhook, - data: { - ...webhook.data, - enabled: !webhook.data.enabled, - }, - }; - setWebhook(newData); - - try { - const resp = await save({ - ...config, - name: "notifyChannels", - content: { - ...config.content, - webhook: { - ...newData.data, - }, - }, - }); - - setChannels(resp); - } catch (e) { - const msg = getErrMsg(e); - - toast({ - title: t("common.text.operation_failed"), - description: `${t("settings.notification.config.failed.message")}: ${msg}`, - variant: "destructive", - }); - } - }; - - return ( -
-
- - { - const newData = { - ...webhook, - data: { - ...webhook.data, - url: e.target.value, - }, - }; - - checkChanged(newData.data); - setWebhook(newData); - }} - /> -
- -
-
- - -
- -
- - - - - - - -
-
-
- ); -}; - -export default Webhook; diff --git a/ui/src/components/workflow/DeployToLocal.tsx b/ui/src/components/workflow/DeployToLocal.tsx index 8b133e8f..ef9bfc77 100644 --- a/ui/src/components/workflow/DeployToLocal.tsx +++ b/ui/src/components/workflow/DeployToLocal.tsx @@ -41,13 +41,14 @@ const formSchema = z keyPath: z .string() .min(0, t("domain.deployment.form.file_key_path.placeholder")) - .max(255, t("common.errmsg.string_max", { max: 255 })), - pfxPassword: z.string().optional(), - jksAlias: z.string().optional(), - jksKeypass: z.string().optional(), - jksStorepass: z.string().optional(), - preCommand: z.string().optional(), - command: z.string().optional(), + .max(255, t("common.errmsg.string_max", { max: 255 })) + .nullish(), + pfxPassword: z.string().nullish(), + jksAlias: z.string().nullish(), + jksKeypass: z.string().nullish(), + jksStorepass: z.string().nullish(), + preCommand: z.string().nullish(), + command: z.string().nullish(), shell: z.union([z.literal("sh"), z.literal("cmd"), z.literal("powershell")], { message: t("domain.deployment.form.shell.placeholder"), }), diff --git a/ui/src/components/workflow/DeployToSSH.tsx b/ui/src/components/workflow/DeployToSSH.tsx index ee2c3279..968395cb 100644 --- a/ui/src/components/workflow/DeployToSSH.tsx +++ b/ui/src/components/workflow/DeployToSSH.tsx @@ -40,13 +40,14 @@ const formSchema = z keyPath: z .string() .min(0, t("domain.deployment.form.file_key_path.placeholder")) - .max(255, t("common.errmsg.string_max", { max: 255 })), - pfxPassword: z.string().optional(), - jksAlias: z.string().optional(), - jksKeypass: z.string().optional(), - jksStorepass: z.string().optional(), - preCommand: z.string().optional(), - command: z.string().optional(), + .max(255, t("common.errmsg.string_max", { max: 255 })) + .nullish(), + pfxPassword: z.string().nullish(), + jksAlias: z.string().nullish(), + jksKeypass: z.string().nullish(), + jksStorepass: z.string().nullish(), + preCommand: z.string().nullish(), + command: z.string().nullish(), }) .refine((data) => (data.format === "pem" ? !!data.keyPath?.trim() : true), { message: t("domain.deployment.form.file_key_path.placeholder"), diff --git a/ui/src/components/workflow/Node.tsx b/ui/src/components/workflow/Node.tsx index 4e5995a6..0496d8fa 100644 --- a/ui/src/components/workflow/Node.tsx +++ b/ui/src/components/workflow/Node.tsx @@ -9,7 +9,7 @@ import PanelBody from "./PanelBody"; import { useTranslation } from "react-i18next"; import Show from "../Show"; import { deployTargetsMap } from "@/domain/domain"; -import { channelLabelMap } from "@/domain/settings"; +import { notifyChannelsMap } from "@/domain/settings"; type NodeProps = { data: WorkflowNode; @@ -69,10 +69,10 @@ const Node = ({ data }: NodeProps) => { ); } case WorkflowNodeType.Notify: { - const channelLabel = channelLabelMap.get(data.config?.channel as string); + const channelLabel = notifyChannelsMap.get(data.config?.channel as string); return (
-
{t(channelLabel?.label ?? "")}
+
{t(channelLabel?.name ?? "")}
{(data.config?.title as string) ?? ""}
); diff --git a/ui/src/components/workflow/NotifyForm.tsx b/ui/src/components/workflow/NotifyForm.tsx index ea48cc3c..a632ddb2 100644 --- a/ui/src/components/workflow/NotifyForm.tsx +++ b/ui/src/components/workflow/NotifyForm.tsx @@ -10,9 +10,9 @@ import { useShallow } from "zustand/shallow"; import { usePanel } from "./PanelProvider"; import { useTranslation } from "react-i18next"; import { Button } from "../ui/button"; -import { useNotifyContext } from "@/providers/notify"; +import { useNotifyChannelStore } from "@/stores/notify"; import { useEffect, useState } from "react"; -import { NotifyChannels, channels as supportedChannels } from "@/domain/settings"; +import { notifyChannelsMap } from "@/domain/settings"; import { SelectValue } from "@radix-ui/react-select"; import { Textarea } from "../ui/textarea"; import { RefreshCw, Settings } from "lucide-react"; @@ -25,7 +25,7 @@ const selectState = (state: WorkflowState) => ({ updateNode: state.updateNode, }); type ChannelName = { - name: string; + key: string; label: string; }; @@ -34,28 +34,23 @@ const NotifyForm = ({ data }: NotifyFormProps) => { const { updateNode } = useWorkflowStore(useShallow(selectState)); const { hidePanel } = usePanel(); const { t } = useTranslation(); - const { config: notifyConfig, initChannels } = useNotifyContext(); + const { channels: supportedChannels, fetchChannels } = useNotifyChannelStore(); - const [chanels, setChanels] = useState([]); + const [channels, setChannels] = useState([]); useEffect(() => { - setChanels(getChannels()); - }, [notifyConfig]); + fetchChannels(); + }, [fetchChannels]); - const getChannels = () => { + useEffect(() => { const rs: ChannelName[] = []; - if (!notifyConfig.content) { - return rs; - } - - const chanels = notifyConfig.content as NotifyChannels; - for (const channel of supportedChannels) { - if (chanels[channel.name] && chanels[channel.name].enabled) { - rs.push(channel); + for (const channel of notifyChannelsMap.values()) { + if (supportedChannels[channel.type]?.enabled) { + rs.push({ key: channel.type, label: channel.name }); } } - return rs; - }; + setChannels(rs); + }, [supportedChannels]); const formSchema = z.object({ channel: z.string(), @@ -103,10 +98,10 @@ const NotifyForm = ({ data }: NotifyFormProps) => {
{t(`${i18nPrefix}.channel.label`)}
- initChannels()} /> + fetchChannels()} />
@@ -126,8 +121,8 @@ const NotifyForm = ({ data }: NotifyFormProps) => { - {chanels.map((item) => ( - + {channels.map((item) => ( +
{t(item.label)}
))} diff --git a/ui/src/components/workflow/WorkflowProvider.tsx b/ui/src/components/workflow/WorkflowProvider.tsx index 5e260348..1a44d266 100644 --- a/ui/src/components/workflow/WorkflowProvider.tsx +++ b/ui/src/components/workflow/WorkflowProvider.tsx @@ -1,14 +1,9 @@ import React from "react"; -import { NotifyProvider } from "@/providers/notify"; import { PanelProvider } from "./PanelProvider"; const WorkflowProvider = ({ children }: { children: React.ReactNode }) => { - return ( - - {children} - - ); + return {children}; }; export default WorkflowProvider; diff --git a/ui/src/domain/settings.ts b/ui/src/domain/settings.ts index c78ad13a..e6ee60bc 100644 --- a/ui/src/domain/settings.ts +++ b/ui/src/domain/settings.ts @@ -1,71 +1,64 @@ +export const SETTINGS_NAME_EMAILS = "emails" as const; +export const SETTINGS_NAME_NOTIFYTEMPLATES = "notifyTemplates" as const; +export const SETTINGS_NAME_NOTIFYCHANNELS = "notifyChannels" as const; +export const SETTINGS_NAME_SSLPROVIDER = "sslProvider" as const; +export const SETTINGS_NAMES = Object.freeze({ + EMAILS: SETTINGS_NAME_EMAILS, + NOTIFY_TEMPLATES: SETTINGS_NAME_NOTIFYTEMPLATES, + NOTIFY_CHANNELS: SETTINGS_NAME_NOTIFYCHANNELS, + SSL_PROVIDER: SETTINGS_NAME_SSLPROVIDER, +} as const); + export interface SettingsModel extends BaseModel { name: string; content: T; } +// #region Settings: Emails export type EmailsSettingsContent = { emails: string[]; }; +// #endregion -export type NotifyTemplates = { +// #region Settings: NotifyTemplates +export type NotifyTemplatesSettingsContent = { notifyTemplates: NotifyTemplate[]; }; export type NotifyTemplate = { - title: string; - content: string; + subject: string; + message: string; }; -export type NotifyChannels = { - [key: string]: NotifyChannel; +export const defaultNotifyTemplate: NotifyTemplate = { + subject: "您有 {COUNT} 张证书即将过期", + message: "有 {COUNT} 张证书即将过期,域名分别为 {DOMAINS},请保持关注!", +}; +// #endregion + +// #region Settings: NotifyChannels +export type NotifyChannelsSettingsContent = { + /* + 注意:如果追加新的类型,请保持以 ASCII 排序。 + NOTICE: If you add new type, please keep ASCII order. + */ + [key: string]: ({ enabled?: boolean } & Record) | undefined; + bark?: BarkNotifyChannelConfig; + dingtalk?: DingTalkNotifyChannelConfig; + email?: EmailNotifyChannelConfig; + lark?: LarkNotifyChannelConfig; + serverchan?: ServerChanNotifyChannelConfig; + telegram?: TelegramNotifyChannelConfig; + webhook?: WebhookNotifyChannelConfig; }; -export type NotifyChannel = - | NotifyChannelEmail - | NotifyChannelWebhook - | NotifyChannelDingTalk - | NotifyChannelLark - | NotifyChannelTelegram - | NotifyChannelServerChan - | NotifyChannelBark; - -type ChannelLabel = { - name: string; - label: string; +export type BarkNotifyChannelConfig = { + deviceKey: string; + serverUrl: string; + enabled?: boolean; }; -export const channels: ChannelLabel[] = [ - { - name: "dingtalk", - label: "common.notifier.dingtalk", - }, - { - name: "lark", - label: "common.notifier.lark", - }, - { - name: "telegram", - label: "common.notifier.telegram", - }, - { - name: "webhook", - label: "common.notifier.webhook", - }, - { - name: "serverchan", - label: "common.notifier.serverchan", - }, - { - name: "email", - label: "common.notifier.email", - }, - { - name: "bark", - label: "common.notifier.bark", - }, -]; -export const channelLabelMap: Map = new Map(channels.map((item) => [item.name, item])); -export type NotifyChannelEmail = { +export type EmailNotifyChannelConfig = { smtpHost: string; smtpPort: number; smtpTLS: boolean; @@ -73,47 +66,55 @@ export type NotifyChannelEmail = { password: string; senderAddress: string; receiverAddress: string; - enabled: boolean; + enabled?: boolean; }; -export type NotifyChannelWebhook = { - url: string; - enabled: boolean; -}; - -export type NotifyChannelDingTalk = { +export type DingTalkNotifyChannelConfig = { accessToken: string; secret: string; - enabled: boolean; + enabled?: boolean; }; -export type NotifyChannelLark = { +export type LarkNotifyChannelConfig = { webhookUrl: string; - enabled: boolean; + enabled?: boolean; }; -export type NotifyChannelTelegram = { +export type ServerChanNotifyChannelConfig = { + url: string; + enabled?: boolean; +}; + +export type TelegramNotifyChannelConfig = { apiToken: string; chatId: string; - enabled: boolean; + enabled?: boolean; }; -export type NotifyChannelServerChan = { +export type WebhookNotifyChannelConfig = { url: string; - enabled: boolean; + enabled?: boolean; }; -export type NotifyChannelBark = { - deviceKey: string; - serverUrl: string; - enabled: boolean; +export type NotifyChannel = { + type: string; + name: string; }; -export const defaultNotifyTemplate: NotifyTemplate = { - title: "您有 {COUNT} 张证书即将过期", - content: "有 {COUNT} 张证书即将过期,域名分别为 {DOMAINS},请保持关注!", -}; +export const notifyChannelsMap: Map = new Map( + [ + ["email", "common.notifier.email"], + ["dingtalk", "common.notifier.dingtalk"], + ["lark", "common.notifier.lark"], + ["telegram", "common.notifier.telegram"], + ["serverchan", "common.notifier.serverchan"], + ["bark", "common.notifier.bark"], + ["webhook", "common.notifier.webhook"], + ].map(([type, name]) => [type, { type, name }]) +); +// #endregion +// #region Settings: SSLProvider export type SSLProvider = "letsencrypt" | "zerossl" | "gts"; export type SSLProviderSetting = { @@ -124,3 +125,4 @@ export type SSLProviderSetting = { }; }; }; +// #endregion diff --git a/ui/src/i18n/locales/en/nls.settings.json b/ui/src/i18n/locales/en/nls.settings.json index bb299557..3f02a3ae 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -16,47 +16,58 @@ "settings.password.form.password.errmsg.not_matched": "Passwords do not match", "settings.notification.tab": "Notification", - "settings.notification.template.label": "Template", - "settings.notification.template.saved.message": "Notification template saved successfully", - "settings.notification.template.variables.tips.title": "Optional variables ({COUNT}: number of expiring soon)", - "settings.notification.template.variables.tips.content": "Optional variables ({COUNT}: number of expiring soon. {DOMAINS}: Domain list)", - "settings.notification.config.enable": "Enable", - "settings.notification.config.saved.message": "Configuration saved successfully", - "settings.notification.config.failed.message": "Configuration save failed", - "settings.notification.push_test_message": "Send test notification", - "settings.notification.push_test_message.succeeded.message": "Send test notification successfully", - "settings.notification.push_test_message.failed.message": "Send test notification failed", - "settings.notification.email.smtp_host.label": "SMTP Host", - "settings.notification.email.smtp_host.placeholder": "Please enter SMTP host", - "settings.notification.email.smtp_port.label": "SMTP Port", - "settings.notification.email.smtp_port.placeholder": "Please enter SMTP port", - "settings.notification.email.smtp_tls.label": "Use TLS/SSL", - "settings.notification.email.username.label": "Username", - "settings.notification.email.username.placeholder": "please enter username", - "settings.notification.email.password.label": "Password", - "settings.notification.email.password.placeholder": "please enter password", - "settings.notification.email.sender_address.label": "Sender Email Address", - "settings.notification.email.sender_address.placeholder": "Please enter sender email address", - "settings.notification.email.receiver_address.label": "Receiver Email Address", - "settings.notification.email.receiver_address.placeholder": "Please enter receiver email address", - "settings.notification.webhook.url.label": "Webhook URL", - "settings.notification.webhook.url.placeholder": "Please enter Webhook URL", - "settings.notification.dingtalk.access_token.label": "AccessToken", - "settings.notification.dingtalk.access_token.placeholder": "Please enter access token", - "settings.notification.dingtalk.secret.label": "Secret", - "settings.notification.dingtalk.secret.placeholder": "Please enter secret", - "settings.notification.lark.webhook_url.label": "Webhook URL", - "settings.notification.lark.webhook_url.placeholder": "Please enter Webhook URL", - "settings.notification.telegram.api_token.label": "API Token", - "settings.notification.telegram.api_token.placeholder": "Please enter API token", - "settings.notification.telegram.chat_id.label": "Chat ID", - "settings.notification.telegram.chat_id.placeholder": "Please enter Telegram chat ID", - "settings.notification.serverchan.url.label": "Server URL", - "settings.notification.serverchan.url.placeholder": "Please enter server URL (e.g. https://sctapi.ftqq.com/*****.send)", - "settings.notification.bark.server_url.label": "Server URL", - "settings.notification.bark.server_url.placeholder": "Please enter server URL (e.g. https://your-bark-server.com. Leave it blank to use the bark default server)", - "settings.notification.bark.device_key.label": "Device Key", - "settings.notification.bark.device_key.placeholder": "Please enter device key", + "settings.notification.template.card.title": "Template", + "settings.notification.template.form.subject.label": "Subject", + "settings.notification.template.form.subject.placeholder": "Please enter notification subject", + "settings.notification.template.form.subject.tooltip": "Optional variables ({COUNT}: number of expiring soon)", + "settings.notification.template.form.message.label": "Message", + "settings.notification.template.form.message.placeholder": "Please enter notification message", + "settings.notification.template.form.message.tooltip": "Optional variables ({COUNT}: number of expiring soon. {DOMAINS}: Domain list)", + "settings.notification.channels.card.title": "Channels", + "settings.notification.channel.enabled.on": "On", + "settings.notification.channel.enabled.off": "Off", + "settings.notification.push_test.button": "Send Test Notification", + "settings.notification.push_test.tooltip": "Note: Please save settings before testing push.", + "settings.notification.push_test.pushed": "Sent", + "settings.notification.channel.form.bark_server_url.label": "Server URL", + "settings.notification.channel.form.bark_server_url.placeholder": "Please enter server URL", + "settings.notification.channel.form.bark_server_url.tooltip": "For more information, see
https://bark.day.app/

Leave blank to use the default Bark server.", + "settings.notification.channel.form.bark_device_key.label": "Device Key", + "settings.notification.channel.form.bark_device_key.placeholder": "Please enter device key", + "settings.notification.channel.form.bark_device_key.tooltip": "For more information, see https://bark.day.app/", + "settings.notification.channel.form.dingtalk_access_token.label": "Robot AccessToken", + "settings.notification.channel.form.dingtalk_access_token.placeholder": "Please enter Robot Access Token", + "settings.notification.channel.form.dingtalk_access_token.tooltip": "For more information, see https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages", + "settings.notification.channel.form.dingtalk_secret.label": "Robot Secret", + "settings.notification.channel.form.dingtalk_secret.placeholder": "Please enter Robot Secret", + "settings.notification.channel.form.dingtalk_secret.tooltip": "For more information, see https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages", + "settings.notification.channel.form.email_smtp_host.label": "SMTP Host", + "settings.notification.channel.form.email_smtp_host.placeholder": "Please enter SMTP host", + "settings.notification.channel.form.email_smtp_port.label": "SMTP Port", + "settings.notification.channel.form.email_smtp_port.placeholder": "Please enter SMTP port", + "settings.notification.channel.form.email_smtp_tls.label": "Use TLS/SSL", + "settings.notification.channel.form.email_username.label": "Username", + "settings.notification.channel.form.email_username.placeholder": "please enter username", + "settings.notification.channel.form.email_password.label": "Password", + "settings.notification.channel.form.email_password.placeholder": "please enter password", + "settings.notification.channel.form.email_sender_address.label": "Sender Email Address", + "settings.notification.channel.form.email_sender_address.placeholder": "Please enter sender email address", + "settings.notification.channel.form.email_receiver_address.label": "Receiver Email Address", + "settings.notification.channel.form.email_receiver_address.placeholder": "Please enter receiver email address", + "settings.notification.channel.form.lark_webhook_url.label": "Webhook URL", + "settings.notification.channel.form.lark_webhook_url.placeholder": "Please enter Webhook URL", + "settings.notification.channel.form.lark_webhook_url.tooltip": "For more information, see https://www.feishu.cn/hc/en-US/articles/807992406756", + "settings.notification.channel.form.serverchan_url.label": "Server URL", + "settings.notification.channel.form.serverchan_url.placeholder": "Please enter ServerChan server URL (e.g. https://sctapi.ftqq.com/*****.send)", + "settings.notification.channel.form.serverchan_url.tooltip": "For more information, see https://sct.ftqq.com/forward", + "settings.notification.channel.form.telegram_api_token.label": "Bot API Token", + "settings.notification.channel.form.telegram_api_token.placeholder": "Please enter bot API token", + "settings.notification.channel.form.telegram_api_token.tooltip": "For more information, see https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a", + "settings.notification.channel.form.telegram_chat_id.label": "Chat ID", + "settings.notification.channel.form.telegram_chat_id.placeholder": "Please enter chat ID", + "settings.notification.channel.form.telegram_chat_id.tooltip": "For more information, see https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a", + "settings.notification.channel.form.webhook_url.label": "Webhook URL", + "settings.notification.channel.form.webhook_url.placeholder": "Please enter Webhook URL", "settings.ca.tab": "Certificate Authority", "settings.ca.provider.errmsg.empty": "Please select a Certificate Authority", diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index e2d9f67f..3ae5f6dc 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -68,5 +68,5 @@ "workflow.node.notify.form.content.placeholder": "Please enter content", "workflow.node.notify.form.channel.label": "Channel", "workflow.node.notify.form.channel.placeholder": "Please select channel", - "workflow.node.notify.form.settingChannel.label": "Setting Channel" + "workflow.node.notify.form.settingChannel.label": "Configure Channels" } diff --git a/ui/src/i18n/locales/zh/nls.settings.json b/ui/src/i18n/locales/zh/nls.settings.json index 02ab610a..ddbff40e 100644 --- a/ui/src/i18n/locales/zh/nls.settings.json +++ b/ui/src/i18n/locales/zh/nls.settings.json @@ -16,47 +16,58 @@ "settings.password.form.password.errmsg.not_matched": "两次密码不一致", "settings.notification.tab": "消息推送", - "settings.notification.template.label": "内容模板", - "settings.notification.template.saved.message": "通知模板保存成功", - "settings.notification.template.variables.tips.title": "可选的变量({COUNT}: 即将过期张数)", - "settings.notification.template.variables.tips.content": "可选的变量({COUNT}: 即将过期张数;{DOMAINS}: 域名列表)", - "settings.notification.config.enable": "是否启用", - "settings.notification.config.saved.message": "配置保存成功", - "settings.notification.config.failed.message": "配置保存失败", - "settings.notification.push_test_message": "推送测试消息", - "settings.notification.push_test_message.failed.message": "推送测试消息失败", - "settings.notification.push_test_message.succeeded.message": "推送测试消息成功", - "settings.notification.email.smtp_host.label": "SMTP 服务器地址", - "settings.notification.email.smtp_host.placeholder": "请输入 SMTP 服务器地址", - "settings.notification.email.smtp_port.label": "SMTP 服务器端口", - "settings.notification.email.smtp_port.placeholder": "请输入 SMTP 服务器端口", - "settings.notification.email.smtp_tls.label": "TLS/SSL 连接", - "settings.notification.email.username.label": "用户名", - "settings.notification.email.username.placeholder": "请输入用户名", - "settings.notification.email.password.label": "密码", - "settings.notification.email.password.placeholder": "请输入密码", - "settings.notification.email.sender_address.label": "发送邮箱地址", - "settings.notification.email.sender_address.placeholder": "请输入发送邮箱地址", - "settings.notification.email.receiver_address.label": "接收邮箱地址", - "settings.notification.email.receiver_address.placeholder": "请输入接收邮箱地址", - "settings.notification.webhook.url.label": "Webhook 回调地址", - "settings.notification.webhook.url.placeholder": "请输入 Webhook 回调地址", - "settings.notification.dingtalk.access_token.label": "AccessToken", - "settings.notification.dingtalk.access_token.placeholder": "请输入 AccessToken", - "settings.notification.dingtalk.secret.label": "签名密钥", - "settings.notification.dingtalk.secret.placeholder": "请输入签名密钥", - "settings.notification.lark.webhook_url.label": "Webhook URL", - "settings.notification.lark.webhook_url.placeholder": "请输入 Webhook URL", - "settings.notification.telegram.api_token.label": "API Token", - "settings.notification.telegram.api_token.placeholder": "请输入 API token", - "settings.notification.telegram.chat_id.label": "会话 ID", - "settings.notification.telegram.chat_id.placeholder": "请输入 Telegram 会话 ID", - "settings.notification.serverchan.url.label": "服务器 URL", - "settings.notification.serverchan.url.placeholder": "请输入服务器 URL(形如: https://sctapi.ftqq.com/*****.send)", - "settings.notification.bark.server_url.label": "服务器 URL", - "settings.notification.bark.server_url.placeholder": "请输入服务器 URL(形如: https://your-bark-server.com;留空则使用 Bark 默认服务器)", - "settings.notification.bark.device_key.label": "设备密钥", - "settings.notification.bark.device_key.placeholder": "请输入设备密钥", + "settings.notification.template.card.title": "通知模板", + "settings.notification.template.form.subject.label": "通知主题", + "settings.notification.template.form.subject.placeholder": "请输入通知主题", + "settings.notification.template.form.subject.tooltip": "可选的变量({COUNT}: 即将过期张数)", + "settings.notification.template.form.message.label": "通知内容", + "settings.notification.template.form.message.placeholder": "请输入通知内容", + "settings.notification.template.form.message.tooltip": "可选的变量({COUNT}: 即将过期张数;{DOMAINS}: 域名列表)", + "settings.notification.channels.card.title": "通知渠道", + "settings.notification.channel.enabled.on": "启用", + "settings.notification.channel.enabled.off": "未启用", + "settings.notification.push_test.button": "推送测试消息", + "settings.notification.push_test.tooltip": "提示:修改后请先保存设置再测试推送。", + "settings.notification.push_test.pushed": "已推送", + "settings.notification.channel.form.bark_server_url.label": "服务器地址", + "settings.notification.channel.form.bark_server_url.placeholder": "请输入服务器地址", + "settings.notification.channel.form.bark_server_url.tooltip": "这是什么?请参阅 https://bark.day.app/

为空时,将使用 Bark 默认服务器。", + "settings.notification.channel.form.bark_device_key.label": "设备密钥", + "settings.notification.channel.form.bark_device_key.placeholder": "请输入设备密钥", + "settings.notification.channel.form.bark_device_key.tooltip": "这是什么?请参阅 https://bark.day.app/", + "settings.notification.channel.form.dingtalk_access_token.label": "机器人 AccessToken", + "settings.notification.channel.form.dingtalk_access_token.placeholder": "请输入机器人 AccessToken", + "settings.notification.channel.form.dingtalk_access_token.tooltip": "这是什么?请参阅 https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages", + "settings.notification.channel.form.dingtalk_secret.label": "机器人加签密钥", + "settings.notification.channel.form.dingtalk_secret.placeholder": "请输入机器人加签密钥", + "settings.notification.channel.form.dingtalk_secret.tooltip": "这是什么?请参阅 https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages", + "settings.notification.channel.form.email_smtp_host.label": "SMTP 服务器地址", + "settings.notification.channel.form.email_smtp_host.placeholder": "请输入 SMTP 服务器地址", + "settings.notification.channel.form.email_smtp_port.label": "SMTP 服务器端口", + "settings.notification.channel.form.email_smtp_port.placeholder": "请输入 SMTP 服务器端口", + "settings.notification.channel.form.email_smtp_tls.label": "TLS/SSL 连接", + "settings.notification.channel.form.email_username.label": "用户名", + "settings.notification.channel.form.email_username.placeholder": "请输入用户名", + "settings.notification.channel.form.email_password.label": "密码", + "settings.notification.channel.form.email_password.placeholder": "请输入密码", + "settings.notification.channel.form.email_sender_address.label": "发送邮箱地址", + "settings.notification.channel.form.email_sender_address.placeholder": "请输入发送邮箱地址", + "settings.notification.channel.form.email_receiver_address.label": "接收邮箱地址", + "settings.notification.channel.form.email_receiver_address.placeholder": "请输入接收邮箱地址", + "settings.notification.channel.form.lark_webhook_url.label": "Webhook 地址", + "settings.notification.channel.form.lark_webhook_url.placeholder": "请输入 Webhook 地址", + "settings.notification.channel.form.lark_webhook_url.tooltip": "这是什么?请参阅 https://www.feishu.cn/hc/zh-CN/articles/807992406756", + "settings.notification.channel.form.serverchan_url.label": "服务器地址", + "settings.notification.channel.form.serverchan_url.placeholder": "请输入服务器地址(形如: https://sctapi.ftqq.com/*****.send)", + "settings.notification.channel.form.serverchan_url.tooltip": "这是什么?请参阅 https://sct.ftqq.com/forward", + "settings.notification.channel.form.telegram_api_token.label": "机器人 API Token", + "settings.notification.channel.form.telegram_api_token.placeholder": "请输入机器人 API token", + "settings.notification.channel.form.telegram_api_token.tooltip": "这是什么?请参阅 https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a", + "settings.notification.channel.form.telegram_chat_id.label": "会话 ID", + "settings.notification.channel.form.telegram_chat_id.placeholder": "请输入会话 ID", + "settings.notification.channel.form.telegram_chat_id.tooltip": "这是什么?请参阅 https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a", + "settings.notification.channel.form.webhook_url.label": "Webhook 回调地址", + "settings.notification.channel.form.webhook_url.placeholder": "请输入 Webhook 回调地址", "settings.ca.tab": "证书颁发机构(CA)", "settings.ca.provider.errmsg.empty": "请选择证书分发机构", diff --git a/ui/src/pages/settings/Notification.tsx b/ui/src/pages/settings/Notification.tsx deleted file mode 100644 index 02dd727f..00000000 --- a/ui/src/pages/settings/Notification.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useTranslation } from "react-i18next"; - -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; -import DingTalk from "@/components/notify/DingTalk"; -import Lark from "@/components/notify/Lark"; -import NotifyTemplate from "@/components/notify/NotifyTemplate"; -import Telegram from "@/components/notify/Telegram"; -import Webhook from "@/components/notify/Webhook"; -import ServerChan from "@/components/notify/ServerChan"; -import Email from "@/components/notify/Email"; -import Bark from "@/components/notify/Bark"; -import { NotifyProvider } from "@/providers/notify"; - -const Notification = () => { - const { t } = useTranslation(); - - return ( - <> - -
- - - {t("settings.notification.template.label")} - - - - - -
- -
- - - {t("common.notifier.email")} - - - - - - - {t("common.notifier.webhook")} - - - - - - - {t("common.notifier.dingtalk")} - - - - - - - {t("common.notifier.lark")} - - - - - - - {t("common.notifier.telegram")} - - - - - - - {t("common.notifier.serverchan")} - - - - - - - {t("common.notifier.bark")} - - - - - -
-
- - ); -}; - -export default Notification; diff --git a/ui/src/pages/settings/SSLProvider.tsx b/ui/src/pages/settings/SSLProvider.tsx index 34477613..baafadaa 100644 --- a/ui/src/pages/settings/SSLProvider.tsx +++ b/ui/src/pages/settings/SSLProvider.tsx @@ -3,6 +3,7 @@ import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; +import { produce } from "immer"; import { cn } from "@/components/ui/utils"; import { Button } from "@/components/ui/button"; @@ -11,10 +12,9 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { useToast } from "@/components/ui/use-toast"; -import { getErrMsg } from "@/utils/error"; -import { SSLProvider as SSLProviderType, SSLProviderSetting, SettingsModel } from "@/domain/settings"; +import { SETTINGS_NAMES, SSLProvider as SSLProviderType, SSLProviderSetting, SettingsModel } from "@/domain/settings"; import { get, save } from "@/repository/settings"; -import { produce } from "immer"; +import { getErrMsg } from "@/utils/error"; type SSLProviderContext = { setting: SettingsModel; @@ -28,6 +28,16 @@ export const useSSLProviderContext = () => { return useContext(Context); }; +const getConfigStr = (content: SSLProviderSetting, kind: string, key: string) => { + if (!content.config) { + return ""; + } + if (!content.config[kind]) { + return ""; + } + return content.config[kind][key] ?? ""; +}; + const SSLProvider = () => { const { t } = useTranslation(); @@ -42,7 +52,7 @@ const SSLProvider = () => { useEffect(() => { const fetchData = async () => { - const setting = await get("ssl-provider"); + const setting = await get(SETTINGS_NAMES.SSL_PROVIDER); if (setting) { setConfig(setting); @@ -95,7 +105,7 @@ const SSLProvider = () => { return ( <> -
+
{ case "zerossl": return ; case "gts": - return ; + return ; default: return ; } @@ -160,16 +170,6 @@ const SSLProviderForm = ({ kind }: { kind: string }) => { ); }; -const getConfigStr = (content: SSLProviderSetting, kind: string, key: string) => { - if (!content.config) { - return ""; - } - if (!content.config[kind]) { - return ""; - } - return content.config[kind][key] ?? ""; -}; - const SSLProviderLetsEncryptForm = () => { const { t } = useTranslation(); @@ -227,6 +227,7 @@ const SSLProviderLetsEncryptForm = () => { ); }; + const SSLProviderZeroSSLForm = () => { const { t } = useTranslation(); @@ -334,7 +335,7 @@ const SSLProviderZeroSSLForm = () => { ); }; -const SSLProviderGtsForm = () => { +const SSLProviderGoogleTrustServicesForm = () => { const { t } = useTranslation(); const { setting, onSubmit } = useSSLProviderContext(); diff --git a/ui/src/pages/settings/Settings.tsx b/ui/src/pages/settings/Settings.tsx index 94939a68..9b70a454 100644 --- a/ui/src/pages/settings/Settings.tsx +++ b/ui/src/pages/settings/Settings.tsx @@ -5,8 +5,6 @@ import { Card, Space } from "antd"; import { PageHeader } from "@ant-design/pro-components"; import { KeyRound as KeyRoundIcon, Megaphone as MegaphoneIcon, ShieldCheck as ShieldCheckIcon, UserRound as UserRoundIcon } from "lucide-react"; -import { Toaster } from "@/components/ui/toaster"; - const Settings = () => { const location = useLocation(); const navigate = useNavigate(); @@ -73,7 +71,6 @@ const Settings = () => { navigate(`/settings/${key}`); }} > - diff --git a/ui/src/pages/settings/SettingsAccount.tsx b/ui/src/pages/settings/SettingsAccount.tsx index f2b7fb53..3a2a8ce0 100644 --- a/ui/src/pages/settings/SettingsAccount.tsx +++ b/ui/src/pages/settings/SettingsAccount.tsx @@ -47,7 +47,7 @@ const SettingsAccount = () => { navigate("/login"); }, 500); } catch (err) { - notificationApi.error({ message: t("common.text.request_error"), description: <>{getErrMsg(err)} }); + notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); } finally { setFormPending(false); } @@ -58,7 +58,7 @@ const SettingsAccount = () => { {MessageContextHolder} {NotificationContextHolder} -
+
diff --git a/ui/src/pages/settings/SettingsNotification.tsx b/ui/src/pages/settings/SettingsNotification.tsx new file mode 100644 index 00000000..555af3a1 --- /dev/null +++ b/ui/src/pages/settings/SettingsNotification.tsx @@ -0,0 +1,30 @@ +import { useTranslation } from "react-i18next"; +import { Card, Divider } from "antd"; + +import NotifyChannels from "@/components/notification/NotifyChannels"; +import NotifyTemplate from "@/components/notification/NotifyTemplate"; +import { useNotifyChannelStore } from "@/stores/notify"; + +const SettingsNotification = () => { + const { t } = useTranslation(); + + const { initialized } = useNotifyChannelStore(); + + return ( +
+ +
+ +
+
+ + + + + + +
+ ); +}; + +export default SettingsNotification; diff --git a/ui/src/pages/settings/SettingsPassword.tsx b/ui/src/pages/settings/SettingsPassword.tsx index 01dd3a56..39dfa84b 100644 --- a/ui/src/pages/settings/SettingsPassword.tsx +++ b/ui/src/pages/settings/SettingsPassword.tsx @@ -60,7 +60,7 @@ const SettingsPassword = () => { navigate("/login"); }, 500); } catch (err) { - notificationApi.error({ message: t("common.text.request_error"), description: <>{getErrMsg(err)} }); + notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); } finally { setFormPending(false); } @@ -71,7 +71,7 @@ const SettingsPassword = () => { {MessageContextHolder} {NotificationContextHolder} -
+
diff --git a/ui/src/providers/notify/index.tsx b/ui/src/providers/notify/index.tsx deleted file mode 100644 index f90b6b63..00000000 --- a/ui/src/providers/notify/index.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { ReactNode, useContext, createContext, useEffect, useReducer, useCallback } from "react"; - -import { NotifyChannel, NotifyChannels, SettingsModel } from "@/domain/settings"; -import { get } from "@/repository/settings"; -import { notifyReducer } from "./reducer"; - -export type NotifyContext = { - config: SettingsModel; - setChannel: (data: { channel: string; data: NotifyChannel }) => void; - setChannels: (data: SettingsModel) => void; - initChannels: () => void; -}; - -const Context = createContext({} as NotifyContext); - -export const useNotifyContext = () => useContext(Context); - -interface NotifyProviderProps { - children: ReactNode; -} - -export const NotifyProvider = ({ children }: NotifyProviderProps) => { - const [notify, dispatchNotify] = useReducer(notifyReducer, {} as SettingsModel); - - useEffect(() => { - featchData(); - }, []); - - const featchData = async () => { - const chanels = await get("notifyChannels"); - dispatchNotify({ - type: "SET_CHANNELS", - payload: chanels, - }); - }; - - const initChannels = useCallback(() => { - featchData(); - }, []); - - const setChannel = useCallback((data: { channel: string; data: NotifyChannel }) => { - dispatchNotify({ - type: "SET_CHANNEL", - payload: data, - }); - }, []); - - const setChannels = useCallback((setting: SettingsModel) => { - dispatchNotify({ - type: "SET_CHANNELS", - payload: setting, - }); - }, []); - - return ( - - {children} - - ); -}; diff --git a/ui/src/providers/notify/reducer.tsx b/ui/src/providers/notify/reducer.tsx deleted file mode 100644 index cea2092c..00000000 --- a/ui/src/providers/notify/reducer.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { NotifyChannel, NotifyChannels, SettingsModel } from "@/domain/settings"; - -type Action = - | { - type: "SET_CHANNEL"; - payload: { - channel: string; - data: NotifyChannel; - }; - } - | { - type: "SET_CHANNELS"; - payload: SettingsModel; - }; - -export const notifyReducer = (state: SettingsModel, action: Action) => { - switch (action.type) { - case "SET_CHANNEL": { - const channel = action.payload.channel; - return { - ...state, - content: { - ...state.content, - [channel]: action.payload.data, - }, - }; - } - case "SET_CHANNELS": { - return { ...action.payload }; - } - - default: - return state; - } -}; diff --git a/ui/src/repository/settings.ts b/ui/src/repository/settings.ts index 62772d2a..7bd2aec0 100644 --- a/ui/src/repository/settings.ts +++ b/ui/src/repository/settings.ts @@ -1,15 +1,23 @@ -import { type SettingsModel } from "@/domain/settings"; +import { ClientResponseError } from "pocketbase"; + +import { SETTINGS_NAMES, type SettingsModel } from "@/domain/settings"; import { getPocketBase } from "./pocketbase"; -export const get = async (name: string) => { +export const get = async (name: (typeof SETTINGS_NAMES)[keyof typeof SETTINGS_NAMES]) => { try { - const resp = await getPocketBase().collection("settings").getFirstListItem>(`name='${name}'`); + const resp = await getPocketBase().collection("settings").getFirstListItem>(`name='${name}'`, { + requestKey: null, + }); return resp; - } catch { - return { - name: name, - content: {} as T, - } as SettingsModel; + } catch (err) { + if (err instanceof ClientResponseError && err.status === 404) { + return { + name: name, + content: {} as T, + } as SettingsModel; + } + + throw err; } }; diff --git a/ui/src/router.tsx b/ui/src/router.tsx index 3dbcb36d..ef74a31d 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -11,8 +11,8 @@ import CertificateList from "./pages/certificates/CertificateList"; import Settings from "./pages/settings/Settings"; import SettingsAccount from "./pages/settings/SettingsAccount"; import SettingsPassword from "./pages/settings/SettingsPassword"; -import SettingsNotification from "./pages/settings/Notification"; -import SettingsSSLProvider from "./pages/settings/SSLProvider"; +import SettingsNotification from "./pages/settings/SettingsNotification"; +import SSLProvider from "./pages/settings/SSLProvider"; export const router = createHashRouter([ { @@ -57,7 +57,7 @@ export const router = createHashRouter([ }, { path: "/settings/ssl-provider", - element: , + element: , }, ], }, diff --git a/ui/src/stores/notify/index.ts b/ui/src/stores/notify/index.ts new file mode 100644 index 00000000..9a53b63c --- /dev/null +++ b/ui/src/stores/notify/index.ts @@ -0,0 +1,57 @@ +import { create } from "zustand"; +import { produce } from "immer"; + +import { SETTINGS_NAMES, type NotifyChannelsSettingsContent, type SettingsModel } from "@/domain/settings"; +import { get as getSettings, save as saveSettings } from "@/repository/settings"; + +export interface NotifyChannelState { + initialized: boolean; + channels: NotifyChannelsSettingsContent; + setChannel: (channel: keyof NotifyChannelsSettingsContent, config: NotifyChannelsSettingsContent[keyof NotifyChannelsSettingsContent]) => void; + setChannels: (channels: NotifyChannelsSettingsContent) => void; + fetchChannels: () => Promise; +} + +export const useNotifyChannelStore = create((set, get) => { + let fetcher: Promise> | null = null; // 防止多次重复请求 + let settings: SettingsModel; // 记录当前设置的其他字段,保存回数据库时用 + + return { + initialized: false, + channels: {}, + + setChannel: async (channel, config) => { + settings ??= await getSettings(SETTINGS_NAMES.NOTIFY_CHANNELS); + return get().setChannels({ + ...settings.content, + [channel]: { ...settings.content[channel], ...config }, + }); + }, + + setChannels: async (channels) => { + settings ??= await getSettings(SETTINGS_NAMES.NOTIFY_CHANNELS); + settings = await saveSettings({ + ...settings, + content: channels, + }); + + set( + produce((state: NotifyChannelState) => { + state.channels = settings.content; + state.initialized = true; + }) + ); + }, + + fetchChannels: async () => { + fetcher ??= getSettings(SETTINGS_NAMES.NOTIFY_CHANNELS); + + try { + settings = await fetcher; + set({ channels: settings.content ?? {}, initialized: true }); + } finally { + fetcher = null; + } + }, + }; +}); diff --git a/ui/src/utils/url.ts b/ui/src/utils/url.ts deleted file mode 100644 index a175f2d4..00000000 --- a/ui/src/utils/url.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function isValidURL(url: string): boolean { - try { - new URL(url); - return true; - } catch (error) { - return false; - } -}