From 7088fdd1bd90c35105de98d7764b363d44dffa26 Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Wed, 20 Dec 2023 00:35:52 -0800 Subject: [PATCH] notifications menu overhaul complete --- .../components/Header/Notifications/index.tsx | 195 --------- frontend/src/components/Header/index.tsx | 8 +- .../src/components/Notifications/index.tsx | 394 ++++++++++++++++++ frontend/src/layout/AppLayout.tsx | 11 + frontend/src/models/organization.ts | 2 +- 5 files changed, 412 insertions(+), 198 deletions(-) delete mode 100644 frontend/src/components/Header/Notifications/index.tsx create mode 100644 frontend/src/components/Notifications/index.tsx diff --git a/frontend/src/components/Header/Notifications/index.tsx b/frontend/src/components/Header/Notifications/index.tsx deleted file mode 100644 index 9c10c21..0000000 --- a/frontend/src/components/Header/Notifications/index.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { ReactNode, useEffect, useState } from 'react'; -import { AlertOctagon, AlertTriangle, Bell, Info } from 'react-feather'; -import { useParams } from 'react-router-dom'; -import Organization from '../../../models/organization'; -import { databaseTimestampFromNow } from '../../../utils/data'; -import ChromaLogo from '../../../images/vectordbs/chroma.png'; -import PineconeLogo from '../../../images/vectordbs/pinecone.png'; -import qDrantLogo from '../../../images/vectordbs/qdrant.png'; -import WeaviateLogo from '../../../images/vectordbs/weaviate.png'; - -const POLLING_INTERVAL = 30_000; - -export type INotification = { - id: number; - organization_id: number; - seen: boolean; - textContent: string; - symbol?: - | 'info' - | 'warning' - | 'error' - | 'chroma' - | 'pinecone' - | 'weaviate' - | 'qdrant'; - link?: string; - target?: '_blank' | 'self'; - createdAt: string; - lastUpdatedAt: string; -}; - -export default function Notifications() { - const { slug } = useParams(); - const [loading, setLoading] = useState(true); - const [notifications, setNotifications] = useState([]); - const [hasUnseen, setHasUnseen] = useState(false); - const [showNotifs, setShowNotifs] = useState(false); - - async function handleClick() { - if (!showNotifs) { - !!slug && Organization.markNotificationsSeen(slug); - setShowNotifs(true); - setHasUnseen(false); - } else { - setShowNotifs(false); - } - } - - async function fetchNotifications() { - if (!slug) { - setLoading(false); - return; - } - - const { notifications: _notifications } = await Organization.notifications( - slug - ); - setNotifications(_notifications); - setHasUnseen(_notifications.some((notif) => notif.seen === false)); - setLoading(false); - } - - useEffect(() => { - if (!slug) return; - fetchNotifications(); - setInterval(() => { - fetchNotifications(); - }, POLLING_INTERVAL); - }, [slug]); - - if (loading) return null; - - return ( -
- - - - ); -} - -function NotificationImage({ notification }: { notification: INotification }) { - switch (notification.symbol) { - case 'info': - return ; - case 'warning': - return ; - case 'error': - return ; - case 'chroma': - return ; - case 'pinecone': - return ; - case 'qdrant': - return ; - case 'weaviate': - return ; - default: - return ; - } -} - -function NotificationWrapper({ - notification, - children, -}: { - notification: INotification; - children: ReactNode; -}) { - if (!!notification.link) { - return ( - - {children} - - ); - } - return ( -
- {children} -
- ); -} - -function Notification({ notification }: { notification: INotification }) { - return ( - -
- -
-
-
- {notification.textContent} -
-
- {databaseTimestampFromNow(notification.createdAt)} -
-
-
- ); -} diff --git a/frontend/src/components/Header/index.tsx b/frontend/src/components/Header/index.tsx index a99e794..76966ea 100644 --- a/frontend/src/components/Header/index.tsx +++ b/frontend/src/components/Header/index.tsx @@ -5,7 +5,7 @@ import { useEffect, useState } from 'react'; import paths from '../../utils/paths'; import { STORE_TOKEN, STORE_USER } from '../../utils/constants'; import truncate from 'truncate'; -import Notifications from './Notifications'; +import Notifications from '../Notifications'; export default function Header(props: { entity?: any | null; @@ -134,9 +134,13 @@ export default function Header(props: {
{extendedItems}
+ {/* Container for notifications */} + {/*
+ +
*/}
); } diff --git a/frontend/src/components/Notifications/index.tsx b/frontend/src/components/Notifications/index.tsx new file mode 100644 index 0000000..4361f2b --- /dev/null +++ b/frontend/src/components/Notifications/index.tsx @@ -0,0 +1,394 @@ +import { ReactNode, useEffect, useRef, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import Organization from '../../models/organization'; +import { databaseTimestampFromNow } from '../../utils/data'; +import ChromaLogo from '../../images/vectordbs/chroma.png'; +import PineconeLogo from '../../images/vectordbs/pinecone-inverted.png'; +import qDrantLogo from '../../images/vectordbs/qdrant.png'; +import WeaviateLogo from '../../images/vectordbs/weaviate.png'; +import { Bell, Info, Warning, WarningOctagon } from '@phosphor-icons/react'; + +const POLLING_INTERVAL = 30_000; + +export type INotification = { + id: number; + organization_id: number; + seen: boolean; + textContent: string; + symbol?: + | 'info' + | 'warning' + | 'error' + | 'chroma' + | 'pinecone' + | 'weaviate' + | 'qdrant'; + link?: string; + target?: '_blank' | 'self'; + createdAt: string; + lastUpdatedAt: string; +}; + +export default function Notifications() { + const { slug } = useParams(); + const [loading, setLoading] = useState(true); + const [notifications, setNotifications] = useState([]); + const [hasUnseen, setHasUnseen] = useState(false); + const [showNotifs, setShowNotifs] = useState(false); + const notificationRef = useRef(null); + const bellButtonRef = useRef(null); + + async function handleClick() { + if (!showNotifs) { + !!slug && Organization.markNotificationsSeen(slug); + setShowNotifs(true); + setHasUnseen(false); + } else { + setShowNotifs(false); + } + } + // Close notifications when clicking outside + useEffect(() => { + function handleClickOutside(event: any) { + if ( + bellButtonRef.current && + bellButtonRef.current.contains(event.target) + ) { + // Click is inside bell button, do nothing + return; + } + if ( + notificationRef.current && + !notificationRef.current.contains(event.target) + ) { + // Click is outside notification menu, close it + setShowNotifs(false); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + async function fetchNotifications() { + if (!slug) { + setLoading(false); + return; + } + + const { notifications: _notifications } = await Organization.notifications( + slug + ); + setNotifications(_notifications); + setHasUnseen(_notifications.some((notif) => notif.seen === false)); + setLoading(false); + } + + useEffect(() => { + if (!slug) return; + fetchNotifications(); + setInterval(() => { + fetchNotifications(); + }, POLLING_INTERVAL); + }, [slug]); + + if (loading) return null; + + return ( +
+ + + +
+ ); +} + +function NotificationImage({ notification }: { notification: INotification }) { + switch (notification.symbol) { + case 'info': + return ( +
+ +
+ ); + case 'warning': + return ( +
+ +
+ ); + case 'error': + return ( +
+ +
+ ); + case 'chroma': + return ( +
+ Chroma Logo +
+ ); + case 'pinecone': + return ( +
+ Pinecone Logo +
+ ); + case 'qdrant': + return ( +
+ qDrant Logo +
+ ); + case 'weaviate': + return ( +
+ Weaviate Logo +
+ ); + default: + return ( +
+ +
+ ); + } +} + +function NotificationWrapper({ + notification, + children, +}: { + notification: INotification; + children: ReactNode; +}) { + if (!!notification.link) { + return ( + + {children} + + ); + } + return ( +
+ {children} +
+ ); +} + +function Notification({ notification }: { notification: INotification }) { + return ( + +
+ +
+
+
+ {notification.textContent} +
+
+ {databaseTimestampFromNow(notification.createdAt)} +
+
+
+ ); +} + +// +// +// +// +// +// +// {' '} +// {' '} +// diff --git a/frontend/src/layout/AppLayout.tsx b/frontend/src/layout/AppLayout.tsx index 2a29122..8b123fc 100644 --- a/frontend/src/layout/AppLayout.tsx +++ b/frontend/src/layout/AppLayout.tsx @@ -1,6 +1,8 @@ import { ReactNode, useState } from 'react'; import Header from '../components/Header'; import Sidebar from '../components/Sidebar'; +import Notifications from '../components/Notifications'; +import useUser from '../hooks/useUser'; interface DefaultLayoutProps { headerEntity: any; @@ -30,6 +32,7 @@ const AppLayout = ({ hasQuickActions = false, }: DefaultLayoutProps) => { const [sidebarOpen, setSidebarOpen] = useState(false); + const { user } = useUser(); return (
@@ -58,6 +61,14 @@ const AppLayout = ({ />
)} + +
+ + {/* first 2 chars of user.email */} +
+ {user?.email?.slice(0, 2).toUpperCase()} +
+
{children} diff --git a/frontend/src/models/organization.ts b/frontend/src/models/organization.ts index 0b57745..7348694 100644 --- a/frontend/src/models/organization.ts +++ b/frontend/src/models/organization.ts @@ -1,4 +1,4 @@ -import { INotification } from '../components/Header/Notifications'; +import { INotification } from '../components/Notifications'; import { API_BASE } from '../utils/constants'; import { baseHeaders, getAPIUrlString } from '../utils/request';