From ebe5e0472f121525a315077e8b3924cbe75bd8ed Mon Sep 17 00:00:00 2001 From: Bradley Erickson Date: Fri, 27 Sep 2024 16:58:19 -0400 Subject: [PATCH 01/15] added dashboard to demo LOConnection and helper utilities --- learning_observer/learning_observer/routes.py | 9 ++- .../components/utilities/LOConnection.jsx | 53 ++++++++++++++ .../utilities/LOConnectionDataManager.jsx | 64 +++++++++++++++++ .../utilities/LOConnectionLastUpdated.jsx | 71 +++++++++++++++++++ modules/toy-assess/next.config.js | 22 +++--- modules/toy-assess/src/app/changer/page.js | 2 +- modules/toy-assess/src/app/websocket/page.js | 43 +++++++++++ 7 files changed, 254 insertions(+), 10 deletions(-) create mode 100644 modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx create mode 100644 modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx create mode 100644 modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx create mode 100644 modules/toy-assess/src/app/websocket/page.js diff --git a/learning_observer/learning_observer/routes.py b/learning_observer/learning_observer/routes.py index 2a49cd77..5034b81b 100644 --- a/learning_observer/learning_observer/routes.py +++ b/learning_observer/learning_observer/routes.py @@ -451,7 +451,11 @@ def register_extra_views(app): def create_nextjs_handler(path): async def _nextjs_handler(request): - return aiohttp.web.FileResponse(os.path.join(path, 'index.html')) + sub_url = request.match_info.get('tail') + if sub_url is None: + return aiohttp.web.FileResponse(os.path.join(path, 'index.html')) + # TODO will this handle multi-layered sub-urls? /foo/bar + return aiohttp.web.FileResponse(os.path.join(path, f'{sub_url}.html')) return _nextjs_handler @@ -467,6 +471,9 @@ def register_nextjs_routes(app): static_path = f'/_next{page_path}_next/static/' app.router.add_static(static_path, os.path.join(full_path, '_next', 'static')) app.router.add_get(page_path, create_nextjs_handler(full_path)) + # The tail path handles suburls + tail_path = page_path + '{tail:.*}' + app.router.add_get(tail_path, create_nextjs_handler(full_path)) def register_wsgi_routes(app): diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx new file mode 100644 index 00000000..d07785d2 --- /dev/null +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx @@ -0,0 +1,53 @@ +import React, { useEffect, useRef, useState } from 'react'; + +export const LOConnection = ({ + url, dataScope +}) => { + const [readyState, setReadyState] = useState(WebSocket.CLOSED); + const [message, setMessage] = useState(null); + const [error, setError] = useState(null); + const clientRef = useRef(null); + + useEffect(() => { + const protocol = { "http:": "ws:", "https:": "wss:" }[window.location.protocol]; + const newUrl = url ? url : `${protocol}//${window.location.hostname}:${window.location.port}/wsapi/communication_protocol`; + const client = new WebSocket(newUrl); + clientRef.current = client; + + client.onopen = () => { + setReadyState(WebSocket.OPEN); + if (typeof dataScope !== 'undefined') { + client.send(JSON.stringify(dataScope)); + } + }; + + client.onmessage = (event) => { + setMessage(event.data); + }; + + client.onerror = (event) => { + setError(event.message); + }; + + client.onclose = () => { + setReadyState(WebSocket.CLOSED); + }; + + return () => { + if (clientRef.current) { + clientRef.current.close(); + } + }; + }, [url]); + + // Function to send a message via WebSocket + const sendMessage = (message) => { + if (clientRef.current && readyState === WebSocket.OPEN) { + clientRef.current.send(message); + } else { + console.warn('WebSocket is not open. Ready state:', readyState); + } + }; + + return { sendMessage, message, error, readyState }; +}; diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx new file mode 100644 index 00000000..a611a273 --- /dev/null +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx @@ -0,0 +1,64 @@ +// TODO this file is untested, but goes along the with +// communication protocol changes introduced in PR #162. +// This won't do anything until that branch is merged in. +import React, { useEffect, useState } from 'react'; + +export const LOConnectionDataManager = ({ message, onDataUpdate }) => { + const [userData, setUserData] = useState({}); + const [errors, setErrors] = useState({}); + + useEffect(() => { + if (message) { + try { + const messages = JSON.parse(message.data); + setUserData((prevData) => { + const updatedData = { ...prevData }; + messages.forEach((msg) => { + const pathKeys = msg.path.split('.'); + let current = updatedData; + + // Traverse the path to get to the right location + for (let i = 0; i < pathKeys.length - 1; i++) { + const key = pathKeys[i]; + if (!(key in current)) { + current[key] = {}; // Create path if it doesn't exist + } + current = current[key]; + } + + const finalKey = pathKeys[pathKeys.length - 1]; + if ('error' in msg.value) { + setErrors((prevErrors) => ({ + ...prevErrors, + [msg.path]: msg.value, + })); + } else { + setErrors((prevErrors) => { + const newErrors = { ...prevErrors }; + delete newErrors[msg.path]; + return newErrors; + }); + if (msg.op === 'update') { + // Update the user data with new information + current[finalKey] = { + ...current[finalKey], // Existing data + ...msg.value, // New data (overwrites where necessary) + }; + } + } + }); + return updatedData; + }); + } catch (e) { + console.error('Failed to parse incoming message:', e); + } + } + }, [message]); + + useEffect(() => { + // Notify parent component of the updated data and errors + onDataUpdate({ userData, errors }); + }, [userData, errors, onDataUpdate]); + + return null; // No rendering needed, just a data handler. +}; diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx new file mode 100644 index 00000000..252d6791 --- /dev/null +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx @@ -0,0 +1,71 @@ +import React, { useState, useEffect } from 'react'; + +function renderTime(t) { + /* + Convert seconds to a time string. + + Compact representation. + 10 ==> 10s + 125 ==> 2m + 3600 ==> 1h + 7601 ==> 2h + 764450 ==> 8d + */ + var seconds = Math.floor(t) % 60; + var minutes = Math.floor(t / 60) % 60; + var hours = Math.floor(t / 3600) % 60; + var days = Math.floor(t / 3600 / 24); + + if (days > 0) { + return String(days) + 'd'; + } + if (hours > 0) { + return String(hours) + 'h'; + } + if (minutes > 0) { + return String(minutes) + 'm'; + } + if (seconds > 0) { + return String(seconds) + 's'; + } + return '-'; +} + +function renderReadableTimeSinceUpdate(timeDifference) { + if (timeDifference < 3) { + return 'Just now' + } + return `${renderTime(timeDifference)} ago` +} + +export const LOConnectionLastUpdated = ({ message, readyState }) => { + const [lastUpdated, setLastUpdated] = useState(null); + const [lastUpdatedMessage, setLastUpdatedMessage] = useState(''); + + const icons = ['fas fa-sync-alt', 'fas fa-check text-success', 'fas fa-sync-alt', 'fas fa-times text-danger']; + const titles = ['Connecting to server', 'Connected to server', 'Closing connection', 'Disconnected from server']; + + useEffect(() => { + if (message) { + setLastUpdated(new Date()); + } + }, [message]); + + useEffect(() => { + const interval = setInterval(() => { + if (lastUpdated) { + const now = new Date(); + const timeDifference = Math.floor((now - lastUpdated) / 1000); // Time difference in seconds + setLastUpdatedMessage(renderReadableTimeSinceUpdate(timeDifference)); + } else { setLastUpdatedMessage('Never') } + }, 1000); + return () => clearInterval(interval); // Cleanup interval on unmount + }, [lastUpdated]); + + return ( +
+ + {lastUpdatedMessage} +
+ ); +}; diff --git a/modules/toy-assess/next.config.js b/modules/toy-assess/next.config.js index 5d65a05a..8e8c8514 100644 --- a/modules/toy-assess/next.config.js +++ b/modules/toy-assess/next.config.js @@ -11,7 +11,7 @@ const nextConfig = { crypto: 'crypto', ws: 'ws', 'indexeddb-js': 'indexeddb-js' - }) + }); config.module.rules.push({ test: /\.jsx?$/, // This regex matches both .js and .jsx files include: [/node_modules\/lo_event/], // Include only the lo_event module @@ -19,12 +19,18 @@ const nextConfig = { loader: 'babel-loader', options: { // Add your babel presets here (e.g., @babel/preset-react) - presets: ['@babel/preset-react'], // Assuming you use React - }, - }, - }) - return config + presets: ['@babel/preset-react'] // Assuming you use React + } + } + }); + return config; }, -} + eslint: { + ignoreDuringBuilds: true + }, + output: 'export', + // TODO this is only needed when building for use within LO + // basePath: '/_next/learning_observer_template/toy_assess' +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/modules/toy-assess/src/app/changer/page.js b/modules/toy-assess/src/app/changer/page.js index dd529bf4..e5c6e7a0 100644 --- a/modules/toy-assess/src/app/changer/page.js +++ b/modules/toy-assess/src/app/changer/page.js @@ -1,4 +1,4 @@ -// This is a little demo which changes the style of text +// This is a little demo that retypes text as different characters. 'use client'; // @refresh reset diff --git a/modules/toy-assess/src/app/websocket/page.js b/modules/toy-assess/src/app/websocket/page.js new file mode 100644 index 00000000..999d7d47 --- /dev/null +++ b/modules/toy-assess/src/app/websocket/page.js @@ -0,0 +1,43 @@ +// Demo to show the Websocket + +'use client'; +// @refresh reset + +// We need to import this to call init() for now +import { } from '../components.js'; + +import React, { useState, useEffect } from 'react'; +import { LOConnection, LOConnectionLastUpdated, Button } from 'lo_event/lo_event/lo_assess/components/components.jsx'; + +export default function Home ({ children }) { + const decoded = {}; + decoded.course_id = '123456'; + const dataScope = { + wo: { + execution_dag: 'wo_bulk_essay_analysis', + target_exports: ['gpt_bulk'], + kwargs: decoded + } + }; + + const { readyState, message, error, sendMessage } = LOConnection({ url: 'ws://localhost:8888/wsapi/communication_protocol', dataScope }); + + return ( +
+

WebSocket Connection Page

+
+ +
+
+ +
+
+

Received Message

+ {message ?

{message}

:

No messages received yet.

} +
+ {error &&
Error: {error}
} +
+ ); +}; From dd5da473aa63a375b69e8d4135fc74dba9db7f4c Mon Sep 17 00:00:00 2001 From: Bradley Erickson Date: Mon, 30 Sep 2024 11:26:10 -0400 Subject: [PATCH 02/15] documented the components --- .../components/utilities/LOConnection.jsx | 16 +++ .../utilities/LOConnectionDataManager.jsx | 105 +++++++++++------- .../utilities/LOConnectionLastUpdated.jsx | 18 ++- 3 files changed, 97 insertions(+), 42 deletions(-) diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx index d07785d2..3a5c00a3 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx @@ -1,3 +1,19 @@ +/** + * LOConnection is a websocket component used for connecting to + * the communication protocl on Learning Observer. + * + * The server expects some data before it will start sending messages. + * When the connection opens, LOConnection will send the `dataScope`, + * if available, to initiate receiving messages from the server. + * Otherwise, users should use the `sendMessage` function to provide + * data to LO. + * + * LOConnection exposes the following items: + * - `sendMessage`: function to send messages to the server + * - `message`: the most recent message received + * - `error`: any errors that occured + * - `readyState`: the current status of the websocket connection + */ import React, { useEffect, useRef, useState } from 'react'; export const LOConnection = ({ diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx index a611a273..af80fb87 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx @@ -1,54 +1,77 @@ // TODO this file is untested, but goes along the with // communication protocol changes introduced in PR #162. // This won't do anything until that branch is merged in. +/** + * LOConnectionDataManager handles storing and processing incoming + * messages from the communication protocol websocket connection. + * The communication protocol sends batches of updates to apply + * to a clientside data object. + * + * When the internal data is updated, we call the `onDataUpdate` + * parameter so parents can update accordingly. + * + * Usage: + * ```js + * const { message, } = LOConnection({ url, dataScope }); // or some other websocket + * const handleDataUpdate = ({ dataObject, errors }) => { + * setUserData(dataObject); + * setErrors(errors); + * }; + * return ( ); + * ``` + */ import React, { useEffect, useState } from 'react'; export const LOConnectionDataManager = ({ message, onDataUpdate }) => { - const [userData, setUserData] = useState({}); + const [dataObject, setDataObject] = useState({}); const [errors, setErrors] = useState({}); + // TODO this function ought to be broken up into smaller functions. + // Revisit this during testing. + const processMessages = (messages, data) => { + const updatedData = { ...data }; + messages.forEach((msg) => { + const pathKeys = msg.path.split('.'); + let current = updatedData; + + // Traverse the path to get to the right location + for (let i = 0; i < pathKeys.length - 1; i++) { + const key = pathKeys[i]; + if (!(key in current)) { + current[key] = {}; // Create path if it doesn't exist + } + current = current[key]; + } + + const finalKey = pathKeys[pathKeys.length - 1]; + if ('error' in msg.value) { + setErrors((prevErrors) => ({ + ...prevErrors, + [msg.path]: msg.value, + })); + } else { + setErrors((prevErrors) => { + const newErrors = { ...prevErrors }; + delete newErrors[msg.path]; + return newErrors; + }); + if (msg.op === 'update') { + // Update the user data with new information + current[finalKey] = { + ...current[finalKey], // Existing data + ...msg.value, // New data (overwrites where necessary) + }; + } + } + }); + return updatedData; + }; + useEffect(() => { if (message) { try { const messages = JSON.parse(message.data); - setUserData((prevData) => { - const updatedData = { ...prevData }; - messages.forEach((msg) => { - const pathKeys = msg.path.split('.'); - let current = updatedData; - - // Traverse the path to get to the right location - for (let i = 0; i < pathKeys.length - 1; i++) { - const key = pathKeys[i]; - if (!(key in current)) { - current[key] = {}; // Create path if it doesn't exist - } - current = current[key]; - } - - const finalKey = pathKeys[pathKeys.length - 1]; - if ('error' in msg.value) { - setErrors((prevErrors) => ({ - ...prevErrors, - [msg.path]: msg.value, - })); - } else { - setErrors((prevErrors) => { - const newErrors = { ...prevErrors }; - delete newErrors[msg.path]; - return newErrors; - }); - if (msg.op === 'update') { - // Update the user data with new information - current[finalKey] = { - ...current[finalKey], // Existing data - ...msg.value, // New data (overwrites where necessary) - }; - } - } - }); - return updatedData; - }); + setDataObject((prevData) => processMessages(messages, prevData)); } catch (e) { console.error('Failed to parse incoming message:', e); } @@ -57,8 +80,8 @@ export const LOConnectionDataManager = ({ message, onDataUpdate }) => { useEffect(() => { // Notify parent component of the updated data and errors - onDataUpdate({ userData, errors }); - }, [userData, errors, onDataUpdate]); + onDataUpdate({ dataObject, errors }); + }, [dataObject, errors, onDataUpdate]); return null; // No rendering needed, just a data handler. }; diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx index 252d6791..2b0b3607 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx @@ -1,3 +1,14 @@ +/** + * LOConnectionLastUpdated is a helper function for displaying + * connection information and last received message information + * about a websocket. + * + * Usage: + * ```js + * const { readyState, message, } = LOConnection({ url, dataScope }); // or some other websocket + * return ( ); + * ``` + */ import React, { useState, useEffect } from 'react'; function renderTime(t) { @@ -10,6 +21,9 @@ function renderTime(t) { 3600 ==> 1h 7601 ==> 2h 764450 ==> 8d + + TODO this code exists in `liblo.js` include these functions + or migrate them to a utilities file in this LO Event. */ var seconds = Math.floor(t) % 60; var minutes = Math.floor(t / 60) % 60; @@ -32,7 +46,7 @@ function renderTime(t) { } function renderReadableTimeSinceUpdate(timeDifference) { - if (timeDifference < 3) { + if (timeDifference < 5) { return 'Just now' } return `${renderTime(timeDifference)} ago` @@ -45,12 +59,14 @@ export const LOConnectionLastUpdated = ({ message, readyState }) => { const icons = ['fas fa-sync-alt', 'fas fa-check text-success', 'fas fa-sync-alt', 'fas fa-times text-danger']; const titles = ['Connecting to server', 'Connected to server', 'Closing connection', 'Disconnected from server']; + // Set last updated time when new message arrives useEffect(() => { if (message) { setLastUpdated(new Date()); } }, [message]); + // Every second update last updated message useEffect(() => { const interval = setInterval(() => { if (lastUpdated) { From 6df138d50cd8f4f4443d4b4e846db8a665f9f220 Mon Sep 17 00:00:00 2001 From: Bradley Erickson Date: Mon, 30 Sep 2024 14:30:52 -0400 Subject: [PATCH 03/15] linted files --- .../components/utilities/LOConnection.jsx | 8 +++--- .../utilities/LOConnectionDataManager.jsx | 8 +++--- .../utilities/LOConnectionLastUpdated.jsx | 26 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx index 3a5c00a3..30fe44c1 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx @@ -14,7 +14,7 @@ * - `error`: any errors that occured * - `readyState`: the current status of the websocket connection */ -import React, { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; export const LOConnection = ({ url, dataScope @@ -25,11 +25,11 @@ export const LOConnection = ({ const clientRef = useRef(null); useEffect(() => { - const protocol = { "http:": "ws:", "https:": "wss:" }[window.location.protocol]; - const newUrl = url ? url : `${protocol}//${window.location.hostname}:${window.location.port}/wsapi/communication_protocol`; + const protocol = { 'http:': 'ws:', 'https:': 'wss:' }[window.location.protocol]; + const newUrl = url || `${protocol}//${window.location.hostname}:${window.location.port}/wsapi/communication_protocol`; const client = new WebSocket(newUrl); clientRef.current = client; - + client.onopen = () => { setReadyState(WebSocket.OPEN); if (typeof dataScope !== 'undefined') { diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx index af80fb87..fbf36b65 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx @@ -1,4 +1,4 @@ -// TODO this file is untested, but goes along the with +// TODO this file is untested, but goes along the with // communication protocol changes introduced in PR #162. // This won't do anything until that branch is merged in. /** @@ -20,7 +20,7 @@ * return ( ); * ``` */ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; export const LOConnectionDataManager = ({ message, onDataUpdate }) => { const [dataObject, setDataObject] = useState({}); @@ -47,7 +47,7 @@ export const LOConnectionDataManager = ({ message, onDataUpdate }) => { if ('error' in msg.value) { setErrors((prevErrors) => ({ ...prevErrors, - [msg.path]: msg.value, + [msg.path]: msg.value })); } else { setErrors((prevErrors) => { @@ -59,7 +59,7 @@ export const LOConnectionDataManager = ({ message, onDataUpdate }) => { // Update the user data with new information current[finalKey] = { ...current[finalKey], // Existing data - ...msg.value, // New data (overwrites where necessary) + ...msg.value // New data (overwrites where necessary) }; } } diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx index 2b0b3607..797d9b5a 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx @@ -2,16 +2,16 @@ * LOConnectionLastUpdated is a helper function for displaying * connection information and last received message information * about a websocket. - * + * * Usage: * ```js * const { readyState, message, } = LOConnection({ url, dataScope }); // or some other websocket * return ( ); * ``` */ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; -function renderTime(t) { +function renderTime (t) { /* Convert seconds to a time string. @@ -25,10 +25,10 @@ function renderTime(t) { TODO this code exists in `liblo.js` include these functions or migrate them to a utilities file in this LO Event. */ - var seconds = Math.floor(t) % 60; - var minutes = Math.floor(t / 60) % 60; - var hours = Math.floor(t / 3600) % 60; - var days = Math.floor(t / 3600 / 24); + const seconds = Math.floor(t) % 60; + const minutes = Math.floor(t / 60) % 60; + const hours = Math.floor(t / 3600) % 60; + const days = Math.floor(t / 3600 / 24); if (days > 0) { return String(days) + 'd'; @@ -45,11 +45,11 @@ function renderTime(t) { return '-'; } -function renderReadableTimeSinceUpdate(timeDifference) { - if (timeDifference < 5) { - return 'Just now' - } - return `${renderTime(timeDifference)} ago` +function renderReadableTimeSinceUpdate (timeDifference) { + if (timeDifference < 5) { + return 'Just now'; + } + return `${renderTime(timeDifference)} ago`; } export const LOConnectionLastUpdated = ({ message, readyState }) => { @@ -73,7 +73,7 @@ export const LOConnectionLastUpdated = ({ message, readyState }) => { const now = new Date(); const timeDifference = Math.floor((now - lastUpdated) / 1000); // Time difference in seconds setLastUpdatedMessage(renderReadableTimeSinceUpdate(timeDifference)); - } else { setLastUpdatedMessage('Never') } + } else { setLastUpdatedMessage('Never'); } }, 1000); return () => clearInterval(interval); // Cleanup interval on unmount }, [lastUpdated]); From 66d4828c0ed4ed1aa02c549fd19e8774bec7a175 Mon Sep 17 00:00:00 2001 From: Bradley Erickson Date: Fri, 8 Nov 2024 15:51:31 -0500 Subject: [PATCH 04/15] confirmed LOConnectionDataManager works with teh communication protocol and cleaned up some of the other code --- .../components/utilities/LOConnection.jsx | 33 +++++++++++++++-- .../utilities/LOConnectionDataManager.jsx | 27 ++++++++++++-- .../utilities/LOConnectionLastUpdated.jsx | 1 + modules/toy-assess/lo_event.sh | 2 +- modules/toy-assess/package.json | 2 +- modules/toy-assess/src/app/websocket/page.js | 35 +++++++++++++++---- 6 files changed, 85 insertions(+), 15 deletions(-) diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx index 30fe44c1..1434bdf8 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx @@ -13,7 +13,10 @@ * - `message`: the most recent message received * - `error`: any errors that occured * - `readyState`: the current status of the websocket connection + * - `openConnection`: function that opens the connection when called + * - `closeConnection`: function that closes the connection when called */ +import React from 'react'; import { useEffect, useRef, useState } from 'react'; export const LOConnection = ({ @@ -24,7 +27,14 @@ export const LOConnection = ({ const [error, setError] = useState(null); const clientRef = useRef(null); - useEffect(() => { + // Function to open the WebSocket connection + const openConnection = () => { + // Prevent opening a new connection if one is already open or connecting + if (clientRef.current && (readyState === WebSocket.OPEN || readyState === WebSocket.CONNECTING)) { + console.warn("WebSocket connection is already open or in progress."); + return; + } + const protocol = { 'http:': 'ws:', 'https:': 'wss:' }[window.location.protocol]; const newUrl = url || `${protocol}//${window.location.hostname}:${window.location.port}/wsapi/communication_protocol`; const client = new WebSocket(newUrl); @@ -32,6 +42,7 @@ export const LOConnection = ({ client.onopen = () => { setReadyState(WebSocket.OPEN); + setError(null); // Clear any previous errors upon a successful connection if (typeof dataScope !== 'undefined') { client.send(JSON.stringify(dataScope)); } @@ -48,13 +59,20 @@ export const LOConnection = ({ client.onclose = () => { setReadyState(WebSocket.CLOSED); }; + }; + + // Automatically attempt to open connection on mount + useEffect(() => { + openConnection(); + // Cleanup on unmount return () => { if (clientRef.current) { clientRef.current.close(); } }; - }, [url]); + }, [url]); // Include `url` as a dependency in case it changes and requires a reconnection + // Function to send a message via WebSocket const sendMessage = (message) => { @@ -65,5 +83,14 @@ export const LOConnection = ({ } }; - return { sendMessage, message, error, readyState }; + // Function to close the WebSocket connection manually + const closeConnection = () => { + if (clientRef.current && readyState === WebSocket.OPEN) { + clientRef.current.close(); + } else { + console.warn("WebSocket is not open; no connection to close."); + } + }; + + return { sendMessage, message, error, readyState, openConnection, closeConnection }; }; diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx index fbf36b65..7dff438e 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx @@ -12,14 +12,35 @@ * * Usage: * ```js - * const { message, } = LOConnection({ url, dataScope }); // or some other websocket + * const [userData, setUserData] = useState({}); + * const [errors, setErrors] = useState({}); + * const { message } = LOConnection({ url, dataScope }); // or some other websocket * const handleDataUpdate = ({ dataObject, errors }) => { * setUserData(dataObject); * setErrors(errors); * }; - * return ( ); + * + * return ( + *
+ * + *
+ *

User Data

+ * {Object.keys(userData).length > 0 + * ? (
{JSON.stringify(userData, null, 2)}
) + * : (

No user data available.

) + * } + *
+ * {Object.keys(errors).length > 0 && ( + *
+ *

Errors

+ *
{JSON.stringify(errors, null, 2)}
+ *
+ * )} + *
+ * ); * ``` */ +import React from 'react'; import { useEffect, useState } from 'react'; export const LOConnectionDataManager = ({ message, onDataUpdate }) => { @@ -70,7 +91,7 @@ export const LOConnectionDataManager = ({ message, onDataUpdate }) => { useEffect(() => { if (message) { try { - const messages = JSON.parse(message.data); + const messages = JSON.parse(message); setDataObject((prevData) => processMessages(messages, prevData)); } catch (e) { console.error('Failed to parse incoming message:', e); diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx index 797d9b5a..de79ac63 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx @@ -9,6 +9,7 @@ * return ( ); * ``` */ +import React from 'react'; import { useState, useEffect } from 'react'; function renderTime (t) { diff --git a/modules/toy-assess/lo_event.sh b/modules/toy-assess/lo_event.sh index 47a29fdf..4efe395b 100755 --- a/modules/toy-assess/lo_event.sh +++ b/modules/toy-assess/lo_event.sh @@ -11,4 +11,4 @@ pushd ../lo_event/ npm run prebuild npm pack popd -npm install ../lo_event/lo_event-0.0.2.tgz --no-save +npm install ../lo_event/lo_event-0.0.3.tgz --no-save diff --git a/modules/toy-assess/package.json b/modules/toy-assess/package.json index 8834e660..3d749bb6 100644 --- a/modules/toy-assess/package.json +++ b/modules/toy-assess/package.json @@ -16,7 +16,7 @@ "bufferutil": "^4.0.8", "indexeddb-js": "^0.0.14", "jasmine": "^5.1.0", - "lo_event": "file:../lo_event/lo_event-0.0.2.tgz", + "lo_event": "file:../lo_event/lo_event-0.0.3.tgz", "mapbox": "^1.0.0-beta10", "mock-aws-s3": "^4.0.2", "next": "13.5.5", diff --git a/modules/toy-assess/src/app/websocket/page.js b/modules/toy-assess/src/app/websocket/page.js index 999d7d47..2a310e18 100644 --- a/modules/toy-assess/src/app/websocket/page.js +++ b/modules/toy-assess/src/app/websocket/page.js @@ -7,37 +7,58 @@ import { } from '../components.js'; import React, { useState, useEffect } from 'react'; -import { LOConnection, LOConnectionLastUpdated, Button } from 'lo_event/lo_event/lo_assess/components/components.jsx'; +import { LOConnection, LOConnectionLastUpdated, LOConnectionDataManager, Button } from 'lo_event/lo_event/lo_assess/components/components.jsx'; export default function Home ({ children }) { const decoded = {}; decoded.course_id = '123456'; const dataScope = { wo: { - execution_dag: 'wo_bulk_essay_analysis', - target_exports: ['gpt_bulk'], + execution_dag: 'writing_observer', + target_exports: ['docs_with_nlp_annotations'], kwargs: decoded } }; - const { readyState, message, error, sendMessage } = LOConnection({ url: 'ws://localhost:8888/wsapi/communication_protocol', dataScope }); + const [userData, setUserData] = useState({}); + const [errors, setErrors] = useState({}); + const { readyState, message, error, sendMessage, openConnection, closeConnection } = LOConnection({ url: 'ws://localhost:8888/wsapi/communication_protocol', dataScope }); + const handleDataUpdate = ({ dataObject, errors }) => { + setUserData(dataObject); + setErrors(errors); + }; return (

WebSocket Connection Page

+
+ +
-

Received Message

- {message ?

{message}

:

No messages received yet.

} +

User Data

+ {Object.keys(userData).length > 0 + ? (
{JSON.stringify(userData, null, 2)}
) + : (

No user data available.

) + }
- {error &&
Error: {error}
} + {Object.keys(errors).length > 0 && ( +
+

Errors

+
{JSON.stringify(errors, null, 2)}
+
+ )}
); }; From b1c1b037beb377be6bbe857322f2691c759a1979 Mon Sep 17 00:00:00 2001 From: Bradley Erickson Date: Wed, 20 Nov 2024 15:40:23 -0500 Subject: [PATCH 05/15] removed completed todo --- .../lo_assess/components/utilities/LOConnectionDataManager.jsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx index 7dff438e..207517e8 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx @@ -1,6 +1,3 @@ -// TODO this file is untested, but goes along the with -// communication protocol changes introduced in PR #162. -// This won't do anything until that branch is merged in. /** * LOConnectionDataManager handles storing and processing incoming * messages from the communication protocol websocket connection. From 787fbd6ebdf5d6a0ebdc07f7997f284f8b54dd3b Mon Sep 17 00:00:00 2001 From: Bradley Erickson Date: Tue, 26 Nov 2024 15:21:04 -0500 Subject: [PATCH 06/15] renamed loconnection --- .../utilities/{LOConnection.jsx => useLOConnection.jsx} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename modules/lo_event/lo_event/lo_assess/components/utilities/{LOConnection.jsx => useLOConnection.jsx} (93%) diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnection.jsx similarity index 93% rename from modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx rename to modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnection.jsx index 1434bdf8..3bb739e1 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnection.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnection.jsx @@ -1,14 +1,14 @@ /** - * LOConnection is a websocket component used for connecting to + * useLOConnection is a websocket hook used for connecting to * the communication protocl on Learning Observer. * * The server expects some data before it will start sending messages. - * When the connection opens, LOConnection will send the `dataScope`, + * When the connection opens, useLOConnection will send the `dataScope`, * if available, to initiate receiving messages from the server. * Otherwise, users should use the `sendMessage` function to provide * data to LO. * - * LOConnection exposes the following items: + * useLOConnection exposes the following items: * - `sendMessage`: function to send messages to the server * - `message`: the most recent message received * - `error`: any errors that occured @@ -19,7 +19,7 @@ import React from 'react'; import { useEffect, useRef, useState } from 'react'; -export const LOConnection = ({ +export const useLOConnection = ({ url, dataScope }) => { const [readyState, setReadyState] = useState(WebSocket.CLOSED); From 0518ba8b0c7ef1f8a41a223e8e00db1758d37f48 Mon Sep 17 00:00:00 2001 From: Bradley Erickson Date: Tue, 26 Nov 2024 16:03:34 -0500 Subject: [PATCH 07/15] updated page with hooks --- .../utilities/LOConnectionDataManager.jsx | 105 -------------- .../utilities/useLOConnectionDataManager.jsx | 130 ++++++++++++++++++ modules/toy-assess/src/app/websocket/page.js | 18 +-- 3 files changed, 135 insertions(+), 118 deletions(-) delete mode 100644 modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx create mode 100644 modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnectionDataManager.jsx diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx deleted file mode 100644 index 207517e8..00000000 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionDataManager.jsx +++ /dev/null @@ -1,105 +0,0 @@ -/** - * LOConnectionDataManager handles storing and processing incoming - * messages from the communication protocol websocket connection. - * The communication protocol sends batches of updates to apply - * to a clientside data object. - * - * When the internal data is updated, we call the `onDataUpdate` - * parameter so parents can update accordingly. - * - * Usage: - * ```js - * const [userData, setUserData] = useState({}); - * const [errors, setErrors] = useState({}); - * const { message } = LOConnection({ url, dataScope }); // or some other websocket - * const handleDataUpdate = ({ dataObject, errors }) => { - * setUserData(dataObject); - * setErrors(errors); - * }; - * - * return ( - *
- * - *
- *

User Data

- * {Object.keys(userData).length > 0 - * ? (
{JSON.stringify(userData, null, 2)}
) - * : (

No user data available.

) - * } - *
- * {Object.keys(errors).length > 0 && ( - *
- *

Errors

- *
{JSON.stringify(errors, null, 2)}
- *
- * )} - *
- * ); - * ``` - */ -import React from 'react'; -import { useEffect, useState } from 'react'; - -export const LOConnectionDataManager = ({ message, onDataUpdate }) => { - const [dataObject, setDataObject] = useState({}); - const [errors, setErrors] = useState({}); - - // TODO this function ought to be broken up into smaller functions. - // Revisit this during testing. - const processMessages = (messages, data) => { - const updatedData = { ...data }; - messages.forEach((msg) => { - const pathKeys = msg.path.split('.'); - let current = updatedData; - - // Traverse the path to get to the right location - for (let i = 0; i < pathKeys.length - 1; i++) { - const key = pathKeys[i]; - if (!(key in current)) { - current[key] = {}; // Create path if it doesn't exist - } - current = current[key]; - } - - const finalKey = pathKeys[pathKeys.length - 1]; - if ('error' in msg.value) { - setErrors((prevErrors) => ({ - ...prevErrors, - [msg.path]: msg.value - })); - } else { - setErrors((prevErrors) => { - const newErrors = { ...prevErrors }; - delete newErrors[msg.path]; - return newErrors; - }); - if (msg.op === 'update') { - // Update the user data with new information - current[finalKey] = { - ...current[finalKey], // Existing data - ...msg.value // New data (overwrites where necessary) - }; - } - } - }); - return updatedData; - }; - - useEffect(() => { - if (message) { - try { - const messages = JSON.parse(message); - setDataObject((prevData) => processMessages(messages, prevData)); - } catch (e) { - console.error('Failed to parse incoming message:', e); - } - } - }, [message]); - - useEffect(() => { - // Notify parent component of the updated data and errors - onDataUpdate({ dataObject, errors }); - }, [dataObject, errors, onDataUpdate]); - - return null; // No rendering needed, just a data handler. -}; diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnectionDataManager.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnectionDataManager.jsx new file mode 100644 index 00000000..b5665634 --- /dev/null +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnectionDataManager.jsx @@ -0,0 +1,130 @@ +/** + * useLOConnectionDataManager handles storing and processing incoming + * messages from the communication protocol websocket connection. + * This hook wraps the useLOConnection hook to fetch updates. + * The communication protocol sends batches of updates to apply + * to a clientside data object. + * + * When the internal data is updated, we call the `onDataUpdate` + * parameter so parents can update accordingly. + * + * useLOConnectionDataManager exposes the following items: + * - `data`: current overall data received from websocket messages + * - `errors`: information about any errors received + * - all items from useLOConnection as well + * + * Usage: + * ```js + const { data, errors, sendMessage } = useLOConnectionDataManager({ url, dataScope }); + + return ( +
+
+

User Data

+ {Object.keys(data).length > 0 ? ( +
{JSON.stringify(data, null, 2)}
+ ) : ( +

No user data available.

+ )} +
+ {Object.keys(errors).length > 0 && ( +
+

Errors

+
{JSON.stringify(errors, null, 2)}
+
+ )} +
+ ); + * ``` + */ +import { useReducer, useEffect } from 'react'; +import { useLOConnection } from './useLOConnection'; // Assuming LOConnection is renamed to useLOConnection + +// Reducer function for managing state updates +const dataReducer = (state, action) => { + switch (action.type) { + case 'update': { + const { path, value } = action.payload; + const updatedState = { ...state }; + const pathKeys = path.split('.'); + let current = updatedState; + + // Traverse the path to get to the right location + for (let i = 0; i < pathKeys.length - 1; i++) { + const key = pathKeys[i]; + if (!(key in current)) { + current[key] = {}; // Create path if it doesn't exist + } + current = current[key]; + } + + const finalKey = pathKeys[pathKeys.length - 1]; + // TODO this doesn't handle a deep merge + current[finalKey] = { + ...current[finalKey], // Existing data + ...value, // New data (overwrites where necessary) + }; + + return updatedState; + } + case 'error': { + const { path, value } = action.payload; + return { + ...state, + errors: { + ...state.errors, + [path]: value, + }, + }; + } + case 'clearError': { + const { path } = action.payload; + const newErrors = { ...state.errors }; + delete newErrors[path]; + return { + ...state, + errors: newErrors, + }; + } + default: + console.warn(`Unhandled action type: ${action.type}`); + return state; + } +}; + +// Initial state for the reducer +const initialState = { + data: {}, + errors: {}, +}; + +// Custom hook +export const useLOConnectionDataManager = ({ url, dataScope }) => { + const { message, ...connection } = useLOConnection({ url, dataScope }); + const [state, dispatch] = useReducer(dataReducer, initialState); + + useEffect(() => { + if (message) { + try { + const messages = JSON.parse(message); + + messages.forEach((msg) => { + if ('error' in msg.value) { + dispatch({ type: 'error', payload: { path: msg.path, value: msg.value } }); + } else { + dispatch({ type: 'clearError', payload: { path: msg.path } }); + dispatch({ type: msg.op, payload: { path: msg.path, value: msg.value } }); + } + }); + } catch (e) { + console.error('Failed to parse incoming message:', e); + } + } + }, [message]); + + return { + ...connection, + data: state.data, + errors: state.errors, + }; +}; diff --git a/modules/toy-assess/src/app/websocket/page.js b/modules/toy-assess/src/app/websocket/page.js index 2a310e18..95d25e0b 100644 --- a/modules/toy-assess/src/app/websocket/page.js +++ b/modules/toy-assess/src/app/websocket/page.js @@ -7,7 +7,7 @@ import { } from '../components.js'; import React, { useState, useEffect } from 'react'; -import { LOConnection, LOConnectionLastUpdated, LOConnectionDataManager, Button } from 'lo_event/lo_event/lo_assess/components/components.jsx'; +import { LOConnectionLastUpdated, useLOConnectionDataManager, Button } from 'lo_event/lo_event/lo_assess/components/components.jsx'; export default function Home ({ children }) { const decoded = {}; @@ -20,20 +20,12 @@ export default function Home ({ children }) { } }; - const [userData, setUserData] = useState({}); - const [errors, setErrors] = useState({}); - - const { readyState, message, error, sendMessage, openConnection, closeConnection } = LOConnection({ url: 'ws://localhost:8888/wsapi/communication_protocol', dataScope }); - const handleDataUpdate = ({ dataObject, errors }) => { - setUserData(dataObject); - setErrors(errors); - }; + const { data, errors, readyState, sendMessage, openConnection, closeConnection } = useLOConnectionDataManager({ url: 'ws://localhost:8888/wsapi/communication_protocol', dataScope }); return (

WebSocket Connection Page

- - +

User Data

- {Object.keys(userData).length > 0 - ? (
{JSON.stringify(userData, null, 2)}
) + {Object.keys(data).length > 0 + ? (
{JSON.stringify(data, null, 2)}
) : (

No user data available.

) }
From f9db7639a49451cbc7e0b0399026a3a77b104804 Mon Sep 17 00:00:00 2001 From: Bradley Erickson Date: Tue, 26 Nov 2024 20:29:09 -0500 Subject: [PATCH 08/15] added lo connection constants --- .../constants/LO_CONNECTION_STATUS.jsx | 7 +++++ .../utilities/LOConnectionLastUpdated.jsx | 27 ++++++++++++++----- .../components/utilities/useLOConnection.jsx | 19 ++++++------- modules/toy-assess/src/app/websocket/page.js | 12 ++++----- 4 files changed, 43 insertions(+), 22 deletions(-) create mode 100644 modules/lo_event/lo_event/lo_assess/components/constants/LO_CONNECTION_STATUS.jsx diff --git a/modules/lo_event/lo_event/lo_assess/components/constants/LO_CONNECTION_STATUS.jsx b/modules/lo_event/lo_event/lo_assess/components/constants/LO_CONNECTION_STATUS.jsx new file mode 100644 index 00000000..92179bcd --- /dev/null +++ b/modules/lo_event/lo_event/lo_assess/components/constants/LO_CONNECTION_STATUS.jsx @@ -0,0 +1,7 @@ +export const LO_CONNECTION_STATUS = { + UNINSTANTIATED: -1, + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3 +} diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx index de79ac63..ff968c24 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx @@ -5,12 +5,13 @@ * * Usage: * ```js - * const { readyState, message, } = LOConnection({ url, dataScope }); // or some other websocket - * return ( ); + * const { connectionStatus, message, } = LOConnection({ url, dataScope }); // or some other websocket + * return ( ); * ``` */ import React from 'react'; import { useState, useEffect } from 'react'; +import { LO_CONNECTION_STATUS } from '../constants/LO_CONNECTION_STATUS'; function renderTime (t) { /* @@ -53,12 +54,24 @@ function renderReadableTimeSinceUpdate (timeDifference) { return `${renderTime(timeDifference)} ago`; } -export const LOConnectionLastUpdated = ({ message, readyState }) => { +export const LOConnectionLastUpdated = ({ message, connectionStatus }) => { const [lastUpdated, setLastUpdated] = useState(null); const [lastUpdatedMessage, setLastUpdatedMessage] = useState(''); - const icons = ['fas fa-sync-alt', 'fas fa-check text-success', 'fas fa-sync-alt', 'fas fa-times text-danger']; - const titles = ['Connecting to server', 'Connected to server', 'Closing connection', 'Disconnected from server']; + const icons = { + [LO_CONNECTION_STATUS.UNINSTANTIATED]: 'fas fa-circle', + [LO_CONNECTION_STATUS.CONNECTING]: 'fas fa-sync-alt', + [LO_CONNECTION_STATUS.OPEN]: 'fas fa-check text-success', + [LO_CONNECTION_STATUS.CLOSING]: 'fas fa-sync-alt', + [LO_CONNECTION_STATUS.CLOSED]: 'fas fa-times text-danger' + }; + const titles = { + [LO_CONNECTION_STATUS.UNINSTANTIATED]: 'Uninstantiated', + [LO_CONNECTION_STATUS.CONNECTING]: 'Connecting to server', + [LO_CONNECTION_STATUS.OPEN]: 'Connected to server', + [LO_CONNECTION_STATUS.CLOSING]: 'Closing connection', + [LO_CONNECTION_STATUS.CLOSED]: 'Disconnected from server' + }; // Set last updated time when new message arrives useEffect(() => { @@ -80,8 +93,8 @@ export const LOConnectionLastUpdated = ({ message, readyState }) => { }, [lastUpdated]); return ( -
- +
+ {lastUpdatedMessage}
); diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnection.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnection.jsx index 3bb739e1..b3afcb8e 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnection.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnection.jsx @@ -12,17 +12,18 @@ * - `sendMessage`: function to send messages to the server * - `message`: the most recent message received * - `error`: any errors that occured - * - `readyState`: the current status of the websocket connection + * - `connectionStatus`: the current status of the websocket connection * - `openConnection`: function that opens the connection when called * - `closeConnection`: function that closes the connection when called */ import React from 'react'; import { useEffect, useRef, useState } from 'react'; +import { LO_CONNECTION_STATUS } from '../constants/LO_CONNECTION_STATUS'; export const useLOConnection = ({ url, dataScope }) => { - const [readyState, setReadyState] = useState(WebSocket.CLOSED); + const [connectionStatus, setConnectionStatus] = useState(LO_CONNECTION_STATUS.CLOSED); const [message, setMessage] = useState(null); const [error, setError] = useState(null); const clientRef = useRef(null); @@ -30,7 +31,7 @@ export const useLOConnection = ({ // Function to open the WebSocket connection const openConnection = () => { // Prevent opening a new connection if one is already open or connecting - if (clientRef.current && (readyState === WebSocket.OPEN || readyState === WebSocket.CONNECTING)) { + if (clientRef.current && (connectionStatus === LO_CONNECTION_STATUS.OPEN || connectionStatus === LO_CONNECTION_STATUS.CONNECTING)) { console.warn("WebSocket connection is already open or in progress."); return; } @@ -41,7 +42,7 @@ export const useLOConnection = ({ clientRef.current = client; client.onopen = () => { - setReadyState(WebSocket.OPEN); + setConnectionStatus(LO_CONNECTION_STATUS.OPEN); setError(null); // Clear any previous errors upon a successful connection if (typeof dataScope !== 'undefined') { client.send(JSON.stringify(dataScope)); @@ -57,7 +58,7 @@ export const useLOConnection = ({ }; client.onclose = () => { - setReadyState(WebSocket.CLOSED); + setConnectionStatus(LO_CONNECTION_STATUS.CLOSED); }; }; @@ -76,21 +77,21 @@ export const useLOConnection = ({ // Function to send a message via WebSocket const sendMessage = (message) => { - if (clientRef.current && readyState === WebSocket.OPEN) { + if (clientRef.current && connectionStatus === LO_CONNECTION_STATUS.OPEN) { clientRef.current.send(message); } else { - console.warn('WebSocket is not open. Ready state:', readyState); + console.warn('WebSocket is not open. Ready state:', connectionStatus); } }; // Function to close the WebSocket connection manually const closeConnection = () => { - if (clientRef.current && readyState === WebSocket.OPEN) { + if (clientRef.current && connectionStatus === LO_CONNECTION_STATUS.OPEN) { clientRef.current.close(); } else { console.warn("WebSocket is not open; no connection to close."); } }; - return { sendMessage, message, error, readyState, openConnection, closeConnection }; + return { sendMessage, message, error, connectionStatus, openConnection, closeConnection }; }; diff --git a/modules/toy-assess/src/app/websocket/page.js b/modules/toy-assess/src/app/websocket/page.js index 95d25e0b..301de484 100644 --- a/modules/toy-assess/src/app/websocket/page.js +++ b/modules/toy-assess/src/app/websocket/page.js @@ -7,7 +7,7 @@ import { } from '../components.js'; import React, { useState, useEffect } from 'react'; -import { LOConnectionLastUpdated, useLOConnectionDataManager, Button } from 'lo_event/lo_event/lo_assess/components/components.jsx'; +import { LOConnectionLastUpdated, useLOConnectionDataManager, LO_CONNECTION_STATUS, Button } from 'lo_event/lo_event/lo_assess/components/components.jsx'; export default function Home ({ children }) { const decoded = {}; @@ -20,21 +20,21 @@ export default function Home ({ children }) { } }; - const { data, errors, readyState, sendMessage, openConnection, closeConnection } = useLOConnectionDataManager({ url: 'ws://localhost:8888/wsapi/communication_protocol', dataScope }); + const { data, errors, connectionStatus, sendMessage, openConnection, closeConnection } = useLOConnectionDataManager({ url: 'ws://localhost:8888/wsapi/communication_protocol', dataScope }); return (

WebSocket Connection Page

- +
- - -
From f7bd298f7a8b8ec975c159a623c4b76f49df61a8 Mon Sep 17 00:00:00 2001 From: Bradley Erickson Date: Wed, 27 Nov 2024 08:59:11 -0500 Subject: [PATCH 09/15] updated buildconfig and buildlib to work with hooks --- modules/lo_event/lo_event/lo_assess/components/buildConfig.js | 2 +- modules/lo_event/lo_event/lo_assess/components/buildlib.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/lo_event/lo_event/lo_assess/components/buildConfig.js b/modules/lo_event/lo_event/lo_assess/components/buildConfig.js index 2cd7678e..7b56dc57 100644 --- a/modules/lo_event/lo_event/lo_assess/components/buildConfig.js +++ b/modules/lo_event/lo_event/lo_assess/components/buildConfig.js @@ -9,7 +9,7 @@ export function getTemplatePath(templateFileName) { const _baseConfig = { loa: { - jsPattern: './lo_event/lo_assess/components/**/[A-Z]*.jsx', + jsPattern: './lo_event/lo_assess/components/**/{[A-Z]*,use[A-Z]*}.jsx', xmlPattern: './lo_event/lo_assess/components/**/[A-Z]*.xml', componentsFile: "./lo_event/lo_assess/components/components.jsx", }, diff --git a/modules/lo_event/lo_event/lo_assess/components/buildlib.js b/modules/lo_event/lo_event/lo_assess/components/buildlib.js index 8a84c092..29985771 100644 --- a/modules/lo_event/lo_event/lo_assess/components/buildlib.js +++ b/modules/lo_event/lo_event/lo_assess/components/buildlib.js @@ -28,7 +28,7 @@ function parsePath(path) { const lastSlashIndex = path.lastIndexOf('/'); const fileName = path.substring(lastSlashIndex + 1).split('.')[0]; - const componentName = fileName.charAt(0).toUpperCase() + fileName.slice(1); + const componentName = fileName; const fullPathWithoutExtension = path.split('.').slice(0, -1).join('.'); const jsxFullPath = fullPathWithoutExtension + ".jsx"; const xmlFullPath = fullPathWithoutExtension + ".xml"; From 8683487103963ece653e0350d0de256f0edba305 Mon Sep 17 00:00:00 2001 From: Bradley Erickson Date: Wed, 27 Nov 2024 09:05:09 -0500 Subject: [PATCH 10/15] moved rendertime to util --- .../utilities/LOConnectionLastUpdated.jsx | 35 +------------------ modules/lo_event/lo_event/util.js | 31 ++++++++++++++++ 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx index ff968c24..4e6924ab 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx @@ -12,40 +12,7 @@ import React from 'react'; import { useState, useEffect } from 'react'; import { LO_CONNECTION_STATUS } from '../constants/LO_CONNECTION_STATUS'; - -function renderTime (t) { - /* - Convert seconds to a time string. - - Compact representation. - 10 ==> 10s - 125 ==> 2m - 3600 ==> 1h - 7601 ==> 2h - 764450 ==> 8d - - TODO this code exists in `liblo.js` include these functions - or migrate them to a utilities file in this LO Event. - */ - const seconds = Math.floor(t) % 60; - const minutes = Math.floor(t / 60) % 60; - const hours = Math.floor(t / 3600) % 60; - const days = Math.floor(t / 3600 / 24); - - if (days > 0) { - return String(days) + 'd'; - } - if (hours > 0) { - return String(hours) + 'h'; - } - if (minutes > 0) { - return String(minutes) + 'm'; - } - if (seconds > 0) { - return String(seconds) + 's'; - } - return '-'; -} +import { renderTime } from '../../../util'; function renderReadableTimeSinceUpdate (timeDifference) { if (timeDifference < 5) { diff --git a/modules/lo_event/lo_event/util.js b/modules/lo_event/lo_event/util.js index ee53636f..b74e7b86 100644 --- a/modules/lo_event/lo_event/util.js +++ b/modules/lo_event/lo_event/util.js @@ -426,3 +426,34 @@ export function dispatchCustomEvent(eventName, detail) { console.warn("Event dispatching is not supported in this environment."); } } + +/** + * Convert seconds to a time string. + * + * Compact representation. + * 10 ==> 10s + * 125 ==> 2m + * 3600 ==> 1h + * 7601 ==> 2h + * 764450 ==> 8d + */ +export function renderTime (t) { + const seconds = Math.floor(t) % 60; + const minutes = Math.floor(t / 60) % 60; + const hours = Math.floor(t / 3600) % 60; + const days = Math.floor(t / 3600 / 24); + + if (days > 0) { + return String(days) + 'd'; + } + if (hours > 0) { + return String(hours) + 'h'; + } + if (minutes > 0) { + return String(minutes) + 'm'; + } + if (seconds > 0) { + return String(seconds) + 's'; + } + return '-'; +} From b514af883bbb815f8068faa95710a16195135b14 Mon Sep 17 00:00:00 2001 From: Bradley Erickson Date: Wed, 27 Nov 2024 17:51:25 -0500 Subject: [PATCH 11/15] fixed bug with immutable data --- .../utilities/useLOConnectionDataManager.jsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnectionDataManager.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnectionDataManager.jsx index b5665634..bea55311 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnectionDataManager.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnectionDataManager.jsx @@ -45,15 +45,17 @@ const dataReducer = (state, action) => { switch (action.type) { case 'update': { const { path, value } = action.payload; - const updatedState = { ...state }; const pathKeys = path.split('.'); - let current = updatedState; - // Traverse the path to get to the right location + // Create a new `data` object with the updated value at the correct path + const newData = { ...state.data }; + let current = newData; // Start at the top-level copy for (let i = 0; i < pathKeys.length - 1; i++) { const key = pathKeys[i]; if (!(key in current)) { current[key] = {}; // Create path if it doesn't exist + } else { + current[key] = { ...current[key] }; // Copy the existing nested object } current = current[key]; } @@ -65,7 +67,10 @@ const dataReducer = (state, action) => { ...value, // New data (overwrites where necessary) }; - return updatedState; + return { + ...state, + data: newData, + }; } case 'error': { const { path, value } = action.payload; From 79d42e12f89c0d456f5206dd641588a7766d5022 Mon Sep 17 00:00:00 2001 From: Bradley Erickson Date: Wed, 27 Nov 2024 17:58:49 -0500 Subject: [PATCH 12/15] resolved namespace confusion --- .../utilities/useLOConnectionDataManager.jsx | 5 ++--- modules/toy-assess/src/app/websocket/page.js | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnectionDataManager.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnectionDataManager.jsx index bea55311..1bcb1a2b 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnectionDataManager.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnectionDataManager.jsx @@ -11,7 +11,7 @@ * useLOConnectionDataManager exposes the following items: * - `data`: current overall data received from websocket messages * - `errors`: information about any errors received - * - all items from useLOConnection as well + * - `connection`: all returned items from useLOConnection * * Usage: * ```js @@ -103,7 +103,6 @@ const initialState = { errors: {}, }; -// Custom hook export const useLOConnectionDataManager = ({ url, dataScope }) => { const { message, ...connection } = useLOConnection({ url, dataScope }); const [state, dispatch] = useReducer(dataReducer, initialState); @@ -128,7 +127,7 @@ export const useLOConnectionDataManager = ({ url, dataScope }) => { }, [message]); return { - ...connection, + connection, data: state.data, errors: state.errors, }; diff --git a/modules/toy-assess/src/app/websocket/page.js b/modules/toy-assess/src/app/websocket/page.js index 301de484..1717b46d 100644 --- a/modules/toy-assess/src/app/websocket/page.js +++ b/modules/toy-assess/src/app/websocket/page.js @@ -20,21 +20,21 @@ export default function Home ({ children }) { } }; - const { data, errors, connectionStatus, sendMessage, openConnection, closeConnection } = useLOConnectionDataManager({ url: 'ws://localhost:8888/wsapi/communication_protocol', dataScope }); + const { data, errors, connection } = useLOConnectionDataManager({ url: 'ws://localhost:8888/wsapi/communication_protocol', dataScope }); return (

WebSocket Connection Page

- +
- - -
From 85a8477749d338f7d7032cb20d7480aa4b7f30a5 Mon Sep 17 00:00:00 2001 From: Bradley Erickson Date: Wed, 27 Nov 2024 18:38:01 -0500 Subject: [PATCH 13/15] more cleanups --- .../utilities/LOConnectionLastUpdated.jsx | 3 ++- .../components/utilities/useLOConnection.jsx | 26 +++++++++---------- modules/toy-assess/src/app/websocket/page.js | 2 +- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx index 4e6924ab..1c9bbb00 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/LOConnectionLastUpdated.jsx @@ -21,7 +21,7 @@ function renderReadableTimeSinceUpdate (timeDifference) { return `${renderTime(timeDifference)} ago`; } -export const LOConnectionLastUpdated = ({ message, connectionStatus }) => { +export const LOConnectionLastUpdated = ({ message, connectionStatus, showText=false }) => { const [lastUpdated, setLastUpdated] = useState(null); const [lastUpdatedMessage, setLastUpdatedMessage] = useState(''); @@ -62,6 +62,7 @@ export const LOConnectionLastUpdated = ({ message, connectionStatus }) => { return (
+ {showText ? {titles[connectionStatus]} : ''} {lastUpdatedMessage}
); diff --git a/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnection.jsx b/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnection.jsx index b3afcb8e..01a370fd 100644 --- a/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnection.jsx +++ b/modules/lo_event/lo_event/lo_assess/components/utilities/useLOConnection.jsx @@ -31,7 +31,7 @@ export const useLOConnection = ({ // Function to open the WebSocket connection const openConnection = () => { // Prevent opening a new connection if one is already open or connecting - if (clientRef.current && (connectionStatus === LO_CONNECTION_STATUS.OPEN || connectionStatus === LO_CONNECTION_STATUS.CONNECTING)) { + if (clientRef.current && (clientRef.current.readyState === LO_CONNECTION_STATUS.OPEN || clientRef.current.readyState === LO_CONNECTION_STATUS.CONNECTING)) { console.warn("WebSocket connection is already open or in progress."); return; } @@ -62,19 +62,26 @@ export const useLOConnection = ({ }; }; + // Function to close the WebSocket connection manually + const closeConnection = () => { + if (clientRef.current && connectionStatus === LO_CONNECTION_STATUS.OPEN) { + clientRef.current.close(); + clientRef.current = null; + } else { + console.warn("WebSocket is not open; no connection to close."); + } + }; + // Automatically attempt to open connection on mount useEffect(() => { openConnection(); // Cleanup on unmount return () => { - if (clientRef.current) { - clientRef.current.close(); - } + closeConnection(); }; }, [url]); // Include `url` as a dependency in case it changes and requires a reconnection - // Function to send a message via WebSocket const sendMessage = (message) => { if (clientRef.current && connectionStatus === LO_CONNECTION_STATUS.OPEN) { @@ -84,14 +91,5 @@ export const useLOConnection = ({ } }; - // Function to close the WebSocket connection manually - const closeConnection = () => { - if (clientRef.current && connectionStatus === LO_CONNECTION_STATUS.OPEN) { - clientRef.current.close(); - } else { - console.warn("WebSocket is not open; no connection to close."); - } - }; - return { sendMessage, message, error, connectionStatus, openConnection, closeConnection }; }; diff --git a/modules/toy-assess/src/app/websocket/page.js b/modules/toy-assess/src/app/websocket/page.js index 1717b46d..0453bee7 100644 --- a/modules/toy-assess/src/app/websocket/page.js +++ b/modules/toy-assess/src/app/websocket/page.js @@ -25,7 +25,7 @@ export default function Home ({ children }) {

WebSocket Connection Page

- +