diff --git a/frontend/src/App.css b/frontend/src/App.css index 61344d3..4d4d857 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -15,4 +15,7 @@ input.instantUpdate { } */ .notificationToast { background-color: rgba(114, 214, 253, 0.5) !important; +} +.exceptionToast { + background-color: rgba(216, 41, 18, 0.678) !important; } \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5df51ed..d6cf064 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -20,9 +20,12 @@ type State = DataServiceJSON | null; type Action = | { type: 'SET_DATA'; data: DataServiceJSON } | { type: 'UPDATE_ATTRIBUTE'; parent_path: string; name: string; value: ValueType }; -type NotificationElement = { +type UpdateNotification = { data: { parent_path: string; name: string; value: object }; }; +type ExceptionNotification = { + data: { exception: string; type: string }; +}; /** * A function to update a specific property in a deeply nested object. @@ -128,7 +131,7 @@ const App = () => { const handleCloseSettings = () => setShowSettings(false); const handleShowSettings = () => setShowSettings(true); - function onNotify(value: NotificationElement) { + function onNotify(value: UpdateNotification) { dispatch({ type: 'UPDATE_ATTRIBUTE', parent_path: value.data.parent_path, @@ -142,6 +145,15 @@ const App = () => { setNotifications((prevNotifications) => [newNotification, ...prevNotifications]); } + function onException(value: ExceptionNotification) { + const newNotification = { + type: 'exception', + id: Math.random(), + text: `${value.data.type}: ${value.data.exception}.` + }; + setNotifications((prevNotifications) => [newNotification, ...prevNotifications]); + } + useEffect(() => { // Fetch data from the API when the component mounts fetch(`http://${hostname}:${port}/service-properties`) @@ -149,9 +161,11 @@ const App = () => { .then((data: DataServiceJSON) => dispatch({ type: 'SET_DATA', data })); socket.on('notify', onNotify); + socket.on('exception', onException); return () => { socket.off('notify', onNotify); + socket.off('exception', onException); }; }, []); @@ -175,7 +189,11 @@ const App = () => { style={{ position: 'fixed' }}> {notifications.map((notification) => ( { removeNotificationById(notification.id); @@ -184,13 +202,25 @@ const App = () => { removeNotificationById(notification.id); }} onMouseLeave={() => { - removeNotificationById(notification.id); + // For exception type notifications, do not dismiss on mouse leave + if (notification.type !== 'exception') { + removeNotificationById(notification.id); + } }} show={true} - autohide - delay={2000}> - - Notification + autohide={notification.type !== 'exception'} // Do not autohide for 'exception' type notifications + delay={notification.type === 'exception' ? 0 : 2000} // No delay for 'exception' type notifications + > + + + {notification.type === 'exception' ? 'Exception' : 'Notification'} + {notification.text} diff --git a/src/pydase/server/server.py b/src/pydase/server/server.py index a3af4ef..0ce71a1 100644 --- a/src/pydase/server/server.py +++ b/src/pydase/server/server.py @@ -396,15 +396,18 @@ class Server: if self._enable_web: async def emit_exception() -> None: - await self._wapi.sio.emit( # type: ignore - "exception", - { - "data": { - "exception": str(exc), - "type": exc.__class__.__name__, - } - }, - ) + try: + await self._wapi.sio.emit( # type: ignore + "exception", + { + "data": { + "exception": str(exc), + "type": exc.__class__.__name__, + } + }, + ) + except Exception as e: + logger.warning(f"Failed to send notification: {e}") loop.create_task(emit_exception()) else: