message push config

This commit is contained in:
yoan 2024-09-23 23:13:34 +08:00
parent 5422f17fab
commit 4c9095400e
13 changed files with 727 additions and 60 deletions

View File

@ -25,6 +25,7 @@ import { update } from "@/repository/settings";
import { ClientResponseError } from "pocketbase";
import { PbErrorData } from "@/domain/base";
import { useState } from "react";
import { EmailsSetting } from "@/domain/settings";
type EmailsEditProps = {
className?: string;
@ -51,7 +52,7 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
if (emails.content.emails.includes(data.email)) {
if ((emails.content as EmailsSetting).emails.includes(data.email)) {
form.setError("email", {
message: "邮箱已存在",
});
@ -59,7 +60,7 @@ const EmailsEdit = ({ className, trigger }: EmailsEditProps) => {
}
// 保存到 config
const newEmails = [...emails.content.emails, data.email];
const newEmails = [...(emails.content as EmailsSetting).emails, data.email];
try {
const resp = await update({

View File

@ -0,0 +1,146 @@
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { Switch } from "../ui/switch";
import { Label } from "../ui/label";
import { useNotify } from "@/providers/notify";
import { NotifyChannelDingTalk, NotifyChannels } from "@/domain/settings";
import { useEffect, useState } from "react";
import { update } from "@/repository/settings";
import { getErrMessage } from "@/lib/error";
import { useToast } from "../ui/use-toast";
type DingTalkSetting = {
id: string;
name: string;
data: NotifyChannelDingTalk;
};
const DingTalk = () => {
const { config, setChannels } = useNotify();
const [dingtalk, setDingtalk] = useState<DingTalkSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
accessToken: "",
secret: "",
enabled: false,
},
});
useEffect(() => {
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 data = getDetailDingTalk();
setDingtalk({
id: config.id ?? "",
name: "dingtalk",
data,
});
}, [config]);
const { toast } = useToast();
const handleSaveClick = async () => {
try {
const resp = await update({
...config,
name: "notifyChannels",
content: {
...config.content,
dingtalk: {
...dingtalk.data,
},
},
});
setChannels(resp);
toast({
title: "保存成功",
description: "配置保存成功",
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: "保存失败",
description: "配置保存失败:" + msg,
variant: "destructive",
});
}
};
return (
<div>
<Input
placeholder="AccessToken"
value={dingtalk.data.accessToken}
onChange={(e) => {
setDingtalk({
...dingtalk,
data: {
...dingtalk.data,
accessToken: e.target.value,
},
});
}}
/>
<Input
placeholder="加签的签名"
className="mt-2"
value={dingtalk.data.secret}
onChange={(e) => {
setDingtalk({
...dingtalk,
data: {
...dingtalk.data,
secret: e.target.value,
},
});
}}
/>
<div className="flex items-center space-x-1 mt-2">
<Switch
id="airplane-mode"
checked={dingtalk.data.enabled}
onCheckedChange={() => {
setDingtalk({
...dingtalk,
data: {
...dingtalk.data,
enabled: !dingtalk.data.enabled,
},
});
}}
/>
<Label htmlFor="airplane-mode"></Label>
</div>
<div className="flex justify-end mt-2">
<Button
onClick={() => {
handleSaveClick();
}}
>
</Button>
</div>
</div>
);
};
export default DingTalk;

View File

@ -0,0 +1,104 @@
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import { Button } from "../ui/button";
import { useEffect, useState } from "react";
import {
defaultNotifyTemplate,
NotifyTemplates,
NotifyTemplate as NotifyTemplateT,
} from "@/domain/settings";
import { getSetting, update } from "@/repository/settings";
import { useToast } from "../ui/use-toast";
const NotifyTemplate = () => {
const [id, setId] = useState("");
const [templates, setTemplates] = useState<NotifyTemplateT[]>([
defaultNotifyTemplate,
]);
const { toast } = useToast();
useEffect(() => {
const featchData = async () => {
const resp = await getSetting("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 update({
id: id,
content: {
notifyTemplates: templates,
},
name: "templates",
});
if (resp.id) {
setId(resp.id);
}
toast({
title: "保存成功",
description: "通知模板保存成功",
});
};
return (
<div>
<Input
value={templates[0].title}
onChange={(e) => {
handleTitleChange(e.target.value);
}}
/>
<div className="text-muted-foreground text-sm mt-1">
, COUNT:即将过期张数
</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">
, COUNT:即将过期张数DOMAINS:域名列表
</div>
<div className="flex justify-end mt-2">
<Button onClick={handleSaveClick}></Button>
</div>
</div>
);
};
export default NotifyTemplate;

View File

@ -0,0 +1,131 @@
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { Switch } from "../ui/switch";
import { Label } from "../ui/label";
import { useNotify } from "@/providers/notify";
import { NotifyChannels, NotifyChannelTelegram } from "@/domain/settings";
import { useEffect, useState } from "react";
import { update } from "@/repository/settings";
import { getErrMessage } from "@/lib/error";
import { useToast } from "../ui/use-toast";
type TelegramSetting = {
id: string;
name: string;
data: NotifyChannelTelegram;
};
const Telegram = () => {
const { config, setChannels } = useNotify();
const [telegram, setTelegram] = useState<TelegramSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
apiToken: "",
enabled: false,
},
});
useEffect(() => {
const getDetailTelegram = () => {
const df: NotifyChannelTelegram = {
apiToken: "",
enabled: false,
};
if (!config.content) {
return df;
}
const chanels = config.content as NotifyChannels;
if (!chanels.telegram) {
return df;
}
return chanels.telegram as NotifyChannelTelegram;
};
const data = getDetailTelegram();
setTelegram({
id: config.id ?? "",
name: "telegram",
data,
});
}, [config]);
const { toast } = useToast();
const handleSaveClick = async () => {
try {
const resp = await update({
...config,
name: "notifyChannels",
content: {
...config.content,
telegram: {
...telegram.data,
},
},
});
setChannels(resp);
toast({
title: "保存成功",
description: "配置保存成功",
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: "保存失败",
description: "配置保存失败:" + msg,
variant: "destructive",
});
}
};
return (
<div>
<Input
placeholder="ApiToken"
value={telegram.data.apiToken}
onChange={(e) => {
setTelegram({
...telegram,
data: {
...telegram.data,
apiToken: e.target.value,
},
});
}}
/>
<div className="flex items-center space-x-1 mt-2">
<Switch
id="airplane-mode"
checked={telegram.data.enabled}
onCheckedChange={() => {
setTelegram({
...telegram,
data: {
...telegram.data,
enabled: !telegram.data.enabled,
},
});
}}
/>
<Label htmlFor="airplane-mode"></Label>
</div>
<div className="flex justify-end mt-2">
<Button
onClick={() => {
handleSaveClick();
}}
>
</Button>
</div>
</div>
);
};
export default Telegram;

View File

@ -0,0 +1,131 @@
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { Switch } from "../ui/switch";
import { Label } from "../ui/label";
import { useNotify } from "@/providers/notify";
import { NotifyChannels, NotifyChannelWebhook } from "@/domain/settings";
import { useEffect, useState } from "react";
import { update } from "@/repository/settings";
import { getErrMessage } from "@/lib/error";
import { useToast } from "../ui/use-toast";
type WebhookSetting = {
id: string;
name: string;
data: NotifyChannelWebhook;
};
const Webhook = () => {
const { config, setChannels } = useNotify();
const [webhook, setWebhook] = useState<WebhookSetting>({
id: config.id ?? "",
name: "notifyChannels",
data: {
url: "",
enabled: false,
},
});
useEffect(() => {
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 data = getDetailWebhook();
setWebhook({
id: config.id ?? "",
name: "webhook",
data,
});
}, [config]);
const { toast } = useToast();
const handleSaveClick = async () => {
try {
const resp = await update({
...config,
name: "notifyChannels",
content: {
...config.content,
webhook: {
...webhook.data,
},
},
});
setChannels(resp);
toast({
title: "保存成功",
description: "配置保存成功",
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: "保存失败",
description: "配置保存失败:" + msg,
variant: "destructive",
});
}
};
return (
<div>
<Input
placeholder="Url"
value={webhook.data.url}
onChange={(e) => {
setWebhook({
...webhook,
data: {
...webhook.data,
url: e.target.value,
},
});
}}
/>
<div className="flex items-center space-x-1 mt-2">
<Switch
id="airplane-mode"
checked={webhook.data.enabled}
onCheckedChange={() => {
setWebhook({
...webhook,
data: {
...webhook.data,
enabled: !webhook.data.enabled,
},
});
}}
/>
<Label htmlFor="airplane-mode"></Label>
</div>
<div className="flex justify-end mt-2">
<Button
onClick={() => {
handleSaveClick();
}}
>
</Button>
</div>
</div>
);
};
export default Webhook;

View File

@ -1,9 +1,50 @@
export type Setting = {
id?: string;
name?: string;
content: EmailsSetting;
content?: EmailsSetting | NotifyTemplates | NotifyChannels;
};
type EmailsSetting = {
export type EmailsSetting = {
emails: string[];
};
export type NotifyTemplates = {
notifyTemplates: NotifyTemplate[];
};
export type NotifyTemplate = {
title: string;
content: string;
};
export type NotifyChannels = {
dingtalk?: NotifyChannel;
telegram?: NotifyChannel;
webhook?: NotifyChannel;
};
export type NotifyChannel =
| NotifyChannelDingTalk
| NotifyChannelTelegram
| NotifyChannelWebhook;
export type NotifyChannelDingTalk = {
accessToken: string;
secret: string;
enabled: boolean;
};
export type NotifyChannelTelegram = {
apiToken: string;
enabled: boolean;
};
export type NotifyChannelWebhook = {
url: string;
enabled: boolean;
};
export const defaultNotifyTemplate: NotifyTemplate = {
title: "您有{COUNT}张证书即将过期",
content: "有{COUNT}张证书即将过期,域名分别为{DOMAINS},请保持关注!",
};

View File

@ -1,11 +1,3 @@
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from "@/components/ui/navigation-menu";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Toaster } from "@/components/ui/toaster";
import { KeyRound, Megaphone, UserRound } from "lucide-react";

View File

@ -37,6 +37,7 @@ import { accessTypeMap } from "@/domain/access";
import EmailsEdit from "@/components/certimate/EmailsEdit";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { EmailsSetting } from "@/domain/settings";
const Edit = () => {
const {
@ -270,11 +271,13 @@ const Edit = () => {
<SelectContent>
<SelectGroup>
<SelectLabel></SelectLabel>
{emails.content.emails.map((item) => (
<SelectItem key={item} value={item}>
<div>{item}</div>
</SelectItem>
))}
{(emails.content as EmailsSetting).emails.map(
(item) => (
<SelectItem key={item} value={item}>
<div>{item}</div>
</SelectItem>
)
)}
</SelectGroup>
</SelectContent>
</Select>

View File

@ -1,56 +1,54 @@
import DingTalk from "@/components/notify/DingTalk";
import NotifyTemplate from "@/components/notify/NotifyTemplate";
import Telegram from "@/components/notify/Telegram";
import Webhook from "@/components/notify/Webhook";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import React from "react";
import { NotifyProvider } from "@/providers/notify";
const Notify = () => {
return (
<>
<div className="border rounded-sm p-5">
<Accordion type={"multiple"} className="dark:text-stone-200">
<AccordionItem value="item-1" className="dark:border-stone-200">
<AccordionTrigger></AccordionTrigger>
<AccordionContent>
<Input value="您有证书即将过期" />
<Textarea
className="mt-2"
value={
"有{COUNT}张证书即将过期,域名分别为{DOMAINS},请保持关注!"
}
></Textarea>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<div className="border rounded-md p-5 mt-7">
<Accordion type={"multiple"} className="dark:text-stone-200">
<AccordionItem value="item-2" className="dark:border-stone-200">
<AccordionTrigger></AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
<NotifyProvider>
<div className="border rounded-sm p-5 shadow-lg">
<Accordion type={"multiple"} className="dark:text-stone-200">
<AccordionItem value="item-1" className="dark:border-stone-200">
<AccordionTrigger></AccordionTrigger>
<AccordionContent>
<NotifyTemplate />
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<div className="border rounded-md p-5 mt-7 shadow-lg">
<Accordion type={"multiple"} className="dark:text-stone-200">
<AccordionItem value="item-2" className="dark:border-stone-200">
<AccordionTrigger></AccordionTrigger>
<AccordionContent>
<DingTalk />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-4" className="dark:border-stone-200">
<AccordionTrigger>Telegram</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-4" className="dark:border-stone-200">
<AccordionTrigger>Telegram</AccordionTrigger>
<AccordionContent>
<Telegram />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-5" className="dark:border-stone-200">
<AccordionTrigger>Webhook</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<AccordionItem value="item-5" className="dark:border-stone-200">
<AccordionTrigger>Webhook</AccordionTrigger>
<AccordionContent>
<Webhook />
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</NotifyProvider>
</>
);
};

View File

@ -1,6 +1,6 @@
import { Access } from "@/domain/access";
import { ConfigData } from ".";
import { Setting } from "@/domain/settings";
import { EmailsSetting, Setting } from "@/domain/settings";
import { AccessGroup } from "@/domain/access_groups";
type Action =
@ -57,7 +57,10 @@ export const configReducer = (
emails: {
...state.emails,
content: {
emails: [...state.emails.content.emails, action.payload],
emails: [
...(state.emails.content as EmailsSetting).emails,
action.payload,
],
},
},
};

View File

@ -0,0 +1,68 @@
import { NotifyChannel, Setting } from "@/domain/settings";
import { getSetting } from "@/repository/settings";
import {
ReactNode,
useContext,
createContext,
useEffect,
useReducer,
useCallback,
} from "react";
import { notifyReducer } from "./reducer";
export type NotifyContext = {
config: Setting;
setChannel: (data: { channel: string; data: NotifyChannel }) => void;
setChannels: (data: Setting) => void;
};
const Context = createContext({} as NotifyContext);
export const useNotify = () => useContext(Context);
interface ContainerProps {
children: ReactNode;
}
export const NotifyProvider = ({ children }: ContainerProps) => {
const [notify, dispatchNotify] = useReducer(notifyReducer, {});
useEffect(() => {
const featchData = async () => {
const chanels = await getSetting("notifyChannels");
dispatchNotify({
type: "SET_CHANNELS",
payload: chanels,
});
};
featchData();
}, []);
const setChannel = useCallback(
(data: { channel: string; data: NotifyChannel }) => {
dispatchNotify({
type: "SET_CHANNEL",
payload: data,
});
},
[]
);
const setChannels = useCallback((setting: Setting) => {
dispatchNotify({
type: "SET_CHANNELS",
payload: setting,
});
}, []);
return (
<Context.Provider
value={{
config: notify,
setChannel,
setChannels,
}}
>
{children}
</Context.Provider>
);
};

View File

@ -0,0 +1,35 @@
import { NotifyChannel, Setting } from "@/domain/settings";
type Action =
| {
type: "SET_CHANNEL";
payload: {
channel: string;
data: NotifyChannel;
};
}
| {
type: "SET_CHANNELS";
payload: Setting;
};
export const notifyReducer = (state: Setting, action: Action) => {
switch (action.type) {
case "SET_CHANNEL": {
const channel = action.payload.channel;
return {
...state,
content: {
...state.content,
[channel]: action.payload.data,
},
};
}
case "SET_CHANNELS": {
return { ...action.payload };
}
default:
return state;
}
};

View File

@ -14,6 +14,20 @@ export const getEmails = async () => {
}
};
export const getSetting = async (name: string) => {
try {
const resp = await getPb()
.collection("settings")
.getFirstListItem<Setting>(`name='${name}'`);
return resp;
} catch (e) {
const rs: Setting = {
name: name,
};
return rs;
}
};
export const update = async (setting: Setting) => {
const pb = getPb();
let resp: Setting;