diff --git a/apps/web/src/app/app.tsx b/apps/web/src/app/app.tsx index 2759ac28030569d1152be8f44211f984e282a8b6..814d865fcb617f14670e1f7225a8353f942d28c3 100644 --- a/apps/web/src/app/app.tsx +++ b/apps/web/src/app/app.tsx @@ -28,6 +28,7 @@ import { logout } from '@graphpolaris/shared/lib/data-access/store/authSlice'; import { SchemaFromBackend } from '@graphpolaris/shared/lib/schema'; import { LinkPredictionInstance, setMLResult, allMLTypes } from '@graphpolaris/shared/lib/data-access/store/mlSlice'; import { Resizable } from '@graphpolaris/shared/lib/components/Resizable'; +import { BrokerAlerts } from '@graphpolaris/shared/lib/data-access/socket/broker/brokerAlerts'; export interface App {} @@ -105,6 +106,7 @@ export function App(props: App) { return ( <div className="h-screen w-screen"> + <BrokerAlerts /> <div className={'h-screen w-screen ' + (!auth.authorized ? 'blur-sm pointer-events-none ' : '')}> <div className="flex flex-col h-screen max-h-screen relative"> <aside className="h-[4rem]"> diff --git a/libs/config/tailwind.config.js b/libs/config/tailwind.config.js index fa3bfc85e6298c3a96ed5b6ae2d0e7bae8c54a5a..59520b7a09e48e30c34f894097e069b514955207 100644 --- a/libs/config/tailwind.config.js +++ b/libs/config/tailwind.config.js @@ -41,7 +41,7 @@ export default { 'base-100': '#F7F9FA', // 'base-200': '#F9FBFC', info: '#99622E', - success: '#2DC177', + success: '#35a06a', warning: '#eab308', error: '#dc2626', diff --git a/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.tsx b/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.tsx index 64b41b9dffb20eb5dfa7afd0baee6ec8f721fcee..d61e843a267060afeffa51b33b5bd0c537fcbea5 100644 --- a/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.tsx +++ b/libs/shared/lib/data-access/socket/backend-message-receiver/WebSocketHandler.tsx @@ -67,8 +67,6 @@ export class WebSocketHandler implements BackendMessageReceiver { */ public onWebSocketMessage = (event: MessageEvent<any>) => { let data = JSON.parse(event.data); - console.debug('WS message: ', data); - Broker.instance().publish(data.value, data.type); }; diff --git a/libs/shared/lib/data-access/socket/broker/brokerAlerts.tsx b/libs/shared/lib/data-access/socket/broker/brokerAlerts.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a2b96066b0e2c2ddeca47c58945e47dbc9cc790b --- /dev/null +++ b/libs/shared/lib/data-access/socket/broker/brokerAlerts.tsx @@ -0,0 +1,91 @@ +import React, { ReactNode, useEffect, useState } from 'react'; +import Broker from '@graphpolaris/shared/lib/data-access/socket/broker'; +import { useImmer } from 'use-immer'; +import styles from './brokerAlerts.module.scss'; +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; + +type Message = { + message: ReactNode; + className: string; +}; + +export const BrokerAlerts = (props: { timer?: number }) => { + const [messages, setMessages] = useImmer<{ data: any; routingKey: string; message: Message; showing: boolean }[]>([]); + const timer = props.timer || 1200; + + useEffect(() => { + Broker.instance().subscribeDefault((data: any, routingKey: string) => { + let message: Message | undefined = undefined; + + // Use the logic below to define which broker messages should be shown as alerts. + if (routingKey === 'schema_result') { + message = { + message: ( + <> + <CheckCircleOutlineIcon /> Schema graph updated + </> + ), + className: 'alert-success', + }; + } else if (routingKey === 'query_result') { + message = { + message: ( + <> + <CheckCircleOutlineIcon /> Query Executed! + </> + ), + className: 'alert-success', + }; + } + + // Queue the message to be shown for props.time time, then fade it out matching the animation of animate-closemenu fade out time + if (message) { + setMessages((draft) => { + draft.unshift({ data, routingKey, message: message as Message, showing: true }); + return draft; + }); + setTimeout(() => { + setMessages((draft) => { + draft[draft.length - 1].showing = false; + // draft.pop(); + return draft; + }); + + setTimeout(() => { + setMessages((draft) => { + draft.pop(); + return draft; + }); + }, 300); // matches the animation of animate-closemenu + }, timer); + } + }); + return () => { + Broker.instance().unSubscribeDefault(); + }; + }, []); + + return ( + <> + {messages && + messages.map((m, i) => { + return ( + <div + key={i} + className={ + `absolute bottom-5 right-5 w-fit min-w-[20rem] alert z-50 ` + + (m.showing ? 'animate-openmenu ' : 'animate-closemenu ') + + (m.message?.className || '') + } + style={{ + transform: `translateY(-${i * 5}rem)`, + }} + > + <span className="flex flex-row content-center gap-3 text-white">{m.message.message}</span> + {/* {!message && (m.data?.status ? m.data.status : m.routingKey)} */} + </div> + ); + })} + </> + ); +}; diff --git a/libs/shared/lib/data-access/socket/broker/index.tsx b/libs/shared/lib/data-access/socket/broker/index.tsx index 098413fd9772a19905aee82bdbf9021aa57100e6..cd47d192142c79c217440a6f1771b7fcbd2b7c23 100644 --- a/libs/shared/lib/data-access/socket/broker/index.tsx +++ b/libs/shared/lib/data-access/socket/broker/index.tsx @@ -20,6 +20,7 @@ export default class Broker { private static singletonInstance: Broker; private listeners: Record<string, Record<string, Function>> = {}; + private catchAllListener: Function | undefined; /** mostRecentMessages is a dictionary with <routingkey, messageObject>. It stores the most recent message for that routingkey. */ private mostRecentMessages: Record<string, unknown> = {}; @@ -38,15 +39,34 @@ export default class Broker { public publish(jsonObject: unknown, routingKey: string): void { this.mostRecentMessages[routingKey] = jsonObject; - if (this.listeners[routingKey] && Object.keys(this.listeners[routingKey]).length != 0) + if (this.listeners[routingKey] && Object.keys(this.listeners[routingKey]).length != 0) { + if (this.catchAllListener) { + this.catchAllListener(jsonObject, routingKey); + } Object.values(this.listeners[routingKey]).forEach((listener) => listener(jsonObject, routingKey)); - // If there are no listeners, log the message - else console.debug( - `no listeners for message with routing key %c${routingKey}`, + `message processed with routing key %c${routingKey}`, 'font-weight:bold; color: blue; background-color: white;', jsonObject ); + } + // If there are no listeners, log the message + else { + if (this.catchAllListener) { + this.catchAllListener(jsonObject, routingKey); + console.debug( + `catch all used for message with routing key %c${routingKey}`, + 'font-weight:bold; color: blue; background-color: white;', + jsonObject + ); + } else { + console.debug( + `no listeners for message with routing key %c${routingKey}`, + 'font-weight:bold; color: blue; background-color: white;', + jsonObject + ); + } + } } /** @@ -74,6 +94,16 @@ export default class Broker { return key; } + /** + * Subscribe a listener to messages with the specified routingKey. + * @param {Function} newListener The listener to subscribe. + * @param {string} routingKey The routingkey to subscribe to. + * @param {boolean} consumeMostRecentMessage If true and there is a message for this routingkey available, notify the new listener. Default true. + */ + public subscribeDefault(newListener: Function): void { + this.catchAllListener = newListener; + } + /** * Unsubscribes a listener from messages with specified routingkey. * @param {string} routingKey The routing key to unsubscribe from @@ -85,6 +115,13 @@ export default class Broker { } } + /** + * Unsubscribes the catch all listener from messages + */ + public unSubscribeDefault(): void { + this.catchAllListener = undefined; + } + /** * Unsubscribes all listeners from messages with specified routingkey. * @param {string} routingKey The routing key to unsubscribe from