feat: bot setting
This commit is contained in:
@@ -6,7 +6,7 @@ import { fillUsers } from "../slices/users";
|
||||
import BASE_URL, { ContentTypes } from "../config";
|
||||
import { onMessageSendStarted } from "./handlers";
|
||||
import handleChatMessage from "../../common/hook/useStreaming/chat.handler";
|
||||
import { AutoDeleteMsgDTO, User, UserCreateDTO, UserDTO, UserForAdmin, UserForAdminDTO } from "../../types/user";
|
||||
import { AutoDeleteMsgDTO, BotAPIKey, User, UserCreateDTO, UserDTO, UserForAdmin, UserForAdminDTO } from "../../types/user";
|
||||
import { ChatMessage, ContentTypeKey, MuteDTO } from "../../types/message";
|
||||
import { RootState } from "../store";
|
||||
|
||||
@@ -100,6 +100,23 @@ export const userApi = createApi({
|
||||
getUserByAdmin: builder.query<UserForAdmin, number>({
|
||||
query: (uid) => ({ url: `/admin/user/${uid}` })
|
||||
}),
|
||||
// bot operations
|
||||
createBotAPIKey: builder.mutation<void, { uid: number, name: string }>({
|
||||
query: ({ uid, name }) => ({
|
||||
url: `admin/user/bot-api-key/${uid}`,
|
||||
method: "POST",
|
||||
body: { name },
|
||||
responseHandler: (response: Response) => response.text()
|
||||
}),
|
||||
|
||||
}),
|
||||
getBotAPIKeys: builder.query<BotAPIKey[], number>({
|
||||
query: (uid) => ({ url: `/admin/user/bot-api-key/${uid}` })
|
||||
}),
|
||||
deleteBotAPIKey: builder.query<void, { uid: number, kid: number }>({
|
||||
query: ({ uid, kid }) => ({ url: `/admin/user/bot-api-key/${uid}/${kid}`, method: "DELETE" })
|
||||
}),
|
||||
// bot operations end
|
||||
updateInfo: builder.mutation<User, UserDTO>({
|
||||
query: (data) => ({
|
||||
url: `user`,
|
||||
@@ -157,5 +174,8 @@ export const {
|
||||
useUpdateAvatarMutation,
|
||||
useGetUsersQuery,
|
||||
useLazyGetUsersQuery,
|
||||
useSendMsgMutation
|
||||
useSendMsgMutation,
|
||||
useCreateBotAPIKeyMutation,
|
||||
useLazyDeleteBotAPIKeyQuery,
|
||||
useGetBotAPIKeysQuery
|
||||
} = userApi;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 21V13H7V21M7 3V8H15M19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H16L21 8V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21Z" stroke="#616161" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 463 B |
@@ -54,9 +54,12 @@ export default function useStreaming() {
|
||||
window.clearTimeout(aliveInter);
|
||||
// 比15秒多5秒
|
||||
aliveInter = window.setTimeout(() => {
|
||||
// 重启连接
|
||||
stopStreaming();
|
||||
startStreaming();
|
||||
// 有网的情况再试
|
||||
if (navigator.onLine) {
|
||||
// 重启连接
|
||||
stopStreaming();
|
||||
startStreaming();
|
||||
}
|
||||
}, timeout ?? 20000);
|
||||
};
|
||||
const startStreaming = useCallback(
|
||||
@@ -114,7 +117,7 @@ export default function useStreaming() {
|
||||
return;
|
||||
}
|
||||
// 重连
|
||||
connectionIsOpen = false;
|
||||
// connectionIsOpen = false;
|
||||
keepAlive(2000);
|
||||
};
|
||||
SSE.onmessage = (evt) => {
|
||||
@@ -306,8 +309,8 @@ export default function useStreaming() {
|
||||
if (SSE) {
|
||||
SSE.close();
|
||||
SSE = undefined;
|
||||
connectionIsOpen = false;
|
||||
}
|
||||
connectionIsOpen = false;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// import Tippy from '@tippyjs/react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetBotAPIKeysQuery } from '../../../app/services/user';
|
||||
import IconDelete from '../../../assets/icons/delete.svg';
|
||||
import CreateAPIKeyModal from './CreateAPIKeyModal';
|
||||
import DeleteAPIKeyModal from './DeleteAPIKeyModal';
|
||||
|
||||
type Props = {
|
||||
uid: number
|
||||
}
|
||||
|
||||
// const APIKeyTable = () => {
|
||||
|
||||
|
||||
// return <table>
|
||||
|
||||
// </table>;
|
||||
// };
|
||||
type DeleteAPIKeyProps = { uid: number, kid: number }
|
||||
const tdClass = "p-1 whitespace-nowrap text-xs text-gray-500 align-middle px-1";
|
||||
const BotAPIKeys = ({ uid }: Props) => {
|
||||
const { t } = useTranslation("setting", { keyPrefix: "bot" });
|
||||
const [currentUid, setCurrentUid] = useState<number | undefined>();
|
||||
const [deleteApiKey, setDeleteApiKey] = useState<DeleteAPIKeyProps | undefined>();
|
||||
const { data, refetch } = useGetBotAPIKeysQuery(uid);
|
||||
const toggleCreateModal = (param?: number) => {
|
||||
if (!param) refetch();
|
||||
setCurrentUid(param);
|
||||
};
|
||||
const toggleDeleteModal = (param?: DeleteAPIKeyProps) => {
|
||||
if (!param) refetch();
|
||||
setDeleteApiKey(param);
|
||||
};
|
||||
if (!data) return null;
|
||||
return (
|
||||
<div className='flex flex-col gap-2 items-start'>
|
||||
<button onClick={toggleCreateModal.bind(null, uid)} type='button' className="rounded-full bg-green-50 text-green-600 text-xs py-0.5 px-2">
|
||||
New API Key
|
||||
</button>
|
||||
{data.length > 0 ? <div className='border-t border-solid border-b border-gray-200 py-2'>
|
||||
<table className="min-w-full table-auto font-mono">
|
||||
<thead >
|
||||
<tr >
|
||||
{[t("col_key_name"), t('col_key_value'), t('col_key_create_time'), t('col_key_last_used'), ""].map(title => <th key={title} scope="col" className="text-xs text-gray-900 px-1 text-left pb-2">
|
||||
{title}
|
||||
</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map(ak => {
|
||||
const { id, name, key, created_at, last_used } = ak;
|
||||
return <tr key={id} className="group" >
|
||||
<td className={tdClass}>
|
||||
{name}
|
||||
</td>
|
||||
<td className={`${tdClass} w-40`}>
|
||||
{`${key.slice(0, 4)} ... ... ${key.slice(-6)}`}
|
||||
</td>
|
||||
<td className={tdClass}>
|
||||
{dayjs(created_at).format("YYYY-MM-DD HH:mm:ss")}
|
||||
</td>
|
||||
<td className={tdClass}>
|
||||
|
||||
{last_used ? dayjs(last_used).format("YYYY-MM-DD HH:mm:ss") : "Unused"}
|
||||
</td>
|
||||
<td className={`${tdClass} invisible group-hover:visible`}>
|
||||
<button onClick={toggleDeleteModal.bind(null, { kid: id, uid })}>
|
||||
<IconDelete />
|
||||
</button>
|
||||
</td>
|
||||
</tr>;
|
||||
})}
|
||||
</tbody>
|
||||
</table></div> : null}
|
||||
{currentUid && <CreateAPIKeyModal uid={currentUid} closeModal={toggleCreateModal.bind(null, undefined)} />}
|
||||
{deleteApiKey && <DeleteAPIKeyModal uid={deleteApiKey.uid} kid={deleteApiKey.kid} closeModal={toggleDeleteModal.bind(null, undefined)} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BotAPIKeys;
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCreateBotAPIKeyMutation } from '../../../app/services/user';
|
||||
import Modal from '../../../common/component/Modal';
|
||||
import Button from '../../../common/component/styled/Button';
|
||||
import Input from '../../../common/component/styled/Input';
|
||||
import StyledModal from '../../../common/component/styled/Modal';
|
||||
import useCopy from '../../../common/hook/useCopy';
|
||||
|
||||
type Props = {
|
||||
uid: number,
|
||||
closeModal: () => void
|
||||
}
|
||||
const CreateAPIKeyModal = ({ closeModal, uid }: Props) => {
|
||||
const { copy } = useCopy();
|
||||
const [createBotAPIKey, { error, isSuccess, isLoading, data }] = useCreateBotAPIKeyMutation();
|
||||
const formRef = useRef(null);
|
||||
const { t } = useTranslation("setting", { keyPrefix: "bot" });
|
||||
const { t: ct } = useTranslation();
|
||||
// const [input, setInput] = useState("");
|
||||
const handleCreateBot = () => {
|
||||
if (!formRef || !formRef.current) return;
|
||||
const formEle = formRef.current as HTMLFormElement;
|
||||
if (!formEle.checkValidity()) {
|
||||
formEle.reportValidity();
|
||||
return;
|
||||
|
||||
}
|
||||
createBotAPIKey({
|
||||
uid,
|
||||
name: formEle.querySelector('input')?.value || ""
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
switch (error.status) {
|
||||
case 406:
|
||||
toast.error("Invalid Webhook URL!");
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [error]);
|
||||
const handleCopy = () => {
|
||||
copy(data);
|
||||
toast.success("API Key Copied!");
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal id="modal-modal">
|
||||
<StyledModal
|
||||
title={t("create_key_title")}
|
||||
description={t("create_key_desc")}
|
||||
buttons={
|
||||
isSuccess ? <Button onClick={handleCopy}>
|
||||
{t("key_copy_and_close")}
|
||||
</Button> : <>
|
||||
<Button className="cancel" onClick={closeModal}>
|
||||
{ct("action.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleCreateBot}>{isLoading ? "..." : ct("action.done")}</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{isSuccess ? <div className="flex flex-col gap-2 text-sm">
|
||||
<div className='border-green-600 bg-green-200/50 rounded border border-solid p-2 max-w-md w-full whitespace-pre-wrap break-all' >
|
||||
{data}
|
||||
</div> <div className='text-red-400'>
|
||||
⚠️ {t("create_key_warning")}
|
||||
</div>
|
||||
</div>
|
||||
: <form ref={formRef} className="w-full flex flex-col gap-2 items-center" action="/">
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<label htmlFor={"name"} className="text-sm text-[#6b7280]">Name</label>
|
||||
<Input name={"name"} required placeholder='Please input API Key name'></Input>
|
||||
</div>
|
||||
</form>}
|
||||
</StyledModal>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateAPIKeyModal;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCreateUserMutation } from '../../../app/services/user';
|
||||
import Modal from '../../../common/component/Modal';
|
||||
import Button from '../../../common/component/styled/Button';
|
||||
import Input from '../../../common/component/styled/Input';
|
||||
import StyledModal from '../../../common/component/styled/Modal';
|
||||
|
||||
type Props = {
|
||||
closeModal: () => void
|
||||
}
|
||||
type CreateDTO = {
|
||||
name: string,
|
||||
webhook_url?: string
|
||||
}
|
||||
const CreateModal = ({ closeModal }: Props) => {
|
||||
const [createUser, { isSuccess, isLoading, error }] = useCreateUserMutation();
|
||||
const formRef = useRef(null);
|
||||
const { t } = useTranslation("setting", { keyPrefix: "bot" });
|
||||
const { t: ct } = useTranslation();
|
||||
// const [input, setInput] = useState("");
|
||||
const handleCreateBot = () => {
|
||||
if (!formRef || !formRef.current) return;
|
||||
const formEle = formRef.current as HTMLFormElement;
|
||||
if (!formEle.checkValidity()) {
|
||||
formEle.reportValidity();
|
||||
return;
|
||||
|
||||
}
|
||||
const myFormData = new FormData(formEle);
|
||||
const formDataObj: CreateDTO = { name: "" };
|
||||
myFormData.forEach((value, key) => {
|
||||
if (value) {
|
||||
formDataObj[key] = value;
|
||||
}
|
||||
});
|
||||
createUser({
|
||||
is_bot: true,
|
||||
is_admin: false,
|
||||
gender: 1,
|
||||
email: `bot_${new Date().getTime()}@voce.chat`,
|
||||
password: "",
|
||||
...formDataObj
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
switch (error.status) {
|
||||
case 406:
|
||||
toast.error("Invalid Webhook URL!");
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [error]);
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
toast.success("Create Bot Successfully!");
|
||||
closeModal();
|
||||
}
|
||||
}, [isSuccess]);
|
||||
|
||||
return (
|
||||
<Modal id="modal-modal">
|
||||
<StyledModal
|
||||
title={t("create_title")}
|
||||
description={t("create_desc")}
|
||||
buttons={
|
||||
<>
|
||||
<Button className="cancel" onClick={closeModal}>
|
||||
{ct("action.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleCreateBot}>{isLoading ? "Updating" : ct("action.done")}</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form ref={formRef} className="w-full flex flex-col gap-2 items-center" action="/">
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<label htmlFor={"name"} className="text-sm text-[#6b7280]">Name</label>
|
||||
<Input name={"name"} required placeholder='Please input bot name'></Input>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<label htmlFor={"webhook_url"} className="text-sm text-[#6b7280]">Webhook URL</label>
|
||||
<Input name={"webhook_url"} type="url" placeholder='Please input webhook url'></Input>
|
||||
</div>
|
||||
</form>
|
||||
</StyledModal>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateModal;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useEffect } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLazyDeleteBotAPIKeyQuery } from '../../../app/services/user';
|
||||
import Modal from '../../../common/component/Modal';
|
||||
import Button from '../../../common/component/styled/Button';
|
||||
import StyledModal from '../../../common/component/styled/Modal';
|
||||
|
||||
type Props = {
|
||||
uid: number,
|
||||
kid: number,
|
||||
closeModal: () => void
|
||||
}
|
||||
const DeleteAPIKeyModal = ({ closeModal, uid, kid }: Props) => {
|
||||
const [deleteKey, { isSuccess, isLoading }] = useLazyDeleteBotAPIKeyQuery();
|
||||
const { t } = useTranslation("setting", { keyPrefix: "bot" });
|
||||
const { t: ct } = useTranslation();
|
||||
// const [input, setInput] = useState("");
|
||||
const handleDeleteBot = () => {
|
||||
deleteKey({ uid, kid });
|
||||
};
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
toast.success("Delete API Key Successfully!");
|
||||
closeModal();
|
||||
}
|
||||
}, [isSuccess]);
|
||||
|
||||
return (
|
||||
<Modal id="modal-modal">
|
||||
<StyledModal
|
||||
title={`${t("delete_key_title")} ${name}`}
|
||||
description={t("delete_key_desc")}
|
||||
buttons={
|
||||
<>
|
||||
<Button className="cancel" onClick={closeModal}>
|
||||
{ct("action.cancel")}
|
||||
</Button>
|
||||
<Button className='danger' onClick={handleDeleteBot}>{isLoading ? "Deleting" : ct("action.done")}</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
</StyledModal>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteAPIKeyModal;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useEffect } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLazyDeleteUserQuery } from '../../../app/services/user';
|
||||
import Modal from '../../../common/component/Modal';
|
||||
import Button from '../../../common/component/styled/Button';
|
||||
import StyledModal from '../../../common/component/styled/Modal';
|
||||
|
||||
type Props = {
|
||||
uid: number,
|
||||
name: string,
|
||||
closeModal: () => void
|
||||
}
|
||||
const DeleteModal = ({ closeModal, uid, name }: Props) => {
|
||||
const [deleteUser, { isSuccess, isLoading }] = useLazyDeleteUserQuery();
|
||||
const { t } = useTranslation("setting", { keyPrefix: "bot" });
|
||||
const { t: ct } = useTranslation();
|
||||
// const [input, setInput] = useState("");
|
||||
const handleDeleteBot = () => {
|
||||
deleteUser(uid);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
toast.success("Delete Bot Successfully!");
|
||||
closeModal();
|
||||
}
|
||||
}, [isSuccess]);
|
||||
|
||||
return (
|
||||
<Modal id="modal-modal">
|
||||
<StyledModal
|
||||
title={`${t("delete_title")} ${name}`}
|
||||
description={t("delete_desc")}
|
||||
buttons={
|
||||
<>
|
||||
<Button className="cancel" onClick={closeModal.bind(null, undefined)}>
|
||||
{ct("action.cancel")}
|
||||
</Button>
|
||||
<Button className='danger' onClick={handleDeleteBot}>{isLoading ? "Deleting" : ct("action.done")}</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
</StyledModal>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteModal;
|
||||
@@ -0,0 +1,87 @@
|
||||
import clsx from 'clsx';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { Orbit } from "@uiball/loaders";
|
||||
import { useGetUserByAdminQuery, useUpdateUserMutation } from '../../../app/services/user';
|
||||
import IconEdit from '../../../assets/icons/edit.svg';
|
||||
import IconSave from '../../../assets/icons/save.svg';
|
||||
import IconCancel from '../../../assets/icons/close.circle.svg';
|
||||
type Props = {
|
||||
uid: number
|
||||
}
|
||||
|
||||
const WebhookEdit = ({ uid }: Props) => {
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const [editable, setEditable] = useState(false);
|
||||
const [url, setUrl] = useState("");
|
||||
const { data, isSuccess, refetch } = useGetUserByAdminQuery(uid);
|
||||
const [updateUser, { isSuccess: updateSuccess, isLoading: isUpdating }] = useUpdateUserMutation();
|
||||
useEffect(() => {
|
||||
if (isSuccess && data) {
|
||||
setUrl(data.webhook_url || "");
|
||||
}
|
||||
}, [data, isSuccess]);
|
||||
useEffect(() => {
|
||||
if (updateSuccess) {
|
||||
refetch();
|
||||
}
|
||||
}, [updateSuccess]);
|
||||
|
||||
const handleEdit = async () => {
|
||||
if (editable && formRef) {
|
||||
const form = formRef.current;
|
||||
// 检查格式
|
||||
if (!form?.checkValidity()) {
|
||||
form?.reportValidity();
|
||||
return;
|
||||
}
|
||||
// 保存编辑
|
||||
const webhook_url = new FormData(form).get("webhook") as string;
|
||||
const resp = await updateUser({ id: uid, webhook_url });
|
||||
console.log("ressssss", resp);
|
||||
if ("error" in resp) {
|
||||
switch (resp.error.status) {
|
||||
case 406:
|
||||
toast.error("Not Valid URL!");
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
setEditable(prev => !prev);
|
||||
};
|
||||
const handleEditable = () => {
|
||||
setEditable(true);
|
||||
};
|
||||
const handleCancelEdit = () => {
|
||||
setEditable(false);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
{(url || editable || updateSuccess) ?
|
||||
<div className="flex gap-2">
|
||||
<form action="/" ref={formRef} >
|
||||
<input readOnly={!editable} required autoFocus type="url" name='webhook' defaultValue={url} className={clsx("text-sm text-gray-500 px-2 py-1", editable ? "border border-solid border-gray-200 bg-gray-50" : "bg-transparent")} />
|
||||
</form>
|
||||
<button type='button' disabled={isUpdating} onClick={handleEdit}>
|
||||
{isUpdating ? <Orbit size={16} /> : editable ?
|
||||
<IconSave />
|
||||
|
||||
: <IconEdit />}
|
||||
</button>
|
||||
{editable && !isUpdating && <button type='button' disabled={isUpdating} onClick={handleCancelEdit}>
|
||||
<IconCancel className="!w-6 !h-6" />
|
||||
</button>}
|
||||
</div>
|
||||
:
|
||||
<button type='button' className="rounded-full bg-primary-50 text-green-600 text-xs py-0.5 px-2" onClick={handleEditable}>
|
||||
Set Webhook
|
||||
</button>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebhookEdit;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useRef, useState, useEffect, ChangeEvent } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useUpdateUserMutation } from '../../../app/services/user';
|
||||
import Modal from '../../../common/component/Modal';
|
||||
import Button from '../../../common/component/styled/Button';
|
||||
import Input from '../../../common/component/styled/Input';
|
||||
import StyledModal from '../../../common/component/styled/Modal';
|
||||
|
||||
type Props = {
|
||||
uid: number,
|
||||
webhook?: string,
|
||||
closeModal: () => void
|
||||
}
|
||||
const WebhookModal = ({ uid, webhook, closeModal }: Props) => {
|
||||
const [url, setUrl] = useState(webhook);
|
||||
const [updateUser, { isSuccess, isLoading }] = useUpdateUserMutation();
|
||||
const formRef = useRef(null);
|
||||
const { t } = useTranslation("setting", { keyPrefix: "bot" });
|
||||
const { t: ct } = useTranslation();
|
||||
// const [input, setInput] = useState("");
|
||||
const handleUrlChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
setUrl(evt.target.value);
|
||||
};
|
||||
const handleUpdateWebhook = () => {
|
||||
if (!formRef || !formRef.current) return;
|
||||
const formEle = formRef.current as HTMLFormElement;
|
||||
if (!formEle.checkValidity()) {
|
||||
formEle.reportValidity();
|
||||
return;
|
||||
}
|
||||
const myFormData = new FormData(formEle);
|
||||
const webhook_url = myFormData.get("webhook")?.toString() || "";
|
||||
updateUser({
|
||||
id: uid,
|
||||
webhook_url
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
toast.success("Update Webhook URL Successfully!");
|
||||
closeModal();
|
||||
}
|
||||
}, [isSuccess]);
|
||||
|
||||
return (
|
||||
<Modal id="modal-modal">
|
||||
<StyledModal
|
||||
title={t("webhook_title")}
|
||||
description={t("webhook_desc")}
|
||||
buttons={
|
||||
<>
|
||||
<Button className="cancel" onClick={closeModal.bind(null, undefined)}>
|
||||
{ct("action.cancel")}
|
||||
</Button>
|
||||
<Button disabled={!url} onClick={handleUpdateWebhook}>{isLoading ? "Updating" : ct("action.done")}</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form ref={formRef} className="w-full flex flex-col gap-2" action="/">
|
||||
<label htmlFor={"webhook"} className="text-sm text-[#6b7280]">Webhook URL</label>
|
||||
<Input name={"webhook"} value={url} onChange={handleUrlChange} type="url"></Input>
|
||||
</form>
|
||||
</StyledModal>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebhookModal;
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useEffect } from "react";
|
||||
// import dayjs from "dayjs";
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useUpdateAvatarByAdminMutation } from '../../../app/services/user';
|
||||
import { useAppSelector } from '../../../app/store';
|
||||
import AvatarUploader from '../../../common/component/AvatarUploader';
|
||||
import Button from '../../../common/component/styled/Button';
|
||||
import IconDelete from '../../../assets/icons/delete.svg';
|
||||
import CreateModal from './CreateModal';
|
||||
import WebhookEdit from './WebhookEdit';
|
||||
import WebhookModal from './WebhookModal';
|
||||
import DeleteModal from './DeleteModal';
|
||||
import BotAPIKeys from './BotAPIKeys';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
type TipProps = { title: string, desc: string };
|
||||
const Tip = ({ title, desc }: TipProps) => {
|
||||
|
||||
return <div className="flex flex-col text-sm">
|
||||
<h2 className="">{title}</h2>
|
||||
<p className="text-gray-500">{desc}</p>
|
||||
</div>;
|
||||
};
|
||||
const tdClass = "px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 align-top";
|
||||
type WebhookParams = { webhook?: string, uid: number };
|
||||
type DeleteParams = { name: string, uid: number };
|
||||
export default function BotConfig() {
|
||||
const [updateAvatar, { isSuccess: updateAvatarSuccess }] = useUpdateAvatarByAdminMutation();
|
||||
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||||
const [currWebhookParams, setCurrWebhookParams] = useState<WebhookParams | undefined>(undefined);
|
||||
const [currDeleteParams, setCurrDeleteParams] = useState<DeleteParams | undefined>(undefined);
|
||||
const bots = useAppSelector(store => Object.values(store.users.byId).filter(u => !!u.is_bot));
|
||||
const { t } = useTranslation("setting", { keyPrefix: "bot" });
|
||||
|
||||
const toggleCreateModalVisible = () => {
|
||||
setCreateModalVisible(prev => !prev);
|
||||
};
|
||||
const toggleWebhookModalVisible = (obj?: WebhookParams) => {
|
||||
console.log("webhook modal", obj);
|
||||
|
||||
setCurrWebhookParams(obj);
|
||||
};
|
||||
const toggleDeleteModalVisible = (obj?: DeleteParams) => {
|
||||
setCurrDeleteParams(obj);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (updateAvatarSuccess) {
|
||||
toast.success("Update Bot Avatar Successfully!");
|
||||
}
|
||||
}, [updateAvatarSuccess]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col justify-start items-start gap-4">
|
||||
<div className="flex flex-col gap-4 max-w-[634px] mb-4">
|
||||
<Tip title={t("bot_tip_title")} desc={t("bot_tip_desc")} />
|
||||
<Tip title={t("webhook_tip_title")} desc={t("webhook_tip_desc")} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className='font-semibold'>{t("manage")}</h2>
|
||||
<p className='text-gray-500 text-xs'>{t("manage_desc")}</p>
|
||||
</div>
|
||||
|
||||
<div className="w-fit overflow-hidden">
|
||||
<table className="min-w-full table-auto">
|
||||
<thead className="border-b bg-gray-50">
|
||||
<tr>
|
||||
{[t("col_avatar"), t('col_name'), t('col_api_key'), t('col_webhook'), t('col_opt')].map(title => <th key={title} scope="col" className="text-sm font-bold text-gray-900 px-6 py-4 text-left">
|
||||
{title}
|
||||
</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bots.map(bot => {
|
||||
const { uid, name, avatar } = bot;
|
||||
return <tr key={uid} className="bg-white border-b transition duration-300 ease-in-out hover:bg-gray-100">
|
||||
<td className="px-4 py-2 flex flex-col gap-1 items-start">
|
||||
<AvatarUploader uid={uid} url={avatar} uploadImage={updateAvatar} name={name} className="!w-14 !h-14" />
|
||||
<span className='text-xs text-gray-500'>
|
||||
#{uid}
|
||||
</span>
|
||||
</td>
|
||||
<td className={tdClass}>
|
||||
{name}
|
||||
</td>
|
||||
<td className={tdClass}>
|
||||
<BotAPIKeys uid={uid} />
|
||||
</td>
|
||||
<td className={tdClass}>
|
||||
<WebhookEdit uid={uid} />
|
||||
</td>
|
||||
<td className={tdClass}>
|
||||
<button type='button' onClick={toggleDeleteModalVisible.bind(null, { uid, name })} >
|
||||
<IconDelete className="hover:opacity-80" />
|
||||
</button>
|
||||
{/* <Button className='mini' onClick={toggleWebhookModalVisible.bind(null, { webhook: webhook_url, uid })} >Set Webhook</Button> */}
|
||||
</td>
|
||||
</tr>;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Button onClick={toggleCreateModalVisible} className="ghost small">Add</Button>
|
||||
|
||||
</div>
|
||||
{createModalVisible && <CreateModal closeModal={toggleCreateModalVisible} />}
|
||||
{currWebhookParams && <WebhookModal closeModal={toggleWebhookModalVisible} {...currWebhookParams} />}
|
||||
{currDeleteParams && <DeleteModal closeModal={toggleDeleteModalVisible} {...currDeleteParams} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import ConfigSMTP from "./config/SMTP";
|
||||
import APIConfig from "./APIConfig";
|
||||
import License from "./License/License";
|
||||
import Widget from "./Widget";
|
||||
import BotConfig from "./BotConfig";
|
||||
import APIDocument from "./APIDocument";
|
||||
import ManageMembers from "../../common/component/ManageMembers";
|
||||
import FAQ from "../../common/component/FAQ";
|
||||
@@ -35,6 +36,11 @@ const navs = [
|
||||
{
|
||||
title: "config",
|
||||
items: [
|
||||
{
|
||||
name: "bot",
|
||||
component: <BotConfig />,
|
||||
admin: true
|
||||
},
|
||||
{
|
||||
name: "firebase",
|
||||
component: <ConfigFirebase />
|
||||
|
||||
+20
-2
@@ -1,14 +1,20 @@
|
||||
export type Gender = 0 | 1;
|
||||
|
||||
export interface AutoDeleteMsgDTO {
|
||||
users?: { uid: number, expires_in: number }[],
|
||||
groups?: { gid: number, expires_in: number }[],
|
||||
}
|
||||
export interface User {
|
||||
uid: number;
|
||||
email: string;
|
||||
name: string;
|
||||
gender: Gender;
|
||||
language: string;
|
||||
language?: string;
|
||||
is_admin: boolean;
|
||||
avatar_updated_at: number;
|
||||
create_by: string;
|
||||
webhook_url?: string;
|
||||
is_bot?: boolean;
|
||||
}
|
||||
export type UserStatus = "normal" | "frozen";
|
||||
export type UserDevice = {
|
||||
@@ -16,6 +22,13 @@ export type UserDevice = {
|
||||
device_token?: string;
|
||||
is_online: boolean;
|
||||
};
|
||||
export type BotAPIKey = {
|
||||
id: number,
|
||||
name: string,
|
||||
key: string,
|
||||
created_at: number,
|
||||
last_used: number
|
||||
};
|
||||
|
||||
export interface UserForAdmin extends User {
|
||||
password: string;
|
||||
@@ -27,4 +40,9 @@ export interface UserForAdmin extends User {
|
||||
export interface UserForAdminDTO extends Partial<UserForAdmin> {
|
||||
id?: number;
|
||||
}
|
||||
export interface UserDTO extends Partial<Pick<User, "name" | "gender" | "language" | "email">> {}
|
||||
export interface UserDTO extends Partial<Pick<User, "name" | "gender" | "language" | "email" | "webhook_url">> {
|
||||
password?: string
|
||||
}
|
||||
export interface UserCreateDTO extends Pick<User, "name" | "gender" | "language" | "email" | "webhook_url" | "is_bot" | "is_admin"> {
|
||||
password: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user