From c104fbcaa6369fd2919751c36f4d77149d1d5fe6 Mon Sep 17 00:00:00 2001 From: Ben Elferink Date: Tue, 5 Nov 2024 17:20:38 +0200 Subject: [PATCH 1/3] feat: refactor existing components in preparation for notifications --- frontend/webapp/app/page.tsx | 41 +++-- .../connection-notification.tsx | 15 +- .../destinations/add-destination/index.tsx | 2 +- .../add-rule-modal/index.tsx | 2 +- frontend/webapp/hooks/useSSE.ts | 54 +++---- .../reuseable-components/divider/index.tsx | 24 +-- .../nodes-data-flow/nodes/base-node.tsx | 2 +- .../notification-note/index.tsx | 142 +++++++++--------- .../reuseable-components/status/index.tsx | 2 +- frontend/webapp/utils/functions/icons.ts | 24 ++- 10 files changed, 139 insertions(+), 169 deletions(-) diff --git a/frontend/webapp/app/page.tsx b/frontend/webapp/app/page.tsx index ec28d1ded..ea08044df 100644 --- a/frontend/webapp/app/page.tsx +++ b/frontend/webapp/app/page.tsx @@ -1,46 +1,39 @@ 'use client'; import { useEffect } from 'react'; -import { useConfig } from '@/hooks'; import { ROUTES, CONFIG } from '@/utils'; import { useRouter } from 'next/navigation'; -import { addNotification, store } from '@/store'; +import { useConfig, useNotify } from '@/hooks'; import { Loader } from '@keyval-dev/design-system'; export default function App() { const router = useRouter(); + const notify = useNotify(); const { data, error } = useConfig(); useEffect(() => { - data && renderCurrentPage(); - }, [data, error]); - - useEffect(() => { - if (!error) return; - store.dispatch( - addNotification({ - id: '1', + if (error) { + notify({ message: 'An error occurred', title: 'Error', type: 'error', target: 'notification', crdType: 'notification', - }) - ); - router.push(ROUTES.OVERVIEW); - }, [error]); + }); - function renderCurrentPage() { - const { installation } = data; + router.push(ROUTES.OVERVIEW); + } else if (data) { + const { installation } = data; - switch (installation) { - case CONFIG.NEW: - case CONFIG.APPS_SELECTED: - router.push(ROUTES.CHOOSE_SOURCES); - break; - case CONFIG.FINISHED: - router.push(ROUTES.OVERVIEW); + switch (installation) { + case CONFIG.NEW: + case CONFIG.APPS_SELECTED: + router.push(ROUTES.CHOOSE_SOURCES); + break; + case CONFIG.FINISHED: + router.push(ROUTES.OVERVIEW); + } } - } + }, [data, error]); return ; } diff --git a/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/connection-notification.tsx b/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/connection-notification.tsx index 960bc3630..c94d7ef21 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/connection-notification.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/connection-notification.tsx @@ -1,25 +1,16 @@ import { NotificationNote } from '@/reuseable-components'; import styled from 'styled-components'; -export const ConnectionNotification = ({ - showConnectionError, - destination, -}) => ( +export const ConnectionNotification = ({ showConnectionError, destination }) => ( <> {showConnectionError && ( - + )} {destination?.fields && !showConnectionError && ( - + )} diff --git a/frontend/webapp/containers/main/destinations/add-destination/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/index.tsx index 0f63dc7a9..b3356b2b3 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/index.tsx @@ -80,7 +80,7 @@ export function ChooseDestinationContainer() { router.push('/choose-sources'), diff --git a/frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/index.tsx b/frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/index.tsx index 5c96ad3a8..6af49d440 100644 --- a/frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/index.tsx +++ b/frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/index.tsx @@ -70,7 +70,7 @@ export const AddRuleModal: React.FC = ({ isOpen, onClose }) => { /> Date.now() - 2000 - ) { + if (eventBuffer.current[key] && eventBuffer.current[key].id > Date.now() - 2000) { eventBuffer.current[key] = notification; return; } else { @@ -37,16 +35,13 @@ export function useSSE() { } // Dispatch the notification to the store - store.dispatch( - addNotification({ - id: eventBuffer.current[key].id, - message: eventBuffer.current[key].message, - title: eventBuffer.current[key].title, - type: eventBuffer.current[key].type, - target: eventBuffer.current[key].target, - crdType: eventBuffer.current[key].crdType, - }) - ); + notify({ + message: eventBuffer.current[key].message, + title: eventBuffer.current[key].title, + type: eventBuffer.current[key].type, + target: eventBuffer.current[key].target, + crdType: eventBuffer.current[key].crdType, + }); // Reset retry count on successful connection setRetryCount(0); @@ -60,10 +55,7 @@ export function useSSE() { setRetryCount((prevRetryCount) => { if (prevRetryCount < maxRetries) { const newRetryCount = prevRetryCount + 1; - const retryTimeout = Math.min( - 10000, - 1000 * Math.pow(2, newRetryCount) - ); + const retryTimeout = Math.min(10000, 1000 * Math.pow(2, newRetryCount)); setTimeout(() => { connect(); @@ -71,20 +63,16 @@ export function useSSE() { return newRetryCount; } else { - console.error( - 'Max retries reached. Could not reconnect to EventSource.' - ); - store.dispatch( - addNotification({ - id: Date.now().toString(), - message: - 'Connection to the server failed. Please reboot the application.', - title: 'Connection Error', - type: 'error', - target: 'system', - crdType: 'connection', - }) - ); + console.error('Max retries reached. Could not reconnect to EventSource.'); + + notify({ + message: 'Connection to the server failed. Please reboot the application.', + title: 'Connection Error', + type: 'error', + target: 'system', + crdType: 'connection', + }); + return prevRetryCount; } }); diff --git a/frontend/webapp/reuseable-components/divider/index.tsx b/frontend/webapp/reuseable-components/divider/index.tsx index 5ba268e48..26c80541b 100644 --- a/frontend/webapp/reuseable-components/divider/index.tsx +++ b/frontend/webapp/reuseable-components/divider/index.tsx @@ -9,28 +9,14 @@ interface DividerProps { } const StyledDivider = styled.div` - width: ${({ orientation, thickness }) => - orientation === 'vertical' ? `${thickness}px` : '100%'}; - height: ${({ orientation, thickness }) => - orientation === 'horizontal' ? `${thickness}px` : '100%'}; + width: ${({ orientation, thickness }) => (orientation === 'vertical' ? `${thickness}px` : '100%')}; + height: ${({ orientation, thickness }) => (orientation === 'horizontal' ? `${thickness}px` : '100%')}; background-color: ${({ color, theme }) => color || theme.colors.border}; - margin: ${({ margin }) => margin || '8px 0'}; + margin: ${({ orientation, margin }) => margin || (orientation === 'horizontal' ? '8px 0' : '0 8px')}; `; -const Divider: React.FC = ({ - thickness = 1, - color, - margin, - orientation = 'horizontal', -}) => { - return ( - - ); +const Divider: React.FC = ({ thickness = 1, color, margin, orientation = 'horizontal' }) => { + return ; }; export { Divider }; diff --git a/frontend/webapp/reuseable-components/nodes-data-flow/nodes/base-node.tsx b/frontend/webapp/reuseable-components/nodes-data-flow/nodes/base-node.tsx index ff66266f5..39844275c 100644 --- a/frontend/webapp/reuseable-components/nodes-data-flow/nodes/base-node.tsx +++ b/frontend/webapp/reuseable-components/nodes-data-flow/nodes/base-node.tsx @@ -141,7 +141,7 @@ const BaseNode = ({ nodeWidth, isConnectable, data }: BaseNodeProps) => { {renderStatus()} - {isError ? : null} + {isError ? : null} {renderHandles()} ); diff --git a/frontend/webapp/reuseable-components/notification-note/index.tsx b/frontend/webapp/reuseable-components/notification-note/index.tsx index d61749943..0978a9cbe 100644 --- a/frontend/webapp/reuseable-components/notification-note/index.tsx +++ b/frontend/webapp/reuseable-components/notification-note/index.tsx @@ -1,14 +1,15 @@ import React from 'react'; -import styled, { css } from 'styled-components'; import Image from 'next/image'; import { Text } from '../text'; +import { Divider } from '../divider'; +import styled from 'styled-components'; -// Define the notification types type NotificationType = 'warning' | 'error' | 'success' | 'info' | 'default'; interface NotificationProps { type: NotificationType; - text: string; + title?: string; + message?: string; action?: { label: string; onClick: () => void; @@ -16,103 +17,104 @@ interface NotificationProps { style?: React.CSSProperties; } +const getTextColor = ({ type }: { type: NotificationType }) => { + switch (type) { + case 'warning': + return '#E9CF35'; + case 'error': + return '#E25A5A'; + case 'success': + return '#81AF65'; + case 'info': + return '#B8B8B8'; + case 'default': + default: + return '#AABEF7'; + } +}; + +const getBackgroundColor = ({ type }: { type: NotificationType }) => { + switch (type) { + case 'warning': + return '#472300'; + case 'error': + return '#431919'; + case 'success': + return '#172013'; + case 'info': + return '#242424'; + case 'default': + default: + return '#181944'; + } +}; + +const getIconSource = ({ type }: { type: NotificationType }) => { + switch (type) { + case 'warning': + return '/icons/notification/warning-icon.svg'; + case 'error': + return '/icons/notification/error-icon.svg'; + case 'success': + return '/icons/notification/success-icon.svg'; + case 'info': + return '/icons/common/info.svg'; + default: + return '/brand/odigos-icon.svg'; + } +}; + const NotificationContainer = styled.div<{ type: NotificationType }>` display: flex; align-items: center; - justify-content: space-between; padding: 12px 16px; border-radius: 32px; - - background-color: ${({ type }) => { - switch (type) { - case 'warning': - return '#472300'; // Orange - case 'error': - return 'rgba(226, 90, 90, 0.12);'; - case 'success': - return '#28A745'; // Green - case 'info': - return '#F9F9F90A'; // Default to info color - case 'default': - default: - return '#181944'; // Blue - } - }}; + background-color: ${getBackgroundColor}; `; -const IconWrapper = styled.div` - margin-right: 12px; +const TextWrapper = styled.div` display: flex; - justify-content: center; align-items: center; + margin: 0 12px; + height: 12px; `; const Title = styled(Text)<{ type: NotificationType }>` font-size: 14px; - color: ${({ type }) => { - switch (type) { - case 'warning': - return '#E9CF35'; - case 'error': - return '#E25A5A'; - case 'success': - return '#28A745'; - case 'info': - return '#B8B8B8'; - case 'default': - default: - return '#AABEF7'; - } - }}; + color: ${getTextColor}; `; -const TitleWrapper = styled.div` - display: flex; - align-items: center; +const Message = styled(Text)<{ type: NotificationType }>` + font-size: 12px; + color: ${getTextColor}; `; const ActionButtonWrapper = styled.div` - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; + margin-left: 40px; `; const ActionButton = styled(Text)` - text-decoration: underline; text-transform: uppercase; + text-decoration: underline; font-size: 14px; - font-weight: 400; font-family: ${({ theme }) => theme.font_family.secondary}; + cursor: pointer; `; -const NotificationIcon = ({ type }: { type: NotificationType }) => { - switch (type) { - case 'warning': - return warning; - case 'error': - return error; - case 'success': - return success; - case 'info': - return info; - default: - return info; - } -}; - -const NotificationNote: React.FC = ({ type, text, action, style }) => { +const NotificationNote: React.FC = ({ type, title, message, action, style }) => { return ( - - - - - {text} - + {type} + + + {title && {title}} + {title && message && } + {message && {message}} + + {action && ( - {action.label} + {action.label} )} diff --git a/frontend/webapp/reuseable-components/status/index.tsx b/frontend/webapp/reuseable-components/status/index.tsx index 59038ae18..967d90df1 100644 --- a/frontend/webapp/reuseable-components/status/index.tsx +++ b/frontend/webapp/reuseable-components/status/index.tsx @@ -72,7 +72,7 @@ const Status: React.FC = (props) => { {withIcon && ( - status + status )} diff --git a/frontend/webapp/utils/functions/icons.ts b/frontend/webapp/utils/functions/icons.ts index 2be9175f4..5cb4ca5ac 100644 --- a/frontend/webapp/utils/functions/icons.ts +++ b/frontend/webapp/utils/functions/icons.ts @@ -2,27 +2,37 @@ import type { ActionsType, InstrumentationRuleType } from '@/types'; const BRAND_ICON = '/brand/odigos-icon.svg'; -export const getStatusIcon = (active?: boolean) => { - const path = '/icons/notification/'; +export const getStatusIcon = (status?: 'success' | 'error' | 'info') => { + if (!status) return BRAND_ICON; - return `${path}${active ? 'success-icon' : 'error-icon2'}.svg`; + switch (status) { + case 'success': + return '/icons/notification/success-icon.svg'; + + case 'error': + return '/icons/notification/error-icon2.svg'; + + case 'info': + return '/icons/common/info.svg'; + + default: + return BRAND_ICON; + } }; export const getRuleIcon = (type?: InstrumentationRuleType) => { if (!type) return BRAND_ICON; - const path = '/icons/rules/'; const typeLowerCased = type.replaceAll('-', '').toLowerCase(); - return `${path}${typeLowerCased}.svg`; + return `/icons/rules/${typeLowerCased}.svg`; }; export const getActionIcon = (type?: ActionsType | 'sampler') => { if (!type) return BRAND_ICON; - const path = '/icons/actions/'; const typeLowerCased = type.toLowerCase(); const isSampler = typeLowerCased.includes('sampler'); - return `${path}${isSampler ? 'sampler' : typeLowerCased}.svg`; + return `/icons/actions/${isSampler ? 'sampler' : typeLowerCased}.svg`; }; From a256c2264abf0bcb122e07f18d8cfb06b2dbefbc Mon Sep 17 00:00:00 2001 From: Ben Elferink Date: Wed, 6 Nov 2024 09:48:33 +0200 Subject: [PATCH 2/3] feat: replace redux with zustand, use new toasts --- frontend/webapp/app/layout.tsx | 44 ++++------ frontend/webapp/components/index.ts | 4 +- .../notification/notification-list-item.tsx | 35 ++------ .../notification/notification-list.tsx | 22 ++--- .../notification/notification-manager.tsx | 34 -------- .../components/notification/notification.tsx | 86 ------------------- .../components/notification/toast-list.tsx | 29 +++++++ .../webapp/components/notification/toast.tsx | 60 +++++++++++++ frontend/webapp/hooks/useNotify.ts | 36 +++++--- .../notification-note/index.tsx | 7 +- frontend/webapp/store/index.ts | 17 +--- frontend/webapp/store/redux-provider.tsx | 10 --- frontend/webapp/store/slices/index.ts | 1 - .../webapp/store/slices/notification-slice.ts | 54 ------------ frontend/webapp/store/useNotificationStore.ts | 61 +++++++++++++ frontend/webapp/types/common.ts | 15 ++-- 16 files changed, 217 insertions(+), 298 deletions(-) delete mode 100644 frontend/webapp/components/notification/notification-manager.tsx delete mode 100644 frontend/webapp/components/notification/notification.tsx create mode 100644 frontend/webapp/components/notification/toast-list.tsx create mode 100644 frontend/webapp/components/notification/toast.tsx delete mode 100644 frontend/webapp/store/redux-provider.tsx delete mode 100644 frontend/webapp/store/slices/index.ts delete mode 100644 frontend/webapp/store/slices/notification-slice.ts create mode 100644 frontend/webapp/store/useNotificationStore.ts diff --git a/frontend/webapp/app/layout.tsx b/frontend/webapp/app/layout.tsx index bb8b58cab..448606300 100644 --- a/frontend/webapp/app/layout.tsx +++ b/frontend/webapp/app/layout.tsx @@ -4,11 +4,9 @@ import React from 'react'; import { useSSE } from '@/hooks'; import theme from '@/styles/theme'; import { ApolloWrapper } from '@/lib'; +import { ToastList } from '@/components'; import { ThemeProvider } from 'styled-components'; -import { NotificationManager } from '@/components'; -import ReduxProvider from '@/store/redux-provider'; import { QueryClient, QueryClientProvider } from 'react-query'; -// import { ThemeProviderWrapper } from '@keyval-dev/design-system'; const LAYOUT_STYLE: React.CSSProperties = { margin: 0, @@ -18,34 +16,30 @@ const LAYOUT_STYLE: React.CSSProperties = { height: '100vh', }; -export default function RootLayout({ children }: { children: React.ReactNode }) { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 10000, - refetchOnWindowFocus: false, - }, +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 10000, + refetchOnWindowFocus: false, }, - }); + }, +}); +export default function RootLayout({ children }: { children: React.ReactNode }) { useSSE(); return ( - - - - - {/* */} - - {children} - - - {/* */} - - - - + + + + + {children} + + + + + ); } diff --git a/frontend/webapp/components/index.ts b/frontend/webapp/components/index.ts index f047da4e2..506fca846 100644 --- a/frontend/webapp/components/index.ts +++ b/frontend/webapp/components/index.ts @@ -2,8 +2,8 @@ export * from './setup'; export * from './lists'; export * from './overview'; export * from './common'; -export * from './notification/notification-manager'; -export * from './notification/notification-list'; export * from './destinations'; export * from './main'; export * from './modals'; +export * from './notification/notification-list'; // old +export * from './notification/toast-list'; // new diff --git a/frontend/webapp/components/notification/notification-list-item.tsx b/frontend/webapp/components/notification/notification-list-item.tsx index 894657220..4ca0cb922 100644 --- a/frontend/webapp/components/notification/notification-list-item.tsx +++ b/frontend/webapp/components/notification/notification-list-item.tsx @@ -5,20 +5,8 @@ import { ROUTES, timeAgo } from '@/utils'; import { useRouter } from 'next/navigation'; import { getIcon } from './notification-icon'; import { KeyvalLink, KeyvalText } from '@/design.system'; -import { - NotificationButtonContainer, - NotificationDetailsWrapper, -} from './notification-container'; -interface NotificationListItemProps { - id: string; - message: string; - type: 'success' | 'error' | 'info'; - seen: boolean; - title?: string; - crdType?: string; - time?: string; - target?: string; -} +import { NotificationButtonContainer, NotificationDetailsWrapper } from './notification-container'; +import { Notification } from '@/types'; const NotificationItemContainer = styled.div<{ seen: boolean }>` border-bottom: 1px solid ${theme.colors.blue_grey}; @@ -27,8 +15,7 @@ const NotificationItemContainer = styled.div<{ seen: boolean }>` display: flex; justify-content: space-between; align-items: center; - background-color: ${({ seen }) => - seen ? theme.colors.light_dark : theme.colors.dark}; + background-color: ${({ seen }) => (seen ? theme.colors.light_dark : theme.colors.dark)}; &:hover { background-color: ${theme.colors.dark}; @@ -41,15 +28,7 @@ const NotificationContent = styled.div` gap: 8px; `; -const NotificationListItem: React.FC = ({ - message, - type, - seen, - title, - crdType, - target, - time, -}) => { +const NotificationListItem: React.FC = ({ message, type, seen, title, crdType, target, time }) => { const router = useRouter(); function onDetailsClick() { @@ -83,11 +62,7 @@ const NotificationListItem: React.FC = ({ )} - - {!!target && ( - - )} - + {!!target && } ); }; diff --git a/frontend/webapp/components/notification/notification-list.tsx b/frontend/webapp/components/notification/notification-list.tsx index 4d8d4d5cd..40f5d2880 100644 --- a/frontend/webapp/components/notification/notification-list.tsx +++ b/frontend/webapp/components/notification/notification-list.tsx @@ -1,13 +1,11 @@ import React, { useEffect, useRef, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { RootState, markAsSeen } from '@/store'; - import styled from 'styled-components'; import theme from '@/styles/palette'; import { BellIcon } from '@keyval-dev/design-system'; import { KeyvalText } from '@/design.system'; import { useOnClickOutside } from '@/hooks'; import NotificationListItem from './notification-list-item'; +import { useNotificationStore } from '@/store'; const NotificationListContainer = styled.div` position: absolute; @@ -58,18 +56,12 @@ const NotificationHeader = styled.div` export const NotificationList: React.FC = () => { const [showNotifications, setShowNotifications] = useState(false); + const { notifications, markAsSeen } = useNotificationStore(); - const notifications = useSelector( - (state: RootState) => state.notification.notifications - ); - - const dispatch = useDispatch(); const containerRef = useRef(null); const isInitialRender = useRef(true); useOnClickOutside(containerRef, () => setShowNotifications(false)); - const unseenCount = notifications.filter( - (notification) => !notification.seen - ).length; + const unseenCount = notifications.filter((notification) => !notification.seen).length; useEffect(() => { if (isInitialRender.current) { @@ -85,7 +77,7 @@ export const NotificationList: React.FC = () => { function markAllAsSeen() { notifications.forEach((notification) => { if (!notification.seen) { - dispatch(markAsSeen(notification.id)); + markAsSeen(notification.id); } }); } @@ -93,11 +85,7 @@ export const NotificationList: React.FC = () => { return notifications.length > 0 ? (
- setShowNotifications(!showNotifications)} - /> + setShowNotifications(!showNotifications)} /> {unseenCount > 0 && ( {unseenCount} diff --git a/frontend/webapp/components/notification/notification-manager.tsx b/frontend/webapp/components/notification/notification-manager.tsx deleted file mode 100644 index b091ea56f..000000000 --- a/frontend/webapp/components/notification/notification-manager.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, { useEffect } from 'react'; -import { useSelector } from 'react-redux'; -// import Notification from './notification'; -import styled from 'styled-components'; -import { RootState } from '@/store'; - -const NotificationsWrapper = styled.div` - position: fixed; - top: 20px; - right: 20px; - display: flex; - flex-direction: column; - align-items: flex-end; - z-index: 1000; -`; - -export const NotificationManager: React.FC = () => { - const notifications = useSelector((state: RootState) => state.notification.notifications); - - // temporary - until we fix the "theme" error on import from "design.system" - useEffect(() => { - if (notifications.length) alert(notifications[notifications.length - 1].message); - }, [notifications.length]); - - return ( - - {/* {notifications - .filter((notification) => notification.isNew) - .map((notification) => ( - - ))} */} - - ); -}; diff --git a/frontend/webapp/components/notification/notification.tsx b/frontend/webapp/components/notification/notification.tsx deleted file mode 100644 index 978e4733a..000000000 --- a/frontend/webapp/components/notification/notification.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { ROUTES } from '@/utils'; -import { useDispatch } from 'react-redux'; -import { useRouter } from 'next/navigation'; -import { getIcon } from './notification-icon'; -import { markAsOld, markAsSeen } from '@/store'; -import { KeyvalLink, KeyvalText } from '@/design.system'; -import { - NotificationContainer, - NotificationContentWrapper, - NotificationDetailsWrapper, - NotificationButtonContainer, -} from './notification-container'; - -interface NotificationProps { - id: string; - message: string; - title?: string; - type: 'success' | 'error' | 'info'; - time?: string; - onClick?: () => void; - crdType?: string; - target?: string; -} - -const Notification: React.FC = ({ - id, - message, - type, - title, - crdType, - target, -}) => { - const dispatch = useDispatch(); - const router = useRouter(); - const [isLeaving, setIsLeaving] = useState(false); - - useEffect(() => { - const timer = setTimeout(() => { - setIsLeaving(true); - setTimeout(() => dispatch(markAsOld(id)), 500); - }, 5000); - - return () => clearTimeout(timer); - }, [id, dispatch]); - - function onDetailsClick() { - dispatch(markAsSeen(id)); - dispatch(markAsOld(id)); - - if (target) { - switch (crdType) { - case 'Destination': - router.push(`${ROUTES.UPDATE_DESTINATION}${target}`); - break; - case 'InstrumentedApplication': - case 'InstrumentationInstance': - router.push(`${ROUTES.MANAGE_SOURCE}?${target}`); - break; - default: - break; - } - } - } - - return ( - - -
{getIcon(type)}
- - - {title} - - {message} - -
- - {!!target && ( - - )} - -
- ); -}; - -export default Notification; diff --git a/frontend/webapp/components/notification/toast-list.tsx b/frontend/webapp/components/notification/toast-list.tsx new file mode 100644 index 000000000..0a4f3cca8 --- /dev/null +++ b/frontend/webapp/components/notification/toast-list.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import Toast from './toast'; +import styled from 'styled-components'; +import { useNotificationStore } from '@/store'; + +const Container = styled.div` + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 10000; + display: flex; + flex-direction: column; + gap: 6px; +`; + +export const ToastList: React.FC = () => { + const { notifications } = useNotificationStore(); + + return ( + + {notifications + .filter(({ dismissed }) => !dismissed) + .map((notif) => ( + + ))} + + ); +}; diff --git a/frontend/webapp/components/notification/toast.tsx b/frontend/webapp/components/notification/toast.tsx new file mode 100644 index 000000000..faaca2ad2 --- /dev/null +++ b/frontend/webapp/components/notification/toast.tsx @@ -0,0 +1,60 @@ +import React, { useEffect, useState } from 'react'; +import type { Notification } from '@/types'; +import { useNotificationStore } from '@/store'; +import { NotificationNote } from '@/reuseable-components'; + +interface Props extends Notification {} + +const Toast: React.FC = ({ id, message, type, title, crdType, target }) => { + // const router = useRouter(); + const { markAsDismissed, markAsSeen } = useNotificationStore(); + const [isLeaving, setIsLeaving] = useState(false); + + useEffect(() => { + const timer = setTimeout(() => { + setIsLeaving(true); + setTimeout(() => markAsDismissed(id), 500); + }, 5000); + + return () => clearTimeout(timer); + }, [id, markAsDismissed]); + + const onClick = () => { + markAsDismissed(id); + markAsSeen(id); + + alert('TODO'); + + // switch (crdType) { + // case 'Destination': + // // TODO: open drawer + // // router.push(`${ROUTES.UPDATE_DESTINATION}${target}`); + // break; + // case 'InstrumentedApplication': + // case 'InstrumentationInstance': + // // TODO: open drawer + // // router.push(`${ROUTES.MANAGE_SOURCE}?${target}`); + // break; + // default: + // break; + // } + }; + + return ( + + ); +}; + +export default Toast; diff --git a/frontend/webapp/hooks/useNotify.ts b/frontend/webapp/hooks/useNotify.ts index c36eae1a9..20beed4f3 100644 --- a/frontend/webapp/hooks/useNotify.ts +++ b/frontend/webapp/hooks/useNotify.ts @@ -1,23 +1,35 @@ -import { addNotification, store } from '@/store'; +import { useNotificationStore } from '@/store'; +import { Notification } from '@/types'; export const useNotify = () => { - const dispatch = store.dispatch; + const { addNotification } = useNotificationStore(); const notify = ({ - message, - title, type, - target, + title, + message, crdType, + target, }: { - message: string; - title: string; - type: 'success' | 'error' | 'info'; - target: string; - crdType: string; + type: Notification['type']; + title: Notification['title']; + message: Notification['message']; + crdType: Notification['crdType']; + target: Notification['target']; }) => { - const id = new Date().getTime().toString(); - dispatch(addNotification({ id, message, title, type, target, crdType })); + const date = new Date(); + + addNotification({ + id: date.getTime().toString(), + type, + title, + message, + crdType, + target, + isNew: true, + seen: false, + time: date.toISOString(), + }); }; return notify; diff --git a/frontend/webapp/reuseable-components/notification-note/index.tsx b/frontend/webapp/reuseable-components/notification-note/index.tsx index 0978a9cbe..e27fd8787 100644 --- a/frontend/webapp/reuseable-components/notification-note/index.tsx +++ b/frontend/webapp/reuseable-components/notification-note/index.tsx @@ -3,13 +3,12 @@ import Image from 'next/image'; import { Text } from '../text'; import { Divider } from '../divider'; import styled from 'styled-components'; - -type NotificationType = 'warning' | 'error' | 'success' | 'info' | 'default'; +import type { Notification, NotificationType } from '@/types'; interface NotificationProps { type: NotificationType; - title?: string; - message?: string; + title: Notification['title']; + message: Notification['message']; action?: { label: string; onClick: () => void; diff --git a/frontend/webapp/store/index.ts b/frontend/webapp/store/index.ts index f116f1d8a..d9e795740 100644 --- a/frontend/webapp/store/index.ts +++ b/frontend/webapp/store/index.ts @@ -1,19 +1,4 @@ -import { notificationReducer } from './slices'; -import { combineReducers, configureStore } from '@reduxjs/toolkit'; - -const rootReducer = combineReducers({ - notification: notificationReducer, -}); - -export const store = configureStore({ - reducer: rootReducer, - middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false }), -}); - -export type RootState = ReturnType; -export type AppDispatch = typeof store.dispatch; - -export * from './slices'; export * from './useAppStore'; export * from './useDrawerStore'; export * from './useModalStore'; +export * from './useNotificationStore'; diff --git a/frontend/webapp/store/redux-provider.tsx b/frontend/webapp/store/redux-provider.tsx deleted file mode 100644 index 0f86f47c1..000000000 --- a/frontend/webapp/store/redux-provider.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { store } from '.'; -import { Provider } from 'react-redux'; - -export default function ReduxProvider({ - children, -}: { - children: React.ReactNode; -}) { - return {children}; -} diff --git a/frontend/webapp/store/slices/index.ts b/frontend/webapp/store/slices/index.ts deleted file mode 100644 index 981b2f31a..000000000 --- a/frontend/webapp/store/slices/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './notification-slice'; diff --git a/frontend/webapp/store/slices/notification-slice.ts b/frontend/webapp/store/slices/notification-slice.ts deleted file mode 100644 index 397c8cf1d..000000000 --- a/frontend/webapp/store/slices/notification-slice.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { Notification } from '@/types'; - -interface NotificationState { - notifications: Notification[]; -} - -const initialState: NotificationState = { - notifications: [], -}; - -export const notificationSlice = createSlice({ - name: 'notification', - initialState, - reducers: { - addNotification: ( - state, - action: PayloadAction> - ) => { - state.notifications.push({ - ...action.payload, - seen: false, - isNew: true, - time: new Date().toISOString(), - }); - }, - markAsSeen: (state, action: PayloadAction) => { - const notification = state.notifications.find( - (n) => n.id === action.payload - ); - if (notification) { - notification.seen = true; - } - }, - markAsOld: (state, action: PayloadAction) => { - const notification = state.notifications.find( - (n) => n.id === action.payload - ); - if (notification) { - notification.isNew = false; - } - }, - removeNotification: (state, action: PayloadAction) => { - state.notifications = state.notifications.filter( - (notification) => notification.id !== action.payload - ); - }, - }, -}); - -export const { addNotification, removeNotification, markAsSeen, markAsOld } = - notificationSlice.actions; - -export const notificationReducer = notificationSlice.reducer; diff --git a/frontend/webapp/store/useNotificationStore.ts b/frontend/webapp/store/useNotificationStore.ts new file mode 100644 index 000000000..f30af71f3 --- /dev/null +++ b/frontend/webapp/store/useNotificationStore.ts @@ -0,0 +1,61 @@ +import { create } from 'zustand'; +import type { Notification } from '@/types'; + +interface StoreState { + notifications: Notification[]; + addNotification: (notif: Notification) => void; + markAsDismissed: (id: string) => void; + markAsSeen: (id: string) => void; + removeNotification: (id: string) => void; +} + +export const useNotificationStore = create((set) => ({ + notifications: [], + + addNotification: (notif) => + set((state) => ({ + notifications: [...state.notifications, notif], + })), + + markAsDismissed: (id) => { + set((state) => { + const foundIdx = state.notifications.findIndex((notif) => notif.id === id); + + if (foundIdx !== -1) { + state.notifications[foundIdx].dismissed = true; + } + + return { + notifications: state.notifications, + }; + }); + }, + + markAsSeen: (id) => { + set((state) => { + const foundIdx = state.notifications.findIndex((notif) => notif.id === id); + + if (foundIdx !== -1) { + state.notifications[foundIdx].seen = true; + } + + return { + notifications: state.notifications, + }; + }); + }, + + removeNotification: (id) => { + set((state) => { + const foundIdx = state.notifications.findIndex((notif) => notif.id === id); + + if (foundIdx !== -1) { + state.notifications.splice(foundIdx, 1); + } + + return { + notifications: state.notifications, + }; + }); + }, +})); diff --git a/frontend/webapp/types/common.ts b/frontend/webapp/types/common.ts index 3fbceedb2..78bdec9a5 100644 --- a/frontend/webapp/types/common.ts +++ b/frontend/webapp/types/common.ts @@ -11,17 +11,18 @@ export interface Condition { lastTransitionTime: string; } +export type NotificationType = 'warning' | 'error' | 'success' | 'info' | 'default'; + export interface Notification { id: string; - message: string; + type: NotificationType; title?: string; - seen: boolean; - isNew?: boolean; - time?: string; - target?: string; - event?: string; + message?: string; crdType?: string; - type: 'success' | 'error' | 'info'; + target?: string; + dismissed: boolean; + seen: boolean; + time: string; } export type Config = { From 8e0134b6cf7308ee6e6badc4ec06e0d939256259 Mon Sep 17 00:00:00 2001 From: Ben Elferink Date: Wed, 6 Nov 2024 09:57:08 +0200 Subject: [PATCH 3/3] fix: key name --- frontend/webapp/hooks/useNotify.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/webapp/hooks/useNotify.ts b/frontend/webapp/hooks/useNotify.ts index 20beed4f3..63969d4d1 100644 --- a/frontend/webapp/hooks/useNotify.ts +++ b/frontend/webapp/hooks/useNotify.ts @@ -26,7 +26,7 @@ export const useNotify = () => { message, crdType, target, - isNew: true, + dismissed: false, seen: false, time: date.toISOString(), });