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