feat(ui): new SettingsNotification using antd

This commit is contained in:
Fu Diwei 2024-12-20 13:56:29 +08:00
parent cae33cfc4f
commit 7c1a2d5f91
60 changed files with 1105 additions and 2450 deletions

View File

@ -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{},

View File

@ -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

View File

@ -50,7 +50,7 @@ const AccessEditForm = forwardRef<AccessEditFormInstance, AccessEditFormProps>((
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 })),

View File

@ -22,6 +22,8 @@ const initModel = () => {
return {
endpoint: "https://example.com/api/",
mode: "",
username: "",
password: "",
} as AccessEditFormACMEHttpReqConfigModelType;
};

View File

@ -20,7 +20,10 @@ export type AccessEditFormAWSConfigProps = {
const initModel = () => {
return {
accessKeyId: "",
secretAccessKey: "",
region: "us-east-1",
hostedZoneId: "",
} as AccessEditFormAWSConfigModelType;
};

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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) => {

View File

@ -20,6 +20,8 @@ export type AccessEditFormHuaweiCloudConfigProps = {
const initModel = () => {
return {
accessKeyId: "",
secretAccessKey: "",
region: "cn-north-1",
} as AccessEditFormHuaweiCloudConfigModelType;
};

View File

@ -59,7 +59,7 @@ const AccessEditFormKubernetesConfig = ({ form, formName, disabled, loading, mod
setKubeFileList([]);
}
flushSync(() => onModelChange?.(form.getFieldsValue()));
flushSync(() => onModelChange?.(form.getFieldsValue(true)));
};
return (

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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) => {

View File

@ -94,7 +94,7 @@ const AccessEditFormSSHConfig = ({ form, formName, disabled, loading, model, onM
setKeyFileList([]);
}
flushSync(() => onModelChange?.(form.getFieldsValue()));
flushSync(() => onModelChange?.(form.getFieldsValue(true)));
};
return (

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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<NotifyChannelEditFormModelType>;
};
const NotifyChannelEditForm = forwardRef<NotifyChannelEditFormInstance, NotifyChannelEditFormProps>(
({ 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 <NotifyChannelEditFormBarkFields />;
case "dingtalk":
return <NotifyChannelEditFormDingTalkFields />;
case "email":
return <NotifyChannelEditFormEmailFields />;
case "lark":
return <NotifyChannelEditFormLarkFields />;
case "serverchan":
return <NotifyChannelEditFormServerChanFields />;
case "telegram":
return <NotifyChannelEditFormTelegramFields />;
case "webhook":
return <NotifyChannelEditFormWebhookFields />;
}
}, [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 (
<Form
className={className}
style={style}
form={form}
disabled={loading || disabled}
initialValues={initialValues}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
{formFieldsComponent}
</Form>
);
}
);
export default NotifyChannelEditForm;

View File

@ -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 (
<>
<Form.Item
name="serverUrl"
label={t("settings.notification.channel.form.bark_server_url.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.bark_server_url.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.bark_server_url.placeholder")} />
</Form.Item>
<Form.Item
name="deviceKey"
label={t("settings.notification.channel.form.bark_device_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.bark_device_key.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.bark_device_key.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormBarkFields;

View File

@ -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 (
<>
<Form.Item
name="accessToken"
label={t("settings.notification.channel.form.dingtalk_access_token.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.dingtalk_access_token.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("settings.notification.channel.form.dingtalk_access_token.placeholder")} />
</Form.Item>
<Form.Item
name="secret"
label={t("settings.notification.channel.form.dingtalk_secret.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.dingtalk_secret.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("settings.notification.channel.form.dingtalk_secret.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormDingTalkFields;

View File

@ -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 (
<>
<div className="flex space-x-2">
<div className="w-2/5">
<Form.Item name="smtpHost" label={t("settings.notification.channel.form.email_smtp_host.label")} rules={[formRule]}>
<Input placeholder={t("settings.notification.channel.form.email_smtp_host.placeholder")} />
</Form.Item>
</div>
<div className="w-2/5">
<Form.Item name="smtpPort" label={t("settings.notification.channel.form.email_smtp_port.label")} rules={[formRule]} initialValue={465}>
<InputNumber className="w-full" placeholder={t("settings.notification.channel.form.email_smtp_port.placeholder")} min={1} max={65535} />
</Form.Item>
</div>
<div className="w-1/5">
<Form.Item name="smtpTLS" label={t("settings.notification.channel.form.email_smtp_tls.label")} rules={[formRule]} initialValue={true}>
<Switch onChange={handleTLSSwitchChange} />
</Form.Item>
</div>
</div>
<div className="flex space-x-2">
<div className="w-1/2">
<Form.Item name="username" label={t("settings.notification.channel.form.email_username.label")} rules={[formRule]}>
<Input placeholder={t("settings.notification.channel.form.email_username.placeholder")} />
</Form.Item>
</div>
<div className="w-1/2">
<Form.Item name="password" label={t("settings.notification.channel.form.email_password.label")} rules={[formRule]}>
<Input.Password autoComplete="new-password" placeholder={t("settings.notification.channel.form.email_password.placeholder")} />
</Form.Item>
</div>
</div>
<Form.Item name="senderAddress" label={t("settings.notification.channel.form.email_sender_address.label")} rules={[formRule]}>
<Input type="email" placeholder={t("settings.notification.channel.form.email_sender_address.placeholder")} />
</Form.Item>
<Form.Item name="receiverAddress" label={t("settings.notification.channel.form.email_receiver_address.label")} rules={[formRule]}>
<Input type="email" placeholder={t("settings.notification.channel.form.email_receiver_address.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormEmailFields;

View File

@ -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 (
<>
<Form.Item
name="webhookUrl"
label={t("settings.notification.channel.form.lark_webhook_url.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.lark_webhook_url.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.lark_webhook_url.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormLarkFields;

View File

@ -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 (
<>
<Form.Item
name="url"
label={t("settings.notification.channel.form.serverchan_url.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.serverchan_url.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.serverchan_url.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormServerChanFields;

View File

@ -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 (
<>
<Form.Item
name="apiToken"
label={t("settings.notification.channel.form.telegram_api_token.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.telegram_api_token.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("settings.notification.channel.form.telegram_api_token.placeholder")} />
</Form.Item>
<Form.Item
name="chatId"
label={t("settings.notification.channel.form.telegram_chat_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.telegram_chat_id.tooltip") }}></span>}
>
<Input type="number" placeholder={t("settings.notification.channel.form.telegram_chat_id.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormTelegramFields;

View File

@ -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 (
<div>
<Form.Item name="url" label={t("settings.notification.channel.form.webhook_url.label")} rules={[formRule]}>
<Input placeholder={t("settings.notification.channel.form.webhook_url.placeholder")} />
</Form.Item>
</div>
);
};
export default NotifyChannelEditFormWebhookFields;

View File

@ -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<Record<NotifyChannelsSemanticDOM, string>>;
style?: React.CSSProperties;
styles?: Partial<Record<NotifyChannelsSemanticDOM, React.CSSProperties>>;
};
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<Array<NotifyChannelEditFormInstance | null>>([]);
const channelCollapseItems: CollapseProps["items"] = useDeepCompareMemo(
() =>
Array.from(notifyChannelsMap.values()).map((channel, index) => {
return {
key: `channel-${channel.type}`,
label: <>{t(channel.name)}</>,
children: (
<div className={classNames?.form} style={styles?.form}>
<NotifyChannelEditForm ref={(ref) => (channelFormRefs.current[index] = ref)} model={channels[channel.type]} channel={channel.type} />
<Space>
<Button type="primary" onClick={() => handleClickSubmit(channel.type, index)}>
{t("common.button.save")}
</Button>
{channels[channel.type] ? (
<Tooltip title={t("settings.notification.push_test.tooltip")}>
<>
<NotifyTestButton channel={channel.type} />
</>
</Tooltip>
) : null}
</Space>
</div>
),
extra: (
<div onClick={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} onMouseUp={(e) => e.stopPropagation()}>
<Switch
defaultChecked={channels[channel.type]?.enabled as boolean}
disabled={channels[channel.type] == null}
checkedChildren={t("settings.notification.channel.enabled.on")}
unCheckedChildren={t("settings.notification.channel.enabled.off")}
onChange={(checked) => handleSwitchChange(channel.type, checked)}
/>
</div>
),
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 (
<div className={className} style={style}>
{MessageContextHolder}
{NotificationContextHolder}
{!initialized ? (
<Skeleton active />
) : (
<Collapse className={classNames?.collapse} style={styles?.collapse} accordion={true} bordered={false} items={channelCollapseItems} />
)}
</div>
);
};
export default NotifyChannels;

View File

@ -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<z.infer<typeof formSchema>>();
const [formPending, setFormPending] = useState(false);
const [initialValues, setInitialValues] = useState<Partial<z.infer<typeof formSchema>>>();
const [initialChanged, setInitialChanged] = useState(false);
const { loading } = useRequest(
() => {
return getSettings<NotifyTemplatesSettingsContent>(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<typeof formSchema>) => {
setFormPending(true);
try {
const settings = await getSettings<NotifyTemplatesSettingsContent>(SETTINGS_NAMES.NOTIFY_TEMPLATES);
await saveSettings<NotifyTemplatesSettingsContent>({
...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 (
<div className={className} style={style}>
{MessageContextHolder}
{NotificationContextHolder}
{loading ? (
<Skeleton active />
) : (
<Form form={form} disabled={formPending} initialValues={initialValues} layout="vertical" onFinish={handleFormFinish}>
<Form.Item
name="subject"
label={t("settings.notification.template.form.subject.label")}
extra={t("settings.notification.template.form.subject.tooltip")}
rules={[formRule]}
>
<Input placeholder={t("settings.notification.template.form.subject.placeholder")} onChange={handleInputChange} />
</Form.Item>
<Form.Item
name="message"
label={t("settings.notification.template.form.message.label")}
extra={t("settings.notification.template.form.message.tooltip")}
rules={[formRule]}
>
<Input.TextArea
autoSize={{ minRows: 3, maxRows: 5 }}
placeholder={t("settings.notification.template.form.message.placeholder")}
onChange={handleInputChange}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={!initialChanged} loading={formPending}>
{t("common.button.save")}
</Button>
</Form.Item>
</Form>
)}
</div>
);
};
export default NotifyTemplateForm;

View File

@ -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}
<Button className={className} style={style} disabled={disabled} loading={loading} size={size} onClick={handleClick}>
{t("settings.notification.push_test.button")}
</Button>
</>
);
};
export default NotifyTestButton;

View File

@ -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<boolean>(false);
const [bark, setBark] = useState<BarkSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
serverUrl: "",
deviceKey: "",
enabled: false,
},
});
const [originBark, setOriginBark] = useState<BarkSetting>({
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<boolean>(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 (
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.bark.server_url.label")}</Label>
<Input
placeholder={t("settings.notification.bark.server_url.placeholder")}
value={bark.data.serverUrl}
onChange={(e) => {
const newData = {
...bark,
data: {
...bark.data,
serverUrl: e.target.value,
},
};
checkChanged(newData.data);
setBark(newData);
}}
/>
</div>
<div>
<Label>{t("settings.notification.bark.device_key.label")}</Label>
<Input
placeholder={t("settings.notification.bark.device_key.placeholder")}
value={bark.data.deviceKey}
onChange={(e) => {
const newData = {
...bark,
data: {
...bark.data,
deviceKey: e.target.value,
},
};
checkChanged(newData.data);
setBark(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={bark.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.button.save")}
</Button>
</Show>
<Show when={!changed && bark.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default Bark;

View File

@ -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<boolean>(false);
const [dingtalk, setDingtalk] = useState<DingTalkSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
accessToken: "",
secret: "",
enabled: false,
},
});
const [originDingtalk, setOriginDingtalk] = useState<DingTalkSetting>({
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<boolean>(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 (
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.dingtalk.access_token.label")}</Label>
<Input
placeholder={t("settings.notification.dingtalk.access_token.placeholder")}
value={dingtalk.data.accessToken}
onChange={(e) => {
const newData = {
...dingtalk,
data: {
...dingtalk.data,
accessToken: e.target.value,
},
};
checkChanged(newData.data);
setDingtalk(newData);
}}
/>
</div>
<div>
<Label>{t("settings.notification.dingtalk.secret.label")}</Label>
<Input
placeholder={t("settings.notification.dingtalk.secret.placeholder")}
value={dingtalk.data.secret}
onChange={(e) => {
const newData = {
...dingtalk,
data: {
...dingtalk.data,
secret: e.target.value,
},
};
checkChanged(newData.data);
setDingtalk(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={dingtalk.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.button.save")}
</Button>
</Show>
<Show when={!changed && dingtalk.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default DingTalk;

View File

@ -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<boolean>(false);
const [mail, setMail] = useState<EmailSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
smtpHost: "",
smtpPort: 465,
smtpTLS: true,
username: "",
password: "",
senderAddress: "",
receiverAddress: "",
enabled: false,
},
});
const [originMail, setOriginMail] = useState<EmailSetting>({
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<boolean>(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 (
<div className="flex flex-col space-y-4">
<div className="flex space-x-4">
<div className="w-2/5">
<Label>{t("settings.notification.email.smtp_host.label")}</Label>
<Input
placeholder={t("settings.notification.email.smtp_host.placeholder")}
value={mail.data.smtpHost}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
smtpHost: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="w-2/5">
<Label>{t("settings.notification.email.smtp_port.label")}</Label>
<Input
type="number"
placeholder={t("settings.notification.email.smtp_port.placeholder")}
value={mail.data.smtpPort}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
smtpPort: +e.target.value || 0,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="w-1/5">
<Label>{t("settings.notification.email.smtp_tls.label")}</Label>
<Switch
className="block mt-2"
checked={mail.data.smtpTLS}
onCheckedChange={(e) => {
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);
}}
/>
</div>
</div>
<div className="flex space-x-4">
<div className="w-1/2">
<Label>{t("settings.notification.email.username.label")}</Label>
<Input
placeholder={t("settings.notification.email.username.placeholder")}
value={mail.data.username}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
username: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="w-1/2">
<Label>{t("settings.notification.email.password.label")}</Label>
<Input
placeholder={t("settings.notification.email.password.placeholder")}
value={mail.data.password}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
password: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
</div>
<div>
<Label>{t("settings.notification.email.sender_address.label")}</Label>
<Input
placeholder={t("settings.notification.email.sender_address.placeholder")}
value={mail.data.senderAddress}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
senderAddress: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div>
<Label>{t("settings.notification.email.receiver_address.label")}</Label>
<Input
placeholder={t("settings.notification.email.receiver_address.placeholder")}
value={mail.data.receiverAddress}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
receiverAddress: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={mail.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.button.save")}
</Button>
</Show>
<Show when={!changed && mail.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default Mail;

View File

@ -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<boolean>(false);
const [lark, setLark] = useState<LarkSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
webhookUrl: "",
enabled: false,
},
});
const [originLark, setOriginLark] = useState<LarkSetting>({
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<boolean>(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 (
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.lark.webhook_url.label")}</Label>
<Input
placeholder={t("settings.notification.lark.webhook_url.placeholder")}
value={lark.data.webhookUrl}
onChange={(e) => {
const newData = {
...lark,
data: {
...lark.data,
webhookUrl: e.target.value,
},
};
checkChanged(newData.data);
setLark(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={lark.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.button.save")}
</Button>
</Show>
<Show when={!changed && lark.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default Lark;

View File

@ -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<NotifyTemplateT[]>([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 (
<div>
<Input
value={templates?.[0]?.title}
onChange={(e) => {
handleTitleChange(e.target.value);
}}
/>
<div className="text-muted-foreground text-sm mt-1">{t("settings.notification.template.variables.tips.title")}</div>
<Textarea
className="mt-2"
value={templates?.[0]?.content}
onChange={(e) => {
handleContentChange(e.target.value);
}}
></Textarea>
<div className="text-muted-foreground text-sm mt-1">{t("settings.notification.template.variables.tips.content")}</div>
<div className="flex justify-end mt-2">
<Button onClick={handleSaveClick}>{t("common.button.save")}</Button>
</div>
</div>
);
};
export default NotifyTemplate;

View File

@ -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<boolean>(false);
const [serverchan, setServerChan] = useState<ServerChanSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
url: "",
enabled: false,
},
});
const [originServerChan, setOriginServerChan] = useState<ServerChanSetting>({
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<boolean>(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 (
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.serverchan.url.label")}</Label>
<Input
placeholder={t("settings.notification.serverchan.url.placeholder")}
value={serverchan.data.url}
onChange={(e) => {
const newData = {
...serverchan,
data: {
...serverchan.data,
url: e.target.value,
},
};
checkChanged(newData.data);
setServerChan(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={serverchan.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.button.save")}
</Button>
</Show>
<Show when={!changed && serverchan.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default ServerChan;

View File

@ -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<boolean>(false);
const [telegram, setTelegram] = useState<TelegramSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
apiToken: "",
chatId: "",
enabled: false,
},
});
const [originTelegram, setOriginTelegram] = useState<TelegramSetting>({
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<boolean>(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 (
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.telegram.api_token.label")}</Label>
<Input
placeholder={t("settings.notification.telegram.api_token.placeholder")}
value={telegram.data.apiToken}
onChange={(e) => {
const newData = {
...telegram,
data: {
...telegram.data,
apiToken: e.target.value,
},
};
checkChanged(newData.data);
setTelegram(newData);
}}
/>
</div>
<div>
<Label>{t("settings.notification.telegram.chat_id.label")}</Label>
<Input
placeholder={t("settings.notification.telegram.chat_id.placeholder")}
value={telegram.data.chatId}
onChange={(e) => {
const newData = {
...telegram,
data: {
...telegram.data,
chatId: e.target.value,
},
};
checkChanged(newData.data);
setTelegram(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={telegram.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.button.save")}
</Button>
</Show>
<Show when={!changed && telegram.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default Telegram;

View File

@ -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<boolean>(false);
const [webhook, setWebhook] = useState<WebhookSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
url: "",
enabled: false,
},
});
const [originWebhook, setOriginWebhook] = useState<WebhookSetting>({
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<boolean>(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 (
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.webhook.url.label")}</Label>
<Input
placeholder={t("settings.notification.webhook.url.placeholder")}
value={webhook.data.url}
onChange={(e) => {
const newData = {
...webhook,
data: {
...webhook.data,
url: e.target.value,
},
};
checkChanged(newData.data);
setWebhook(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={webhook.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.button.save")}
</Button>
</Show>
<Show when={!changed && webhook.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default Webhook;

View File

@ -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"),
}),

View File

@ -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"),

View File

@ -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 (
<div className="flex space-x-2 items-baseline">
<div className="text-stone-700 w-12 truncate">{t(channelLabel?.label ?? "")}</div>
<div className="text-stone-700 w-12 truncate">{t(channelLabel?.name ?? "")}</div>
<div className="text-muted-foreground truncate">{(data.config?.title as string) ?? ""}</div>
</div>
);

View File

@ -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<ChannelName[]>([]);
const [channels, setChannels] = useState<ChannelName[]>([]);
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) => {
<FormLabel className="flex justify-between items-center">
<div className="flex space-x-2 items-center">
<div>{t(`${i18nPrefix}.channel.label`)}</div>
<RefreshCw size={16} className="cursor-pointer" onClick={() => initChannels()} />
<RefreshCw size={16} className="cursor-pointer" onClick={() => fetchChannels()} />
</div>
<a
href="#/setting/notify"
href="#/settings/notification"
target="_blank"
className="flex justify-between items-center space-x-1 font-normal text-primary hover:underline cursor-pointer"
>
@ -126,8 +121,8 @@ const NotifyForm = ({ data }: NotifyFormProps) => {
</SelectTrigger>
<SelectContent>
<SelectGroup>
{chanels.map((item) => (
<SelectItem key={item.name} value={item.name}>
{channels.map((item) => (
<SelectItem key={item.key} value={item.key}>
<div>{t(item.label)}</div>
</SelectItem>
))}

View File

@ -1,14 +1,9 @@
import React from "react";
import { NotifyProvider } from "@/providers/notify";
import { PanelProvider } from "./PanelProvider";
const WorkflowProvider = ({ children }: { children: React.ReactNode }) => {
return (
<NotifyProvider>
<PanelProvider>{children}</PanelProvider>
</NotifyProvider>
);
return <PanelProvider>{children}</PanelProvider>;
};
export default WorkflowProvider;

View File

@ -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<T> 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<string, unknown>) | 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<string, ChannelLabel> = 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<NotifyChannel["type"], NotifyChannel> = 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

View File

@ -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 <a href=\"https://bark.day.app/\" target=\"_blank\">https://bark.day.app/</a><br><br>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 <a href=\"https://bark.day.app/\" target=\"_blank\">https://bark.day.app/</a>",
"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 <a href=\"https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages\" target=\"_blank\">https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages</a>",
"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 <a href=\"https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages\" target=\"_blank\">https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages</a>",
"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 <a href=\"https://www.feishu.cn/hc/en-US/articles/807992406756\" target=\"_blank\">https://www.feishu.cn/hc/en-US/articles/807992406756</a>",
"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 <a href=\"https://sct.ftqq.com/forward\" target=\"_blank\">https://sct.ftqq.com/forward</a>",
"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 <a href=\"https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a\" target=\"_blank\">https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a</a>",
"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 <a href=\"https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a\" target=\"_blank\">https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a</a>",
"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",

View File

@ -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"
}

View File

@ -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": "这是什么?请参阅 <a href=\"https://bark.day.app/\" target=\"_blank\">https://bark.day.app/</a><br><br>为空时,将使用 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": "这是什么?请参阅 <a href=\"https://bark.day.app/\" target=\"_blank\">https://bark.day.app/</a>",
"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": "这是什么?请参阅 <a href=\"https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages\" target=\"_blank\">https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages</a>",
"settings.notification.channel.form.dingtalk_secret.label": "机器人加签密钥",
"settings.notification.channel.form.dingtalk_secret.placeholder": "请输入机器人加签密钥",
"settings.notification.channel.form.dingtalk_secret.tooltip": "这是什么?请参阅 <a href=\"https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages\" target=\"_blank\">https://open.dingtalk.com/document/orgapp/custom-bot-to-send-group-chat-messages</a>",
"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": "这是什么?请参阅 <a href=\"https://www.feishu.cn/hc/zh-CN/articles/807992406756\" target=\"_blank\">https://www.feishu.cn/hc/zh-CN/articles/807992406756</a>",
"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": "这是什么?请参阅 <a href=\"https://sct.ftqq.com/forward\" target=\"_blank\">https://sct.ftqq.com/forward</a>",
"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": "这是什么?请参阅 <a href=\"https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a\" target=\"_blank\">https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a</a>",
"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": "这是什么?请参阅 <a href=\"https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a\" target=\"_blank\">https://gist.github.com/nafiesl/4ad622f344cd1dc3bb1ecbe468ff9f8a</a>",
"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": "请选择证书分发机构",

View File

@ -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 (
<>
<NotifyProvider>
<div className="border rounded-sm p-5 shadow-lg">
<Accordion type={"multiple"} className="dark:text-stone-200">
<AccordionItem value="item-1" className="dark:border-stone-200">
<AccordionTrigger>{t("settings.notification.template.label")}</AccordionTrigger>
<AccordionContent>
<NotifyTemplate />
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<div className="border rounded-md p-5 mt-7 shadow-lg">
<Accordion type={"single"} collapsible={true} className="dark:text-stone-200">
<AccordionItem value="item-email" className="dark:border-stone-200">
<AccordionTrigger>{t("common.notifier.email")}</AccordionTrigger>
<AccordionContent>
<Email />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-webhook" className="dark:border-stone-200">
<AccordionTrigger>{t("common.notifier.webhook")}</AccordionTrigger>
<AccordionContent>
<Webhook />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-dingtalk" className="dark:border-stone-200">
<AccordionTrigger>{t("common.notifier.dingtalk")}</AccordionTrigger>
<AccordionContent>
<DingTalk />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-lark" className="dark:border-stone-200">
<AccordionTrigger>{t("common.notifier.lark")}</AccordionTrigger>
<AccordionContent>
<Lark />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-telegram" className="dark:border-stone-200">
<AccordionTrigger>{t("common.notifier.telegram")}</AccordionTrigger>
<AccordionContent>
<Telegram />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-serverchan" className="dark:border-stone-200">
<AccordionTrigger>{t("common.notifier.serverchan")}</AccordionTrigger>
<AccordionContent>
<ServerChan />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-bark" className="dark:border-stone-200">
<AccordionTrigger>{t("common.notifier.bark")}</AccordionTrigger>
<AccordionContent>
<Bark />
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</NotifyProvider>
</>
);
};
export default Notification;

View File

@ -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<SSLProviderSetting>;
@ -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<SSLProviderSetting>("ssl-provider");
const setting = await get<SSLProviderSetting>(SETTINGS_NAMES.SSL_PROVIDER);
if (setting) {
setConfig(setting);
@ -95,7 +105,7 @@ const SSLProvider = () => {
return (
<>
<Context.Provider value={{ onSubmit, setConfig, setting: config }}>
<div className="w-full md:max-w-[35em]">
<div className="md:max-w-[40rem]">
<Label className="dark:text-stone-200">{t("common.text.ca")}</Label>
<RadioGroup
className="flex mt-3 dark:text-stone-200"
@ -147,7 +157,7 @@ const SSLProviderForm = ({ kind }: { kind: string }) => {
case "zerossl":
return <SSLProviderZeroSSLForm />;
case "gts":
return <SSLProviderGtsForm />;
return <SSLProviderGoogleTrustServicesForm />;
default:
return <SSLProviderLetsEncryptForm />;
}
@ -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 = () => {
</Form>
);
};
const SSLProviderZeroSSLForm = () => {
const { t } = useTranslation();
@ -334,7 +335,7 @@ const SSLProviderZeroSSLForm = () => {
);
};
const SSLProviderGtsForm = () => {
const SSLProviderGoogleTrustServicesForm = () => {
const { t } = useTranslation();
const { setting, onSubmit } = useSSLProviderContext();

View File

@ -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}`);
}}
>
<Toaster />
<Outlet />
</Card>
</>

View File

@ -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}
<div className="w-full md:max-w-[35em]">
<div className="md:max-w-[40rem]">
<Form form={form} disabled={formPending} initialValues={initialValues} layout="vertical" onFinish={handleFormFinish}>
<Form.Item name="username" label={t("settings.account.form.email.label")} rules={[formRule]}>
<Input placeholder={t("settings.account.form.email.placeholder")} onChange={handleInputChange} />

View File

@ -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 (
<div>
<Card className="shadow" title={t("settings.notification.template.card.title")}>
<div className="md:max-w-[40rem]">
<NotifyTemplate />
</div>
</Card>
<Divider />
<Card className="shadow" styles={{ body: initialized ? { padding: 0 } : {} }} title={t("settings.notification.channels.card.title")}>
<NotifyChannels classNames={{ form: "md:max-w-[40rem]" }} />
</Card>
</div>
);
};
export default SettingsNotification;

View File

@ -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}
<div className="w-full md:max-w-[35em]">
<div className="md:max-w-[40rem]">
<Form form={form} disabled={formPending} layout="vertical" onFinish={handleFormFinish}>
<Form.Item name="oldPassword" label={t("settings.password.form.old_password.label")} rules={[formRule]}>
<Input.Password placeholder={t("settings.password.form.old_password.placeholder")} onChange={handleInputChange} />

View File

@ -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<NotifyChannels>;
setChannel: (data: { channel: string; data: NotifyChannel }) => void;
setChannels: (data: SettingsModel<NotifyChannels>) => 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<NotifyChannels>);
useEffect(() => {
featchData();
}, []);
const featchData = async () => {
const chanels = await get<NotifyChannels>("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<NotifyChannels>) => {
dispatchNotify({
type: "SET_CHANNELS",
payload: setting,
});
}, []);
return (
<Context.Provider
value={{
config: notify,
setChannel,
setChannels,
initChannels,
}}
>
{children}
</Context.Provider>
);
};

View File

@ -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<NotifyChannels>;
};
export const notifyReducer = (state: SettingsModel<NotifyChannels>, 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;
}
};

View File

@ -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 <T>(name: string) => {
export const get = async <T>(name: (typeof SETTINGS_NAMES)[keyof typeof SETTINGS_NAMES]) => {
try {
const resp = await getPocketBase().collection("settings").getFirstListItem<SettingsModel<T>>(`name='${name}'`);
const resp = await getPocketBase().collection("settings").getFirstListItem<SettingsModel<T>>(`name='${name}'`, {
requestKey: null,
});
return resp;
} catch {
return {
name: name,
content: {} as T,
} as SettingsModel<T>;
} catch (err) {
if (err instanceof ClientResponseError && err.status === 404) {
return {
name: name,
content: {} as T,
} as SettingsModel<T>;
}
throw err;
}
};

View File

@ -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: <SettingsSSLProvider />,
element: <SSLProvider />,
},
],
},

View File

@ -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<void>;
}
export const useNotifyChannelStore = create<NotifyChannelState>((set, get) => {
let fetcher: Promise<SettingsModel<NotifyChannelsSettingsContent>> | null = null; // 防止多次重复请求
let settings: SettingsModel<NotifyChannelsSettingsContent>; // 记录当前设置的其他字段,保存回数据库时用
return {
initialized: false,
channels: {},
setChannel: async (channel, config) => {
settings ??= await getSettings<NotifyChannelsSettingsContent>(SETTINGS_NAMES.NOTIFY_CHANNELS);
return get().setChannels({
...settings.content,
[channel]: { ...settings.content[channel], ...config },
});
},
setChannels: async (channels) => {
settings ??= await getSettings<NotifyChannelsSettingsContent>(SETTINGS_NAMES.NOTIFY_CHANNELS);
settings = await saveSettings<NotifyChannelsSettingsContent>({
...settings,
content: channels,
});
set(
produce((state: NotifyChannelState) => {
state.channels = settings.content;
state.initialized = true;
})
);
},
fetchChannels: async () => {
fetcher ??= getSettings<NotifyChannelsSettingsContent>(SETTINGS_NAMES.NOTIFY_CHANNELS);
try {
settings = await fetcher;
set({ channels: settings.content ?? {}, initialized: true });
} finally {
fetcher = null;
}
},
};
});

View File

@ -1,8 +0,0 @@
export function isValidURL(url: string): boolean {
try {
new URL(url);
return true;
} catch (error) {
return false;
}
}