mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-04-21 00:40:01 +02:00
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:
commit
d0869b707b
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@ -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": {
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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) => {
|
||||||
|
@ -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) => {
|
||||||
|
@ -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) => {
|
||||||
|
@ -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>;
|
||||||
|
@ -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) => {
|
||||||
|
@ -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(
|
||||||
|
@ -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) => {
|
||||||
|
@ -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) => {
|
||||||
|
@ -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) => {
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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
|
||||||
|
@ -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) => {
|
||||||
|
@ -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) => {
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -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
1
src/pydase/frontend/static/css/main.2d8458eb.css.map
Normal file
1
src/pydase/frontend/static/css/main.2d8458eb.css.map
Normal file
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
1
src/pydase/frontend/static/js/main.08fc7255.js.map
Normal file
1
src/pydase/frontend/static/js/main.08fc7255.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user