notifications

main
alex 2023-09-03 16:36:44 +02:00
parent e4f9acabe6
commit 411b35decd
8 changed files with 293 additions and 48 deletions

View File

@ -392,5 +392,15 @@
},
"createEquipmentDocumentationModal": {
"buttonCreateDocumentation": "Dokumentation erstellen"
},
"header": {
"notificationDrawer": {
"title": "Benachrichtigungen",
"deleteAllButtonText": "Alle löschen",
"deleteAllPopconfirm": {
"title": "Sind Sie sicher, dass Sie alle Benachrichtigungen löschen möchten?"
},
"noNotifications": "Keine Benachrichtigungen"
}
}
}

View File

@ -391,5 +391,15 @@
},
"createEquipmentDocumentationModal": {
"buttonCreateDocumentation": "Create Documentation"
},
"header": {
"notificationDrawer": {
"title": "Notifications",
"deleteAllButtonText": "Delete All",
"deleteAllPopconfirm": {
"title": "Are you sure you want to delete all notifications?"
},
"noNotifications": "No notifications"
}
}
}

View File

@ -12,6 +12,7 @@ import { GroupTasksProvider } from "./Contexts/GroupTasksContext";
import { AdminAreaRolesProvider } from "./Contexts/AdminAreaRolesContext";
import { UserProfileProvider } from "./Contexts/UserProfileContext";
import { UsersProvider } from "./Contexts/UsersContext";
import HeaderProvider from "./Contexts/HeaderContext";
export default function App() {
const [notificationApi, notificationContextHolder] =
@ -35,30 +36,32 @@ export default function App() {
{notificationContextHolder}
<AppProvider>
<SideBarProvider>
<GroupTasksProvider>
<AdminAreaRolesProvider>
<UserProfileProvider>
<UsersProvider>
<WebSocketProvider
userSession={userSession}
setUserSession={setUserSession}
isWebSocketReady={isWebSocketReady}
setIsWebSocketReady={setIsWebSocketReady}
notificationApi={notificationApi}
>
<ReconnectingView isWebSocketReady={isWebSocketReady} />
<DashboardLayout
<HeaderProvider>
<SideBarProvider>
<GroupTasksProvider>
<AdminAreaRolesProvider>
<UserProfileProvider>
<UsersProvider>
<WebSocketProvider
userSession={userSession}
setUserSession={setUserSession}
/>
</WebSocketProvider>
</UsersProvider>
</UserProfileProvider>
</AdminAreaRolesProvider>
</GroupTasksProvider>
</SideBarProvider>
isWebSocketReady={isWebSocketReady}
setIsWebSocketReady={setIsWebSocketReady}
notificationApi={notificationApi}
>
<ReconnectingView isWebSocketReady={isWebSocketReady} />
<DashboardLayout
userSession={userSession}
setUserSession={setUserSession}
/>
</WebSocketProvider>
</UsersProvider>
</UserProfileProvider>
</AdminAreaRolesProvider>
</GroupTasksProvider>
</SideBarProvider>
</HeaderProvider>
</AppProvider>
</Layout>
);

View File

@ -0,0 +1,165 @@
import {
BellOutlined,
CheckCircleOutlined,
CheckOutlined,
CloseCircleOutlined,
CloseOutlined,
DeleteOutlined,
ExclamationCircleOutlined,
InboxOutlined,
InfoCircleOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
QuestionCircleOutlined,
} from "@ant-design/icons";
import { Badge, Button, Drawer, List, Popconfirm, Typography } from "antd";
import { Header } from "antd/es/layout/layout";
import { useEffect, useState } from "react";
import { useHeaderContext } from "../../Contexts/HeaderContext";
import { FormatDatetime, myFetch } from "../../utils";
import { useWebSocketContext } from "../../Contexts/WebSocketContext";
import { SentMessagesCommands } from "../../Handlers/WebSocketMessageHandler";
import { useTranslation } from "react-i18next";
export default function HeaderMenu({
isSideMenuCollapsed,
setIsSideMenuCollapsed,
}) {
const webSocketContext = useWebSocketContext();
const headerContext = useHeaderContext();
const { t } = useTranslation();
const [isNotificationDrawerOpen, setIsNotificationDrawerOpen] =
useState(false);
useEffect(() => {
// fetch will only be called if the drawer is open and there are no notifications
// further notifications will be fetched by the websocket
if (!isNotificationDrawerOpen || headerContext.notifications !== null)
return;
myFetch("/notifications", "GET").then((data) =>
headerContext.setNotifications(data.Notifications)
);
}, [isNotificationDrawerOpen]);
return (
<Header
style={{
position: "sticky",
top: 0,
zIndex: 10,
width: "100%",
display: "flex",
alignItems: "center",
padding: 0,
background: "#fff",
justifyContent: "space-between",
}}
>
<Button
type="text"
icon={
isSideMenuCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />
}
onClick={() => setIsSideMenuCollapsed(!isSideMenuCollapsed)}
style={{ fontSize: "16px", width: 64, height: 64 }}
/>
<h1 style={{ fontSize: "16px", margin: 0 }}>Header</h1>
<Button
type="text"
icon={
<Badge count={headerContext.totalNotifications} offset={[2, -2]}>
<BellOutlined style={{ fontSize: "16px" }} />
</Badge>
}
onClick={() => setIsNotificationDrawerOpen(true)}
style={{ fontSize: "16px", width: 64, height: 64 }}
/>
<Drawer
title={t("header.notificationDrawer.title")}
placement="right"
open={isNotificationDrawerOpen}
onClose={() => setIsNotificationDrawerOpen(false)}
extra={
headerContext.totalNotifications > 0 && (
<Popconfirm
title={t("header.notificationDrawer.deleteAllPopconfirm.title")}
okText={t("common.button.confirm")}
cancelText={t("common.button.cancel")}
onConfirm={() => {
webSocketContext.SendSocketMessage(
SentMessagesCommands.DeleteAllNotifications,
{}
);
setIsNotificationDrawerOpen(false);
}}
>
<Button type="link" icon={<DeleteOutlined />}>
{t("header.notificationDrawer.deleteAllButtonText")}
</Button>
</Popconfirm>
)
}
>
{headerContext.totalNotifications === 0 ||
headerContext.notifications === null ? (
<div style={{ textAlign: "center" }}>
<InboxOutlined style={{ fontSize: 32, marginBottom: 10 }} />
<Typography.Title level={5}>
{t("header.notificationDrawer.noNotifications")}
</Typography.Title>
</div>
) : (
<>
<List
dataSource={headerContext.notifications.sort((a, b) => {
return new Date(b.CreatedAt) - new Date(a.CreatedAt);
})}
renderItem={(item) => (
<List.Item>
<List.Item.Meta
avatar={<NotificationTypeIcon type={item.Type} />}
title={item.Title}
description={FormatDatetime(item.CreatedAt)}
/>
<CloseOutlined
onClick={() =>
webSocketContext.SendSocketMessage(
SentMessagesCommands.DeleteOneNotification,
{
notificationId: item.Id,
}
)
}
/>
</List.Item>
)}
/>
</>
)}
</Drawer>
</Header>
);
}
function NotificationTypeIcon({ type }) {
switch (type) {
case 1:
return <CheckCircleOutlined color="#33a834" style={{ fontSize: 16 }} />;
case 2:
return <InfoCircleOutlined color="#0c69d7" style={{ fontSize: 16 }} />;
case 3:
return (
<ExclamationCircleOutlined color="#dd9433" style={{ fontSize: 16 }} />
);
case 4:
return <CloseCircleOutlined color="#e5444b" style={{ fontSize: 16 }} />;
default:
return <QuestionCircleOutlined color="#fff" style={{ fontSize: 16 }} />;
}
}

View File

@ -1,7 +1,7 @@
import { Content, Header } from "antd/es/layout/layout";
import { Content } from "antd/es/layout/layout";
import AppRoutes from "../AppRoutes";
import { Button, Layout } from "antd";
import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons";
import { Layout } from "antd";
import HeaderMenu from "../Header";
export default function PageContent({
isSideMenuCollapsed,
@ -9,29 +9,10 @@ export default function PageContent({
}) {
return (
<Layout style={{ marginLeft: isSideMenuCollapsed ? 0 : 200 }}>
<Header
style={{
position: "sticky",
top: 0,
zIndex: 10,
width: "100%",
display: "flex",
alignItems: "center",
padding: 0,
background: "#fff",
}}
>
<Button
type="text"
icon={
isSideMenuCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />
}
onClick={() => setIsSideMenuCollapsed(!isSideMenuCollapsed)}
style={{ fontSize: "16px", width: 64, height: 64 }}
/>
<h1 style={{ fontSize: "16px", margin: 0 }}>Header</h1>
</Header>
<HeaderMenu
isSideMenuCollapsed={isSideMenuCollapsed}
setIsSideMenuCollapsed={setIsSideMenuCollapsed}
/>
<Content
style={{

View File

@ -0,0 +1,29 @@
import { createContext, useContext, useState } from "react";
const preview = {
totalNotifications: 0,
notifications: [],
};
const HeaderContext = createContext(preview);
export const useHeaderContext = () => useContext(HeaderContext);
export default function HeaderProvider({ children }) {
const [totalNotifications, setTotalNotifications] = useState(0);
// initially null, then set to [...] on first fetch
const [notifications, setNotifications] = useState(null);
return (
<HeaderContext.Provider
value={{
totalNotifications,
setTotalNotifications,
notifications,
setNotifications,
}}
>
{children}
</HeaderContext.Provider>
);
}

View File

@ -8,6 +8,7 @@ import { useNavigate } from "react-router-dom";
import { useUserProfileContext } from "./UserProfileContext";
import { useAdminAreaRolesContext } from "./AdminAreaRolesContext";
import { useUsersContext } from "./UsersContext";
import { useHeaderContext } from "./HeaderContext";
const WebSocketContext = createContext(null);
@ -25,6 +26,7 @@ export default function WebSocketProvider({
const wsMessageCache = useRef([]);
const navigate = useNavigate();
const appContext = useAppContext();
const headerContext = useHeaderContext();
const sideBarContext = useSideBarContext();
const groupTasksContext = useGroupTasksContext();
const userProfileContext = useUserProfileContext();
@ -45,6 +47,7 @@ export default function WebSocketProvider({
data.Permissions === null ? [] : data.Permissions
);
appContext.setUsers(data.Users);
headerContext.setTotalNotifications(data.TotalNotifications);
sideBarContext.setUsername(data.Username);
sideBarContext.setAvatar(data.Avatar);
sideBarContext.setAvailableCategoryGroups(data.AvailableCategoryGroups);
@ -67,6 +70,7 @@ export default function WebSocketProvider({
notificationApi,
sideBarContext,
appContext,
headerContext,
groupTasksContext,
userProfileContext,
adminAreaRolesContext,

View File

@ -42,6 +42,9 @@ export const ReceivedMessagesCommands = {
InstallingGlobalPythonPackagesFinished: 38,
UpdateUsers: 39,
CheckingForGroupTasksCategoryGroupChanges: 40,
NewNotification: 41,
AllNotificationsDeleted: 42,
OneNotificationDeleted: 43,
};
// commands sent to the backend server
@ -69,6 +72,8 @@ export const SentMessagesCommands = {
GroupTasksInstallPythonPackages: 21,
GroupTasksInstallGlobalPythonPackages: 22,
SubscribeToTopic: 23,
DeleteAllNotifications: 24,
DeleteOneNotification: 25,
};
/*
@ -83,6 +88,7 @@ export function handleWebSocketMessage(
notificationApi,
sideBarContext,
appContext,
headerContext,
groupTasksContext,
userProfileContext,
adminAreaRolesContext,
@ -962,6 +968,43 @@ export function handleWebSocketMessage(
description: `This may take a while`,
});
break;
case ReceivedMessagesCommands.NewNotification:
headerContext.setTotalNotifications(
(totalNotifications) => totalNotifications + 1
);
headerContext.setNotifications((arr) => {
// only add notifications to the list if the list is not null
// this has to do with the get fetch that is executed when the list is empty
if (arr === null) return arr;
const newArr = [...arr];
newArr.push(body);
return newArr;
});
break;
case ReceivedMessagesCommands.AllNotificationsDeleted:
headerContext.setTotalNotifications(0);
headerContext.setNotifications([]);
break;
case ReceivedMessagesCommands.OneNotificationDeleted:
headerContext.setTotalNotifications(
(totalNotifications) => totalNotifications - 1
);
headerContext.setNotifications((arr) => {
if (arr === null) return arr;
let newArr = [...arr];
newArr = newArr.filter((notification) => notification.Id !== body);
return newArr;
});
break;
default:
console.error("unknown command", cmd);
break;