import { useEffect, useReducer, useState } from 'react'; import { Navbar, Form, Offcanvas, Container, Toast, ToastContainer } from 'react-bootstrap'; import { hostname, port, socket } from './socket'; import { DataServiceComponent, DataServiceJSON } from './components/DataServiceComponent'; import './App.css'; type ValueType = boolean | string | number | object; type State = DataServiceJSON | null; type Action = | { type: 'SET_DATA'; data: DataServiceJSON } | { type: 'UPDATE_ATTRIBUTE'; parent_path: string; name: string; value: ValueType }; type UpdateNotification = { data: { parent_path: string; name: string; value: object }; }; type ExceptionNotification = { data: { exception: string; type: string }; }; /** * A function to update a specific property in a deeply nested object. * The property to be updated is specified by a path array. * * Each path element can be a regular object key or an array index of the * form "attribute[index]", where "attribute" is the key of the array in * the object and "index" is the index of the element in the array. * * For array indices, the element at the specified index in the array is * updated. * * If the property to be updated is an object or an array, it is updated * recursively. * * @param {Array} path - An array where each element is a key in the object, * forming a path to the property to be updated. * @param {object} obj - The object to be updated. * @param {object} value - The new value for the property specified by the path. * @return {object} - A new object with the specified property updated. */ function updateNestedObject(path: Array, obj: object, value: ValueType) { // Base case: If the path is empty, return the new value. // This means we've reached the nested property to be updated. if (path.length === 0) { return value; } // Recursive case: If the path is not empty, split it into the first key and the rest // of the path. const [first, ...rest] = path; // Check if 'first' is an array index. const indexMatch = first.match(/^(\w+)\[(\d+)\]$/); // If 'first' is an array index of the form "attribute[index]", then update the // element at the specified index in the array. Otherwise, update the property // specified by 'first' in the object. if (indexMatch) { const attribute = indexMatch[1]; const index = parseInt(indexMatch[2]); if (Array.isArray(obj[attribute]?.value)) { return { ...obj, [attribute]: { ...obj[attribute], value: obj[attribute].value.map((item, i) => i === index ? { ...item, value: updateNestedObject(rest, item.value || {}, value) } : item ) } }; } else { throw new Error( `Expected ${attribute}.value to be an array, but received ${typeof obj[ attribute ]?.value}` ); } } else { return { ...obj, [first]: { ...obj[first], value: updateNestedObject(rest, obj[first]?.value || {}, value) } }; } } const reducer = (state: State, action: Action): State => { switch (action.type) { case 'SET_DATA': return action.data; case 'UPDATE_ATTRIBUTE': { const path = action.parent_path.split('.').slice(1).concat(action.name); return updateNestedObject(path, state, action.value); } default: throw new Error(); } }; const App = () => { const [state, dispatch] = useReducer(reducer, null); const [isInstantUpdate, setIsInstantUpdate] = useState(true); const [showSettings, setShowSettings] = useState(false); const [showNotification, setShowNotification] = useState(true); const [notifications, setNotifications] = useState([]); const removeNotificationById = (id: number) => { setNotifications((prevNotifications) => prevNotifications.filter((n) => n.id !== id) ); }; const handleCloseSettings = () => setShowSettings(false); const handleShowSettings = () => setShowSettings(true); function onNotify(value: UpdateNotification) { const currentTime = new Date(); const timeString = currentTime.toISOString().substr(11, 8); dispatch({ type: 'UPDATE_ATTRIBUTE', parent_path: value.data.parent_path, name: value.data.name, value: value.data.value }); const newNotification = { id: Math.random(), time: timeString, text: `Attribute ${value.data.parent_path}.${value.data.name} updated to ${value.data.value}.` }; setNotifications((prevNotifications) => [newNotification, ...prevNotifications]); } function onException(value: ExceptionNotification) { const currentTime = new Date(); const timeString = currentTime.toISOString().substr(11, 8); const newNotification = { type: 'exception', id: Math.random(), time: timeString, text: `${value.data.type}: ${value.data.exception}.` }; setNotifications((prevNotifications) => [newNotification, ...prevNotifications]); } useEffect(() => { // Fetch data from the API when the component mounts fetch(`http://${hostname}:${port}/service-properties`) .then((response) => response.json()) .then((data: DataServiceJSON) => dispatch({ type: 'SET_DATA', data })); socket.on('notify', onNotify); socket.on('exception', onException); return () => { socket.off('notify', onNotify); socket.off('exception', onException); }; }, []); // While the data is loading if (!state) { return

Loading...

; } return ( <> Data Service App {showNotification && ( {notifications.map((notification) => ( { removeNotificationById(notification.id); }} onClick={() => { removeNotificationById(notification.id); }} onMouseLeave={() => { // For exception type notifications, do not dismiss on mouse leave if (notification.type !== 'exception') { removeNotificationById(notification.id); } }} show={true} autohide={notification.type !== 'exception'} // Do not autohide for 'exception' type notifications delay={notification.type === 'exception' ? 0 : 2000} // No delay for 'exception' type notifications > {notification.type === 'exception' ? 'Exception' : 'Notification'} {notification.time} {notification.text} ))} )} Settings setIsInstantUpdate(e.target.checked)} type="switch" label="Enable Instant Update" /> setShowNotification(e.target.checked)} type="switch" label="Show Notifications" /> {/* Add any additional controls you want here */}
); }; export default App;