feat(ui): new SettingsNotification using antd

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

View File

@@ -50,7 +50,7 @@ const AccessEditForm = forwardRef<AccessEditFormInstance, AccessEditFormProps>((
const formSchema = z.object({
name: z
.string()
.string({ message: t("access.form.name.placeholder") })
.trim()
.min(1, t("access.form.name.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 })),

View File

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

View File

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

View File

@@ -19,7 +19,10 @@ export type AccessEditFormAliyunConfigProps = {
};
const initModel = () => {
return {} as AccessEditFormAliyunConfigModelType;
return {
accessKeyId: "",
accessKeySecret: "",
} as AccessEditFormAliyunConfigModelType;
};
const AccessEditFormAliyunConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormAliyunConfigProps) => {

View File

@@ -19,7 +19,10 @@ export type AccessEditFormBaiduCloudConfigProps = {
};
const initModel = () => {
return {} as AccessEditFormBaiduCloudConfigModelType;
return {
accessKeyId: "",
secretAccessKey: "",
} as AccessEditFormBaiduCloudConfigModelType;
};
const AccessEditFormBaiduCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormBaiduCloudConfigProps) => {

View File

@@ -19,7 +19,10 @@ export type AccessEditFormBytePlusConfigProps = {
};
const initModel = () => {
return {} as AccessEditFormBytePlusConfigModelType;
return {
accessKey: "",
secretKey: "",
} as AccessEditFormBytePlusConfigModelType;
};
const AccessEditFormBytePlusConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormBytePlusConfigProps) => {

View File

@@ -19,7 +19,9 @@ export type AccessEditFormCloudflareConfigProps = {
};
const initModel = () => {
return {} as AccessEditFormCloudflareConfigModelType;
return {
dnsApiToken: "",
} as AccessEditFormCloudflareConfigModelType;
};
const AccessEditFormCloudflareConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormCloudflareConfigProps) => {

View File

@@ -19,7 +19,10 @@ export type AccessEditFormDogeCloudConfigProps = {
};
const initModel = () => {
return {} as AccessEditFormDogeCloudConfigModelType;
return {
accessKey: "",
secretKey: "",
} as AccessEditFormDogeCloudConfigModelType;
};
const AccessEditFormDogeCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormDogeCloudConfigProps) => {

View File

@@ -19,7 +19,10 @@ export type AccessEditFormGoDaddyConfigProps = {
};
const initModel = () => {
return {} as AccessEditFormGoDaddyConfigModelType;
return {
apiKey: "",
apiSecret: "",
} as AccessEditFormGoDaddyConfigModelType;
};
const AccessEditFormGoDaddyConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormGoDaddyConfigProps) => {

View File

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

View File

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

View File

@@ -19,7 +19,9 @@ export type AccessEditFormNameSiloConfigProps = {
};
const initModel = () => {
return {} as AccessEditFormNameSiloConfigModelType;
return {
apiKey: "",
} as AccessEditFormNameSiloConfigModelType;
};
const AccessEditFormNameSiloConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormNameSiloConfigProps) => {

View File

@@ -19,7 +19,10 @@ export type AccessEditFormPowerDNSConfigProps = {
};
const initModel = () => {
return {} as AccessEditFormPowerDNSConfigModelType;
return {
apiUrl: "",
apiKey: "",
} as AccessEditFormPowerDNSConfigModelType;
};
const AccessEditFormPowerDNSConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormPowerDNSConfigProps) => {

View File

@@ -19,7 +19,10 @@ export type AccessEditFormQiniuConfigProps = {
};
const initModel = () => {
return {} as AccessEditFormQiniuConfigModelType;
return {
accessKey: "",
secretKey: "",
} as AccessEditFormQiniuConfigModelType;
};
const AccessEditFormQiniuConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormQiniuConfigProps) => {

View File

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

View File

@@ -19,7 +19,10 @@ export type AccessEditFormTencentCloudConfigProps = {
};
const initModel = () => {
return {} as AccessEditFormTencentCloudConfigModelType;
return {
secretId: "",
secretKey: "",
} as AccessEditFormTencentCloudConfigModelType;
};
const AccessEditFormTencentCloudConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormTencentCloudConfigProps) => {

View File

@@ -19,7 +19,10 @@ export type AccessEditFormVolcEngineConfigProps = {
};
const initModel = () => {
return {} as AccessEditFormVolcEngineConfigModelType;
return {
accessKeyId: "",
secretAccessKey: "",
} as AccessEditFormVolcEngineConfigModelType;
};
const AccessEditFormVolcEngineConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormVolcEngineConfigProps) => {

View File

@@ -18,7 +18,9 @@ export type AccessEditFormWebhookConfigProps = {
};
const initModel = () => {
return {} as AccessEditFormWebhookConfigModelType;
return {
url: "",
} as AccessEditFormWebhookConfigModelType;
};
const AccessEditFormWebhookConfig = ({ form, formName, disabled, loading, model, onModelChange }: AccessEditFormWebhookConfigProps) => {

View File

@@ -0,0 +1,97 @@
import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
import { useCreation, useDeepCompareEffect } from "ahooks";
import { Form } from "antd";
import { type NotifyChannelsSettingsContent } from "@/domain/settings";
import NotifyChannelEditFormBarkFields from "./NotifyChannelEditFormBarkFields";
import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalkFields";
import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields";
import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields";
import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields";
import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields";
import NotifyChannelEditFormWebhookFields from "./NotifyChannelEditFormWebhookFields";
type NotifyChannelEditFormModelType = NotifyChannelsSettingsContent[keyof NotifyChannelsSettingsContent];
export type NotifyChannelEditFormProps = {
className?: string;
style?: React.CSSProperties;
channel: keyof NotifyChannelsSettingsContent;
disabled?: boolean;
loading?: boolean;
model?: NotifyChannelEditFormModelType;
onModelChange?: (model: NotifyChannelEditFormModelType) => void;
};
export type NotifyChannelEditFormInstance = {
getFieldsValue: () => NotifyChannelEditFormModelType;
resetFields: () => void;
validateFields: () => Promise<NotifyChannelEditFormModelType>;
};
const NotifyChannelEditForm = forwardRef<NotifyChannelEditFormInstance, NotifyChannelEditFormProps>(
({ className, style, channel, disabled, loading, model, onModelChange }, ref) => {
const [form] = Form.useForm();
const formName = useCreation(() => `notifyChannelEditForm_${Math.random().toString(36).substring(2, 10)}${new Date().getTime()}`, []);
const formFieldsComponent = useMemo(() => {
/*
注意:如果追加新的子组件,请保持以 ASCII 排序。
NOTICE: If you add new child component, please keep ASCII order.
*/
switch (channel) {
case "bark":
return <NotifyChannelEditFormBarkFields />;
case "dingtalk":
return <NotifyChannelEditFormDingTalkFields />;
case "email":
return <NotifyChannelEditFormEmailFields />;
case "lark":
return <NotifyChannelEditFormLarkFields />;
case "serverchan":
return <NotifyChannelEditFormServerChanFields />;
case "telegram":
return <NotifyChannelEditFormTelegramFields />;
case "webhook":
return <NotifyChannelEditFormWebhookFields />;
}
}, [channel]);
const [initialValues, setInitialValues] = useState(model);
useDeepCompareEffect(() => {
setInitialValues(model);
}, [model]);
const handleFormChange = (_: unknown, fields: NotifyChannelEditFormModelType) => {
onModelChange?.(fields);
};
useImperativeHandle(ref, () => ({
getFieldsValue: () => {
return form.getFieldsValue(true);
},
resetFields: () => {
return form.resetFields();
},
validateFields: () => {
return form.validateFields();
},
}));
return (
<Form
className={className}
style={style}
form={form}
disabled={loading || disabled}
initialValues={initialValues}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
{formFieldsComponent}
</Form>
);
}
);
export default NotifyChannelEditForm;

View File

@@ -0,0 +1,44 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const NotifyChannelEditFormBarkFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
serverUrl: z
.string({ message: t("settings.notification.channel.form.bark_server_url.placeholder") })
.url({ message: t("common.errmsg.url_invalid") })
.nullish(),
deviceKey: z
.string({ message: t("settings.notification.channel.form.bark_device_key.placeholder") })
.min(1, t("settings.notification.channel.form.bark_device_key.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="serverUrl"
label={t("settings.notification.channel.form.bark_server_url.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.bark_server_url.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.bark_server_url.placeholder")} />
</Form.Item>
<Form.Item
name="deviceKey"
label={t("settings.notification.channel.form.bark_device_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.bark_device_key.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.bark_device_key.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormBarkFields;

View File

@@ -0,0 +1,44 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const NotifyChannelEditFormDingTalkFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
accessToken: z
.string({ message: t("settings.notification.channel.form.dingtalk_access_token.placeholder") })
.min(1, t("settings.notification.channel.form.dingtalk_access_token.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })),
secret: z
.string({ message: t("settings.notification.channel.form.dingtalk_secret.placeholder") })
.min(1, t("settings.notification.channel.form.dingtalk_secret.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 })),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="accessToken"
label={t("settings.notification.channel.form.dingtalk_access_token.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.dingtalk_access_token.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("settings.notification.channel.form.dingtalk_access_token.placeholder")} />
</Form.Item>
<Form.Item
name="secret"
label={t("settings.notification.channel.form.dingtalk_secret.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.dingtalk_secret.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("settings.notification.channel.form.dingtalk_secret.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormDingTalkFields;

View File

@@ -0,0 +1,96 @@
import { useTranslation } from "react-i18next";
import { Form, Input, InputNumber, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const NotifyChannelEditFormEmailFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
smtpHost: z
.string({ message: t("settings.notification.channel.form.email_smtp_host.placeholder") })
.min(1, t("settings.notification.channel.form.email_smtp_host.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })),
smtpPort: z
.number({ message: t("settings.notification.channel.form.email_smtp_port.placeholder") })
.int()
.gte(1, t("common.errmsg.port_invalid"))
.lte(65535, t("common.errmsg.port_invalid"))
.transform((v) => +v),
smtpTLS: z.boolean().nullish(),
username: z
.string({ message: t("settings.notification.channel.form.email_username.placeholder") })
.min(1, t("settings.notification.channel.form.email_username.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })),
password: z
.string({ message: t("settings.notification.channel.form.email_password.placeholder") })
.min(1, t("settings.notification.channel.form.email_password.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })),
senderAddress: z
.string({ message: t("settings.notification.channel.form.email_sender_address.placeholder") })
.min(1, t("settings.notification.channel.form.email_sender_address.placeholder"))
.email({ message: t("common.errmsg.email_invalid") }),
receiverAddress: z
.string({ message: t("settings.notification.channel.form.email_receiver_address.placeholder") })
.min(1, t("settings.notification.channel.form.email_receiver_address.placeholder"))
.email({ message: t("common.errmsg.email_invalid") }),
});
const formRule = createSchemaFieldRule(formSchema);
const form = Form.useFormInstance();
const handleTLSSwitchChange = (checked: boolean) => {
const oldPort = form.getFieldValue("smtpPort");
const newPort = checked && (oldPort == null || oldPort === 25) ? 465 : !checked && (oldPort == null || oldPort === 465) ? 25 : oldPort;
if (newPort !== oldPort) {
form.setFieldValue("smtpPort", newPort);
}
};
return (
<>
<div className="flex space-x-2">
<div className="w-2/5">
<Form.Item name="smtpHost" label={t("settings.notification.channel.form.email_smtp_host.label")} rules={[formRule]}>
<Input placeholder={t("settings.notification.channel.form.email_smtp_host.placeholder")} />
</Form.Item>
</div>
<div className="w-2/5">
<Form.Item name="smtpPort" label={t("settings.notification.channel.form.email_smtp_port.label")} rules={[formRule]} initialValue={465}>
<InputNumber className="w-full" placeholder={t("settings.notification.channel.form.email_smtp_port.placeholder")} min={1} max={65535} />
</Form.Item>
</div>
<div className="w-1/5">
<Form.Item name="smtpTLS" label={t("settings.notification.channel.form.email_smtp_tls.label")} rules={[formRule]} initialValue={true}>
<Switch onChange={handleTLSSwitchChange} />
</Form.Item>
</div>
</div>
<div className="flex space-x-2">
<div className="w-1/2">
<Form.Item name="username" label={t("settings.notification.channel.form.email_username.label")} rules={[formRule]}>
<Input placeholder={t("settings.notification.channel.form.email_username.placeholder")} />
</Form.Item>
</div>
<div className="w-1/2">
<Form.Item name="password" label={t("settings.notification.channel.form.email_password.label")} rules={[formRule]}>
<Input.Password autoComplete="new-password" placeholder={t("settings.notification.channel.form.email_password.placeholder")} />
</Form.Item>
</div>
</div>
<Form.Item name="senderAddress" label={t("settings.notification.channel.form.email_sender_address.label")} rules={[formRule]}>
<Input type="email" placeholder={t("settings.notification.channel.form.email_sender_address.placeholder")} />
</Form.Item>
<Form.Item name="receiverAddress" label={t("settings.notification.channel.form.email_receiver_address.label")} rules={[formRule]}>
<Input type="email" placeholder={t("settings.notification.channel.form.email_receiver_address.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormEmailFields;

View File

@@ -0,0 +1,31 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const NotifyChannelEditFormLarkFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
webhookUrl: z
.string({ message: t("settings.notification.channel.form.lark_webhook_url.placeholder") })
.min(1, t("settings.notification.channel.form.lark_webhook_url.placeholder"))
.url({ message: t("common.errmsg.url_invalid") }),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="webhookUrl"
label={t("settings.notification.channel.form.lark_webhook_url.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.lark_webhook_url.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.lark_webhook_url.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormLarkFields;

View File

@@ -0,0 +1,31 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const NotifyChannelEditFormServerChanFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
url: z
.string({ message: t("settings.notification.channel.form.serverchan_url.placeholder") })
.min(1, t("settings.notification.channel.form.serverchan_url.placeholder"))
.url({ message: t("common.errmsg.url_invalid") }),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="url"
label={t("settings.notification.channel.form.serverchan_url.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.serverchan_url.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.serverchan_url.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormServerChanFields;

View File

@@ -0,0 +1,44 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const NotifyChannelEditFormTelegramFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
apiToken: z
.string({ message: t("settings.notification.channel.form.telegram_api_token.placeholder") })
.min(1, t("settings.notification.channel.form.telegram_api_token.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })),
chatId: z
.string({ message: t("settings.notification.channel.form.telegram_chat_id.placeholder") })
.min(1, t("settings.notification.channel.form.telegram_chat_id.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 })),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="apiToken"
label={t("settings.notification.channel.form.telegram_api_token.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.telegram_api_token.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("settings.notification.channel.form.telegram_api_token.placeholder")} />
</Form.Item>
<Form.Item
name="chatId"
label={t("settings.notification.channel.form.telegram_chat_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.telegram_chat_id.tooltip") }}></span>}
>
<Input type="number" placeholder={t("settings.notification.channel.form.telegram_chat_id.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormTelegramFields;

View File

@@ -0,0 +1,26 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const NotifyChannelEditFormWebhookFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
url: z
.string({ message: t("settings.notification.channel.form.webhook_url.placeholder") })
.min(1, t("settings.notification.channel.form.webhook_url.placeholder"))
.url({ message: t("common.errmsg.url_invalid") }),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<div>
<Form.Item name="url" label={t("settings.notification.channel.form.webhook_url.label")} rules={[formRule]}>
<Input placeholder={t("settings.notification.channel.form.webhook_url.placeholder")} />
</Form.Item>
</div>
);
};
export default NotifyChannelEditFormWebhookFields;

View File

@@ -0,0 +1,110 @@
import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useDeepCompareMemo } from "@ant-design/pro-components";
import { Button, Collapse, message, notification, Skeleton, Space, Switch, Tooltip, type CollapseProps } from "antd";
import NotifyChannelEditForm, { type NotifyChannelEditFormInstance } from "./NotifyChannelEditForm";
import NotifyTestButton from "./NotifyTestButton";
import { notifyChannelsMap } from "@/domain/settings";
import { useNotifyChannelStore } from "@/stores/notify";
import { getErrMsg } from "@/utils/error";
type NotifyChannelsSemanticDOM = "collapse" | "form";
export type NotifyChannelsProps = {
className?: string;
classNames?: Partial<Record<NotifyChannelsSemanticDOM, string>>;
style?: React.CSSProperties;
styles?: Partial<Record<NotifyChannelsSemanticDOM, React.CSSProperties>>;
};
const NotifyChannels = ({ className, classNames, style, styles }: NotifyChannelsProps) => {
const { t, i18n } = useTranslation();
const [messageApi, MessageContextHolder] = message.useMessage();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
const { initialized, channels, setChannel, fetchChannels } = useNotifyChannelStore();
useEffect(() => {
fetchChannels();
}, [fetchChannels]);
const channelFormRefs = useRef<Array<NotifyChannelEditFormInstance | null>>([]);
const channelCollapseItems: CollapseProps["items"] = useDeepCompareMemo(
() =>
Array.from(notifyChannelsMap.values()).map((channel, index) => {
return {
key: `channel-${channel.type}`,
label: <>{t(channel.name)}</>,
children: (
<div className={classNames?.form} style={styles?.form}>
<NotifyChannelEditForm ref={(ref) => (channelFormRefs.current[index] = ref)} model={channels[channel.type]} channel={channel.type} />
<Space>
<Button type="primary" onClick={() => handleClickSubmit(channel.type, index)}>
{t("common.button.save")}
</Button>
{channels[channel.type] ? (
<Tooltip title={t("settings.notification.push_test.tooltip")}>
<>
<NotifyTestButton channel={channel.type} />
</>
</Tooltip>
) : null}
</Space>
</div>
),
extra: (
<div onClick={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} onMouseUp={(e) => e.stopPropagation()}>
<Switch
defaultChecked={channels[channel.type]?.enabled as boolean}
disabled={channels[channel.type] == null}
checkedChildren={t("settings.notification.channel.enabled.on")}
unCheckedChildren={t("settings.notification.channel.enabled.off")}
onChange={(checked) => handleSwitchChange(channel.type, checked)}
/>
</div>
),
forceRender: true,
};
}),
[i18n.language, channels]
);
const handleSwitchChange = (channel: string, enabled: boolean) => {
setChannel(channel, { enabled });
};
const handleClickSubmit = async (channel: string, index: number) => {
const form = channelFormRefs.current[index];
if (!form) {
return;
}
await form.validateFields();
try {
setChannel(channel, form.getFieldsValue());
messageApi.success(t("common.text.operation_succeeded"));
} catch (err) {
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
}
};
return (
<div className={className} style={style}>
{MessageContextHolder}
{NotificationContextHolder}
{!initialized ? (
<Skeleton active />
) : (
<Collapse className={classNames?.collapse} style={styles?.collapse} accordion={true} bordered={false} items={channelCollapseItems} />
)}
</div>
);
};
export default NotifyChannels;

View File

@@ -0,0 +1,128 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useRequest } from "ahooks";
import { Button, Form, Input, message, notification, Skeleton } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { ClientResponseError } from "pocketbase";
import { defaultNotifyTemplate, SETTINGS_NAMES, type NotifyTemplatesSettingsContent } from "@/domain/settings";
import { get as getSettings, save as saveSettings } from "@/repository/settings";
import { getErrMsg } from "@/utils/error";
export type NotifyTemplateFormProps = {
className?: string;
style?: React.CSSProperties;
};
const NotifyTemplateForm = ({ className, style }: NotifyTemplateFormProps) => {
const { t } = useTranslation();
const [messageApi, MessageContextHolder] = message.useMessage();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
const formSchema = z.object({
subject: z
.string()
.trim()
.min(1, t("settings.notification.template.form.subject.placeholder"))
.max(1000, t("common.errmsg.string_max", { max: 1000 })),
message: z
.string()
.trim()
.min(1, t("settings.notification.template.form.message.placeholder"))
.max(1000, t("common.errmsg.string_max", { max: 1000 })),
});
const formRule = createSchemaFieldRule(formSchema);
const [form] = Form.useForm<z.infer<typeof formSchema>>();
const [formPending, setFormPending] = useState(false);
const [initialValues, setInitialValues] = useState<Partial<z.infer<typeof formSchema>>>();
const [initialChanged, setInitialChanged] = useState(false);
const { loading } = useRequest(
() => {
return getSettings<NotifyTemplatesSettingsContent>(SETTINGS_NAMES.NOTIFY_TEMPLATES);
},
{
onError: (err) => {
if (err instanceof ClientResponseError && err.isAbort) {
return;
}
console.error(err);
},
onFinally: (_, resp) => {
const template = resp?.content?.notifyTemplates?.[0] ?? defaultNotifyTemplate;
setInitialValues({ ...template });
},
}
);
const handleInputChange = () => {
setInitialChanged(true);
};
const handleFormFinish = async (fields: z.infer<typeof formSchema>) => {
setFormPending(true);
try {
const settings = await getSettings<NotifyTemplatesSettingsContent>(SETTINGS_NAMES.NOTIFY_TEMPLATES);
await saveSettings<NotifyTemplatesSettingsContent>({
...settings,
content: {
notifyTemplates: [fields],
},
});
messageApi.success(t("common.text.operation_succeeded"));
} catch (err) {
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
} finally {
setFormPending(false);
}
};
return (
<div className={className} style={style}>
{MessageContextHolder}
{NotificationContextHolder}
{loading ? (
<Skeleton active />
) : (
<Form form={form} disabled={formPending} initialValues={initialValues} layout="vertical" onFinish={handleFormFinish}>
<Form.Item
name="subject"
label={t("settings.notification.template.form.subject.label")}
extra={t("settings.notification.template.form.subject.tooltip")}
rules={[formRule]}
>
<Input placeholder={t("settings.notification.template.form.subject.placeholder")} onChange={handleInputChange} />
</Form.Item>
<Form.Item
name="message"
label={t("settings.notification.template.form.message.label")}
extra={t("settings.notification.template.form.message.tooltip")}
rules={[formRule]}
>
<Input.TextArea
autoSize={{ minRows: 3, maxRows: 5 }}
placeholder={t("settings.notification.template.form.message.placeholder")}
onChange={handleInputChange}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" disabled={!initialChanged} loading={formPending}>
{t("common.button.save")}
</Button>
</Form.Item>
</Form>
)}
</div>
);
};
export default NotifyTemplateForm;

View File

@@ -0,0 +1,54 @@
import { useRequest } from "ahooks";
import { useTranslation } from "react-i18next";
import { Button, message, notification, type ButtonProps } from "antd";
import { notifyTest } from "@/api/notify";
import { getErrMsg } from "@/utils/error";
export type NotifyTestButtonProps = {
className?: string;
style?: React.CSSProperties;
channel: string;
disabled?: boolean;
size?: ButtonProps["size"];
};
const NotifyTestButton = ({ className, style, channel, disabled, size }: NotifyTestButtonProps) => {
const { t } = useTranslation();
const [messageApi, MessageContextHolder] = message.useMessage();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
const { loading, run: executeNotifyTest } = useRequest(
() => {
return notifyTest(channel);
},
{
refreshDeps: [channel],
manual: true,
onSuccess: () => {
messageApi.success(t("settings.notification.push_test.pushed"));
},
onError: (err) => {
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
},
}
);
const handleClick = () => {
executeNotifyTest();
};
return (
<>
{MessageContextHolder}
{NotificationContextHolder}
<Button className={className} style={style} disabled={disabled} loading={loading} size={size} onClick={handleClick}>
{t("settings.notification.push_test.button")}
</Button>
</>
);
};
export default NotifyTestButton;

View File

@@ -1,262 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { getErrMsg } from "@/utils/error";
import { NotifyChannels, NotifyChannelBark } from "@/domain/settings";
import { save } from "@/repository/settings";
import { useNotifyContext } from "@/providers/notify";
import { notifyTest } from "@/api/notify";
import Show from "@/components/Show";
type BarkSetting = {
id: string;
name: string;
data: NotifyChannelBark;
};
const Bark = () => {
const { config, setChannels } = useNotifyContext();
const { t } = useTranslation();
const [changed, setChanged] = useState<boolean>(false);
const [bark, setBark] = useState<BarkSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
serverUrl: "",
deviceKey: "",
enabled: false,
},
});
const [originBark, setOriginBark] = useState<BarkSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
serverUrl: "",
deviceKey: "",
enabled: false,
},
});
useEffect(() => {
setChanged(false);
}, [config]);
useEffect(() => {
const data = getDetailBark();
setOriginBark({
id: config.id ?? "",
name: "common.notifier.bark",
data,
});
}, [config]);
useEffect(() => {
const data = getDetailBark();
setBark({
id: config.id ?? "",
name: "common.notifier.bark",
data,
});
}, [config]);
const { toast } = useToast();
const checkChanged = (data: NotifyChannelBark) => {
if (data.serverUrl !== originBark.data.serverUrl || data.deviceKey !== originBark.data.deviceKey) {
setChanged(true);
} else {
setChanged(false);
}
};
const getDetailBark = () => {
const df: NotifyChannelBark = {
serverUrl: "",
deviceKey: "",
enabled: false,
};
if (!config.content) {
return df;
}
const chanels = config.content as NotifyChannels;
if (!chanels.bark) {
return df;
}
return chanels.bark as NotifyChannelBark;
};
const handleSaveClick = async () => {
try {
const resp = await save({
...config,
name: "notifyChannels",
content: {
...config.content,
bark: {
...bark.data,
},
},
});
setChannels(resp);
toast({
title: t("common.text.operation_succeeded"),
description: t("settings.notification.config.saved.message"),
});
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("common.text.operation_failed"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => {
if (testing) return;
try {
setTesting(true);
await notifyTest("bark");
toast({
title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.push_test_message.succeeded.message"),
});
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive",
});
} finally {
setTesting(false);
}
};
const handleSwitchChange = async () => {
const newData = {
...bark,
data: {
...bark.data,
enabled: !bark.data.enabled,
},
};
setBark(newData);
try {
const resp = await save({
...config,
name: "notifyChannels",
content: {
...config.content,
bark: {
...newData.data,
},
},
});
setChannels(resp);
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("common.text.operation_failed"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
return (
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.bark.server_url.label")}</Label>
<Input
placeholder={t("settings.notification.bark.server_url.placeholder")}
value={bark.data.serverUrl}
onChange={(e) => {
const newData = {
...bark,
data: {
...bark.data,
serverUrl: e.target.value,
},
};
checkChanged(newData.data);
setBark(newData);
}}
/>
</div>
<div>
<Label>{t("settings.notification.bark.device_key.label")}</Label>
<Input
placeholder={t("settings.notification.bark.device_key.placeholder")}
value={bark.data.deviceKey}
onChange={(e) => {
const newData = {
...bark,
data: {
...bark.data,
deviceKey: e.target.value,
},
};
checkChanged(newData.data);
setBark(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={bark.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.button.save")}
</Button>
</Show>
<Show when={!changed && bark.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default Bark;

View File

@@ -1,260 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { getErrMsg } from "@/utils/error";
import { NotifyChannelDingTalk, NotifyChannels } from "@/domain/settings";
import { useNotifyContext } from "@/providers/notify";
import { save } from "@/repository/settings";
import Show from "@/components/Show";
import { notifyTest } from "@/api/notify";
type DingTalkSetting = {
id: string;
name: string;
data: NotifyChannelDingTalk;
};
const DingTalk = () => {
const { config, setChannels } = useNotifyContext();
const { t } = useTranslation();
const [changed, setChanged] = useState<boolean>(false);
const [dingtalk, setDingtalk] = useState<DingTalkSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
accessToken: "",
secret: "",
enabled: false,
},
});
const [originDingtalk, setOriginDingtalk] = useState<DingTalkSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
accessToken: "",
secret: "",
enabled: false,
},
});
useEffect(() => {
setChanged(false);
}, [config]);
useEffect(() => {
const data = getDetailDingTalk();
setOriginDingtalk({
id: config.id ?? "",
name: "dingtalk",
data,
});
}, [config]);
useEffect(() => {
const data = getDetailDingTalk();
setDingtalk({
id: config.id ?? "",
name: "dingtalk",
data,
});
}, [config]);
const { toast } = useToast();
const getDetailDingTalk = () => {
const df: NotifyChannelDingTalk = {
accessToken: "",
secret: "",
enabled: false,
};
if (!config.content) {
return df;
}
const chanels = config.content as NotifyChannels;
if (!chanels.dingtalk) {
return df;
}
return chanels.dingtalk as NotifyChannelDingTalk;
};
const checkChanged = (data: NotifyChannelDingTalk) => {
if (data.accessToken !== originDingtalk.data.accessToken || data.secret !== originDingtalk.data.secret) {
setChanged(true);
} else {
setChanged(false);
}
};
const handleSaveClick = async () => {
try {
const resp = await save({
...config,
name: "notifyChannels",
content: {
...config.content,
dingtalk: {
...dingtalk.data,
},
},
});
setChannels(resp);
toast({
title: t("common.text.operation_succeeded"),
description: t("settings.notification.config.saved.message"),
});
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("common.text.operation_failed"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
} finally {
setTesting(false);
}
};
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => {
if (testing) return;
try {
setTesting(true);
await notifyTest("dingtalk");
toast({
title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.push_test_message.succeeded.message"),
});
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
const handleSwitchChange = async () => {
const newData = {
...dingtalk,
data: {
...dingtalk.data,
enabled: !dingtalk.data.enabled,
},
};
setDingtalk(newData);
try {
const resp = await save({
...config,
name: "notifyChannels",
content: {
...config.content,
dingtalk: {
...newData.data,
},
},
});
setChannels(resp);
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("common.text.operation_failed"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
return (
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.dingtalk.access_token.label")}</Label>
<Input
placeholder={t("settings.notification.dingtalk.access_token.placeholder")}
value={dingtalk.data.accessToken}
onChange={(e) => {
const newData = {
...dingtalk,
data: {
...dingtalk.data,
accessToken: e.target.value,
},
};
checkChanged(newData.data);
setDingtalk(newData);
}}
/>
</div>
<div>
<Label>{t("settings.notification.dingtalk.secret.label")}</Label>
<Input
placeholder={t("settings.notification.dingtalk.secret.placeholder")}
value={dingtalk.data.secret}
onChange={(e) => {
const newData = {
...dingtalk,
data: {
...dingtalk.data,
secret: e.target.value,
},
};
checkChanged(newData.data);
setDingtalk(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={dingtalk.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.button.save")}
</Button>
</Show>
<Show when={!changed && dingtalk.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default DingTalk;

View File

@@ -1,384 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { getErrMsg } from "@/utils/error";
import { NotifyChannelEmail, NotifyChannels } from "@/domain/settings";
import { useNotifyContext } from "@/providers/notify";
import { save } from "@/repository/settings";
import Show from "@/components/Show";
import { notifyTest } from "@/api/notify";
type EmailSetting = {
id: string;
name: string;
data: NotifyChannelEmail;
};
const Mail = () => {
const { config, setChannels } = useNotifyContext();
const { t } = useTranslation();
const [changed, setChanged] = useState<boolean>(false);
const [mail, setMail] = useState<EmailSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
smtpHost: "",
smtpPort: 465,
smtpTLS: true,
username: "",
password: "",
senderAddress: "",
receiverAddress: "",
enabled: false,
},
});
const [originMail, setOriginMail] = useState<EmailSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
smtpHost: "",
smtpPort: 465,
smtpTLS: true,
username: "",
password: "",
senderAddress: "",
receiverAddress: "",
enabled: false,
},
});
useEffect(() => {
setChanged(false);
}, [config]);
useEffect(() => {
const data = getDetailMail();
setOriginMail({
id: config.id ?? "",
name: "email",
data,
});
}, [config]);
useEffect(() => {
const data = getDetailMail();
setMail({
id: config.id ?? "",
name: "email",
data,
});
}, [config]);
const { toast } = useToast();
const getDetailMail = () => {
const df: NotifyChannelEmail = {
smtpHost: "smtp.example.com",
smtpPort: 465,
smtpTLS: true,
username: "",
password: "",
senderAddress: "",
receiverAddress: "",
enabled: false,
};
if (!config.content) {
return df;
}
const chanels = config.content as NotifyChannels;
if (!chanels.email) {
return df;
}
return chanels.email as NotifyChannelEmail;
};
const checkChanged = (data: NotifyChannelEmail) => {
if (
data.smtpHost !== originMail.data.smtpHost ||
data.smtpPort !== originMail.data.smtpPort ||
data.smtpTLS !== originMail.data.smtpTLS ||
data.username !== originMail.data.username ||
data.password !== originMail.data.password ||
data.senderAddress !== originMail.data.senderAddress ||
data.receiverAddress !== originMail.data.receiverAddress
) {
setChanged(true);
} else {
setChanged(false);
}
};
const handleSaveClick = async () => {
try {
const resp = await save({
...config,
name: "notifyChannels",
content: {
...config.content,
email: {
...mail.data,
},
},
});
setChannels(resp);
toast({
title: t("common.text.operation_succeeded"),
description: t("settings.notification.config.saved.message"),
});
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("common.text.operation_failed"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => {
if (testing) return;
try {
setTesting(true);
await notifyTest("email");
toast({
title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.push_test_message.succeeded.message"),
});
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive",
});
} finally {
setTesting(false);
}
};
const handleSwitchChange = async () => {
const newData = {
...mail,
data: {
...mail.data,
enabled: !mail.data.enabled,
},
};
setMail(newData);
try {
const resp = await save({
...config,
name: "notifyChannels",
content: {
...config.content,
email: {
...newData.data,
},
},
});
setChannels(resp);
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("common.text.operation_failed"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
return (
<div className="flex flex-col space-y-4">
<div className="flex space-x-4">
<div className="w-2/5">
<Label>{t("settings.notification.email.smtp_host.label")}</Label>
<Input
placeholder={t("settings.notification.email.smtp_host.placeholder")}
value={mail.data.smtpHost}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
smtpHost: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="w-2/5">
<Label>{t("settings.notification.email.smtp_port.label")}</Label>
<Input
type="number"
placeholder={t("settings.notification.email.smtp_port.placeholder")}
value={mail.data.smtpPort}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
smtpPort: +e.target.value || 0,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="w-1/5">
<Label>{t("settings.notification.email.smtp_tls.label")}</Label>
<Switch
className="block mt-2"
checked={mail.data.smtpTLS}
onCheckedChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
smtpPort: e && mail.data.smtpPort === 25 ? 465 : !e && mail.data.smtpPort === 465 ? 25 : mail.data.smtpPort,
smtpTLS: e,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
</div>
<div className="flex space-x-4">
<div className="w-1/2">
<Label>{t("settings.notification.email.username.label")}</Label>
<Input
placeholder={t("settings.notification.email.username.placeholder")}
value={mail.data.username}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
username: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="w-1/2">
<Label>{t("settings.notification.email.password.label")}</Label>
<Input
placeholder={t("settings.notification.email.password.placeholder")}
value={mail.data.password}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
password: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
</div>
<div>
<Label>{t("settings.notification.email.sender_address.label")}</Label>
<Input
placeholder={t("settings.notification.email.sender_address.placeholder")}
value={mail.data.senderAddress}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
senderAddress: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div>
<Label>{t("settings.notification.email.receiver_address.label")}</Label>
<Input
placeholder={t("settings.notification.email.receiver_address.placeholder")}
value={mail.data.receiverAddress}
onChange={(e) => {
const newData = {
...mail,
data: {
...mail.data,
receiverAddress: e.target.value,
},
};
checkChanged(newData.data);
setMail(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={mail.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.button.save")}
</Button>
</Show>
<Show when={!changed && mail.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default Mail;

View File

@@ -1,238 +0,0 @@
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { useNotifyContext } from "@/providers/notify";
import { NotifyChannelLark, NotifyChannels } from "@/domain/settings";
import { useEffect, useState } from "react";
import { save } from "@/repository/settings";
import { getErrMsg } from "@/utils/error";
import { useToast } from "@/components/ui/use-toast";
import { useTranslation } from "react-i18next";
import { notifyTest } from "@/api/notify";
import Show from "@/components/Show";
type LarkSetting = {
id: string;
name: string;
data: NotifyChannelLark;
};
const Lark = () => {
const { config, setChannels } = useNotifyContext();
const { t } = useTranslation();
const [changed, setChanged] = useState<boolean>(false);
const [lark, setLark] = useState<LarkSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
webhookUrl: "",
enabled: false,
},
});
const [originLark, setOriginLark] = useState<LarkSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
webhookUrl: "",
enabled: false,
},
});
useEffect(() => {
setChanged(false);
}, [config]);
useEffect(() => {
const data = getDetailLark();
setOriginLark({
id: config.id ?? "",
name: "lark",
data,
});
}, [config]);
useEffect(() => {
const data = getDetailLark();
setLark({
id: config.id ?? "",
name: "lark",
data,
});
}, [config]);
const { toast } = useToast();
const checkChanged = (data: NotifyChannelLark) => {
if (data.webhookUrl !== originLark.data.webhookUrl) {
setChanged(true);
} else {
setChanged(false);
}
};
const getDetailLark = () => {
const df: NotifyChannelLark = {
webhookUrl: "",
enabled: false,
};
if (!config.content) {
return df;
}
const chanels = config.content as NotifyChannels;
if (!chanels.lark) {
return df;
}
return chanels.lark as NotifyChannelLark;
};
const handleSaveClick = async () => {
try {
const resp = await save({
...config,
name: "notifyChannels",
content: {
...config.content,
lark: {
...lark.data,
},
},
});
setChannels(resp);
toast({
title: t("common.text.operation_succeeded"),
description: t("settings.notification.config.saved.message"),
});
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("common.text.operation_failed"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
} finally {
setTesting(false);
}
};
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => {
if (testing) return;
try {
setTesting(true);
await notifyTest("lark");
toast({
title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.push_test_message.succeeded.message"),
});
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
const handleSwitchChange = async () => {
const newData = {
...lark,
data: {
...lark.data,
enabled: !lark.data.enabled,
},
};
setLark(newData);
try {
const resp = await save({
...config,
name: "notifyChannels",
content: {
...config.content,
lark: {
...newData.data,
},
},
});
setChannels(resp);
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("common.text.operation_failed"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
return (
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.lark.webhook_url.label")}</Label>
<Input
placeholder={t("settings.notification.lark.webhook_url.placeholder")}
value={lark.data.webhookUrl}
onChange={(e) => {
const newData = {
...lark,
data: {
...lark.data,
webhookUrl: e.target.value,
},
};
checkChanged(newData.data);
setLark(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={lark.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.button.save")}
</Button>
</Show>
<Show when={!changed && lark.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default Lark;

View File

@@ -1,97 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/components/ui/use-toast";
import { defaultNotifyTemplate, NotifyTemplates, NotifyTemplate as NotifyTemplateT } from "@/domain/settings";
import { get, save } from "@/repository/settings";
const NotifyTemplate = () => {
const [id, setId] = useState("");
const [templates, setTemplates] = useState<NotifyTemplateT[]>([defaultNotifyTemplate]);
const { toast } = useToast();
const { t } = useTranslation();
useEffect(() => {
const featchData = async () => {
const resp = await get("templates");
if (resp.content) {
setTemplates((resp.content as NotifyTemplates).notifyTemplates);
setId(resp.id ? resp.id : "");
}
};
featchData();
}, []);
const handleTitleChange = (val: string) => {
const template = templates?.[0] ?? {};
setTemplates([
{
...template,
title: val,
},
]);
};
const handleContentChange = (val: string) => {
const template = templates?.[0] ?? {};
setTemplates([
{
...template,
content: val,
},
]);
};
const handleSaveClick = async () => {
const resp = await save({
id: id,
content: {
notifyTemplates: templates,
},
name: "templates",
});
if (resp.id) {
setId(resp.id);
}
toast({
title: t("common.text.operation_succeeded"),
description: t("settings.notification.template.saved.message"),
});
};
return (
<div>
<Input
value={templates?.[0]?.title}
onChange={(e) => {
handleTitleChange(e.target.value);
}}
/>
<div className="text-muted-foreground text-sm mt-1">{t("settings.notification.template.variables.tips.title")}</div>
<Textarea
className="mt-2"
value={templates?.[0]?.content}
onChange={(e) => {
handleContentChange(e.target.value);
}}
></Textarea>
<div className="text-muted-foreground text-sm mt-1">{t("settings.notification.template.variables.tips.content")}</div>
<div className="flex justify-end mt-2">
<Button onClick={handleSaveClick}>{t("common.button.save")}</Button>
</div>
</div>
);
};
export default NotifyTemplate;

View File

@@ -1,249 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { getErrMsg } from "@/utils/error";
import { isValidURL } from "@/utils/url";
import { NotifyChannels, NotifyChannelServerChan } from "@/domain/settings";
import { save } from "@/repository/settings";
import { useNotifyContext } from "@/providers/notify";
import { notifyTest } from "@/api/notify";
import Show from "@/components/Show";
type ServerChanSetting = {
id: string;
name: string;
data: NotifyChannelServerChan;
};
const ServerChan = () => {
const { config, setChannels } = useNotifyContext();
const { t } = useTranslation();
const [changed, setChanged] = useState<boolean>(false);
const [serverchan, setServerChan] = useState<ServerChanSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
url: "",
enabled: false,
},
});
const [originServerChan, setOriginServerChan] = useState<ServerChanSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
url: "",
enabled: false,
},
});
useEffect(() => {
setChanged(false);
}, [config]);
useEffect(() => {
const data = getDetailServerChan();
setOriginServerChan({
id: config.id ?? "",
name: "serverchan",
data,
});
}, [config]);
useEffect(() => {
const data = getDetailServerChan();
setServerChan({
id: config.id ?? "",
name: "serverchan",
data,
});
}, [config]);
const { toast } = useToast();
const checkChanged = (data: NotifyChannelServerChan) => {
if (data.url !== originServerChan.data.url) {
setChanged(true);
} else {
setChanged(false);
}
};
const getDetailServerChan = () => {
const df: NotifyChannelServerChan = {
url: "",
enabled: false,
};
if (!config.content) {
return df;
}
const chanels = config.content as NotifyChannels;
if (!chanels.serverchan) {
return df;
}
return chanels.serverchan as NotifyChannelServerChan;
};
const handleSaveClick = async () => {
try {
serverchan.data.url = serverchan.data.url.trim();
if (!isValidURL(serverchan.data.url)) {
toast({
title: t("common.text.operation_failed"),
description: t("common.errmsg.url_invalid"),
variant: "destructive",
});
return;
}
const resp = await save({
...config,
name: "notifyChannels",
content: {
...config.content,
serverchan: {
...serverchan.data,
},
},
});
setChannels(resp);
toast({
title: t("common.text.operation_succeeded"),
description: t("settings.notification.config.saved.message"),
});
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("common.text.operation_failed"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => {
if (testing) return;
try {
setTesting(true);
await notifyTest("serverchan");
toast({
title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.push_test_message.succeeded.message"),
});
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive",
});
} finally {
setTesting(false);
}
};
const handleSwitchChange = async () => {
const newData = {
...serverchan,
data: {
...serverchan.data,
enabled: !serverchan.data.enabled,
},
};
setServerChan(newData);
try {
const resp = await save({
...config,
name: "notifyChannels",
content: {
...config.content,
serverchan: {
...newData.data,
},
},
});
setChannels(resp);
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("common.text.operation_failed"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
return (
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.serverchan.url.label")}</Label>
<Input
placeholder={t("settings.notification.serverchan.url.placeholder")}
value={serverchan.data.url}
onChange={(e) => {
const newData = {
...serverchan,
data: {
...serverchan.data,
url: e.target.value,
},
};
checkChanged(newData.data);
setServerChan(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={serverchan.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.button.save")}
</Button>
</Show>
<Show when={!changed && serverchan.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default ServerChan;

View File

@@ -1,262 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { getErrMsg } from "@/utils/error";
import { NotifyChannels, NotifyChannelTelegram } from "@/domain/settings";
import { save } from "@/repository/settings";
import { useNotifyContext } from "@/providers/notify";
import { notifyTest } from "@/api/notify";
import Show from "@/components/Show";
type TelegramSetting = {
id: string;
name: string;
data: NotifyChannelTelegram;
};
const Telegram = () => {
const { config, setChannels } = useNotifyContext();
const { t } = useTranslation();
const [changed, setChanged] = useState<boolean>(false);
const [telegram, setTelegram] = useState<TelegramSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
apiToken: "",
chatId: "",
enabled: false,
},
});
const [originTelegram, setOriginTelegram] = useState<TelegramSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
apiToken: "",
chatId: "",
enabled: false,
},
});
useEffect(() => {
setChanged(false);
}, [config]);
useEffect(() => {
const data = getDetailTelegram();
setOriginTelegram({
id: config.id ?? "",
name: "common.notifier.telegram",
data,
});
}, [config]);
useEffect(() => {
const data = getDetailTelegram();
setTelegram({
id: config.id ?? "",
name: "common.notifier.telegram",
data,
});
}, [config]);
const { toast } = useToast();
const checkChanged = (data: NotifyChannelTelegram) => {
if (data.apiToken !== originTelegram.data.apiToken || data.chatId !== originTelegram.data.chatId) {
setChanged(true);
} else {
setChanged(false);
}
};
const getDetailTelegram = () => {
const df: NotifyChannelTelegram = {
apiToken: "",
chatId: "",
enabled: false,
};
if (!config.content) {
return df;
}
const chanels = config.content as NotifyChannels;
if (!chanels.telegram) {
return df;
}
return chanels.telegram as NotifyChannelTelegram;
};
const handleSaveClick = async () => {
try {
const resp = await save({
...config,
name: "notifyChannels",
content: {
...config.content,
telegram: {
...telegram.data,
},
},
});
setChannels(resp);
toast({
title: t("common.text.operation_succeeded"),
description: t("settings.notification.config.saved.message"),
});
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("common.text.operation_failed"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => {
if (testing) return;
try {
setTesting(true);
await notifyTest("telegram");
toast({
title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.push_test_message.succeeded.message"),
});
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive",
});
} finally {
setTesting(false);
}
};
const handleSwitchChange = async () => {
const newData = {
...telegram,
data: {
...telegram.data,
enabled: !telegram.data.enabled,
},
};
setTelegram(newData);
try {
const resp = await save({
...config,
name: "notifyChannels",
content: {
...config.content,
telegram: {
...newData.data,
},
},
});
setChannels(resp);
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("common.text.operation_failed"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
return (
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.telegram.api_token.label")}</Label>
<Input
placeholder={t("settings.notification.telegram.api_token.placeholder")}
value={telegram.data.apiToken}
onChange={(e) => {
const newData = {
...telegram,
data: {
...telegram.data,
apiToken: e.target.value,
},
};
checkChanged(newData.data);
setTelegram(newData);
}}
/>
</div>
<div>
<Label>{t("settings.notification.telegram.chat_id.label")}</Label>
<Input
placeholder={t("settings.notification.telegram.chat_id.placeholder")}
value={telegram.data.chatId}
onChange={(e) => {
const newData = {
...telegram,
data: {
...telegram.data,
chatId: e.target.value,
},
};
checkChanged(newData.data);
setTelegram(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={telegram.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.button.save")}
</Button>
</Show>
<Show when={!changed && telegram.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default Telegram;

View File

@@ -1,249 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { getErrMsg } from "@/utils/error";
import { isValidURL } from "@/utils/url";
import { NotifyChannels, NotifyChannelWebhook } from "@/domain/settings";
import { save } from "@/repository/settings";
import { useNotifyContext } from "@/providers/notify";
import { notifyTest } from "@/api/notify";
import Show from "@/components/Show";
type WebhookSetting = {
id: string;
name: string;
data: NotifyChannelWebhook;
};
const Webhook = () => {
const { config, setChannels } = useNotifyContext();
const { t } = useTranslation();
const [changed, setChanged] = useState<boolean>(false);
const [webhook, setWebhook] = useState<WebhookSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
url: "",
enabled: false,
},
});
const [originWebhook, setOriginWebhook] = useState<WebhookSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
url: "",
enabled: false,
},
});
useEffect(() => {
setChanged(false);
}, [config]);
useEffect(() => {
const data = getDetailWebhook();
setOriginWebhook({
id: config.id ?? "",
name: "webhook",
data,
});
}, [config]);
useEffect(() => {
const data = getDetailWebhook();
setWebhook({
id: config.id ?? "",
name: "webhook",
data,
});
}, [config]);
const { toast } = useToast();
const checkChanged = (data: NotifyChannelWebhook) => {
if (data.url !== originWebhook.data.url) {
setChanged(true);
} else {
setChanged(false);
}
};
const getDetailWebhook = () => {
const df: NotifyChannelWebhook = {
url: "",
enabled: false,
};
if (!config.content) {
return df;
}
const chanels = config.content as NotifyChannels;
if (!chanels.webhook) {
return df;
}
return chanels.webhook as NotifyChannelWebhook;
};
const handleSaveClick = async () => {
try {
webhook.data.url = webhook.data.url.trim();
if (!isValidURL(webhook.data.url)) {
toast({
title: t("common.text.operation_failed"),
description: t("common.errmsg.url_invalid"),
variant: "destructive",
});
return;
}
const resp = await save({
...config,
name: "notifyChannels",
content: {
...config.content,
webhook: {
...webhook.data,
},
},
});
setChannels(resp);
toast({
title: t("common.text.operation_succeeded"),
description: t("settings.notification.config.saved.message"),
});
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("common.text.operation_failed"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
const [testing, setTesting] = useState<boolean>(false);
const handlePushTestClick = async () => {
if (testing) return;
try {
setTesting(true);
await notifyTest("webhook");
toast({
title: t("settings.notification.push_test_message.succeeded.message"),
description: t("settings.notification.push_test_message.succeeded.message"),
});
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("settings.notification.push_test_message.failed.message"),
description: `${t("settings.notification.push_test_message.failed.message")}: ${msg}`,
variant: "destructive",
});
} finally {
setTesting(false);
}
};
const handleSwitchChange = async () => {
const newData = {
...webhook,
data: {
...webhook.data,
enabled: !webhook.data.enabled,
},
};
setWebhook(newData);
try {
const resp = await save({
...config,
name: "notifyChannels",
content: {
...config.content,
webhook: {
...newData.data,
},
},
});
setChannels(resp);
} catch (e) {
const msg = getErrMsg(e);
toast({
title: t("common.text.operation_failed"),
description: `${t("settings.notification.config.failed.message")}: ${msg}`,
variant: "destructive",
});
}
};
return (
<div className="flex flex-col space-y-4">
<div>
<Label>{t("settings.notification.webhook.url.label")}</Label>
<Input
placeholder={t("settings.notification.webhook.url.placeholder")}
value={webhook.data.url}
onChange={(e) => {
const newData = {
...webhook,
data: {
...webhook.data,
url: e.target.value,
},
};
checkChanged(newData.data);
setWebhook(newData);
}}
/>
</div>
<div className="flex justify-between gap-4">
<div className="flex items-center space-x-1">
<Switch id="airplane-mode" checked={webhook.data.enabled} onCheckedChange={handleSwitchChange} />
<Label htmlFor="airplane-mode">{t("settings.notification.config.enable")}</Label>
</div>
<div className="flex items-center space-x-1">
<Show when={changed}>
<Button
onClick={() => {
handleSaveClick();
}}
>
{t("common.button.save")}
</Button>
</Show>
<Show when={!changed && webhook.id != ""}>
<Button
variant="secondary"
loading={testing}
onClick={() => {
handlePushTestClick();
}}
>
{t("settings.notification.push_test_message")}
</Button>
</Show>
</div>
</div>
</div>
);
};
export default Webhook;

View File

@@ -41,13 +41,14 @@ const formSchema = z
keyPath: z
.string()
.min(0, t("domain.deployment.form.file_key_path.placeholder"))
.max(255, t("common.errmsg.string_max", { max: 255 })),
pfxPassword: z.string().optional(),
jksAlias: z.string().optional(),
jksKeypass: z.string().optional(),
jksStorepass: z.string().optional(),
preCommand: z.string().optional(),
command: z.string().optional(),
.max(255, t("common.errmsg.string_max", { max: 255 }))
.nullish(),
pfxPassword: z.string().nullish(),
jksAlias: z.string().nullish(),
jksKeypass: z.string().nullish(),
jksStorepass: z.string().nullish(),
preCommand: z.string().nullish(),
command: z.string().nullish(),
shell: z.union([z.literal("sh"), z.literal("cmd"), z.literal("powershell")], {
message: t("domain.deployment.form.shell.placeholder"),
}),

View File

@@ -40,13 +40,14 @@ const formSchema = z
keyPath: z
.string()
.min(0, t("domain.deployment.form.file_key_path.placeholder"))
.max(255, t("common.errmsg.string_max", { max: 255 })),
pfxPassword: z.string().optional(),
jksAlias: z.string().optional(),
jksKeypass: z.string().optional(),
jksStorepass: z.string().optional(),
preCommand: z.string().optional(),
command: z.string().optional(),
.max(255, t("common.errmsg.string_max", { max: 255 }))
.nullish(),
pfxPassword: z.string().nullish(),
jksAlias: z.string().nullish(),
jksKeypass: z.string().nullish(),
jksStorepass: z.string().nullish(),
preCommand: z.string().nullish(),
command: z.string().nullish(),
})
.refine((data) => (data.format === "pem" ? !!data.keyPath?.trim() : true), {
message: t("domain.deployment.form.file_key_path.placeholder"),

View File

@@ -9,7 +9,7 @@ import PanelBody from "./PanelBody";
import { useTranslation } from "react-i18next";
import Show from "../Show";
import { deployTargetsMap } from "@/domain/domain";
import { channelLabelMap } from "@/domain/settings";
import { notifyChannelsMap } from "@/domain/settings";
type NodeProps = {
data: WorkflowNode;
@@ -69,10 +69,10 @@ const Node = ({ data }: NodeProps) => {
);
}
case WorkflowNodeType.Notify: {
const channelLabel = channelLabelMap.get(data.config?.channel as string);
const channelLabel = notifyChannelsMap.get(data.config?.channel as string);
return (
<div className="flex space-x-2 items-baseline">
<div className="text-stone-700 w-12 truncate">{t(channelLabel?.label ?? "")}</div>
<div className="text-stone-700 w-12 truncate">{t(channelLabel?.name ?? "")}</div>
<div className="text-muted-foreground truncate">{(data.config?.title as string) ?? ""}</div>
</div>
);

View File

@@ -10,9 +10,9 @@ import { useShallow } from "zustand/shallow";
import { usePanel } from "./PanelProvider";
import { useTranslation } from "react-i18next";
import { Button } from "../ui/button";
import { useNotifyContext } from "@/providers/notify";
import { useNotifyChannelStore } from "@/stores/notify";
import { useEffect, useState } from "react";
import { NotifyChannels, channels as supportedChannels } from "@/domain/settings";
import { notifyChannelsMap } from "@/domain/settings";
import { SelectValue } from "@radix-ui/react-select";
import { Textarea } from "../ui/textarea";
import { RefreshCw, Settings } from "lucide-react";
@@ -25,7 +25,7 @@ const selectState = (state: WorkflowState) => ({
updateNode: state.updateNode,
});
type ChannelName = {
name: string;
key: string;
label: string;
};
@@ -34,28 +34,23 @@ const NotifyForm = ({ data }: NotifyFormProps) => {
const { updateNode } = useWorkflowStore(useShallow(selectState));
const { hidePanel } = usePanel();
const { t } = useTranslation();
const { config: notifyConfig, initChannels } = useNotifyContext();
const { channels: supportedChannels, fetchChannels } = useNotifyChannelStore();
const [chanels, setChanels] = useState<ChannelName[]>([]);
const [channels, setChannels] = useState<ChannelName[]>([]);
useEffect(() => {
setChanels(getChannels());
}, [notifyConfig]);
fetchChannels();
}, [fetchChannels]);
const getChannels = () => {
useEffect(() => {
const rs: ChannelName[] = [];
if (!notifyConfig.content) {
return rs;
}
const chanels = notifyConfig.content as NotifyChannels;
for (const channel of supportedChannels) {
if (chanels[channel.name] && chanels[channel.name].enabled) {
rs.push(channel);
for (const channel of notifyChannelsMap.values()) {
if (supportedChannels[channel.type]?.enabled) {
rs.push({ key: channel.type, label: channel.name });
}
}
return rs;
};
setChannels(rs);
}, [supportedChannels]);
const formSchema = z.object({
channel: z.string(),
@@ -103,10 +98,10 @@ const NotifyForm = ({ data }: NotifyFormProps) => {
<FormLabel className="flex justify-between items-center">
<div className="flex space-x-2 items-center">
<div>{t(`${i18nPrefix}.channel.label`)}</div>
<RefreshCw size={16} className="cursor-pointer" onClick={() => initChannels()} />
<RefreshCw size={16} className="cursor-pointer" onClick={() => fetchChannels()} />
</div>
<a
href="#/setting/notify"
href="#/settings/notification"
target="_blank"
className="flex justify-between items-center space-x-1 font-normal text-primary hover:underline cursor-pointer"
>
@@ -126,8 +121,8 @@ const NotifyForm = ({ data }: NotifyFormProps) => {
</SelectTrigger>
<SelectContent>
<SelectGroup>
{chanels.map((item) => (
<SelectItem key={item.name} value={item.name}>
{channels.map((item) => (
<SelectItem key={item.key} value={item.key}>
<div>{t(item.label)}</div>
</SelectItem>
))}

View File

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