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

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