Merge pull request #73 from tiqi-group/feat/notify_frontend_about_logged_errors

Adds capability of notifying frontend about logged errors
This commit is contained in:
Mose Müller 2023-11-27 16:17:33 +01:00 committed by GitHub
commit d0869b707b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 153 additions and 159 deletions

View File

@ -3,19 +3,12 @@
"autoDocstring.startOnNewLine": true, "autoDocstring.startOnNewLine": true,
"autoDocstring.generateDocstringOnEnter": true, "autoDocstring.generateDocstringOnEnter": true,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.rulers": [ "editor.rulers": [
88 88
], ],
"python.defaultInterpreterPath": ".venv/bin/python", "python.defaultInterpreterPath": ".venv/bin/python",
"python.formatting.provider": "black",
"python.linting.lintOnSave": true,
"python.linting.enabled": true,
"python.linting.flake8Enabled": true,
"python.linting.mypyEnabled": true,
"[python]": { "[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.tabSize": 4, "editor.tabSize": 4,
"editor.detectIndentation": false, "editor.detectIndentation": false,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {

View File

@ -1,6 +1,6 @@
body { body {
min-width: 576px; min-width: 576px;
max-width: 1200px; max-width: 2000px;
} }
input.instantUpdate { input.instantUpdate {
background-color: rgba(255, 0, 0, 0.1); background-color: rgba(255, 0, 0, 0.1);
@ -17,10 +17,13 @@ input.instantUpdate {
position: fixed !important; position: fixed !important;
padding: 5px; padding: 5px;
} }
.notificationToast { .debugToast, .infoToast {
background-color: rgba(114, 214, 253, 0.5) !important; background-color: rgba(114, 214, 253, 0.5) !important;
} }
.exceptionToast { .warningToast {
background-color: rgba(255, 181, 44, 0.603) !important;
}
.errorToast, .criticalToast {
background-color: rgba(216, 41, 18, 0.678) !important; background-color: rgba(216, 41, 18, 0.678) !important;
} }
.buttonComponent { .buttonComponent {

View File

@ -6,7 +6,11 @@ import {
DataServiceJSON DataServiceJSON
} from './components/DataServiceComponent'; } from './components/DataServiceComponent';
import './App.css'; import './App.css';
import { Notifications } from './components/NotificationsComponent'; import {
Notifications,
Notification,
LevelName
} from './components/NotificationsComponent';
import { ConnectionToast } from './components/ConnectionToast'; import { ConnectionToast } from './components/ConnectionToast';
import { SerializedValue, setNestedValueByPath, State } from './utils/stateUtils'; import { SerializedValue, setNestedValueByPath, State } from './utils/stateUtils';
@ -21,8 +25,9 @@ type Action =
type UpdateMessage = { type UpdateMessage = {
data: { parent_path: string; name: string; value: SerializedValue }; data: { parent_path: string; name: string; value: SerializedValue };
}; };
type ExceptionMessage = { type LogMessage = {
data: { exception: string; type: string }; levelname: LevelName;
message: string;
}; };
const reducer = (state: State, action: Action): State => { const reducer = (state: State, action: Action): State => {
@ -45,8 +50,7 @@ const App = () => {
const [isInstantUpdate, setIsInstantUpdate] = useState(false); const [isInstantUpdate, setIsInstantUpdate] = useState(false);
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
const [showNotification, setShowNotification] = useState(false); const [showNotification, setShowNotification] = useState(false);
const [notifications, setNotifications] = useState([]); const [notifications, setNotifications] = useState<Notification[]>([]);
const [exceptions, setExceptions] = useState([]);
const [connectionStatus, setConnectionStatus] = useState('connecting'); const [connectionStatus, setConnectionStatus] = useState('connecting');
// Keep the state reference up to date // Keep the state reference up to date
@ -88,51 +92,38 @@ const App = () => {
}); });
socket.on('notify', onNotify); socket.on('notify', onNotify);
socket.on('exception', onException); socket.on('log', onLogMessage);
return () => { return () => {
socket.off('notify', onNotify); socket.off('notify', onNotify);
socket.off('exception', onException); socket.off('log', onLogMessage);
}; };
}, []); }, []);
// Adding useCallback to prevent notify to change causing a re-render of all // Adding useCallback to prevent notify to change causing a re-render of all
// components // components
const addNotification = useCallback((text: string) => { const addNotification = useCallback(
// Getting the current time in the required format (message: string, levelname: LevelName = 'DEBUG') => {
const timeString = new Date().toISOString().substring(11, 19); // Getting the current time in the required format
// Adding an id to the notification to provide a way of removing it const timeStamp = new Date().toISOString().substring(11, 19);
const id = Math.random(); // Adding an id to the notification to provide a way of removing it
const id = Math.random();
// Custom logic for notifications // Custom logic for notifications
setNotifications((prevNotifications) => [ setNotifications((prevNotifications) => [
{ id, text, time: timeString }, { levelname, id, message, timeStamp },
...prevNotifications ...prevNotifications
]); ]);
}, []); },
[]
);
const notifyException = (text: string) => {
// Getting the current time in the required format
const timeString = new Date().toISOString().substring(11, 19);
// Adding an id to the notification to provide a way of removing it
const id = Math.random();
// Custom logic for notifications
setExceptions((prevNotifications) => [
{ id, text, time: timeString },
...prevNotifications
]);
};
const removeNotificationById = (id: number) => { const removeNotificationById = (id: number) => {
setNotifications((prevNotifications) => setNotifications((prevNotifications) =>
prevNotifications.filter((n) => n.id !== id) prevNotifications.filter((n) => n.id !== id)
); );
}; };
const removeExceptionById = (id: number) => {
setExceptions((prevNotifications) => prevNotifications.filter((n) => n.id !== id));
};
const handleCloseSettings = () => setShowSettings(false); const handleCloseSettings = () => setShowSettings(false);
const handleShowSettings = () => setShowSettings(true); const handleShowSettings = () => setShowSettings(true);
@ -149,9 +140,8 @@ const App = () => {
}); });
} }
function onException(value: ExceptionMessage) { function onLogMessage(value: LogMessage) {
const newException = `${value.data.type}: ${value.data.exception}.`; addNotification(value.message, value.levelname);
notifyException(newException);
} }
// While the data is loading // While the data is loading
@ -170,9 +160,7 @@ const App = () => {
<Notifications <Notifications
showNotification={showNotification} showNotification={showNotification}
notifications={notifications} notifications={notifications}
exceptions={exceptions}
removeNotificationById={removeNotificationById} removeNotificationById={removeNotificationById}
removeExceptionById={removeExceptionById}
/> />
<Offcanvas <Offcanvas

View File

@ -3,6 +3,7 @@ import { runMethod } from '../socket';
import { InputGroup, Form, Button } from 'react-bootstrap'; import { InputGroup, Form, Button } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent'; import { DocStringComponent } from './DocStringComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils'; import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface AsyncMethodProps { interface AsyncMethodProps {
name: string; name: string;
@ -11,7 +12,7 @@ interface AsyncMethodProps {
value: Record<string, string>; value: Record<string, string>;
docString?: string; docString?: string;
hideOutput?: boolean; hideOutput?: boolean;
addNotification: (message: string) => void; addNotification: (message: string, levelname?: LevelName) => void;
} }
export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => { export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {

View File

@ -3,6 +3,7 @@ import { ToggleButton } from 'react-bootstrap';
import { setAttribute } from '../socket'; import { setAttribute } from '../socket';
import { DocStringComponent } from './DocStringComponent'; import { DocStringComponent } from './DocStringComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils'; import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface ButtonComponentProps { interface ButtonComponentProps {
name: string; name: string;
@ -11,7 +12,7 @@ interface ButtonComponentProps {
readOnly: boolean; readOnly: boolean;
docString: string; docString: string;
mapping?: [string, string]; // Enforce a tuple of two strings mapping?: [string, string]; // Enforce a tuple of two strings
addNotification: (message: string) => void; addNotification: (message: string, levelname?: LevelName) => void;
} }
export const ButtonComponent = React.memo((props: ButtonComponentProps) => { export const ButtonComponent = React.memo((props: ButtonComponentProps) => {

View File

@ -3,6 +3,7 @@ import { InputGroup, Form, Row, Col } from 'react-bootstrap';
import { setAttribute } from '../socket'; import { setAttribute } from '../socket';
import { DocStringComponent } from './DocStringComponent'; import { DocStringComponent } from './DocStringComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils'; import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface ColouredEnumComponentProps { interface ColouredEnumComponentProps {
name: string; name: string;
@ -11,7 +12,7 @@ interface ColouredEnumComponentProps {
docString?: string; docString?: string;
readOnly: boolean; readOnly: boolean;
enumDict: Record<string, string>; enumDict: Record<string, string>;
addNotification: (message: string) => void; addNotification: (message: string, levelname?: LevelName) => void;
} }
export const ColouredEnumComponent = React.memo((props: ColouredEnumComponentProps) => { export const ColouredEnumComponent = React.memo((props: ColouredEnumComponentProps) => {

View File

@ -4,13 +4,14 @@ import { Card, Collapse } from 'react-bootstrap';
import { ChevronDown, ChevronRight } from 'react-bootstrap-icons'; import { ChevronDown, ChevronRight } from 'react-bootstrap-icons';
import { Attribute, GenericComponent } from './GenericComponent'; import { Attribute, GenericComponent } from './GenericComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils'; import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
type DataServiceProps = { type DataServiceProps = {
name: string; name: string;
props: DataServiceJSON; props: DataServiceJSON;
parentPath?: string; parentPath?: string;
isInstantUpdate: boolean; isInstantUpdate: boolean;
addNotification: (message: string) => void; addNotification: (message: string, levelname?: LevelName) => void;
}; };
export type DataServiceJSON = Record<string, Attribute>; export type DataServiceJSON = Record<string, Attribute>;

View File

@ -2,6 +2,7 @@ import React, { useEffect, useRef } from 'react';
import { InputGroup, Form, Row, Col } from 'react-bootstrap'; import { InputGroup, Form, Row, Col } from 'react-bootstrap';
import { setAttribute } from '../socket'; import { setAttribute } from '../socket';
import { DocStringComponent } from './DocStringComponent'; import { DocStringComponent } from './DocStringComponent';
import { LevelName } from './NotificationsComponent';
interface EnumComponentProps { interface EnumComponentProps {
name: string; name: string;
@ -9,7 +10,7 @@ interface EnumComponentProps {
value: string; value: string;
docString?: string; docString?: string;
enumDict: Record<string, string>; enumDict: Record<string, string>;
addNotification: (message: string) => void; addNotification: (message: string, levelname?: LevelName) => void;
} }
export const EnumComponent = React.memo((props: EnumComponentProps) => { export const EnumComponent = React.memo((props: EnumComponentProps) => {

View File

@ -10,6 +10,7 @@ import { ListComponent } from './ListComponent';
import { DataServiceComponent, DataServiceJSON } from './DataServiceComponent'; import { DataServiceComponent, DataServiceJSON } from './DataServiceComponent';
import { ImageComponent } from './ImageComponent'; import { ImageComponent } from './ImageComponent';
import { ColouredEnumComponent } from './ColouredEnumComponent'; import { ColouredEnumComponent } from './ColouredEnumComponent';
import { LevelName } from './NotificationsComponent';
type AttributeType = type AttributeType =
| 'str' | 'str'
@ -40,7 +41,7 @@ type GenericComponentProps = {
name: string; name: string;
parentPath: string; parentPath: string;
isInstantUpdate: boolean; isInstantUpdate: boolean;
addNotification: (message: string) => void; addNotification: (message: string, levelname?: LevelName) => void;
}; };
export const GenericComponent = React.memo( export const GenericComponent = React.memo(

View File

@ -3,6 +3,7 @@ import { Card, Collapse, Image } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent'; import { DocStringComponent } from './DocStringComponent';
import { ChevronDown, ChevronRight } from 'react-bootstrap-icons'; import { ChevronDown, ChevronRight } from 'react-bootstrap-icons';
import { getIdFromFullAccessPath } from '../utils/stringUtils'; import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface ImageComponentProps { interface ImageComponentProps {
name: string; name: string;
@ -11,7 +12,7 @@ interface ImageComponentProps {
readOnly: boolean; readOnly: boolean;
docString: string; docString: string;
format: string; format: string;
addNotification: (message: string) => void; addNotification: (message: string, levelname?: LevelName) => void;
} }
export const ImageComponent = React.memo((props: ImageComponentProps) => { export const ImageComponent = React.memo((props: ImageComponentProps) => {

View File

@ -2,6 +2,7 @@ import React, { useEffect, useRef } from 'react';
import { DocStringComponent } from './DocStringComponent'; import { DocStringComponent } from './DocStringComponent';
import { Attribute, GenericComponent } from './GenericComponent'; import { Attribute, GenericComponent } from './GenericComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils'; import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface ListComponentProps { interface ListComponentProps {
name: string; name: string;
@ -9,7 +10,7 @@ interface ListComponentProps {
value: Attribute[]; value: Attribute[];
docString: string; docString: string;
isInstantUpdate: boolean; isInstantUpdate: boolean;
addNotification: (message: string) => void; addNotification: (message: string, levelname?: LevelName) => void;
} }
export const ListComponent = React.memo((props: ListComponentProps) => { export const ListComponent = React.memo((props: ListComponentProps) => {

View File

@ -3,6 +3,7 @@ import { runMethod } from '../socket';
import { Button, InputGroup, Form, Collapse } from 'react-bootstrap'; import { Button, InputGroup, Form, Collapse } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent'; import { DocStringComponent } from './DocStringComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils'; import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface MethodProps { interface MethodProps {
name: string; name: string;
@ -10,7 +11,7 @@ interface MethodProps {
parameters: Record<string, string>; parameters: Record<string, string>;
docString?: string; docString?: string;
hideOutput?: boolean; hideOutput?: boolean;
addNotification: (message: string) => void; addNotification: (message: string, levelname?: LevelName) => void;
} }
export const MethodComponent = React.memo((props: MethodProps) => { export const MethodComponent = React.memo((props: MethodProps) => {

View File

@ -1,70 +1,71 @@
import React from 'react'; import React from 'react';
import { ToastContainer, Toast } from 'react-bootstrap'; import { ToastContainer, Toast } from 'react-bootstrap';
export type LevelName = 'CRITICAL' | 'ERROR' | 'WARNING' | 'INFO' | 'DEBUG';
export type Notification = { export type Notification = {
id: number; id: number;
time: string; timeStamp: string;
text: string; message: string;
levelname: LevelName;
}; };
type NotificationProps = { type NotificationProps = {
showNotification: boolean; showNotification: boolean;
notifications: Notification[]; notifications: Notification[];
exceptions: Notification[];
removeNotificationById: (id: number) => void; removeNotificationById: (id: number) => void;
removeExceptionById: (id: number) => void;
}; };
export const Notifications = React.memo((props: NotificationProps) => { export const Notifications = React.memo((props: NotificationProps) => {
const { const { showNotification, notifications, removeNotificationById } = props;
showNotification,
notifications,
exceptions,
removeExceptionById,
removeNotificationById
} = props;
return ( return (
<ToastContainer className="navbarOffset toastContainer" position="top-end"> <ToastContainer className="navbarOffset toastContainer" position="top-end">
{showNotification && {notifications.map((notification) => {
notifications.map((notification) => ( // Determine if the toast should be shown
const shouldShow =
notification.levelname === 'ERROR' ||
notification.levelname === 'CRITICAL' ||
(showNotification &&
['WARNING', 'INFO', 'DEBUG'].includes(notification.levelname));
if (!shouldShow) {
return null;
}
return (
<Toast <Toast
className="notificationToast" className={notification.levelname.toLowerCase() + 'Toast'}
key={notification.id} key={notification.id}
onClose={() => removeNotificationById(notification.id)} onClose={() => removeNotificationById(notification.id)}
onClick={() => { onClick={() => removeNotificationById(notification.id)}
removeNotificationById(notification.id);
}}
onMouseLeave={() => { onMouseLeave={() => {
removeNotificationById(notification.id); if (notification.levelname !== 'ERROR') {
removeNotificationById(notification.id);
}
}} }}
show={true} show={true}
autohide={true} autohide={
delay={2000}> notification.levelname === 'WARNING' ||
<Toast.Header closeButton={false} className="notificationToast text-right"> notification.levelname === 'INFO' ||
<strong className="me-auto">Notification</strong> notification.levelname === 'DEBUG'
<small>{notification.time}</small> }
delay={
notification.levelname === 'WARNING' ||
notification.levelname === 'INFO' ||
notification.levelname === 'DEBUG'
? 2000
: undefined
}>
<Toast.Header
closeButton={false}
className={notification.levelname.toLowerCase() + 'Toast text-right'}>
<strong className="me-auto">{notification.levelname}</strong>
<small>{notification.timeStamp}</small>
</Toast.Header> </Toast.Header>
<Toast.Body>{notification.text}</Toast.Body> <Toast.Body>{notification.message}</Toast.Body>
</Toast> </Toast>
))} );
{exceptions.map((exception) => ( })}
<Toast
className="exceptionToast"
key={exception.id}
onClose={() => removeExceptionById(exception.id)}
onClick={() => {
removeExceptionById(exception.id);
}}
show={true}
autohide={false}>
<Toast.Header closeButton className="exceptionToast text-right">
<strong className="me-auto">Exception</strong>
<small>{exception.time}</small>
</Toast.Header>
<Toast.Body>{exception.text}</Toast.Body>
</Toast>
))}
</ToastContainer> </ToastContainer>
); );
}); });

View File

@ -4,6 +4,7 @@ import { setAttribute } from '../socket';
import { DocStringComponent } from './DocStringComponent'; import { DocStringComponent } from './DocStringComponent';
import '../App.css'; import '../App.css';
import { getIdFromFullAccessPath } from '../utils/stringUtils'; import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
// TODO: add button functionality // TODO: add button functionality
@ -23,7 +24,7 @@ interface NumberComponentProps {
value: number, value: number,
callback?: (ack: unknown) => void callback?: (ack: unknown) => void
) => void; ) => void;
addNotification: (message: string) => void; addNotification: (message: string, levelname?: LevelName) => void;
} }
// TODO: highlight the digit that is being changed by setting both selectionStart and // TODO: highlight the digit that is being changed by setting both selectionStart and

View File

@ -5,6 +5,7 @@ import { DocStringComponent } from './DocStringComponent';
import { Slider } from '@mui/material'; import { Slider } from '@mui/material';
import { NumberComponent } from './NumberComponent'; import { NumberComponent } from './NumberComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils'; import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface SliderComponentProps { interface SliderComponentProps {
name: string; name: string;
@ -16,7 +17,7 @@ interface SliderComponentProps {
docString: string; docString: string;
stepSize: number; stepSize: number;
isInstantUpdate: boolean; isInstantUpdate: boolean;
addNotification: (message: string) => void; addNotification: (message: string, levelname?: LevelName) => void;
} }
export const SliderComponent = React.memo((props: SliderComponentProps) => { export const SliderComponent = React.memo((props: SliderComponentProps) => {

View File

@ -4,6 +4,7 @@ import { setAttribute } from '../socket';
import { DocStringComponent } from './DocStringComponent'; import { DocStringComponent } from './DocStringComponent';
import '../App.css'; import '../App.css';
import { getIdFromFullAccessPath } from '../utils/stringUtils'; import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
// TODO: add button functionality // TODO: add button functionality
@ -14,7 +15,7 @@ interface StringComponentProps {
readOnly: boolean; readOnly: boolean;
docString: string; docString: string;
isInstantUpdate: boolean; isInstantUpdate: boolean;
addNotification: (message: string) => void; addNotification: (message: string, levelname?: LevelName) => void;
} }
export const StringComponent = React.memo((props: StringComponentProps) => { export const StringComponent = React.memo((props: StringComponentProps) => {

View File

@ -106,43 +106,3 @@ function parseListAttrAndIndex(attrString: string): [string, number | null] {
return [attrName, index]; return [attrName, index];
} }
const serializationDict = {
attr_list: {
type: 'list',
value: [
{ type: 'int', value: 1, readonly: false, doc: null },
{ type: 'int', value: 2, readonly: false, doc: null },
{
type: 'Quantity',
value: { magnitude: 1.0, unit: 'ms' },
readonly: false,
doc: null
}
],
readonly: false,
doc: null
},
read_sensor_data: {
type: 'method',
value: null,
readonly: true,
doc: null,
async: true,
parameters: {}
},
readout_wait_time: {
type: 'Quantity',
value: { magnitude: 1.0, unit: 'ms' },
readonly: false,
doc: null
}
};
const attrName: string = 'attr_list[2]'; // example attribute name
try {
const result = getNextLevelDictByKey(serializationDict, attrName);
console.log(result);
} catch (error) {
console.error(error);
}

View File

@ -1,13 +1,13 @@
{ {
"files": { "files": {
"main.css": "/static/css/main.c0fa0427.css", "main.css": "/static/css/main.2d8458eb.css",
"main.js": "/static/js/main.e9762f7d.js", "main.js": "/static/js/main.08fc7255.js",
"index.html": "/index.html", "index.html": "/index.html",
"main.c0fa0427.css.map": "/static/css/main.c0fa0427.css.map", "main.2d8458eb.css.map": "/static/css/main.2d8458eb.css.map",
"main.e9762f7d.js.map": "/static/js/main.e9762f7d.js.map" "main.08fc7255.js.map": "/static/js/main.08fc7255.js.map"
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.c0fa0427.css", "static/css/main.2d8458eb.css",
"static/js/main.e9762f7d.js" "static/js/main.08fc7255.js"
] ]
} }

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site displaying a pydase UI."/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>pydase App</title><script defer="defer" src="/static/js/main.e9762f7d.js"></script><link href="/static/css/main.c0fa0427.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site displaying a pydase UI."/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>pydase App</title><script defer="defer" src="/static/js/main.08fc7255.js"></script><link href="/static/css/main.2d8458eb.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -12,6 +12,7 @@ from pydase import DataService
from pydase.data_service.data_service import process_callable_attribute from pydase.data_service.data_service import process_callable_attribute
from pydase.data_service.state_manager import StateManager from pydase.data_service.state_manager import StateManager
from pydase.utils.helpers import get_object_attr_from_path_list from pydase.utils.helpers import get_object_attr_from_path_list
from pydase.utils.logging import SocketIOHandler
from pydase.version import __version__ from pydase.version import __version__
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -91,6 +92,11 @@ class WebAPI:
self.setup_socketio() self.setup_socketio()
self.setup_fastapi_app() self.setup_fastapi_app()
self.setup_logging_handler()
def setup_logging_handler(self) -> None:
logger = logging.getLogger()
logger.addHandler(SocketIOHandler(self.__sio))
def setup_socketio(self) -> None: def setup_socketio(self) -> None:
# the socketio ASGI app, to notify clients when params update # the socketio ASGI app, to notify clients when params update

View File

@ -1,8 +1,10 @@
import asyncio
import logging import logging
import sys import sys
from copy import copy from copy import copy
from typing import Optional from typing import Optional
import socketio
import uvicorn.logging import uvicorn.logging
from uvicorn.config import LOGGING_CONFIG from uvicorn.config import LOGGING_CONFIG
@ -34,6 +36,35 @@ class DefaultFormatter(uvicorn.logging.ColourizedFormatter):
return sys.stderr.isatty() # pragma: no cover return sys.stderr.isatty() # pragma: no cover
class SocketIOHandler(logging.Handler):
"""
Custom logging handler that emits ERROR and CRITICAL log records to a Socket.IO
server, allowing for real-time logging in applications that use Socket.IO for
communication.
"""
def __init__(self, sio: socketio.AsyncServer) -> None:
super().__init__(logging.ERROR)
self._sio = sio
def format(self, record: logging.LogRecord) -> str:
return f"{record.name}:{record.funcName}:{record.lineno} - {record.message}"
def emit(self, record: logging.LogRecord) -> None:
log_entry = self.format(record)
loop = asyncio.get_event_loop()
loop.create_task(
self._sio.emit( # type: ignore[reportUnknownMemberType]
"log",
{
"levelname": record.levelname,
"message": log_entry,
},
)
)
def setup_logging(level: Optional[str | int] = None) -> None: def setup_logging(level: Optional[str | int] = None) -> None:
""" """
Configures the logging settings for the application. Configures the logging settings for the application.