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:
Mose Müller 2023-08-10 15:07:09 +02:00
parent 8205e4d463
commit f7579c3a89
21 changed files with 235 additions and 167 deletions

View File

@ -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>
</> </>

View File

@ -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];

View File

@ -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);

View File

@ -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}
/> />
); );
})} })}

View File

@ -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);

View File

@ -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 {

View File

@ -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>

View File

@ -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}
/> />
); );
})} })}

View File

@ -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})`;

View File

@ -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) => {

View File

@ -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">

View File

@ -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) => {

View File

@ -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 };
};

View File

@ -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];
}

View File

@ -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"
] ]
} }

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.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>

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