mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-04-20 08:20:02 +02:00
feat: components implement their notifications now
- removing nestedObjectUtils and useNotification hook - passing addNotification method to all components - components can use the addNotification method to create their notifications
This commit is contained in:
parent
8205e4d463
commit
f7579c3a89
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useReducer, useRef, useState } from 'react';
|
import { useCallback, useEffect, useReducer, useRef, useState } from 'react';
|
||||||
import { Navbar, Form, Offcanvas, Container } from 'react-bootstrap';
|
import { Navbar, Form, Offcanvas, Container } from 'react-bootstrap';
|
||||||
import { hostname, port, socket } from './socket';
|
import { hostname, port, socket } from './socket';
|
||||||
import {
|
import {
|
||||||
@ -6,9 +6,7 @@ import {
|
|||||||
DataServiceJSON
|
DataServiceJSON
|
||||||
} from './components/DataServiceComponent';
|
} from './components/DataServiceComponent';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import { getDataServiceJSONValueByPathAndKey } from './utils/nestedObjectUtils';
|
|
||||||
import { Notifications } from './components/NotificationsComponent';
|
import { Notifications } from './components/NotificationsComponent';
|
||||||
import { useNotification } from './hooks/useNotification';
|
|
||||||
|
|
||||||
type ValueType = boolean | string | number | object;
|
type ValueType = boolean | string | number | object;
|
||||||
|
|
||||||
@ -114,51 +112,11 @@ const reducer = (state: State, action: Action): State => {
|
|||||||
const App = () => {
|
const App = () => {
|
||||||
const [state, dispatch] = useReducer(reducer, null);
|
const [state, dispatch] = useReducer(reducer, null);
|
||||||
const stateRef = useRef(state); // Declare a reference to hold the current state
|
const stateRef = useRef(state); // Declare a reference to hold the current state
|
||||||
|
|
||||||
const [isInstantUpdate, setIsInstantUpdate] = useState(true);
|
const [isInstantUpdate, setIsInstantUpdate] = useState(true);
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
const [showNotification, setShowNotification] = useState(false);
|
const [showNotification, setShowNotification] = useState(true);
|
||||||
const { notifications, notify, removeNotificationById } = useNotification();
|
const [notifications, setNotifications] = useState([]);
|
||||||
const {
|
const [exceptions, setExceptions] = useState([]);
|
||||||
notifications: exceptions,
|
|
||||||
notify: notifyException,
|
|
||||||
removeNotificationById: removeExceptionById
|
|
||||||
} = useNotification();
|
|
||||||
|
|
||||||
const handleCloseSettings = () => setShowSettings(false);
|
|
||||||
const handleShowSettings = () => setShowSettings(true);
|
|
||||||
|
|
||||||
function onNotify(value: UpdateMessage) {
|
|
||||||
// Extracting data from the notification
|
|
||||||
const { parent_path: parentPath, name, value: newValue } = value.data;
|
|
||||||
|
|
||||||
// Dispatching the update to the reducer
|
|
||||||
dispatch({
|
|
||||||
type: 'UPDATE_ATTRIBUTE',
|
|
||||||
parentPath,
|
|
||||||
name,
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
|
|
||||||
// Formatting the value if it is of type 'Quantity'
|
|
||||||
let notificationMsg: object | string = newValue;
|
|
||||||
const path = parentPath.concat('.', name);
|
|
||||||
if (
|
|
||||||
getDataServiceJSONValueByPathAndKey(stateRef.current, path, 'type') === 'Quantity'
|
|
||||||
) {
|
|
||||||
notificationMsg = `${newValue['magnitude']} ${newValue['unit']}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creating a new notification
|
|
||||||
const newNotification = `${parentPath}.${name} changed to ${notificationMsg}.`;
|
|
||||||
// Adding the new notification to the list
|
|
||||||
notify(newNotification);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onException(value: ExceptionMessage) {
|
|
||||||
const newNotification = `${value.data.type}: ${value.data.exception}.`;
|
|
||||||
notifyException(newNotification);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep the state reference up to date
|
// Keep the state reference up to date
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -180,6 +138,64 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Adding useCallback to prevent notify to change causing a re-render of all
|
||||||
|
// components
|
||||||
|
const addNotification = useCallback((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
|
||||||
|
setNotifications((prevNotifications) => [
|
||||||
|
{ id, text, time: timeString },
|
||||||
|
...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) => {
|
||||||
|
setNotifications((prevNotifications) =>
|
||||||
|
prevNotifications.filter((n) => n.id !== id)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeExceptionById = (id: number) => {
|
||||||
|
setExceptions((prevNotifications) => prevNotifications.filter((n) => n.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseSettings = () => setShowSettings(false);
|
||||||
|
const handleShowSettings = () => setShowSettings(true);
|
||||||
|
|
||||||
|
function onNotify(value: UpdateMessage) {
|
||||||
|
// Extracting data from the notification
|
||||||
|
const { parent_path: parentPath, name, value: newValue } = value.data;
|
||||||
|
|
||||||
|
// Dispatching the update to the reducer
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_ATTRIBUTE',
|
||||||
|
parentPath,
|
||||||
|
name,
|
||||||
|
value: newValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onException(value: ExceptionMessage) {
|
||||||
|
const newException = `${value.data.type}: ${value.data.exception}.`;
|
||||||
|
notifyException(newException);
|
||||||
|
}
|
||||||
|
|
||||||
// While the data is loading
|
// While the data is loading
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return <p>Loading...</p>;
|
return <p>Loading...</p>;
|
||||||
@ -230,6 +246,7 @@ const App = () => {
|
|||||||
<DataServiceComponent
|
<DataServiceComponent
|
||||||
props={state as DataServiceJSON}
|
props={state as DataServiceJSON}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -10,29 +10,13 @@ interface AsyncMethodProps {
|
|||||||
value: Record<string, string>;
|
value: Record<string, string>;
|
||||||
docString?: string;
|
docString?: string;
|
||||||
hideOutput?: boolean;
|
hideOutput?: boolean;
|
||||||
|
addNotification: (string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
|
export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
|
||||||
|
const { name, parentPath, docString, value: runningTask, addNotification } = props;
|
||||||
const renderCount = useRef(0);
|
const renderCount = useRef(0);
|
||||||
const formRef = useRef(null);
|
const formRef = useRef(null);
|
||||||
const { name, parentPath, docString, value: runningTask } = props;
|
|
||||||
|
|
||||||
const execute = async (event: React.FormEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
let method_name: string;
|
|
||||||
const args = {};
|
|
||||||
|
|
||||||
if (runningTask !== undefined && runningTask !== null) {
|
|
||||||
method_name = `stop_${name}`;
|
|
||||||
} else {
|
|
||||||
Object.keys(props.parameters).forEach(
|
|
||||||
(name) => (args[name] = event.target[name].value)
|
|
||||||
);
|
|
||||||
method_name = `start_${name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit_update(method_name, parentPath, { args: args });
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
renderCount.current++;
|
renderCount.current++;
|
||||||
@ -52,6 +36,38 @@ export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
|
|||||||
}
|
}
|
||||||
}, [runningTask]);
|
}, [runningTask]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let message: string;
|
||||||
|
|
||||||
|
if (runningTask === null) {
|
||||||
|
message = `${parentPath}.${name} task was stopped.`;
|
||||||
|
} else {
|
||||||
|
const runningTaskEntries = Object.entries(runningTask)
|
||||||
|
.map(([key, value]) => `${key}: "${value}"`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
message = `${parentPath}.${name} was started with parameters { ${runningTaskEntries} }.`;
|
||||||
|
}
|
||||||
|
addNotification(message);
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
|
const execute = async (event: React.FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
let method_name: string;
|
||||||
|
const args = {};
|
||||||
|
|
||||||
|
if (runningTask !== undefined && runningTask !== null) {
|
||||||
|
method_name = `stop_${name}`;
|
||||||
|
} else {
|
||||||
|
Object.keys(props.parameters).forEach(
|
||||||
|
(name) => (args[name] = event.target[name].value)
|
||||||
|
);
|
||||||
|
method_name = `start_${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit_update(method_name, parentPath, { args: args });
|
||||||
|
};
|
||||||
|
|
||||||
const args = Object.entries(props.parameters).map(([name, type], index) => {
|
const args = Object.entries(props.parameters).map(([name, type], index) => {
|
||||||
const form_name = `${name} (${type})`;
|
const form_name = `${name} (${type})`;
|
||||||
const value = runningTask && runningTask[name];
|
const value = runningTask && runningTask[name];
|
||||||
|
@ -10,17 +10,23 @@ 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: (string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
|
export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
|
||||||
|
const { name, parentPath, value, readOnly, docString, mapping, addNotification } =
|
||||||
|
props;
|
||||||
|
const buttonName = mapping ? (value ? mapping[0] : mapping[1]) : name;
|
||||||
|
|
||||||
const renderCount = useRef(0);
|
const renderCount = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
renderCount.current++;
|
renderCount.current++;
|
||||||
});
|
});
|
||||||
const { name, parentPath, value, readOnly, docString, mapping } = props;
|
|
||||||
|
|
||||||
const buttonName = mapping ? (value ? mapping[0] : mapping[1]) : name;
|
useEffect(() => {
|
||||||
|
addNotification(`${parentPath}.${name} changed to ${value}.`);
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
const setChecked = (checked: boolean) => {
|
const setChecked = (checked: boolean) => {
|
||||||
emit_update(name, parentPath, checked);
|
emit_update(name, parentPath, checked);
|
||||||
|
@ -8,12 +8,18 @@ type DataServiceProps = {
|
|||||||
props: DataServiceJSON;
|
props: DataServiceJSON;
|
||||||
parentPath?: string;
|
parentPath?: string;
|
||||||
isInstantUpdate: boolean;
|
isInstantUpdate: boolean;
|
||||||
|
addNotification: (string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DataServiceJSON = Record<string, Attribute>;
|
export type DataServiceJSON = Record<string, Attribute>;
|
||||||
|
|
||||||
export const DataServiceComponent = React.memo(
|
export const DataServiceComponent = React.memo(
|
||||||
({ props, parentPath = 'DataService', isInstantUpdate }: DataServiceProps) => {
|
({
|
||||||
|
props,
|
||||||
|
parentPath = 'DataService',
|
||||||
|
isInstantUpdate,
|
||||||
|
addNotification
|
||||||
|
}: DataServiceProps) => {
|
||||||
const [open, setOpen] = useState(true);
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -35,6 +41,7 @@ export const DataServiceComponent = React.memo(
|
|||||||
name={key}
|
name={key}
|
||||||
parentPath={parentPath}
|
parentPath={parentPath}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -9,16 +9,28 @@ interface EnumComponentProps {
|
|||||||
value: string;
|
value: string;
|
||||||
docString?: string;
|
docString?: string;
|
||||||
enumDict: Record<string, string>;
|
enumDict: Record<string, string>;
|
||||||
|
addNotification: (string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EnumComponent = React.memo((props: EnumComponentProps) => {
|
export const EnumComponent = React.memo((props: EnumComponentProps) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
parentPath: parentPath,
|
||||||
|
value,
|
||||||
|
docString,
|
||||||
|
enumDict,
|
||||||
|
addNotification
|
||||||
|
} = props;
|
||||||
|
|
||||||
const renderCount = useRef(0);
|
const renderCount = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
renderCount.current++;
|
renderCount.current++;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { name, parentPath: parentPath, value, docString, enumDict } = props;
|
useEffect(() => {
|
||||||
|
addNotification(`${parentPath}.${name} changed to ${value}.`);
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
const handleValueChange = (newValue: string) => {
|
const handleValueChange = (newValue: string) => {
|
||||||
emit_update(name, parentPath, newValue);
|
emit_update(name, parentPath, newValue);
|
||||||
|
@ -38,10 +38,17 @@ type GenericComponentProps = {
|
|||||||
name: string;
|
name: string;
|
||||||
parentPath: string;
|
parentPath: string;
|
||||||
isInstantUpdate: boolean;
|
isInstantUpdate: boolean;
|
||||||
|
addNotification: (string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GenericComponent = React.memo(
|
export const GenericComponent = React.memo(
|
||||||
({ attribute, name, parentPath, isInstantUpdate }: GenericComponentProps) => {
|
({
|
||||||
|
attribute,
|
||||||
|
name,
|
||||||
|
parentPath,
|
||||||
|
isInstantUpdate,
|
||||||
|
addNotification
|
||||||
|
}: GenericComponentProps) => {
|
||||||
if (attribute.type === 'bool') {
|
if (attribute.type === 'bool') {
|
||||||
return (
|
return (
|
||||||
<ButtonComponent
|
<ButtonComponent
|
||||||
@ -50,6 +57,7 @@ export const GenericComponent = React.memo(
|
|||||||
docString={attribute.doc}
|
docString={attribute.doc}
|
||||||
readOnly={attribute.readonly}
|
readOnly={attribute.readonly}
|
||||||
value={Boolean(attribute.value)}
|
value={Boolean(attribute.value)}
|
||||||
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'float' || attribute.type === 'int') {
|
} else if (attribute.type === 'float' || attribute.type === 'int') {
|
||||||
@ -62,6 +70,7 @@ export const GenericComponent = React.memo(
|
|||||||
readOnly={attribute.readonly}
|
readOnly={attribute.readonly}
|
||||||
value={Number(attribute.value)}
|
value={Number(attribute.value)}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'Quantity') {
|
} else if (attribute.type === 'Quantity') {
|
||||||
@ -75,6 +84,7 @@ export const GenericComponent = React.memo(
|
|||||||
value={Number(attribute.value['magnitude'])}
|
value={Number(attribute.value['magnitude'])}
|
||||||
unit={attribute.value['unit']}
|
unit={attribute.value['unit']}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'NumberSlider') {
|
} else if (attribute.type === 'NumberSlider') {
|
||||||
@ -89,6 +99,7 @@ export const GenericComponent = React.memo(
|
|||||||
max={attribute.value['max']['value']}
|
max={attribute.value['max']['value']}
|
||||||
stepSize={attribute.value['step_size']['value']}
|
stepSize={attribute.value['step_size']['value']}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'Enum') {
|
} else if (attribute.type === 'Enum') {
|
||||||
@ -99,6 +110,7 @@ export const GenericComponent = React.memo(
|
|||||||
docString={attribute.doc}
|
docString={attribute.doc}
|
||||||
value={String(attribute.value)}
|
value={String(attribute.value)}
|
||||||
enumDict={attribute.enum}
|
enumDict={attribute.enum}
|
||||||
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'method') {
|
} else if (attribute.type === 'method') {
|
||||||
@ -109,6 +121,7 @@ export const GenericComponent = React.memo(
|
|||||||
parentPath={parentPath}
|
parentPath={parentPath}
|
||||||
docString={attribute.doc}
|
docString={attribute.doc}
|
||||||
parameters={attribute.parameters}
|
parameters={attribute.parameters}
|
||||||
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -119,6 +132,7 @@ export const GenericComponent = React.memo(
|
|||||||
docString={attribute.doc}
|
docString={attribute.doc}
|
||||||
parameters={attribute.parameters}
|
parameters={attribute.parameters}
|
||||||
value={attribute.value as Record<string, string>}
|
value={attribute.value as Record<string, string>}
|
||||||
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -131,6 +145,7 @@ export const GenericComponent = React.memo(
|
|||||||
docString={attribute.doc}
|
docString={attribute.doc}
|
||||||
parentPath={parentPath}
|
parentPath={parentPath}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'DataService') {
|
} else if (attribute.type === 'DataService') {
|
||||||
@ -139,6 +154,7 @@ export const GenericComponent = React.memo(
|
|||||||
props={attribute.value as DataServiceJSON}
|
props={attribute.value as DataServiceJSON}
|
||||||
parentPath={parentPath.concat('.', name)}
|
parentPath={parentPath.concat('.', name)}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'list') {
|
} else if (attribute.type === 'list') {
|
||||||
@ -149,6 +165,7 @@ export const GenericComponent = React.memo(
|
|||||||
docString={attribute.doc}
|
docString={attribute.doc}
|
||||||
parentPath={parentPath}
|
parentPath={parentPath}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (attribute.type === 'Image') {
|
} else if (attribute.type === 'Image') {
|
||||||
@ -161,6 +178,7 @@ export const GenericComponent = React.memo(
|
|||||||
docString={attribute.doc}
|
docString={attribute.doc}
|
||||||
// Add any other specific props for the ImageComponent here
|
// Add any other specific props for the ImageComponent here
|
||||||
format={attribute.value['format']['value'] as string}
|
format={attribute.value['format']['value'] as string}
|
||||||
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { emit_update } from '../socket';
|
|
||||||
import { Card, Image } from 'react-bootstrap';
|
import { Card, Image } from 'react-bootstrap';
|
||||||
import { DocStringComponent } from './DocStringComponent';
|
import { DocStringComponent } from './DocStringComponent';
|
||||||
|
|
||||||
@ -10,16 +9,22 @@ interface ImageComponentProps {
|
|||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
docString: string;
|
docString: string;
|
||||||
format: string;
|
format: string;
|
||||||
|
addNotification: (string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImageComponent = React.memo((props: ImageComponentProps) => {
|
export const ImageComponent = React.memo((props: ImageComponentProps) => {
|
||||||
|
const { name, parentPath, value, docString, format, addNotification } = props;
|
||||||
|
|
||||||
const renderCount = useRef(0);
|
const renderCount = useRef(0);
|
||||||
const { name, parentPath, value, docString, format } = props;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
renderCount.current++;
|
renderCount.current++;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
addNotification(`${parentPath}.${name} changed.`);
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'imageComponent'} id={parentPath.concat('.' + name)}>
|
<div className={'imageComponent'} id={parentPath.concat('.' + name)}>
|
||||||
<Card>
|
<Card>
|
||||||
|
@ -8,16 +8,18 @@ interface ListComponentProps {
|
|||||||
value: Attribute[];
|
value: Attribute[];
|
||||||
docString: string;
|
docString: string;
|
||||||
isInstantUpdate: boolean;
|
isInstantUpdate: boolean;
|
||||||
|
addNotification: (string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ListComponent = React.memo((props: ListComponentProps) => {
|
export const ListComponent = React.memo((props: ListComponentProps) => {
|
||||||
|
const { name, parentPath, value, docString, isInstantUpdate, addNotification } =
|
||||||
|
props;
|
||||||
|
|
||||||
const renderCount = useRef(0);
|
const renderCount = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
renderCount.current++;
|
renderCount.current++;
|
||||||
});
|
}, [props]);
|
||||||
|
|
||||||
const { name, parentPath, value, docString, isInstantUpdate } = props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'listComponent'} id={parentPath.concat(name)}>
|
<div className={'listComponent'} id={parentPath.concat(name)}>
|
||||||
@ -33,6 +35,7 @@ export const ListComponent = React.memo((props: ListComponentProps) => {
|
|||||||
name={`${name}[${index}]`}
|
name={`${name}[${index}]`}
|
||||||
parentPath={parentPath}
|
parentPath={parentPath}
|
||||||
isInstantUpdate={isInstantUpdate}
|
isInstantUpdate={isInstantUpdate}
|
||||||
|
addNotification={addNotification}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -9,16 +9,37 @@ interface MethodProps {
|
|||||||
parameters: Record<string, string>;
|
parameters: Record<string, string>;
|
||||||
docString?: string;
|
docString?: string;
|
||||||
hideOutput?: boolean;
|
hideOutput?: boolean;
|
||||||
|
addNotification: (string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MethodComponent = React.memo((props: MethodProps) => {
|
export const MethodComponent = React.memo((props: MethodProps) => {
|
||||||
|
const { name, parentPath, docString, addNotification } = props;
|
||||||
|
|
||||||
const renderCount = useRef(0);
|
const renderCount = useRef(0);
|
||||||
const [hideOutput, setHideOutput] = useState(false);
|
const [hideOutput, setHideOutput] = useState(false);
|
||||||
|
|
||||||
// Add a new state variable to hold the list of function calls
|
// Add a new state variable to hold the list of function calls
|
||||||
const [functionCalls, setFunctionCalls] = useState([]);
|
const [functionCalls, setFunctionCalls] = useState([]);
|
||||||
|
|
||||||
const { name, parentPath, docString } = props;
|
useEffect(() => {
|
||||||
|
renderCount.current++;
|
||||||
|
if (props.hideOutput !== undefined) {
|
||||||
|
setHideOutput(props.hideOutput);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const triggerNotification = (args: Record<string, string>) => {
|
||||||
|
const argsString = Object.entries(args)
|
||||||
|
.map(([key, value]) => `${key}: "${value}"`)
|
||||||
|
.join(', ');
|
||||||
|
let message = `Method ${parentPath}.${name} was triggered`;
|
||||||
|
|
||||||
|
if (argsString === '') {
|
||||||
|
message += '.';
|
||||||
|
} else {
|
||||||
|
message += ` with arguments {${argsString}}.`;
|
||||||
|
}
|
||||||
|
addNotification(message);
|
||||||
|
};
|
||||||
|
|
||||||
const execute = async (event: React.FormEvent) => {
|
const execute = async (event: React.FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -33,14 +54,9 @@ export const MethodComponent = React.memo((props: MethodProps) => {
|
|||||||
setFunctionCalls((prevCalls) => [...prevCalls, { name, args, result: ack }]);
|
setFunctionCalls((prevCalls) => [...prevCalls, { name, args, result: ack }]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
triggerNotification(args);
|
||||||
renderCount.current++;
|
};
|
||||||
if (props.hideOutput !== undefined) {
|
|
||||||
setHideOutput(props.hideOutput);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const args = Object.entries(props.parameters).map(([name, type], index) => {
|
const args = Object.entries(props.parameters).map(([name, type], index) => {
|
||||||
const form_name = `${name} (${type})`;
|
const form_name = `${name} (${type})`;
|
||||||
|
@ -22,6 +22,7 @@ interface NumberComponentProps {
|
|||||||
value: number,
|
value: number,
|
||||||
callback?: (ack: unknown) => void
|
callback?: (ack: unknown) => void
|
||||||
) => void;
|
) => void;
|
||||||
|
addNotification: (string) => 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
|
||||||
@ -108,7 +109,15 @@ const handleDeleteKey = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
||||||
const { name, parentPath, readOnly, docString, isInstantUpdate, unit } = props;
|
const {
|
||||||
|
name,
|
||||||
|
parentPath,
|
||||||
|
readOnly,
|
||||||
|
docString,
|
||||||
|
isInstantUpdate,
|
||||||
|
unit,
|
||||||
|
addNotification
|
||||||
|
} = props;
|
||||||
|
|
||||||
// Whether to show the name infront of the component (false if used with a slider)
|
// Whether to show the name infront of the component (false if used with a slider)
|
||||||
const showName = props.showName !== undefined ? props.showName : true;
|
const showName = props.showName !== undefined ? props.showName : true;
|
||||||
@ -143,6 +152,15 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
|||||||
if (props.value !== numericInputString) {
|
if (props.value !== numericInputString) {
|
||||||
setInputString(props.value.toString());
|
setInputString(props.value.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// emitting notification
|
||||||
|
let notificationMsg = `${parentPath}.${name} changed to ${props.value}`;
|
||||||
|
if (unit === undefined) {
|
||||||
|
notificationMsg += '.';
|
||||||
|
} else {
|
||||||
|
notificationMsg += ` ${unit}.`;
|
||||||
|
}
|
||||||
|
addNotification(notificationMsg);
|
||||||
}, [props.value]);
|
}, [props.value]);
|
||||||
|
|
||||||
const handleNumericKey = (key: string, value: string, selectionStart: number) => {
|
const handleNumericKey = (key: string, value: string, selectionStart: number) => {
|
||||||
|
@ -15,6 +15,7 @@ interface SliderComponentProps {
|
|||||||
docString: string;
|
docString: string;
|
||||||
stepSize: number;
|
stepSize: number;
|
||||||
isInstantUpdate: boolean;
|
isInstantUpdate: boolean;
|
||||||
|
addNotification: (string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
||||||
@ -34,9 +35,26 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
stepSize,
|
stepSize,
|
||||||
readOnly,
|
readOnly,
|
||||||
docString,
|
docString,
|
||||||
isInstantUpdate
|
isInstantUpdate,
|
||||||
|
addNotification
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
addNotification(`${parentPath}.${name} changed to ${value}.`);
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
addNotification(`${parentPath}.${name}.min changed to ${min}.`);
|
||||||
|
}, [props.min]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
addNotification(`${parentPath}.${name}.max changed to ${max}.`);
|
||||||
|
}, [props.max]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
addNotification(`${parentPath}.${name}.stepSize changed to ${stepSize}.`);
|
||||||
|
}, [props.stepSize]);
|
||||||
|
|
||||||
const emitSliderUpdate = (
|
const emitSliderUpdate = (
|
||||||
name: string,
|
name: string,
|
||||||
parentPath: string,
|
parentPath: string,
|
||||||
@ -122,6 +140,7 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
value={value}
|
value={value}
|
||||||
showName={false}
|
showName={false}
|
||||||
customEmitUpdate={emitSliderUpdate}
|
customEmitUpdate={emitSliderUpdate}
|
||||||
|
addNotification={() => null}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
|
@ -13,13 +13,16 @@ interface StringComponentProps {
|
|||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
docString: string;
|
docString: string;
|
||||||
isInstantUpdate: boolean;
|
isInstantUpdate: boolean;
|
||||||
|
addNotification: (string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StringComponent = React.memo((props: StringComponentProps) => {
|
export const StringComponent = React.memo((props: StringComponentProps) => {
|
||||||
|
const { name, parentPath, readOnly, docString, isInstantUpdate, addNotification } =
|
||||||
|
props;
|
||||||
|
|
||||||
const renderCount = useRef(0);
|
const renderCount = useRef(0);
|
||||||
const [inputString, setInputString] = useState(props.value);
|
const [inputString, setInputString] = useState(props.value);
|
||||||
|
|
||||||
const { name, parentPath, readOnly, docString, isInstantUpdate } = props;
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
renderCount.current++;
|
renderCount.current++;
|
||||||
}, [isInstantUpdate, inputString, renderCount]);
|
}, [isInstantUpdate, inputString, renderCount]);
|
||||||
@ -29,6 +32,7 @@ export const StringComponent = React.memo((props: StringComponentProps) => {
|
|||||||
if (props.value !== inputString) {
|
if (props.value !== inputString) {
|
||||||
setInputString(props.value);
|
setInputString(props.value);
|
||||||
}
|
}
|
||||||
|
addNotification(`${parentPath}.${name} changed to ${props.value}.`);
|
||||||
}, [props.value]);
|
}, [props.value]);
|
||||||
|
|
||||||
const handleChange = (event) => {
|
const handleChange = (event) => {
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Notification } from '../components/NotificationsComponent';
|
|
||||||
|
|
||||||
export const useNotification = () => {
|
|
||||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
||||||
|
|
||||||
const notify = (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
|
|
||||||
setNotifications((prevNotifications) => [
|
|
||||||
{ id, text, time: timeString },
|
|
||||||
...prevNotifications
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeNotificationById = (id: number) => {
|
|
||||||
setNotifications((prevNotifications) =>
|
|
||||||
prevNotifications.filter((n) => n.id !== id)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return { notifications, notify, removeNotificationById };
|
|
||||||
};
|
|
@ -1,46 +0,0 @@
|
|||||||
type Data = {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
const STANDARD_TYPES = [
|
|
||||||
'int',
|
|
||||||
'float',
|
|
||||||
'bool',
|
|
||||||
'str',
|
|
||||||
'Enum',
|
|
||||||
'method',
|
|
||||||
'NoneType',
|
|
||||||
'Quantity'
|
|
||||||
];
|
|
||||||
|
|
||||||
export function getDataServiceJSONValueByPathAndKey(
|
|
||||||
data: Data,
|
|
||||||
path: string,
|
|
||||||
key = 'value'
|
|
||||||
): string {
|
|
||||||
// Split the path into parts
|
|
||||||
const parts = path.split(/\.|(?=\[\d+\])/);
|
|
||||||
parts.shift(); // Remove the first element
|
|
||||||
|
|
||||||
// Traverse the dictionary according to the path parts
|
|
||||||
for (const part of parts) {
|
|
||||||
if (part.startsWith('[')) {
|
|
||||||
// List index
|
|
||||||
const idx = parseInt(part.substring(1, part.length - 1)); // Strip the brackets and convert to integer
|
|
||||||
data = data[idx];
|
|
||||||
} else {
|
|
||||||
// Dictionary key
|
|
||||||
data = data[part];
|
|
||||||
}
|
|
||||||
|
|
||||||
// When the attribute is a class instance, the attributes are nested in the
|
|
||||||
// "value" key
|
|
||||||
if (!STANDARD_TYPES.includes(data['type'])) {
|
|
||||||
data = data['value'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the value at the terminal point of the path
|
|
||||||
return data[key];
|
|
||||||
}
|
|
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"main.css": "/static/css/main.398bc7f8.css",
|
"main.css": "/static/css/main.398bc7f8.css",
|
||||||
"main.js": "/static/js/main.799e0879.js",
|
"main.js": "/static/js/main.39f6b50e.js",
|
||||||
"index.html": "/index.html",
|
"index.html": "/index.html",
|
||||||
"main.398bc7f8.css.map": "/static/css/main.398bc7f8.css.map",
|
"main.398bc7f8.css.map": "/static/css/main.398bc7f8.css.map",
|
||||||
"main.799e0879.js.map": "/static/js/main.799e0879.js.map"
|
"main.39f6b50e.js.map": "/static/js/main.39f6b50e.js.map"
|
||||||
},
|
},
|
||||||
"entrypoints": [
|
"entrypoints": [
|
||||||
"static/css/main.398bc7f8.css",
|
"static/css/main.398bc7f8.css",
|
||||||
"static/js/main.799e0879.js"
|
"static/js/main.39f6b50e.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.799e0879.js"></script><link href="/static/css/main.398bc7f8.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.39f6b50e.js"></script><link href="/static/css/main.398bc7f8.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
3
src/pydase/frontend/static/js/main.39f6b50e.js
Normal file
3
src/pydase/frontend/static/js/main.39f6b50e.js
Normal file
File diff suppressed because one or more lines are too long
1
src/pydase/frontend/static/js/main.39f6b50e.js.map
Normal file
1
src/pydase/frontend/static/js/main.39f6b50e.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
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user