notifications
parent
e4f9acabe6
commit
411b35decd
|
@ -392,5 +392,15 @@
|
||||||
},
|
},
|
||||||
"createEquipmentDocumentationModal": {
|
"createEquipmentDocumentationModal": {
|
||||||
"buttonCreateDocumentation": "Dokumentation erstellen"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -391,5 +391,15 @@
|
||||||
},
|
},
|
||||||
"createEquipmentDocumentationModal": {
|
"createEquipmentDocumentationModal": {
|
||||||
"buttonCreateDocumentation": "Create Documentation"
|
"buttonCreateDocumentation": "Create Documentation"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"notificationDrawer": {
|
||||||
|
"title": "Notifications",
|
||||||
|
"deleteAllButtonText": "Delete All",
|
||||||
|
"deleteAllPopconfirm": {
|
||||||
|
"title": "Are you sure you want to delete all notifications?"
|
||||||
|
},
|
||||||
|
"noNotifications": "No notifications"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
47
src/App.js
47
src/App.js
|
@ -12,6 +12,7 @@ import { GroupTasksProvider } from "./Contexts/GroupTasksContext";
|
||||||
import { AdminAreaRolesProvider } from "./Contexts/AdminAreaRolesContext";
|
import { AdminAreaRolesProvider } from "./Contexts/AdminAreaRolesContext";
|
||||||
import { UserProfileProvider } from "./Contexts/UserProfileContext";
|
import { UserProfileProvider } from "./Contexts/UserProfileContext";
|
||||||
import { UsersProvider } from "./Contexts/UsersContext";
|
import { UsersProvider } from "./Contexts/UsersContext";
|
||||||
|
import HeaderProvider from "./Contexts/HeaderContext";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [notificationApi, notificationContextHolder] =
|
const [notificationApi, notificationContextHolder] =
|
||||||
|
@ -35,30 +36,32 @@ export default function App() {
|
||||||
{notificationContextHolder}
|
{notificationContextHolder}
|
||||||
|
|
||||||
<AppProvider>
|
<AppProvider>
|
||||||
<SideBarProvider>
|
<HeaderProvider>
|
||||||
<GroupTasksProvider>
|
<SideBarProvider>
|
||||||
<AdminAreaRolesProvider>
|
<GroupTasksProvider>
|
||||||
<UserProfileProvider>
|
<AdminAreaRolesProvider>
|
||||||
<UsersProvider>
|
<UserProfileProvider>
|
||||||
<WebSocketProvider
|
<UsersProvider>
|
||||||
userSession={userSession}
|
<WebSocketProvider
|
||||||
setUserSession={setUserSession}
|
|
||||||
isWebSocketReady={isWebSocketReady}
|
|
||||||
setIsWebSocketReady={setIsWebSocketReady}
|
|
||||||
notificationApi={notificationApi}
|
|
||||||
>
|
|
||||||
<ReconnectingView isWebSocketReady={isWebSocketReady} />
|
|
||||||
|
|
||||||
<DashboardLayout
|
|
||||||
userSession={userSession}
|
userSession={userSession}
|
||||||
setUserSession={setUserSession}
|
setUserSession={setUserSession}
|
||||||
/>
|
isWebSocketReady={isWebSocketReady}
|
||||||
</WebSocketProvider>
|
setIsWebSocketReady={setIsWebSocketReady}
|
||||||
</UsersProvider>
|
notificationApi={notificationApi}
|
||||||
</UserProfileProvider>
|
>
|
||||||
</AdminAreaRolesProvider>
|
<ReconnectingView isWebSocketReady={isWebSocketReady} />
|
||||||
</GroupTasksProvider>
|
|
||||||
</SideBarProvider>
|
<DashboardLayout
|
||||||
|
userSession={userSession}
|
||||||
|
setUserSession={setUserSession}
|
||||||
|
/>
|
||||||
|
</WebSocketProvider>
|
||||||
|
</UsersProvider>
|
||||||
|
</UserProfileProvider>
|
||||||
|
</AdminAreaRolesProvider>
|
||||||
|
</GroupTasksProvider>
|
||||||
|
</SideBarProvider>
|
||||||
|
</HeaderProvider>
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 }} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { Content, Header } from "antd/es/layout/layout";
|
import { Content } from "antd/es/layout/layout";
|
||||||
import AppRoutes from "../AppRoutes";
|
import AppRoutes from "../AppRoutes";
|
||||||
import { Button, Layout } from "antd";
|
import { Layout } from "antd";
|
||||||
import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons";
|
import HeaderMenu from "../Header";
|
||||||
|
|
||||||
export default function PageContent({
|
export default function PageContent({
|
||||||
isSideMenuCollapsed,
|
isSideMenuCollapsed,
|
||||||
|
@ -9,29 +9,10 @@ export default function PageContent({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Layout style={{ marginLeft: isSideMenuCollapsed ? 0 : 200 }}>
|
<Layout style={{ marginLeft: isSideMenuCollapsed ? 0 : 200 }}>
|
||||||
<Header
|
<HeaderMenu
|
||||||
style={{
|
isSideMenuCollapsed={isSideMenuCollapsed}
|
||||||
position: "sticky",
|
setIsSideMenuCollapsed={setIsSideMenuCollapsed}
|
||||||
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>
|
|
||||||
|
|
||||||
<Content
|
<Content
|
||||||
style={{
|
style={{
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import { useNavigate } from "react-router-dom";
|
||||||
import { useUserProfileContext } from "./UserProfileContext";
|
import { useUserProfileContext } from "./UserProfileContext";
|
||||||
import { useAdminAreaRolesContext } from "./AdminAreaRolesContext";
|
import { useAdminAreaRolesContext } from "./AdminAreaRolesContext";
|
||||||
import { useUsersContext } from "./UsersContext";
|
import { useUsersContext } from "./UsersContext";
|
||||||
|
import { useHeaderContext } from "./HeaderContext";
|
||||||
|
|
||||||
const WebSocketContext = createContext(null);
|
const WebSocketContext = createContext(null);
|
||||||
|
|
||||||
|
@ -25,6 +26,7 @@ export default function WebSocketProvider({
|
||||||
const wsMessageCache = useRef([]);
|
const wsMessageCache = useRef([]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const appContext = useAppContext();
|
const appContext = useAppContext();
|
||||||
|
const headerContext = useHeaderContext();
|
||||||
const sideBarContext = useSideBarContext();
|
const sideBarContext = useSideBarContext();
|
||||||
const groupTasksContext = useGroupTasksContext();
|
const groupTasksContext = useGroupTasksContext();
|
||||||
const userProfileContext = useUserProfileContext();
|
const userProfileContext = useUserProfileContext();
|
||||||
|
@ -45,6 +47,7 @@ export default function WebSocketProvider({
|
||||||
data.Permissions === null ? [] : data.Permissions
|
data.Permissions === null ? [] : data.Permissions
|
||||||
);
|
);
|
||||||
appContext.setUsers(data.Users);
|
appContext.setUsers(data.Users);
|
||||||
|
headerContext.setTotalNotifications(data.TotalNotifications);
|
||||||
sideBarContext.setUsername(data.Username);
|
sideBarContext.setUsername(data.Username);
|
||||||
sideBarContext.setAvatar(data.Avatar);
|
sideBarContext.setAvatar(data.Avatar);
|
||||||
sideBarContext.setAvailableCategoryGroups(data.AvailableCategoryGroups);
|
sideBarContext.setAvailableCategoryGroups(data.AvailableCategoryGroups);
|
||||||
|
@ -67,6 +70,7 @@ export default function WebSocketProvider({
|
||||||
notificationApi,
|
notificationApi,
|
||||||
sideBarContext,
|
sideBarContext,
|
||||||
appContext,
|
appContext,
|
||||||
|
headerContext,
|
||||||
groupTasksContext,
|
groupTasksContext,
|
||||||
userProfileContext,
|
userProfileContext,
|
||||||
adminAreaRolesContext,
|
adminAreaRolesContext,
|
||||||
|
|
|
@ -42,6 +42,9 @@ export const ReceivedMessagesCommands = {
|
||||||
InstallingGlobalPythonPackagesFinished: 38,
|
InstallingGlobalPythonPackagesFinished: 38,
|
||||||
UpdateUsers: 39,
|
UpdateUsers: 39,
|
||||||
CheckingForGroupTasksCategoryGroupChanges: 40,
|
CheckingForGroupTasksCategoryGroupChanges: 40,
|
||||||
|
NewNotification: 41,
|
||||||
|
AllNotificationsDeleted: 42,
|
||||||
|
OneNotificationDeleted: 43,
|
||||||
};
|
};
|
||||||
|
|
||||||
// commands sent to the backend server
|
// commands sent to the backend server
|
||||||
|
@ -69,6 +72,8 @@ export const SentMessagesCommands = {
|
||||||
GroupTasksInstallPythonPackages: 21,
|
GroupTasksInstallPythonPackages: 21,
|
||||||
GroupTasksInstallGlobalPythonPackages: 22,
|
GroupTasksInstallGlobalPythonPackages: 22,
|
||||||
SubscribeToTopic: 23,
|
SubscribeToTopic: 23,
|
||||||
|
DeleteAllNotifications: 24,
|
||||||
|
DeleteOneNotification: 25,
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -83,6 +88,7 @@ export function handleWebSocketMessage(
|
||||||
notificationApi,
|
notificationApi,
|
||||||
sideBarContext,
|
sideBarContext,
|
||||||
appContext,
|
appContext,
|
||||||
|
headerContext,
|
||||||
groupTasksContext,
|
groupTasksContext,
|
||||||
userProfileContext,
|
userProfileContext,
|
||||||
adminAreaRolesContext,
|
adminAreaRolesContext,
|
||||||
|
@ -962,6 +968,43 @@ export function handleWebSocketMessage(
|
||||||
description: `This may take a while`,
|
description: `This may take a while`,
|
||||||
});
|
});
|
||||||
break;
|
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:
|
default:
|
||||||
console.error("unknown command", cmd);
|
console.error("unknown command", cmd);
|
||||||
break;
|
break;
|
||||||
|
|
Loading…
Reference in New Issue