mirror of
https://github.com/woodchen-ink/certimate.git
synced 2025-07-18 17:31:55 +08:00
feat(ui): new SettingsNotification using antd
This commit is contained in:
parent
cae33cfc4f
commit
7c1a2d5f91
@ -250,7 +250,7 @@ type SSLProviderEab struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func apply(option *ApplyOption, provider challenge.Provider) (*Certificate, error) {
|
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{
|
sslProvider := &SSLProviderConfig{
|
||||||
Config: SSLProviderConfigContent{},
|
Config: SSLProviderConfigContent{},
|
||||||
|
@ -61,7 +61,7 @@ func buildMsg(records []domain.Certificate) *domain.NotifyMessage {
|
|||||||
|
|
||||||
// 查询模板信息
|
// 查询模板信息
|
||||||
settingRepo := repository.NewSettingRepository()
|
settingRepo := repository.NewSettingRepository()
|
||||||
setting, err := settingRepo.GetByName(context.Background(), "templates")
|
setting, err := settingRepo.GetByName(context.Background(), "notifyTemplates")
|
||||||
|
|
||||||
subject := defaultExpireSubject
|
subject := defaultExpireSubject
|
||||||
message := defaultExpireMessage
|
message := defaultExpireMessage
|
||||||
|
@ -50,7 +50,7 @@ const AccessEditForm = forwardRef<AccessEditFormInstance, AccessEditFormProps>((
|
|||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string({ message: t("access.form.name.placeholder") })
|
||||||
.trim()
|
.trim()
|
||||||
.min(1, t("access.form.name.placeholder"))
|
.min(1, t("access.form.name.placeholder"))
|
||||||
.max(64, t("common.errmsg.string_max", { max: 64 })),
|
.max(64, t("common.errmsg.string_max", { max: 64 })),
|
||||||
|
@ -22,6 +22,8 @@ const initModel = () => {
|
|||||||
return {
|
return {
|
||||||
endpoint: "https://example.com/api/",
|
endpoint: "https://example.com/api/",
|
||||||
mode: "",
|
mode: "",
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
} as AccessEditFormACMEHttpReqConfigModelType;
|
} as AccessEditFormACMEHttpReqConfigModelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -20,7 +20,10 @@ export type AccessEditFormAWSConfigProps = {
|
|||||||
|
|
||||||
const initModel = () => {
|
const initModel = () => {
|
||||||
return {
|
return {
|
||||||
|
accessKeyId: "",
|
||||||
|
secretAccessKey: "",
|
||||||
region: "us-east-1",
|
region: "us-east-1",
|
||||||
|
hostedZoneId: "",
|
||||||
} as AccessEditFormAWSConfigModelType;
|
} as AccessEditFormAWSConfigModelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,7 +19,10 @@ export type AccessEditFormAliyunConfigProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initModel = () => {
|
const initModel = () => {
|
||||||
return {} as AccessEditFormAliyunConfigModelType;
|
return {
|
||||||
|
accessKeyId: "",
|
||||||
|
accessKeySecret: "",
|
||||||
|
} as AccessEditFormAliyunConfigModelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AccessEditFormAliyunConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormAliyunConfigProps) => {
|
const AccessEditFormAliyunConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormAliyunConfigProps) => {
|
||||||
|
@ -19,7 +19,10 @@ export type AccessEditFormBaiduCloudConfigProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initModel = () => {
|
const initModel = () => {
|
||||||
return {} as AccessEditFormBaiduCloudConfigModelType;
|
return {
|
||||||
|
accessKeyId: "",
|
||||||
|
secretAccessKey: "",
|
||||||
|
} as AccessEditFormBaiduCloudConfigModelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AccessEditFormBaiduCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormBaiduCloudConfigProps) => {
|
const AccessEditFormBaiduCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormBaiduCloudConfigProps) => {
|
||||||
|
@ -19,7 +19,10 @@ export type AccessEditFormBytePlusConfigProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initModel = () => {
|
const initModel = () => {
|
||||||
return {} as AccessEditFormBytePlusConfigModelType;
|
return {
|
||||||
|
accessKey: "",
|
||||||
|
secretKey: "",
|
||||||
|
} as AccessEditFormBytePlusConfigModelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AccessEditFormBytePlusConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormBytePlusConfigProps) => {
|
const AccessEditFormBytePlusConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormBytePlusConfigProps) => {
|
||||||
|
@ -19,7 +19,9 @@ export type AccessEditFormCloudflareConfigProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initModel = () => {
|
const initModel = () => {
|
||||||
return {} as AccessEditFormCloudflareConfigModelType;
|
return {
|
||||||
|
dnsApiToken: "",
|
||||||
|
} as AccessEditFormCloudflareConfigModelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AccessEditFormCloudflareConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormCloudflareConfigProps) => {
|
const AccessEditFormCloudflareConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormCloudflareConfigProps) => {
|
||||||
|
@ -19,7 +19,10 @@ export type AccessEditFormDogeCloudConfigProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initModel = () => {
|
const initModel = () => {
|
||||||
return {} as AccessEditFormDogeCloudConfigModelType;
|
return {
|
||||||
|
accessKey: "",
|
||||||
|
secretKey: "",
|
||||||
|
} as AccessEditFormDogeCloudConfigModelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AccessEditFormDogeCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormDogeCloudConfigProps) => {
|
const AccessEditFormDogeCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormDogeCloudConfigProps) => {
|
||||||
|
@ -19,7 +19,10 @@ export type AccessEditFormGoDaddyConfigProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initModel = () => {
|
const initModel = () => {
|
||||||
return {} as AccessEditFormGoDaddyConfigModelType;
|
return {
|
||||||
|
apiKey: "",
|
||||||
|
apiSecret: "",
|
||||||
|
} as AccessEditFormGoDaddyConfigModelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AccessEditFormGoDaddyConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormGoDaddyConfigProps) => {
|
const AccessEditFormGoDaddyConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormGoDaddyConfigProps) => {
|
||||||
|
@ -20,6 +20,8 @@ export type AccessEditFormHuaweiCloudConfigProps = {
|
|||||||
|
|
||||||
const initModel = () => {
|
const initModel = () => {
|
||||||
return {
|
return {
|
||||||
|
accessKeyId: "",
|
||||||
|
secretAccessKey: "",
|
||||||
region: "cn-north-1",
|
region: "cn-north-1",
|
||||||
} as AccessEditFormHuaweiCloudConfigModelType;
|
} as AccessEditFormHuaweiCloudConfigModelType;
|
||||||
};
|
};
|
||||||
|
@ -59,7 +59,7 @@ const AccessEditFormKubernetesConfig = ({ form, formName, disabled, loading, mod
|
|||||||
setKubeFileList([]);
|
setKubeFileList([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
flushSync(() => onModelChange?.(form.getFieldsValue()));
|
flushSync(() => onModelChange?.(form.getFieldsValue(true)));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -19,7 +19,9 @@ export type AccessEditFormNameSiloConfigProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initModel = () => {
|
const initModel = () => {
|
||||||
return {} as AccessEditFormNameSiloConfigModelType;
|
return {
|
||||||
|
apiKey: "",
|
||||||
|
} as AccessEditFormNameSiloConfigModelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AccessEditFormNameSiloConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormNameSiloConfigProps) => {
|
const AccessEditFormNameSiloConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormNameSiloConfigProps) => {
|
||||||
|
@ -19,7 +19,10 @@ export type AccessEditFormPowerDNSConfigProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initModel = () => {
|
const initModel = () => {
|
||||||
return {} as AccessEditFormPowerDNSConfigModelType;
|
return {
|
||||||
|
apiUrl: "",
|
||||||
|
apiKey: "",
|
||||||
|
} as AccessEditFormPowerDNSConfigModelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AccessEditFormPowerDNSConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormPowerDNSConfigProps) => {
|
const AccessEditFormPowerDNSConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormPowerDNSConfigProps) => {
|
||||||
|
@ -19,7 +19,10 @@ export type AccessEditFormQiniuConfigProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initModel = () => {
|
const initModel = () => {
|
||||||
return {} as AccessEditFormQiniuConfigModelType;
|
return {
|
||||||
|
accessKey: "",
|
||||||
|
secretKey: "",
|
||||||
|
} as AccessEditFormQiniuConfigModelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AccessEditFormQiniuConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormQiniuConfigProps) => {
|
const AccessEditFormQiniuConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormQiniuConfigProps) => {
|
||||||
|
@ -94,7 +94,7 @@ const AccessEditFormSSHConfig = ({ form, formName, disabled, loading, model, onM
|
|||||||
setKeyFileList([]);
|
setKeyFileList([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
flushSync(() => onModelChange?.(form.getFieldsValue()));
|
flushSync(() => onModelChange?.(form.getFieldsValue(true)));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -19,7 +19,10 @@ export type AccessEditFormTencentCloudConfigProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initModel = () => {
|
const initModel = () => {
|
||||||
return {} as AccessEditFormTencentCloudConfigModelType;
|
return {
|
||||||
|
secretId: "",
|
||||||
|
secretKey: "",
|
||||||
|
} as AccessEditFormTencentCloudConfigModelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AccessEditFormTencentCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormTencentCloudConfigProps) => {
|
const AccessEditFormTencentCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormTencentCloudConfigProps) => {
|
||||||
|
@ -19,7 +19,10 @@ export type AccessEditFormVolcEngineConfigProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initModel = () => {
|
const initModel = () => {
|
||||||
return {} as AccessEditFormVolcEngineConfigModelType;
|
return {
|
||||||
|
accessKeyId: "",
|
||||||
|
secretAccessKey: "",
|
||||||
|
} as AccessEditFormVolcEngineConfigModelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AccessEditFormVolcEngineConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormVolcEngineConfigProps) => {
|
const AccessEditFormVolcEngineConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormVolcEngineConfigProps) => {
|
||||||
|
@ -18,7 +18,9 @@ export type AccessEditFormWebhookConfigProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initModel = () => {
|
const initModel = () => {
|
||||||
return {} as AccessEditFormWebhookConfigModelType;
|
return {
|
||||||
|
url: "",
|
||||||
|
} as AccessEditFormWebhookConfigModelType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AccessEditFormWebhookConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormWebhookConfigProps) => {
|
const AccessEditFormWebhookConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormWebhookConfigProps) => {
|
||||||
|
97
ui/src/components/notification/NotifyChannelEditForm.tsx
Normal file
97
ui/src/components/notification/NotifyChannelEditForm.tsx
Normal 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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
110
ui/src/components/notification/NotifyChannels.tsx
Normal file
110
ui/src/components/notification/NotifyChannels.tsx
Normal 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;
|
128
ui/src/components/notification/NotifyTemplate.tsx
Normal file
128
ui/src/components/notification/NotifyTemplate.tsx
Normal 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;
|
54
ui/src/components/notification/NotifyTestButton.tsx
Normal file
54
ui/src/components/notification/NotifyTestButton.tsx
Normal 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;
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -41,13 +41,14 @@ const formSchema = z
|
|||||||
keyPath: z
|
keyPath: z
|
||||||
.string()
|
.string()
|
||||||
.min(0, t("domain.deployment.form.file_key_path.placeholder"))
|
.min(0, t("domain.deployment.form.file_key_path.placeholder"))
|
||||||
.max(255, t("common.errmsg.string_max", { max: 255 })),
|
.max(255, t("common.errmsg.string_max", { max: 255 }))
|
||||||
pfxPassword: z.string().optional(),
|
.nullish(),
|
||||||
jksAlias: z.string().optional(),
|
pfxPassword: z.string().nullish(),
|
||||||
jksKeypass: z.string().optional(),
|
jksAlias: z.string().nullish(),
|
||||||
jksStorepass: z.string().optional(),
|
jksKeypass: z.string().nullish(),
|
||||||
preCommand: z.string().optional(),
|
jksStorepass: z.string().nullish(),
|
||||||
command: z.string().optional(),
|
preCommand: z.string().nullish(),
|
||||||
|
command: z.string().nullish(),
|
||||||
shell: z.union([z.literal("sh"), z.literal("cmd"), z.literal("powershell")], {
|
shell: z.union([z.literal("sh"), z.literal("cmd"), z.literal("powershell")], {
|
||||||
message: t("domain.deployment.form.shell.placeholder"),
|
message: t("domain.deployment.form.shell.placeholder"),
|
||||||
}),
|
}),
|
||||||
|
@ -40,13 +40,14 @@ const formSchema = z
|
|||||||
keyPath: z
|
keyPath: z
|
||||||
.string()
|
.string()
|
||||||
.min(0, t("domain.deployment.form.file_key_path.placeholder"))
|
.min(0, t("domain.deployment.form.file_key_path.placeholder"))
|
||||||
.max(255, t("common.errmsg.string_max", { max: 255 })),
|
.max(255, t("common.errmsg.string_max", { max: 255 }))
|
||||||
pfxPassword: z.string().optional(),
|
.nullish(),
|
||||||
jksAlias: z.string().optional(),
|
pfxPassword: z.string().nullish(),
|
||||||
jksKeypass: z.string().optional(),
|
jksAlias: z.string().nullish(),
|
||||||
jksStorepass: z.string().optional(),
|
jksKeypass: z.string().nullish(),
|
||||||
preCommand: z.string().optional(),
|
jksStorepass: z.string().nullish(),
|
||||||
command: z.string().optional(),
|
preCommand: z.string().nullish(),
|
||||||
|
command: z.string().nullish(),
|
||||||
})
|
})
|
||||||
.refine((data) => (data.format === "pem" ? !!data.keyPath?.trim() : true), {
|
.refine((data) => (data.format === "pem" ? !!data.keyPath?.trim() : true), {
|
||||||
message: t("domain.deployment.form.file_key_path.placeholder"),
|
message: t("domain.deployment.form.file_key_path.placeholder"),
|
||||||
|
@ -9,7 +9,7 @@ import PanelBody from "./PanelBody";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Show from "../Show";
|
import Show from "../Show";
|
||||||
import { deployTargetsMap } from "@/domain/domain";
|
import { deployTargetsMap } from "@/domain/domain";
|
||||||
import { channelLabelMap } from "@/domain/settings";
|
import { notifyChannelsMap } from "@/domain/settings";
|
||||||
|
|
||||||
type NodeProps = {
|
type NodeProps = {
|
||||||
data: WorkflowNode;
|
data: WorkflowNode;
|
||||||
@ -69,10 +69,10 @@ const Node = ({ data }: NodeProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
case WorkflowNodeType.Notify: {
|
case WorkflowNodeType.Notify: {
|
||||||
const channelLabel = channelLabelMap.get(data.config?.channel as string);
|
const channelLabel = notifyChannelsMap.get(data.config?.channel as string);
|
||||||
return (
|
return (
|
||||||
<div className="flex space-x-2 items-baseline">
|
<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 className="text-muted-foreground truncate">{(data.config?.title as string) ?? ""}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -10,9 +10,9 @@ import { useShallow } from "zustand/shallow";
|
|||||||
import { usePanel } from "./PanelProvider";
|
import { usePanel } from "./PanelProvider";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { useNotifyContext } from "@/providers/notify";
|
import { useNotifyChannelStore } from "@/stores/notify";
|
||||||
import { useEffect, useState } from "react";
|
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 { SelectValue } from "@radix-ui/react-select";
|
||||||
import { Textarea } from "../ui/textarea";
|
import { Textarea } from "../ui/textarea";
|
||||||
import { RefreshCw, Settings } from "lucide-react";
|
import { RefreshCw, Settings } from "lucide-react";
|
||||||
@ -25,7 +25,7 @@ const selectState = (state: WorkflowState) => ({
|
|||||||
updateNode: state.updateNode,
|
updateNode: state.updateNode,
|
||||||
});
|
});
|
||||||
type ChannelName = {
|
type ChannelName = {
|
||||||
name: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -34,28 +34,23 @@ const NotifyForm = ({ data }: NotifyFormProps) => {
|
|||||||
const { updateNode } = useWorkflowStore(useShallow(selectState));
|
const { updateNode } = useWorkflowStore(useShallow(selectState));
|
||||||
const { hidePanel } = usePanel();
|
const { hidePanel } = usePanel();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { config: notifyConfig, initChannels } = useNotifyContext();
|
const { channels: supportedChannels, fetchChannels } = useNotifyChannelStore();
|
||||||
|
|
||||||
const [chanels, setChanels] = useState<ChannelName[]>([]);
|
const [channels, setChannels] = useState<ChannelName[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChanels(getChannels());
|
fetchChannels();
|
||||||
}, [notifyConfig]);
|
}, [fetchChannels]);
|
||||||
|
|
||||||
const getChannels = () => {
|
useEffect(() => {
|
||||||
const rs: ChannelName[] = [];
|
const rs: ChannelName[] = [];
|
||||||
if (!notifyConfig.content) {
|
for (const channel of notifyChannelsMap.values()) {
|
||||||
return rs;
|
if (supportedChannels[channel.type]?.enabled) {
|
||||||
}
|
rs.push({ key: channel.type, label: channel.name });
|
||||||
|
|
||||||
const chanels = notifyConfig.content as NotifyChannels;
|
|
||||||
for (const channel of supportedChannels) {
|
|
||||||
if (chanels[channel.name] && chanels[channel.name].enabled) {
|
|
||||||
rs.push(channel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rs;
|
setChannels(rs);
|
||||||
};
|
}, [supportedChannels]);
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
channel: z.string(),
|
channel: z.string(),
|
||||||
@ -103,10 +98,10 @@ const NotifyForm = ({ data }: NotifyFormProps) => {
|
|||||||
<FormLabel className="flex justify-between items-center">
|
<FormLabel className="flex justify-between items-center">
|
||||||
<div className="flex space-x-2 items-center">
|
<div className="flex space-x-2 items-center">
|
||||||
<div>{t(`${i18nPrefix}.channel.label`)}</div>
|
<div>{t(`${i18nPrefix}.channel.label`)}</div>
|
||||||
<RefreshCw size={16} className="cursor-pointer" onClick={() => initChannels()} />
|
<RefreshCw size={16} className="cursor-pointer" onClick={() => fetchChannels()} />
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href="#/setting/notify"
|
href="#/settings/notification"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="flex justify-between items-center space-x-1 font-normal text-primary hover:underline cursor-pointer"
|
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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{chanels.map((item) => (
|
{channels.map((item) => (
|
||||||
<SelectItem key={item.name} value={item.name}>
|
<SelectItem key={item.key} value={item.key}>
|
||||||
<div>{t(item.label)}</div>
|
<div>{t(item.label)}</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
@ -1,14 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { NotifyProvider } from "@/providers/notify";
|
|
||||||
import { PanelProvider } from "./PanelProvider";
|
import { PanelProvider } from "./PanelProvider";
|
||||||
|
|
||||||
const WorkflowProvider = ({ children }: { children: React.ReactNode }) => {
|
const WorkflowProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
return <PanelProvider>{children}</PanelProvider>;
|
||||||
<NotifyProvider>
|
|
||||||
<PanelProvider>{children}</PanelProvider>
|
|
||||||
</NotifyProvider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WorkflowProvider;
|
export default WorkflowProvider;
|
||||||
|
@ -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 {
|
export interface SettingsModel<T> extends BaseModel {
|
||||||
name: string;
|
name: string;
|
||||||
content: T;
|
content: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #region Settings: Emails
|
||||||
export type EmailsSettingsContent = {
|
export type EmailsSettingsContent = {
|
||||||
emails: string[];
|
emails: string[];
|
||||||
};
|
};
|
||||||
|
// #endregion
|
||||||
|
|
||||||
export type NotifyTemplates = {
|
// #region Settings: NotifyTemplates
|
||||||
|
export type NotifyTemplatesSettingsContent = {
|
||||||
notifyTemplates: NotifyTemplate[];
|
notifyTemplates: NotifyTemplate[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NotifyTemplate = {
|
export type NotifyTemplate = {
|
||||||
title: string;
|
subject: string;
|
||||||
content: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NotifyChannels = {
|
export const defaultNotifyTemplate: NotifyTemplate = {
|
||||||
[key: string]: NotifyChannel;
|
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 =
|
export type BarkNotifyChannelConfig = {
|
||||||
| NotifyChannelEmail
|
deviceKey: string;
|
||||||
| NotifyChannelWebhook
|
serverUrl: string;
|
||||||
| NotifyChannelDingTalk
|
enabled?: boolean;
|
||||||
| NotifyChannelLark
|
|
||||||
| NotifyChannelTelegram
|
|
||||||
| NotifyChannelServerChan
|
|
||||||
| NotifyChannelBark;
|
|
||||||
|
|
||||||
type ChannelLabel = {
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
};
|
};
|
||||||
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 EmailNotifyChannelConfig = {
|
||||||
export type NotifyChannelEmail = {
|
|
||||||
smtpHost: string;
|
smtpHost: string;
|
||||||
smtpPort: number;
|
smtpPort: number;
|
||||||
smtpTLS: boolean;
|
smtpTLS: boolean;
|
||||||
@ -73,47 +66,55 @@ export type NotifyChannelEmail = {
|
|||||||
password: string;
|
password: string;
|
||||||
senderAddress: string;
|
senderAddress: string;
|
||||||
receiverAddress: string;
|
receiverAddress: string;
|
||||||
enabled: boolean;
|
enabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NotifyChannelWebhook = {
|
export type DingTalkNotifyChannelConfig = {
|
||||||
url: string;
|
|
||||||
enabled: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NotifyChannelDingTalk = {
|
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
secret: string;
|
secret: string;
|
||||||
enabled: boolean;
|
enabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NotifyChannelLark = {
|
export type LarkNotifyChannelConfig = {
|
||||||
webhookUrl: string;
|
webhookUrl: string;
|
||||||
enabled: boolean;
|
enabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NotifyChannelTelegram = {
|
export type ServerChanNotifyChannelConfig = {
|
||||||
|
url: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TelegramNotifyChannelConfig = {
|
||||||
apiToken: string;
|
apiToken: string;
|
||||||
chatId: string;
|
chatId: string;
|
||||||
enabled: boolean;
|
enabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NotifyChannelServerChan = {
|
export type WebhookNotifyChannelConfig = {
|
||||||
url: string;
|
url: string;
|
||||||
enabled: boolean;
|
enabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NotifyChannelBark = {
|
export type NotifyChannel = {
|
||||||
deviceKey: string;
|
type: string;
|
||||||
serverUrl: string;
|
name: string;
|
||||||
enabled: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultNotifyTemplate: NotifyTemplate = {
|
export const notifyChannelsMap: Map<NotifyChannel["type"], NotifyChannel> = new Map(
|
||||||
title: "您有 {COUNT} 张证书即将过期",
|
[
|
||||||
content: "有 {COUNT} 张证书即将过期,域名分别为 {DOMAINS},请保持关注!",
|
["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 SSLProvider = "letsencrypt" | "zerossl" | "gts";
|
||||||
|
|
||||||
export type SSLProviderSetting = {
|
export type SSLProviderSetting = {
|
||||||
@ -124,3 +125,4 @@ export type SSLProviderSetting = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
// #endregion
|
||||||
|
@ -16,47 +16,58 @@
|
|||||||
"settings.password.form.password.errmsg.not_matched": "Passwords do not match",
|
"settings.password.form.password.errmsg.not_matched": "Passwords do not match",
|
||||||
|
|
||||||
"settings.notification.tab": "Notification",
|
"settings.notification.tab": "Notification",
|
||||||
"settings.notification.template.label": "Template",
|
"settings.notification.template.card.title": "Template",
|
||||||
"settings.notification.template.saved.message": "Notification template saved successfully",
|
"settings.notification.template.form.subject.label": "Subject",
|
||||||
"settings.notification.template.variables.tips.title": "Optional variables ({COUNT}: number of expiring soon)",
|
"settings.notification.template.form.subject.placeholder": "Please enter notification subject",
|
||||||
"settings.notification.template.variables.tips.content": "Optional variables ({COUNT}: number of expiring soon. {DOMAINS}: Domain list)",
|
"settings.notification.template.form.subject.tooltip": "Optional variables ({COUNT}: number of expiring soon)",
|
||||||
"settings.notification.config.enable": "Enable",
|
"settings.notification.template.form.message.label": "Message",
|
||||||
"settings.notification.config.saved.message": "Configuration saved successfully",
|
"settings.notification.template.form.message.placeholder": "Please enter notification message",
|
||||||
"settings.notification.config.failed.message": "Configuration save failed",
|
"settings.notification.template.form.message.tooltip": "Optional variables ({COUNT}: number of expiring soon. {DOMAINS}: Domain list)",
|
||||||
"settings.notification.push_test_message": "Send test notification",
|
"settings.notification.channels.card.title": "Channels",
|
||||||
"settings.notification.push_test_message.succeeded.message": "Send test notification successfully",
|
"settings.notification.channel.enabled.on": "On",
|
||||||
"settings.notification.push_test_message.failed.message": "Send test notification failed",
|
"settings.notification.channel.enabled.off": "Off",
|
||||||
"settings.notification.email.smtp_host.label": "SMTP Host",
|
"settings.notification.push_test.button": "Send Test Notification",
|
||||||
"settings.notification.email.smtp_host.placeholder": "Please enter SMTP host",
|
"settings.notification.push_test.tooltip": "Note: Please save settings before testing push.",
|
||||||
"settings.notification.email.smtp_port.label": "SMTP Port",
|
"settings.notification.push_test.pushed": "Sent",
|
||||||
"settings.notification.email.smtp_port.placeholder": "Please enter SMTP port",
|
"settings.notification.channel.form.bark_server_url.label": "Server URL",
|
||||||
"settings.notification.email.smtp_tls.label": "Use TLS/SSL",
|
"settings.notification.channel.form.bark_server_url.placeholder": "Please enter server URL",
|
||||||
"settings.notification.email.username.label": "Username",
|
"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.email.username.placeholder": "please enter username",
|
"settings.notification.channel.form.bark_device_key.label": "Device Key",
|
||||||
"settings.notification.email.password.label": "Password",
|
"settings.notification.channel.form.bark_device_key.placeholder": "Please enter device key",
|
||||||
"settings.notification.email.password.placeholder": "please enter password",
|
"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.email.sender_address.label": "Sender Email Address",
|
"settings.notification.channel.form.dingtalk_access_token.label": "Robot AccessToken",
|
||||||
"settings.notification.email.sender_address.placeholder": "Please enter sender email address",
|
"settings.notification.channel.form.dingtalk_access_token.placeholder": "Please enter Robot Access Token",
|
||||||
"settings.notification.email.receiver_address.label": "Receiver Email Address",
|
"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.email.receiver_address.placeholder": "Please enter receiver email address",
|
"settings.notification.channel.form.dingtalk_secret.label": "Robot Secret",
|
||||||
"settings.notification.webhook.url.label": "Webhook URL",
|
"settings.notification.channel.form.dingtalk_secret.placeholder": "Please enter Robot Secret",
|
||||||
"settings.notification.webhook.url.placeholder": "Please enter Webhook URL",
|
"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.dingtalk.access_token.label": "AccessToken",
|
"settings.notification.channel.form.email_smtp_host.label": "SMTP Host",
|
||||||
"settings.notification.dingtalk.access_token.placeholder": "Please enter access token",
|
"settings.notification.channel.form.email_smtp_host.placeholder": "Please enter SMTP host",
|
||||||
"settings.notification.dingtalk.secret.label": "Secret",
|
"settings.notification.channel.form.email_smtp_port.label": "SMTP Port",
|
||||||
"settings.notification.dingtalk.secret.placeholder": "Please enter secret",
|
"settings.notification.channel.form.email_smtp_port.placeholder": "Please enter SMTP port",
|
||||||
"settings.notification.lark.webhook_url.label": "Webhook URL",
|
"settings.notification.channel.form.email_smtp_tls.label": "Use TLS/SSL",
|
||||||
"settings.notification.lark.webhook_url.placeholder": "Please enter Webhook URL",
|
"settings.notification.channel.form.email_username.label": "Username",
|
||||||
"settings.notification.telegram.api_token.label": "API Token",
|
"settings.notification.channel.form.email_username.placeholder": "please enter username",
|
||||||
"settings.notification.telegram.api_token.placeholder": "Please enter API token",
|
"settings.notification.channel.form.email_password.label": "Password",
|
||||||
"settings.notification.telegram.chat_id.label": "Chat ID",
|
"settings.notification.channel.form.email_password.placeholder": "please enter password",
|
||||||
"settings.notification.telegram.chat_id.placeholder": "Please enter Telegram chat ID",
|
"settings.notification.channel.form.email_sender_address.label": "Sender Email Address",
|
||||||
"settings.notification.serverchan.url.label": "Server URL",
|
"settings.notification.channel.form.email_sender_address.placeholder": "Please enter sender email address",
|
||||||
"settings.notification.serverchan.url.placeholder": "Please enter server URL (e.g. https://sctapi.ftqq.com/*****.send)",
|
"settings.notification.channel.form.email_receiver_address.label": "Receiver Email Address",
|
||||||
"settings.notification.bark.server_url.label": "Server URL",
|
"settings.notification.channel.form.email_receiver_address.placeholder": "Please enter receiver email address",
|
||||||
"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.channel.form.lark_webhook_url.label": "Webhook URL",
|
||||||
"settings.notification.bark.device_key.label": "Device Key",
|
"settings.notification.channel.form.lark_webhook_url.placeholder": "Please enter Webhook URL",
|
||||||
"settings.notification.bark.device_key.placeholder": "Please enter device key",
|
"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.tab": "Certificate Authority",
|
||||||
"settings.ca.provider.errmsg.empty": "Please select a Certificate Authority",
|
"settings.ca.provider.errmsg.empty": "Please select a Certificate Authority",
|
||||||
|
@ -68,5 +68,5 @@
|
|||||||
"workflow.node.notify.form.content.placeholder": "Please enter content",
|
"workflow.node.notify.form.content.placeholder": "Please enter content",
|
||||||
"workflow.node.notify.form.channel.label": "Channel",
|
"workflow.node.notify.form.channel.label": "Channel",
|
||||||
"workflow.node.notify.form.channel.placeholder": "Please select 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"
|
||||||
}
|
}
|
||||||
|
@ -16,47 +16,58 @@
|
|||||||
"settings.password.form.password.errmsg.not_matched": "两次密码不一致",
|
"settings.password.form.password.errmsg.not_matched": "两次密码不一致",
|
||||||
|
|
||||||
"settings.notification.tab": "消息推送",
|
"settings.notification.tab": "消息推送",
|
||||||
"settings.notification.template.label": "内容模板",
|
"settings.notification.template.card.title": "通知模板",
|
||||||
"settings.notification.template.saved.message": "通知模板保存成功",
|
"settings.notification.template.form.subject.label": "通知主题",
|
||||||
"settings.notification.template.variables.tips.title": "可选的变量({COUNT}: 即将过期张数)",
|
"settings.notification.template.form.subject.placeholder": "请输入通知主题",
|
||||||
"settings.notification.template.variables.tips.content": "可选的变量({COUNT}: 即将过期张数;{DOMAINS}: 域名列表)",
|
"settings.notification.template.form.subject.tooltip": "可选的变量({COUNT}: 即将过期张数)",
|
||||||
"settings.notification.config.enable": "是否启用",
|
"settings.notification.template.form.message.label": "通知内容",
|
||||||
"settings.notification.config.saved.message": "配置保存成功",
|
"settings.notification.template.form.message.placeholder": "请输入通知内容",
|
||||||
"settings.notification.config.failed.message": "配置保存失败",
|
"settings.notification.template.form.message.tooltip": "可选的变量({COUNT}: 即将过期张数;{DOMAINS}: 域名列表)",
|
||||||
"settings.notification.push_test_message": "推送测试消息",
|
"settings.notification.channels.card.title": "通知渠道",
|
||||||
"settings.notification.push_test_message.failed.message": "推送测试消息失败",
|
"settings.notification.channel.enabled.on": "启用",
|
||||||
"settings.notification.push_test_message.succeeded.message": "推送测试消息成功",
|
"settings.notification.channel.enabled.off": "未启用",
|
||||||
"settings.notification.email.smtp_host.label": "SMTP 服务器地址",
|
"settings.notification.push_test.button": "推送测试消息",
|
||||||
"settings.notification.email.smtp_host.placeholder": "请输入 SMTP 服务器地址",
|
"settings.notification.push_test.tooltip": "提示:修改后请先保存设置再测试推送。",
|
||||||
"settings.notification.email.smtp_port.label": "SMTP 服务器端口",
|
"settings.notification.push_test.pushed": "已推送",
|
||||||
"settings.notification.email.smtp_port.placeholder": "请输入 SMTP 服务器端口",
|
"settings.notification.channel.form.bark_server_url.label": "服务器地址",
|
||||||
"settings.notification.email.smtp_tls.label": "TLS/SSL 连接",
|
"settings.notification.channel.form.bark_server_url.placeholder": "请输入服务器地址",
|
||||||
"settings.notification.email.username.label": "用户名",
|
"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.email.username.placeholder": "请输入用户名",
|
"settings.notification.channel.form.bark_device_key.label": "设备密钥",
|
||||||
"settings.notification.email.password.label": "密码",
|
"settings.notification.channel.form.bark_device_key.placeholder": "请输入设备密钥",
|
||||||
"settings.notification.email.password.placeholder": "请输入密码",
|
"settings.notification.channel.form.bark_device_key.tooltip": "这是什么?请参阅 <a href=\"https://bark.day.app/\" target=\"_blank\">https://bark.day.app/</a>",
|
||||||
"settings.notification.email.sender_address.label": "发送邮箱地址",
|
"settings.notification.channel.form.dingtalk_access_token.label": "机器人 AccessToken",
|
||||||
"settings.notification.email.sender_address.placeholder": "请输入发送邮箱地址",
|
"settings.notification.channel.form.dingtalk_access_token.placeholder": "请输入机器人 AccessToken",
|
||||||
"settings.notification.email.receiver_address.label": "接收邮箱地址",
|
"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.email.receiver_address.placeholder": "请输入接收邮箱地址",
|
"settings.notification.channel.form.dingtalk_secret.label": "机器人加签密钥",
|
||||||
"settings.notification.webhook.url.label": "Webhook 回调地址",
|
"settings.notification.channel.form.dingtalk_secret.placeholder": "请输入机器人加签密钥",
|
||||||
"settings.notification.webhook.url.placeholder": "请输入 Webhook 回调地址",
|
"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.dingtalk.access_token.label": "AccessToken",
|
"settings.notification.channel.form.email_smtp_host.label": "SMTP 服务器地址",
|
||||||
"settings.notification.dingtalk.access_token.placeholder": "请输入 AccessToken",
|
"settings.notification.channel.form.email_smtp_host.placeholder": "请输入 SMTP 服务器地址",
|
||||||
"settings.notification.dingtalk.secret.label": "签名密钥",
|
"settings.notification.channel.form.email_smtp_port.label": "SMTP 服务器端口",
|
||||||
"settings.notification.dingtalk.secret.placeholder": "请输入签名密钥",
|
"settings.notification.channel.form.email_smtp_port.placeholder": "请输入 SMTP 服务器端口",
|
||||||
"settings.notification.lark.webhook_url.label": "Webhook URL",
|
"settings.notification.channel.form.email_smtp_tls.label": "TLS/SSL 连接",
|
||||||
"settings.notification.lark.webhook_url.placeholder": "请输入 Webhook URL",
|
"settings.notification.channel.form.email_username.label": "用户名",
|
||||||
"settings.notification.telegram.api_token.label": "API Token",
|
"settings.notification.channel.form.email_username.placeholder": "请输入用户名",
|
||||||
"settings.notification.telegram.api_token.placeholder": "请输入 API token",
|
"settings.notification.channel.form.email_password.label": "密码",
|
||||||
"settings.notification.telegram.chat_id.label": "会话 ID",
|
"settings.notification.channel.form.email_password.placeholder": "请输入密码",
|
||||||
"settings.notification.telegram.chat_id.placeholder": "请输入 Telegram 会话 ID",
|
"settings.notification.channel.form.email_sender_address.label": "发送邮箱地址",
|
||||||
"settings.notification.serverchan.url.label": "服务器 URL",
|
"settings.notification.channel.form.email_sender_address.placeholder": "请输入发送邮箱地址",
|
||||||
"settings.notification.serverchan.url.placeholder": "请输入服务器 URL(形如: https://sctapi.ftqq.com/*****.send)",
|
"settings.notification.channel.form.email_receiver_address.label": "接收邮箱地址",
|
||||||
"settings.notification.bark.server_url.label": "服务器 URL",
|
"settings.notification.channel.form.email_receiver_address.placeholder": "请输入接收邮箱地址",
|
||||||
"settings.notification.bark.server_url.placeholder": "请输入服务器 URL(形如: https://your-bark-server.com;留空则使用 Bark 默认服务器)",
|
"settings.notification.channel.form.lark_webhook_url.label": "Webhook 地址",
|
||||||
"settings.notification.bark.device_key.label": "设备密钥",
|
"settings.notification.channel.form.lark_webhook_url.placeholder": "请输入 Webhook 地址",
|
||||||
"settings.notification.bark.device_key.placeholder": "请输入设备密钥",
|
"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.tab": "证书颁发机构(CA)",
|
||||||
"settings.ca.provider.errmsg.empty": "请选择证书分发机构",
|
"settings.ca.provider.errmsg.empty": "请选择证书分发机构",
|
||||||
|
@ -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;
|
|
@ -3,6 +3,7 @@ import { useForm } from "react-hook-form";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { produce } from "immer";
|
||||||
|
|
||||||
import { cn } from "@/components/ui/utils";
|
import { cn } from "@/components/ui/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -11,10 +12,9 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { getErrMsg } from "@/utils/error";
|
import { SETTINGS_NAMES, SSLProvider as SSLProviderType, SSLProviderSetting, SettingsModel } from "@/domain/settings";
|
||||||
import { SSLProvider as SSLProviderType, SSLProviderSetting, SettingsModel } from "@/domain/settings";
|
|
||||||
import { get, save } from "@/repository/settings";
|
import { get, save } from "@/repository/settings";
|
||||||
import { produce } from "immer";
|
import { getErrMsg } from "@/utils/error";
|
||||||
|
|
||||||
type SSLProviderContext = {
|
type SSLProviderContext = {
|
||||||
setting: SettingsModel<SSLProviderSetting>;
|
setting: SettingsModel<SSLProviderSetting>;
|
||||||
@ -28,6 +28,16 @@ export const useSSLProviderContext = () => {
|
|||||||
return useContext(Context);
|
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 SSLProvider = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -42,7 +52,7 @@ const SSLProvider = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
const setting = await get<SSLProviderSetting>("ssl-provider");
|
const setting = await get<SSLProviderSetting>(SETTINGS_NAMES.SSL_PROVIDER);
|
||||||
|
|
||||||
if (setting) {
|
if (setting) {
|
||||||
setConfig(setting);
|
setConfig(setting);
|
||||||
@ -95,7 +105,7 @@ const SSLProvider = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Context.Provider value={{ onSubmit, setConfig, setting: config }}>
|
<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>
|
<Label className="dark:text-stone-200">{t("common.text.ca")}</Label>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
className="flex mt-3 dark:text-stone-200"
|
className="flex mt-3 dark:text-stone-200"
|
||||||
@ -147,7 +157,7 @@ const SSLProviderForm = ({ kind }: { kind: string }) => {
|
|||||||
case "zerossl":
|
case "zerossl":
|
||||||
return <SSLProviderZeroSSLForm />;
|
return <SSLProviderZeroSSLForm />;
|
||||||
case "gts":
|
case "gts":
|
||||||
return <SSLProviderGtsForm />;
|
return <SSLProviderGoogleTrustServicesForm />;
|
||||||
default:
|
default:
|
||||||
return <SSLProviderLetsEncryptForm />;
|
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 SSLProviderLetsEncryptForm = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -227,6 +227,7 @@ const SSLProviderLetsEncryptForm = () => {
|
|||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SSLProviderZeroSSLForm = () => {
|
const SSLProviderZeroSSLForm = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -334,7 +335,7 @@ const SSLProviderZeroSSLForm = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SSLProviderGtsForm = () => {
|
const SSLProviderGoogleTrustServicesForm = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { setting, onSubmit } = useSSLProviderContext();
|
const { setting, onSubmit } = useSSLProviderContext();
|
||||||
|
@ -5,8 +5,6 @@ import { Card, Space } from "antd";
|
|||||||
import { PageHeader } from "@ant-design/pro-components";
|
import { PageHeader } from "@ant-design/pro-components";
|
||||||
import { KeyRound as KeyRoundIcon, Megaphone as MegaphoneIcon, ShieldCheck as ShieldCheckIcon, UserRound as UserRoundIcon } from "lucide-react";
|
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 Settings = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -73,7 +71,6 @@ const Settings = () => {
|
|||||||
navigate(`/settings/${key}`);
|
navigate(`/settings/${key}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Toaster />
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
|
@ -47,7 +47,7 @@ const SettingsAccount = () => {
|
|||||||
navigate("/login");
|
navigate("/login");
|
||||||
}, 500);
|
}, 500);
|
||||||
} catch (err) {
|
} 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 {
|
} finally {
|
||||||
setFormPending(false);
|
setFormPending(false);
|
||||||
}
|
}
|
||||||
@ -58,7 +58,7 @@ const SettingsAccount = () => {
|
|||||||
{MessageContextHolder}
|
{MessageContextHolder}
|
||||||
{NotificationContextHolder}
|
{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 form={form} disabled={formPending} initialValues={initialValues} layout="vertical" onFinish={handleFormFinish}>
|
||||||
<Form.Item name="username" label={t("settings.account.form.email.label")} rules={[formRule]}>
|
<Form.Item name="username" label={t("settings.account.form.email.label")} rules={[formRule]}>
|
||||||
<Input placeholder={t("settings.account.form.email.placeholder")} onChange={handleInputChange} />
|
<Input placeholder={t("settings.account.form.email.placeholder")} onChange={handleInputChange} />
|
||||||
|
30
ui/src/pages/settings/SettingsNotification.tsx
Normal file
30
ui/src/pages/settings/SettingsNotification.tsx
Normal 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;
|
@ -60,7 +60,7 @@ const SettingsPassword = () => {
|
|||||||
navigate("/login");
|
navigate("/login");
|
||||||
}, 500);
|
}, 500);
|
||||||
} catch (err) {
|
} 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 {
|
} finally {
|
||||||
setFormPending(false);
|
setFormPending(false);
|
||||||
}
|
}
|
||||||
@ -71,7 +71,7 @@ const SettingsPassword = () => {
|
|||||||
{MessageContextHolder}
|
{MessageContextHolder}
|
||||||
{NotificationContextHolder}
|
{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 form={form} disabled={formPending} layout="vertical" onFinish={handleFormFinish}>
|
||||||
<Form.Item name="oldPassword" label={t("settings.password.form.old_password.label")} rules={[formRule]}>
|
<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} />
|
<Input.Password placeholder={t("settings.password.form.old_password.placeholder")} onChange={handleInputChange} />
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
@ -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";
|
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 {
|
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;
|
return resp;
|
||||||
} catch {
|
} catch (err) {
|
||||||
return {
|
if (err instanceof ClientResponseError && err.status === 404) {
|
||||||
name: name,
|
return {
|
||||||
content: {} as T,
|
name: name,
|
||||||
} as SettingsModel<T>;
|
content: {} as T,
|
||||||
|
} as SettingsModel<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -11,8 +11,8 @@ import CertificateList from "./pages/certificates/CertificateList";
|
|||||||
import Settings from "./pages/settings/Settings";
|
import Settings from "./pages/settings/Settings";
|
||||||
import SettingsAccount from "./pages/settings/SettingsAccount";
|
import SettingsAccount from "./pages/settings/SettingsAccount";
|
||||||
import SettingsPassword from "./pages/settings/SettingsPassword";
|
import SettingsPassword from "./pages/settings/SettingsPassword";
|
||||||
import SettingsNotification from "./pages/settings/Notification";
|
import SettingsNotification from "./pages/settings/SettingsNotification";
|
||||||
import SettingsSSLProvider from "./pages/settings/SSLProvider";
|
import SSLProvider from "./pages/settings/SSLProvider";
|
||||||
|
|
||||||
export const router = createHashRouter([
|
export const router = createHashRouter([
|
||||||
{
|
{
|
||||||
@ -57,7 +57,7 @@ export const router = createHashRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/settings/ssl-provider",
|
path: "/settings/ssl-provider",
|
||||||
element: <SettingsSSLProvider />,
|
element: <SSLProvider />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
57
ui/src/stores/notify/index.ts
Normal file
57
ui/src/stores/notify/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
@ -1,8 +0,0 @@
|
|||||||
export function isValidURL(url: string): boolean {
|
|
||||||
try {
|
|
||||||
new URL(url);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user