From e27040ee17e3ae2e6099eb7b55a7dfa4303b7bb7 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Mon, 21 Jul 2025 16:28:03 +0000 Subject: [PATCH 01/27] Updated the data returned by 'checkMultigridControllerStatus', and updated the redirect check in the 'Session' route to use the new data structure --- src/loaders/sessionSetup.tsx | 4 ++-- src/routes/Session.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/loaders/sessionSetup.tsx b/src/loaders/sessionSetup.tsx index a79a272..641db50 100644 --- a/src/loaders/sessionSetup.tsx +++ b/src/loaders/sessionSetup.tsx @@ -27,9 +27,9 @@ export const checkMultigridControllerStatus = async (sessionId: string) => { `/instrument_server/sessions/${sessionId}/multigrid_controller/status` ) // Return the response as-is; no need to turn it into a Boolean at this stage - return response.data.exists + return response.data } catch (err) { console.error(err) - return false + return { exists: false } } } diff --git a/src/routes/Session.tsx b/src/routes/Session.tsx index 7c3e1bf..879d484 100644 --- a/src/routes/Session.tsx +++ b/src/routes/Session.tsx @@ -199,7 +199,7 @@ export const Session = () => { // Check if the multigrid controller for the session exists const multigridControllerStatus = await checkMultigridControllerStatus(sessid) - if (!multigridControllerStatus) { + if (!multigridControllerStatus.exists) { // Check if this instrument has a gain reference directory configured if ( !!machineConfig?.gain_reference_directory && From db1493f3a1007ae18930eebacf226ffe9e96a368 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Mon, 21 Jul 2025 17:16:47 +0000 Subject: [PATCH 02/27] Updated the 'SessionRow' component * Added a state to keep track of whether the session is being cleaned up * Disables the bin and broom buttons when a session is being cleaned up * Changes the PuffLoader color logic; it's green when a transfer is in progress, red when cleaning up, and a static grey when the session is disconnected --- src/components/sessionRow.tsx | 44 +++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/src/components/sessionRow.tsx b/src/components/sessionRow.tsx index 6e2dda9..4e8fbc0 100644 --- a/src/components/sessionRow.tsx +++ b/src/components/sessionRow.tsx @@ -1,4 +1,5 @@ import { + Box, Button, GridItem, Heading, @@ -22,6 +23,7 @@ import { import { sessionTokenCheck } from 'loaders/jwt' import { finaliseSession } from 'loaders/rsyncers' import { deleteSessionData } from 'loaders/sessionClients' +import { checkMultigridControllerStatus } from 'loaders/sessionSetup' import React, { useEffect } from 'react' import { GiMagicBroom } from 'react-icons/gi' import { MdDelete } from 'react-icons/md' @@ -31,6 +33,9 @@ import { components } from 'schema/main' type Session = components['schemas']['Session'] export const SessionRow = (session: Session) => { + const [sessionActive, setSessionActive] = React.useState(false) + const [sessionFinalising, setSessionFinalising] = React.useState(false) + const { isOpen: isOpenDelete, onOpen: onOpenDelete, @@ -43,16 +48,25 @@ export const SessionRow = (session: Session) => { } = useDisclosure() const cleanupSession = async (sessid: number) => { - await finaliseSession(sessid) + const response = await finaliseSession(sessid) + if (response.success) { + setSessionFinalising(true) + } onCloseCleanup() } - const [sessionActive, setSessionActive] = React.useState(false) - useEffect(() => { sessionTokenCheck(session.id).then((active) => setSessionActive(active)) + checkMultigridControllerStatus(session.id.toString()).then((status) => + setSessionFinalising(status.finalising) + ) }, [session]) + useEffect(() => { + console.log(`sessionActive:`, sessionActive) + console.log(`sessionFinalising:`, sessionFinalising) + }, [sessionActive, sessionFinalising]) + return ( @@ -142,9 +156,25 @@ export const SessionRow = (session: Session) => { {session.name}: {session.id} {sessionActive ? ( - + ) : ( - <> + // Replace PuffLoader with inactive grey circle + // when session is disconnected + + + )} @@ -155,7 +185,7 @@ export const SessionRow = (session: Session) => { aria-label="Delete session" icon={} onClick={onOpenDelete} - isDisabled={sessionActive} + isDisabled={sessionActive || sessionFinalising} /> @@ -163,7 +193,7 @@ export const SessionRow = (session: Session) => { aria-label="Clean up session" icon={} onClick={onOpenCleanup} - isDisabled={!sessionActive} + isDisabled={!sessionActive || sessionFinalising} /> From 8d36f73b6599722ad025128463cf857ae37dbf9a Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Tue, 22 Jul 2025 14:01:52 +0000 Subject: [PATCH 03/27] Updated 'sessionClients' loader * Renamed 'getSessionsData' to 'getAllSessionsData' to better distinguish it from 'getSessionData' * Similarly renamed 'sessionsLoader' to 'allSessionsLoader' * Migrated query-based functions and variables into the loaders they are called in, to keep dependencies focused * Used `invalidateQueries` in loaders to make sure that fresh data is fetched from the backend every time a page with session-related information is loaded --- src/loaders/sessionClients.tsx | 65 +++++++++++++++------------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/src/loaders/sessionClients.tsx b/src/loaders/sessionClients.tsx index 535dee5..8a079cf 100644 --- a/src/loaders/sessionClients.tsx +++ b/src/loaders/sessionClients.tsx @@ -6,7 +6,7 @@ import { convertUTCToUKNaive, convertUKNaiveToUTC } from 'utils/generic' export const includePage = (endpoint: string, limit: number, page: number) => `${endpoint}${endpoint.includes('?') ? '&' : '?'}page=${page - 1}&limit=${limit}` -const getSessionsData = async () => { +const getAllSessionsData = async () => { if (!sessionStorage.getItem('instrumentName')) return null const response = await client.get( `session_info/instruments/${sessionStorage.getItem('instrumentName')}/sessions` @@ -107,46 +107,39 @@ export const deleteSessionData = async (sessid: number) => { return response.data } -export const sessionsLoader = - (queryClient: QueryClient) => - async ({ request }: { request: Request }) => { - // Get instrument name from the URL query - // By looking for a query, this prompts the loader to reload - const url = new URL(request.url) - const instrumentName = - url.searchParams.get('instrumentName') || - sessionStorage.getItem('instrumentName') - - const queryKey = ['homepageSessions', instrumentName] - const queryFn = async () => { - if (!instrumentName) return null - const data = await getSessionsData() - if (!data) return null - return data - } - const query = { - queryKey: queryKey, - queryFn: queryFn, - } - return ( - (await queryClient.getQueryData(queryKey)) ?? - (await queryClient.fetchQuery(query)) - ) - } +export const allSessionsLoader = (queryClient: QueryClient) => async () => { + // Load the instrument name from sessionStorage + const instrumentName = sessionStorage.getItem('instrumentName') -const queryBuilder = (sessid: string = '0') => { - return { - queryKey: ['sessionId', sessid], - queryFn: () => getSessionData(sessid), - staleTime: 60000, + // Skip loading logic if no instrument name was found + if (!instrumentName) return null + + const queryKey = ['homepageSessions', instrumentName] + const queryFn = async () => { + if (!instrumentName) return null + const data = await getAllSessionsData() + if (!data) return null + return data } + + await queryClient.invalidateQueries({ queryKey }) + return await queryClient.fetchQuery({ queryKey, queryFn }) } export const sessionLoader = (queryClient: QueryClient) => async (params: Params) => { + const sessid = params.sessid + if (!sessid) return null + + const queryBuilder = (sessid: string = '0') => { + return { + queryKey: ['sessionId', sessid], + queryFn: () => getSessionData(sessid), + } + } const singleQuery = queryBuilder(params.sessid) - return ( - (await queryClient.getQueryData(singleQuery.queryKey)) ?? - (await queryClient.fetchQuery(singleQuery)) - ) + const queryKey = singleQuery.queryKey + + await queryClient.invalidateQueries({ queryKey }) + return await queryClient.fetchQuery(singleQuery) } From 24279eac1edcd609822daf1f21b8c5debfdbc5c6 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Tue, 22 Jul 2025 14:08:18 +0000 Subject: [PATCH 04/27] Removed a leftover debug log from the 'sessionRow' component --- src/components/sessionRow.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/sessionRow.tsx b/src/components/sessionRow.tsx index 4e8fbc0..2e677dd 100644 --- a/src/components/sessionRow.tsx +++ b/src/components/sessionRow.tsx @@ -62,11 +62,6 @@ export const SessionRow = (session: Session) => { ) }, [session]) - useEffect(() => { - console.log(`sessionActive:`, sessionActive) - console.log(`sessionFinalising:`, sessionFinalising) - }, [sessionActive, sessionFinalising]) - return ( From ef1309b284e83d86316e5fb74f1f23bc4893821d Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Tue, 22 Jul 2025 14:09:10 +0000 Subject: [PATCH 05/27] Updated index.tsx to reflect new sessions loader name --- src/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 4e3e39c..909e9b7 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -13,7 +13,7 @@ import { sessionParametersLoader, } from 'loaders/processingParameters' import { rsyncerLoader } from 'loaders/rsyncers' -import { sessionsLoader, sessionLoader } from 'loaders/sessionClients' +import { allSessionsLoader, sessionLoader } from 'loaders/sessionClients' import { visitLoader } from 'loaders/visits' import { createRoot } from 'react-dom/client' import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom' @@ -65,7 +65,7 @@ const router = createBrowserRouter([ path: '/home', element: , errorElement: , - loader: sessionsLoader(queryClient), + loader: allSessionsLoader(queryClient), }, { path: '/instruments/:instrumentName/new_session', From b29c66744681c08c06603d56a69fa8a09ebf8e20 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Tue, 22 Jul 2025 14:10:35 +0000 Subject: [PATCH 06/27] Simplified the 'Navbar' component by adding the navigation logic tothe icons directly --- src/components/navbar.tsx | 67 ++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index 2d9560f..fa25afc 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -2,7 +2,6 @@ import { Box, Flex, HStack, - Link, IconButton, useDisclosure, Image, @@ -19,7 +18,7 @@ import { MdOutlineSignalWifiBad, } from 'react-icons/md' import { TbMicroscope, TbSnowflake, TbHomeCog } from 'react-icons/tb' -import { Link as LinkRouter } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' export interface LinkDescriptor { label: string @@ -43,9 +42,10 @@ export const Navbar = ({ logo, ...props }: NavbarProps) => { - const { isOpen, onOpen, onClose } = useDisclosure() const [instrumentConnectionStatus, setInsrumentConnectionStatus] = React.useState(false) + const navigate = useNavigate() + const { isOpen, onOpen, onClose } = useDisclosure() // Check connectivity every few seconds React.useEffect(() => { @@ -95,35 +95,38 @@ export const Navbar = ({ /> ) : null} - - - - - - } - aria-label={'Back to the Hub'} - _hover={{ background: 'transparent', color: 'murfey.500' }} - /> - - - - - - - - - } - aria-label={'Back to the microscope'} - _hover={{ background: 'transparent', color: 'murfey.500' }} - /> - - + + { + navigate(`/hub`) + }} + size={'sm'} + icon={ + <> + + + } + aria-label={'Back to the Hub'} + _hover={{ background: 'transparent', color: 'murfey.500' }} + /> + + {/* Add the instrument name as a URL query parameter to trigger a reload */} + + { + navigate(`/home`) + }} + size={'sm'} + icon={ + <> + + + + } + aria-label="Back to the microscope" + _hover={{ background: 'transparent', color: 'murfey.500' }} + /> + Date: Tue, 22 Jul 2025 14:46:49 +0000 Subject: [PATCH 07/27] Fixed instrument server connectivity indicator so that the Tooltip message appears and disappears correctly; also fixed incorrect polling interval in comment --- src/components/navbar.tsx | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index fa25afc..37cad01 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -60,7 +60,7 @@ export const Navbar = ({ } resolveConnectionStatus() // Fetch data once to start with - // Set it to run every 4s + // Set it to run every 10s const interval = setInterval(resolveConnectionStatus, 10000) return () => clearInterval(interval) }, []) @@ -133,16 +133,17 @@ export const Navbar = ({ ? 'Connected to instrument server' : 'No instrument server connection' } - placement="bottom" > - + + + From 18b7d9780e615205364dd0e84dc43f0a13ea9348 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Tue, 22 Jul 2025 14:49:13 +0000 Subject: [PATCH 08/27] Updated the 'Home' route * Removed the 'useEffect()' for cleaning up the URl now that URL query parameters are no longer needed * Rearranged defined variables and functions by type * Added comments --- src/routes/Home.tsx | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index d659e9f..bd7e04f 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -10,37 +10,28 @@ import { import { InstrumentCard } from 'components/instrumentCard' import { SessionRow } from 'components/sessionRow' import React, { useEffect } from 'react' -import { - Link as LinkRouter, - useLoaderData, - useSearchParams, - useNavigate, -} from 'react-router-dom' +import { Link as LinkRouter, useLoaderData } from 'react-router-dom' import useWebSocket from 'react-use-websocket' import { components } from 'schema/main' import { v4 as uuid4 } from 'uuid' type Session = components['schemas']['Session'] export const Home = () => { - // Get session data from the loader + // Set up loaders const sessions = useLoaderData() as { current: Session[] } | null - // Clean the URL after loading the page - const [searchParams] = useSearchParams() - const navigate = useNavigate() - useEffect(() => { - if (searchParams.has('instrumentName')) { - navigate('/home', { replace: true }) - } - }, [searchParams, navigate]) - + // React states const [UUID, setUUID] = React.useState('') + + // Helper parameters and functions const baseUrl = sessionStorage.getItem('murfeyServerURL') ?? process.env.REACT_APP_API_ENDPOINT const url = baseUrl ? baseUrl.replace('http', 'ws') : 'ws://localhost:8000' + + // Helper function to parse websocket messages const parseWebsocketMessage = (message: any) => { let parsedMessage: any = {} try { @@ -59,6 +50,7 @@ export const Home = () => { setUUID(uuid4()) } }, [UUID]) + // Establish websocket connection to the backend useWebSocket( // 'null' is passed to 'useWebSocket()' if UUID is not yet set to From 86b8a8ac73c9209efed722c40d715753138078eb Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Tue, 22 Jul 2025 14:51:50 +0000 Subject: [PATCH 09/27] Removed the URL query parameter insertion logic when navigating to home, now that it's no longer needed --- src/routes/Hub.tsx | 6 ++---- src/routes/Login.tsx | 4 +--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/routes/Hub.tsx b/src/routes/Hub.tsx index 1c327a5..d6bbee1 100644 --- a/src/routes/Hub.tsx +++ b/src/routes/Hub.tsx @@ -10,7 +10,7 @@ import { Box, SimpleGrid, } from '@chakra-ui/react' -import React, { useEffect } from 'react' +import { useEffect } from 'react' import { TbMicroscope, TbSnowflake } from 'react-icons/tb' import { useLoaderData, useNavigate } from 'react-router-dom' @@ -41,9 +41,7 @@ export const Hub = () => { // Direct users to /login only if authenticating with 'password' if (process.env.REACT_APP_BACKEND_AUTH_TYPE === 'cookie') { - navigate( - `/home?instrumentName=${encodeURIComponent(iinfo.instrument_name)}` - ) + navigate(`/home`) } else { navigate(`/login`, { replace: true }) } diff --git a/src/routes/Login.tsx b/src/routes/Login.tsx index 44ee964..ee6eab8 100644 --- a/src/routes/Login.tsx +++ b/src/routes/Login.tsx @@ -59,9 +59,7 @@ const Login = () => { let instrumentName = sessionStorage.getItem('instrumentName') if (instrumentName) { - navigate( - `/home?instrumentName=${encodeURIComponent(instrumentName)}` - ) + navigate(`/home`) } else { console.error('Could not find instument information') } From 82f1ab6630316297e4ce4ab1202802be8c6370d6 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Tue, 22 Jul 2025 16:51:47 +0000 Subject: [PATCH 10/27] Migrated 'queryBuilder' function into the loader it's contributing to --- src/loaders/rsyncers.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/loaders/rsyncers.tsx b/src/loaders/rsyncers.tsx index 4051a8a..25c3336 100644 --- a/src/loaders/rsyncers.tsx +++ b/src/loaders/rsyncers.tsx @@ -105,16 +105,15 @@ export const flushSkippedRsyncer = async ( return response.data } -const queryBuilder = (sessionId: string = '0') => { - return { - queryKey: ['sessid', sessionId], - queryFn: () => getRsyncerData(sessionId), - staleTime: 60000, - } -} - export const rsyncerLoader = (queryClient: QueryClient) => async (params: Params) => { + const queryBuilder = (sessionId: string = '0') => { + return { + queryKey: ['sessid', sessionId], + queryFn: () => getRsyncerData(sessionId), + staleTime: 60000, + } + } const singleQuery = queryBuilder(params.sessid) return ( (await queryClient.getQueryData(singleQuery.queryKey)) ?? From 581686ba349f529ee60763cdb4b963fa60e7efa7 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Tue, 22 Jul 2025 16:54:15 +0000 Subject: [PATCH 11/27] Migrated WebSocket conection logic to a standalone component which is then implemented inside 'ProtectedRoutes' so that it persists when navigating across all relevant pages --- src/components/protectedRoutes.tsx | 6 +-- src/components/webSocketHandler.tsx | 74 +++++++++++++++++++++++++++++ src/routes/Home.tsx | 49 ------------------- src/routes/Session.tsx | 64 ------------------------- 4 files changed, 77 insertions(+), 116 deletions(-) create mode 100644 src/components/webSocketHandler.tsx diff --git a/src/components/protectedRoutes.tsx b/src/components/protectedRoutes.tsx index 63bb39e..4446b58 100644 --- a/src/components/protectedRoutes.tsx +++ b/src/components/protectedRoutes.tsx @@ -1,12 +1,14 @@ import { Box } from '@chakra-ui/react' import { Navbar } from 'components/navbar' +import { WebSocketHandler } from 'components/webSocketHandler' import { Navigate, Outlet } from 'react-router-dom' -const ProtectedRoutes = () => { +export const ProtectedRoutes = () => { // Read environment variable and demand user login if authenticating with 'password' const sessionToken = sessionStorage.getItem('token') const standard = (
+ @@ -23,5 +25,3 @@ const ProtectedRoutes = () => { ) } - -export { ProtectedRoutes } diff --git a/src/components/webSocketHandler.tsx b/src/components/webSocketHandler.tsx new file mode 100644 index 0000000..6077f46 --- /dev/null +++ b/src/components/webSocketHandler.tsx @@ -0,0 +1,74 @@ +import { useQueryClient } from '@tanstack/react-query' +import { useEffect, useState } from 'react' +import useWebSocket from 'react-use-websocket' +import { v4 as uuid4 } from 'uuid' + +export const WebSocketHandler = () => { + // Create states + const [uuid, setUUID] = useState('') + const queryClient = useQueryClient() + + // Set up a websocket connection that persists across all protected routes + const baseURL = + sessionStorage.getItem('murfeyServerURL') ?? + process.env.REACT_APP_API_ENDPOINT + const wsURL = baseURL ? baseURL.replace('http', 'ws') : 'ws://localhost:8000' + + // Helper function to parse websocket messages + const parseWebsocketMessage = (message: any) => { + let parsedMessage: any = {} + try { + parsedMessage = JSON.parse(message) + } catch (err) { + console.warn(`Invalid WebSocket message:`, message) + return + } + + // Actions to take depending on the type of message received + if (parsedMessage.message === 'refresh') { + // Update session ID queries when a change to the RSyncer is detected + if (parsedMessage.target === 'rsyncer') { + let sessionID: string | null = parsedMessage.session_id + if (!sessionID) return null + queryClient.refetchQueries({ queryKey: ['sessid', sessionID] }) + } + // Update instrument name queries when a change to sessions is detected + if (parsedMessage.target === 'sessions') { + let instrumentName: string | null = parsedMessage.instrument_name + if (!instrumentName) return null + queryClient.refetchQueries({ + queryKey: ['homepageSessions', instrumentName], + }) + } + } + } + + // Generate a UUID if missing + useEffect(() => { + if (!uuid) { + setUUID(uuid4()) + } + }, [uuid]) + + // Establish a websocket connection + useWebSocket( + // 'null' is passed to 'useWebSocket()' if UUID is not set to prevent malformed connections + uuid ? wsURL + `ws/connect/${uuid}` : null, + uuid + ? { + onOpen: () => { + console.log('WebSocket connection established.') + }, + onClose: () => { + console.log('WebSocket connection closed.') + }, + onMessage: (event) => { + parseWebsocketMessage(event.data) + }, + shouldReconnect: () => true, + } + : undefined + ) + + return null // Nothing to render +} diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index bd7e04f..6c0ecd7 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -9,11 +9,8 @@ import { } from '@chakra-ui/react' import { InstrumentCard } from 'components/instrumentCard' import { SessionRow } from 'components/sessionRow' -import React, { useEffect } from 'react' import { Link as LinkRouter, useLoaderData } from 'react-router-dom' -import useWebSocket from 'react-use-websocket' import { components } from 'schema/main' -import { v4 as uuid4 } from 'uuid' type Session = components['schemas']['Session'] export const Home = () => { @@ -22,52 +19,6 @@ export const Home = () => { current: Session[] } | null - // React states - const [UUID, setUUID] = React.useState('') - - // Helper parameters and functions - const baseUrl = - sessionStorage.getItem('murfeyServerURL') ?? - process.env.REACT_APP_API_ENDPOINT - const url = baseUrl ? baseUrl.replace('http', 'ws') : 'ws://localhost:8000' - - // Helper function to parse websocket messages - const parseWebsocketMessage = (message: any) => { - let parsedMessage: any = {} - try { - parsedMessage = JSON.parse(message) - } catch (err) { - return - } - if (parsedMessage.message === 'refresh') { - window.location.reload() - } - } - - // Use existing UUID if present; otherwise, generate a new UUID - useEffect(() => { - if (!UUID) { - setUUID(uuid4()) - } - }, [UUID]) - - // Establish websocket connection to the backend - useWebSocket( - // 'null' is passed to 'useWebSocket()' if UUID is not yet set to - // prevent malformed connections - UUID ? url + `ws/connect/${UUID}` : null, - UUID - ? { - onOpen: () => { - console.log('WebSocket connection established.') - }, - onMessage: (event) => { - parseWebsocketMessage(event.data) - }, - } - : undefined - ) - return (
Murfey diff --git a/src/routes/Session.tsx b/src/routes/Session.tsx index 879d484..2939ad7 100644 --- a/src/routes/Session.tsx +++ b/src/routes/Session.tsx @@ -23,7 +23,6 @@ import { Stack, Switch, VStack, - useToast, } from '@chakra-ui/react' import { InstrumentCard } from 'components/instrumentCard' import { RsyncCard } from 'components/rsyncCard' @@ -53,10 +52,8 @@ import { useParams, useNavigate, } from 'react-router-dom' -import useWebSocket from 'react-use-websocket' import { components } from 'schema/main' import { convertUKNaiveToUTC, convertUTCToUKNaive } from 'utils/generic' -import { v4 as uuid4 } from 'uuid' type RSyncerInfo = components['schemas']['RSyncerInfo'] type SessionSchema = components['schemas']['Session'] @@ -91,9 +88,6 @@ export const Session = () => { // Machine config and instrument information const [machineConfig, setMachineConfig] = React.useState() - // Websocket UUID information - const [UUID, setUUID] = React.useState('') - // Rsyncer information const [rsyncers, setRsyncers] = React.useState( rsyncerLoaderData ?? [] @@ -116,64 +110,6 @@ export const Session = () => { // ---------------------------------------------------------------------------------- // Functions - // Load the Murfey server URL from the environment variable - const baseUrl = - sessionStorage.getItem('murfeyServerURL') ?? - process.env.REACT_APP_API_ENDPOINT - - // Set up websocket connection to Murfey server - const url = baseUrl ? baseUrl.replace('http', 'ws') : 'ws://localhost:8000' - - // Use existing UUID if present; otherwise, generate a new UUID - useEffect(() => { - if (!UUID) { - setUUID(uuid4()) - } - }, [UUID]) - - // Websocket helper function to parse incoming messages - const toast = useToast() - const parseWebsocketMessage = (message: any) => { - let parsedMessage: any = {} - try { - parsedMessage = JSON.parse(message) - } catch (err) { - return - } - if (parsedMessage.message === 'refresh') { - window.location.reload() - } - if ( - parsedMessage.message === 'update' && - typeof sessid !== 'undefined' && - parsedMessage.session_id === parseInt(sessid) - ) { - return toast({ - title: 'Update', - description: parsedMessage.payload, - isClosable: true, - duration: parsedMessage.duration ?? null, - status: parsedMessage.status ?? 'info', - }) - } - } - - // Establish websocket connection to the backend - useWebSocket( - // 'null' is passed to 'useWebSocket()' if UUID is not yet set to - // prevent malformed connections - UUID ? url + `ws/connect/${UUID}` : null, - UUID - ? { - onOpen: () => { - console.log('WebSocket connection established.') - }, - onMessage: (event) => { - parseWebsocketMessage(event.data) - }, - } - : undefined - ) // Get machine config and set up related settings const handleMachineConfig = (mcfg: MachineConfig) => { From 0a0dc94e41e913965f72e0214c55be6fb318a6c0 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Tue, 22 Jul 2025 21:48:29 +0000 Subject: [PATCH 12/27] Converted 'SessionRow' into a proper React component instead of a function --- src/components/sessionRow.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/sessionRow.tsx b/src/components/sessionRow.tsx index 2e677dd..f4de02f 100644 --- a/src/components/sessionRow.tsx +++ b/src/components/sessionRow.tsx @@ -32,7 +32,10 @@ import { PuffLoader } from 'react-spinners' import { components } from 'schema/main' type Session = components['schemas']['Session'] -export const SessionRow = (session: Session) => { +type SessionRowProps = { + session: Session +} +export const SessionRow = ({ session }: SessionRowProps) => { const [sessionActive, setSessionActive] = React.useState(false) const [sessionFinalising, setSessionFinalising] = React.useState(false) From 006f4ff62c03c6af06533c1f112bce7d306a8516 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Tue, 22 Jul 2025 21:48:42 +0000 Subject: [PATCH 13/27] * Refactored 'Home' route to render content with 'useQuery' instead of 'useLoaderData' * Changed 'allSessionsLoader' loader function to return 'queryClient.ensureQueryData()' to prefetch and catch session information before route renders --- src/loaders/sessionClients.tsx | 18 ++++++------------ src/routes/Home.tsx | 25 ++++++++++++++++++++----- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/loaders/sessionClients.tsx b/src/loaders/sessionClients.tsx index 8a079cf..6a2cbbc 100644 --- a/src/loaders/sessionClients.tsx +++ b/src/loaders/sessionClients.tsx @@ -6,10 +6,10 @@ import { convertUTCToUKNaive, convertUKNaiveToUTC } from 'utils/generic' export const includePage = (endpoint: string, limit: number, page: number) => `${endpoint}${endpoint.includes('?') ? '&' : '?'}page=${page - 1}&limit=${limit}` -const getAllSessionsData = async () => { - if (!sessionStorage.getItem('instrumentName')) return null +export const getAllSessionsData = async (instrumentName: string) => { + if (!instrumentName) return null const response = await client.get( - `session_info/instruments/${sessionStorage.getItem('instrumentName')}/sessions` + `session_info/instruments/${instrumentName}/sessions` ) if (response.status !== 200) return null return { @@ -114,16 +114,10 @@ export const allSessionsLoader = (queryClient: QueryClient) => async () => { // Skip loading logic if no instrument name was found if (!instrumentName) return null + // Construct the query key and query function const queryKey = ['homepageSessions', instrumentName] - const queryFn = async () => { - if (!instrumentName) return null - const data = await getAllSessionsData() - if (!data) return null - return data - } - - await queryClient.invalidateQueries({ queryKey }) - return await queryClient.fetchQuery({ queryKey, queryFn }) + const queryFn = () => getAllSessionsData(instrumentName) + return queryClient.ensureQueryData({ queryKey, queryFn }) } export const sessionLoader = diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index 6c0ecd7..9836816 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -7,15 +7,30 @@ import { Link, VStack, } from '@chakra-ui/react' +import { useQuery } from '@tanstack/react-query' import { InstrumentCard } from 'components/instrumentCard' import { SessionRow } from 'components/sessionRow' -import { Link as LinkRouter, useLoaderData } from 'react-router-dom' +import { getAllSessionsData } from 'loaders/sessionClients' +import { Link as LinkRouter } from 'react-router-dom' import { components } from 'schema/main' type Session = components['schemas']['Session'] export const Home = () => { - // Set up loaders - const sessions = useLoaderData() as { + const instrumentName = sessionStorage.getItem('instrumentName') + const queryKey = ['homepageSessions', instrumentName] + const queryFn = () => getAllSessionsData(instrumentName ? instrumentName : '') + + const { data, isLoading, isError } = useQuery({ + queryKey, + queryFn, + enabled: !!instrumentName, + staleTime: 0, // Always refetch on mount unless preloaded + }) + + if (isLoading) return

Loading sessions...

+ if (isError || !data) return

Failed to load sessions.

+ + const sessions = data as { current: Session[] } | null @@ -44,7 +59,7 @@ export const Home = () => { w={{ base: '100%', md: '19.6%' }} _hover={{ textDecor: 'none' }} as={LinkRouter} - to={`../instruments/${sessionStorage.getItem('instrumentName')}/new_session`} + to={`../instruments/${instrumentName}/new_session`} > @@ -60,7 +75,7 @@ export const Home = () => { sessions.current.map((current) => { return ( - {SessionRow(current)} + ) }) From 8080f4bf84b53af497f8524dcb319b9e85bfb638 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Tue, 22 Jul 2025 22:26:08 +0000 Subject: [PATCH 14/27] Further improvements to 'SessionRow' component * Pass it the instrument name from the 'Home' route * Use 'queryClient.refetchQueries()' to update the loaded session information after a delete instead of refreshing the window --- src/components/sessionRow.tsx | 17 +++++++++++++++-- src/routes/Home.tsx | 5 ++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/components/sessionRow.tsx b/src/components/sessionRow.tsx index f4de02f..90b41e5 100644 --- a/src/components/sessionRow.tsx +++ b/src/components/sessionRow.tsx @@ -20,6 +20,7 @@ import { VStack, useDisclosure, } from '@chakra-ui/react' +import { useQueryClient } from '@tanstack/react-query' import { sessionTokenCheck } from 'loaders/jwt' import { finaliseSession } from 'loaders/rsyncers' import { deleteSessionData } from 'loaders/sessionClients' @@ -34,11 +35,20 @@ import { components } from 'schema/main' type Session = components['schemas']['Session'] type SessionRowProps = { session: Session + instrumentName: string | null } -export const SessionRow = ({ session }: SessionRowProps) => { +export const SessionRow = ({ + session, + instrumentName = null, +}: SessionRowProps) => { + // Set up query client + const queryClient = useQueryClient() + + // Set up React states const [sessionActive, setSessionActive] = React.useState(false) const [sessionFinalising, setSessionFinalising] = React.useState(false) + // Set up utility hooks const { isOpen: isOpenDelete, onOpen: onOpenDelete, @@ -90,7 +100,10 @@ export const SessionRow = ({ session }: SessionRowProps) => { variant="ghost" onClick={() => { deleteSessionData(session.id).then(() => - window.location.reload() + // Refetch session information for this instrument + queryClient.refetchQueries({ + queryKey: ['homepageSessions', instrumentName], + }) ) }} > diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index 9836816..3d4092d 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -75,7 +75,10 @@ export const Home = () => { sessions.current.map((current) => { return ( - + ) }) From fb7da7f52f032f02ca50873ef37485220af7656a Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Wed, 23 Jul 2025 00:37:58 +0000 Subject: [PATCH 15/27] Added 'queryClient.invalidateQueries' to 'allSessionsLoader' to ensure session information is always fresh --- src/loaders/sessionClients.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/loaders/sessionClients.tsx b/src/loaders/sessionClients.tsx index 6a2cbbc..3c9c6b8 100644 --- a/src/loaders/sessionClients.tsx +++ b/src/loaders/sessionClients.tsx @@ -117,6 +117,9 @@ export const allSessionsLoader = (queryClient: QueryClient) => async () => { // Construct the query key and query function const queryKey = ['homepageSessions', instrumentName] const queryFn = () => getAllSessionsData(instrumentName) + + // Enssure data is always fresh + await queryClient.invalidateQueries({ queryKey }) return queryClient.ensureQueryData({ queryKey, queryFn }) } From 92238f65e052a0911f28da6e066df9e3e43f10e0 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Wed, 23 Jul 2025 00:40:14 +0000 Subject: [PATCH 16/27] Updated 'rsyncerLoader' so that it returns fresh RSyncer info each time, and is compatible with 'useQuery' --- src/loaders/rsyncers.tsx | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/loaders/rsyncers.tsx b/src/loaders/rsyncers.tsx index 25c3336..9f33345 100644 --- a/src/loaders/rsyncers.tsx +++ b/src/loaders/rsyncers.tsx @@ -107,16 +107,14 @@ export const flushSkippedRsyncer = async ( export const rsyncerLoader = (queryClient: QueryClient) => async (params: Params) => { - const queryBuilder = (sessionId: string = '0') => { - return { - queryKey: ['sessid', sessionId], - queryFn: () => getRsyncerData(sessionId), - staleTime: 60000, - } - } - const singleQuery = queryBuilder(params.sessid) - return ( - (await queryClient.getQueryData(singleQuery.queryKey)) ?? - (await queryClient.fetchQuery(singleQuery)) - ) + const sessionId = params.sessid + if (!sessionId) return null + + // Construct the queryKey and queryFn + const queryKey = ['sessid', sessionId] + const queryFn = () => getRsyncerData(sessionId) + + // Ensure data is always fresh + await queryClient.invalidateQueries({ queryKey }) + return queryClient.ensureQueryData({ queryKey, queryFn }) } From c93ef42fa1ea355194042b8b3dddba32b72860a3 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Wed, 23 Jul 2025 00:42:11 +0000 Subject: [PATCH 17/27] Updated the 'handleReconnect' function so that it correctly sets the 'sessionActive' state and closes the pop-up after successful execution --- src/routes/Session.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/Session.tsx b/src/routes/Session.tsx index 2939ad7..a64bd71 100644 --- a/src/routes/Session.tsx +++ b/src/routes/Session.tsx @@ -331,6 +331,8 @@ export const Session = () => { parseInt(sessid) ) await startMultigridWatcher(parseInt(sessid)) + await checkSessionActivationState() + onCloseReconnect() } } From f90c3894dcc08af176cea50e460483a15cb7abcc Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Thu, 24 Jul 2025 08:22:37 +0000 Subject: [PATCH 18/27] Replaced the transferring status indicator in the 'SessionRow' component with an animated Sync icon instead --- src/components/sessionRow.tsx | 81 ++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/src/components/sessionRow.tsx b/src/components/sessionRow.tsx index 90b41e5..1ee0b27 100644 --- a/src/components/sessionRow.tsx +++ b/src/components/sessionRow.tsx @@ -4,6 +4,7 @@ import { GridItem, Heading, HStack, + Icon, IconButton, Link, Modal, @@ -28,8 +29,8 @@ import { checkMultigridControllerStatus } from 'loaders/sessionSetup' import React, { useEffect } from 'react' import { GiMagicBroom } from 'react-icons/gi' import { MdDelete } from 'react-icons/md' +import { MdSync, MdSyncProblem } from 'react-icons/md' import { Link as LinkRouter } from 'react-router-dom' -import { PuffLoader } from 'react-spinners' import { components } from 'schema/main' type Session = components['schemas']['Session'] @@ -166,27 +167,65 @@ export const SessionRow = ({ > {session.name}: {session.id} - {sessionActive ? ( - - ) : ( - // Replace PuffLoader with inactive grey circle - // when session is disconnected - - + {sessionActive ? ( + // Show a pulsing spinning sync icon when running + + ) : ( + // Show a sync error icon when disconnected + - - )} + )} + From c13795e28c56d1fd713078f7d7d6485804ae7b1f Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Thu, 24 Jul 2025 15:08:25 +0000 Subject: [PATCH 19/27] Standardised how loaders are defined in 'index.tsx' and streamlined loader functions --- src/index.tsx | 16 ++++---- src/loaders/dataCollectionGroups.tsx | 24 ++++++------ src/loaders/gridSquares.tsx | 5 ++- src/loaders/magTable.tsx | 18 +++++---- src/loaders/possibleGainRefs.tsx | 28 +++++++------- src/loaders/processingParameters.tsx | 58 +++++++++++++--------------- src/loaders/rsyncers.tsx | 14 ++++--- src/loaders/sessionClients.tsx | 35 ++++++++--------- src/loaders/visits.tsx | 29 +++++++------- 9 files changed, 113 insertions(+), 114 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 909e9b7..32efdb1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -71,7 +71,7 @@ const router = createBrowserRouter([ path: '/instruments/:instrumentName/new_session', element: , errorElement: , - loader: ({ params }) => visitLoader(queryClient)(params), + loader: visitLoader(queryClient), }, { path: '/new_session/setup/:sessid', @@ -83,43 +83,43 @@ const router = createBrowserRouter([ path: '/new_session/parameters/:sessid', element: , errorElement: , - loader: ({ params }) => sessionLoader(queryClient)(params), + loader: sessionLoader(queryClient), }, { path: '/sessions/:sessid', element: , errorElement: , - loader: ({ params }) => rsyncerLoader(queryClient)(params), + loader: rsyncerLoader(queryClient), }, { path: '/sessions/:sessid/gain_ref_transfer', element: , errorElement: , - loader: ({ params }) => gainRefLoader(queryClient)(params), + loader: gainRefLoader(queryClient), }, { path: '/sessions/:sessid/session_parameters', element: , errorElement: , - loader: ({ params }) => sessionParametersLoader(queryClient)(params), + loader: sessionParametersLoader(queryClient), }, { path: '/sessions/:sessid/session_parameters/extra_parameters', element: , errorElement: , - loader: ({ params }) => processingParametersLoader(queryClient)(params), + loader: processingParametersLoader(queryClient), }, { path: '/sessions/:sessid/data_collection_groups', element: , errorElement: , - loader: ({ params }) => dataCollectionGroupsLoader(queryClient)(params), + loader: dataCollectionGroupsLoader(queryClient), }, { path: '/sessions/:sessid/data_collection_groups/:dcgid/grid_squares', element: , errorElement: , - loader: ({ params }) => gridSquaresLoader(queryClient)(params), + loader: gridSquaresLoader(queryClient), }, { path: '/mag_table', diff --git a/src/loaders/dataCollectionGroups.tsx b/src/loaders/dataCollectionGroups.tsx index 6807420..041ab62 100644 --- a/src/loaders/dataCollectionGroups.tsx +++ b/src/loaders/dataCollectionGroups.tsx @@ -3,7 +3,7 @@ import { Params } from 'react-router-dom' import { client } from 'utils/api/client' const getDataCollectionGroups = async (sessid: string = '0') => { - console.log('data collection groups gather') + console.log(`Getting data collection groups`) const response = await client.get( `session_info/sessions/${sessid}/data_collection_groups` ) @@ -15,17 +15,19 @@ const getDataCollectionGroups = async (sessid: string = '0') => { return response.data } -const queryBuilder = (sessid: string = '0') => { - return { - queryKey: ['sessionId', sessid], - queryFn: () => getDataCollectionGroups(sessid), - staleTime: 60000, - } -} - export const dataCollectionGroupsLoader = - (queryClient: QueryClient) => async (params: Params) => { - const singleQuery = queryBuilder(params.sessid) + (queryClient: QueryClient) => + async ({ params }: { params: Params }) => { + const sessionId = params.sessid + if (!sessionId) return null + + const queryKey = ['dataCollectionGroups', sessionId] + const queryFn = () => getDataCollectionGroups(sessionId) + const singleQuery = { + queryKey: queryKey, + queryFn: queryFn, + staleTime: 60000, + } return ( (await queryClient.getQueryData(singleQuery.queryKey)) ?? (await queryClient.fetchQuery(singleQuery)) diff --git a/src/loaders/gridSquares.tsx b/src/loaders/gridSquares.tsx index 9845aa2..add19a8 100644 --- a/src/loaders/gridSquares.tsx +++ b/src/loaders/gridSquares.tsx @@ -6,7 +6,7 @@ const getGridSquares = async ( sessid: string = '0', dataCollectionGroupId: string = '0' ) => { - console.log('getting grid squares') + console.log('Getting grid squares') const response = await client.get( `session_info/spa/sessions/${sessid}/data_collection_groups/${dataCollectionGroupId}/grid_squares` ) @@ -68,7 +68,8 @@ const queryBuilder = ( } export const gridSquaresLoader = - (queryClient: QueryClient) => async (params: Params) => { + (queryClient: QueryClient) => + async ({ params }: { params: Params }) => { // const singleQuery = queryBuilder(params.sessid, params.dcgid); const singleQuery = queryBuilder(params.sessid) return ( diff --git a/src/loaders/magTable.tsx b/src/loaders/magTable.tsx index 9b198b6..b1515c0 100644 --- a/src/loaders/magTable.tsx +++ b/src/loaders/magTable.tsx @@ -35,12 +35,14 @@ export const removeMagTableRow = async (magnification: number) => { return response.data } -const query = { - queryKey: ['magTable'], - queryFn: getMagTableData, - staleTime: 60000, -} +export const magTableLoader = (queryClient: QueryClient) => async () => { + const queryKey = ['magTable'] + const queryFn = () => getMagTableData() + const singleQuery = { + queryKey: queryKey, + queryFn: queryFn, + staleTime: 60000, + } -export const magTableLoader = (queryClient: QueryClient) => async () => - (await queryClient.getQueryData(query.queryKey)) ?? - (await queryClient.fetchQuery(query)) + return queryClient.ensureQueryData(singleQuery) +} diff --git a/src/loaders/possibleGainRefs.tsx b/src/loaders/possibleGainRefs.tsx index d09da41..5cd5f55 100644 --- a/src/loaders/possibleGainRefs.tsx +++ b/src/loaders/possibleGainRefs.tsx @@ -68,21 +68,19 @@ export const updateCurrentGainReference = async ( return response.data } -const query = (sessid: string) => { - return { - queryKey: ['gainRefs'], - queryFn: () => getGainRefData(sessid), - staleTime: 60000, - } -} - export const gainRefLoader = - (queryClient: QueryClient) => async (params: Params) => { - if (params.sessid) { - const singleQuery = query(params.sessid) - return ( - (await queryClient.getQueryData(singleQuery.queryKey)) ?? - (await queryClient.fetchQuery(singleQuery)) - ) + (queryClient: QueryClient) => + async ({ params }: { params: Params }) => { + const sessionId = params.sessid + if (!sessionId) return null + + const queryKey = ['gainRefs', sessionId] + const queryFn = () => getGainRefData(sessionId) + + const singleQuery = { + queryKey: queryKey, + queryFn: queryFn, + staleTime: 60000, } + return queryClient.ensureQueryData(singleQuery) } diff --git a/src/loaders/processingParameters.tsx b/src/loaders/processingParameters.tsx index 3a7e43b..e5159e7 100644 --- a/src/loaders/processingParameters.tsx +++ b/src/loaders/processingParameters.tsx @@ -16,7 +16,7 @@ export const getSessionProcessingParameterData = async ( return response.data } -const getProcessingParameterData = async (sessid: string = '0') => { +export const getProcessingParameterData = async (sessid: string = '0') => { const response = await client.get( `session_info/spa/sessions/${sessid}/spa_processing_parameters` ) @@ -47,38 +47,34 @@ export const updateSessionProcessingParameters = async ( return response.data } -const queryBuilder = (sessid: string = '0') => { - return { - queryKey: ['sessionId', sessid], - queryFn: () => getProcessingParameterData(sessid), - staleTime: 60000, - } -} - -const querySessionParamsBuilder = (sessid: string = '0') => { - return { - queryKey: ['sessionId', sessid], - queryFn: () => getSessionProcessingParameterData(sessid), - staleTime: 60000, - } -} - export const processingParametersLoader = - (queryClient: QueryClient) => async (params: Params) => { - const singleQuery = queryBuilder(params.sessid) - return ( - (await queryClient.getQueryData(singleQuery.queryKey)) ?? - (await queryClient.fetchQuery(singleQuery)) - ) + (queryClient: QueryClient) => + async ({ params }: { params: Params }) => { + const sessionId = params.sessid + if (!sessionId) return null + + const queryKey = ['extraProcessingParameters', sessionId] + const queryFn = () => getProcessingParameterData(sessionId) + const singleQuery = { + queryKey: queryKey, + queryFn: queryFn, + staleTime: 60000, + } + return queryClient.ensureQueryData(singleQuery) } export const sessionParametersLoader = - (queryClient: QueryClient) => async (params: Params) => { - const singleQuery = querySessionParamsBuilder(params.sessid) - return ( - (await queryClient.getQueryData(singleQuery.queryKey)) ?? - (await queryClient.fetchQuery(singleQuery)) - ) - } + (queryClient: QueryClient) => + async ({ params }: { params: Params }) => { + const sessionId = params.sessid + if (!sessionId) return null -export { getProcessingParameterData } + const queryKey = ['processingParameters', sessionId] + const queryFn = () => getSessionProcessingParameterData(sessionId) + const singleQuery = { + queryKey: queryKey, + queryFn: queryFn, + staleTime: 60000, + } + return queryClient.ensureQueryData(singleQuery) + } diff --git a/src/loaders/rsyncers.tsx b/src/loaders/rsyncers.tsx index 9f33345..e088e00 100644 --- a/src/loaders/rsyncers.tsx +++ b/src/loaders/rsyncers.tsx @@ -106,15 +106,19 @@ export const flushSkippedRsyncer = async ( } export const rsyncerLoader = - (queryClient: QueryClient) => async (params: Params) => { + (queryClient: QueryClient) => + async ({ params }: { params: Params }) => { const sessionId = params.sessid if (!sessionId) return null // Construct the queryKey and queryFn - const queryKey = ['sessid', sessionId] + const queryKey = ['rsyncers', sessionId] const queryFn = () => getRsyncerData(sessionId) + const singleQuery = { + queryKey: queryKey, + queryFn: queryFn, + staleTime: 60000, + } - // Ensure data is always fresh - await queryClient.invalidateQueries({ queryKey }) - return queryClient.ensureQueryData({ queryKey, queryFn }) + return queryClient.ensureQueryData(singleQuery) } diff --git a/src/loaders/sessionClients.tsx b/src/loaders/sessionClients.tsx index 3c9c6b8..2e5faa0 100644 --- a/src/loaders/sessionClients.tsx +++ b/src/loaders/sessionClients.tsx @@ -110,33 +110,32 @@ export const deleteSessionData = async (sessid: number) => { export const allSessionsLoader = (queryClient: QueryClient) => async () => { // Load the instrument name from sessionStorage const instrumentName = sessionStorage.getItem('instrumentName') - - // Skip loading logic if no instrument name was found if (!instrumentName) return null // Construct the query key and query function const queryKey = ['homepageSessions', instrumentName] const queryFn = () => getAllSessionsData(instrumentName) + const singleQuery = { + queryKey: queryKey, + queryFn: queryFn, + staleTime: 60000, + } - // Enssure data is always fresh - await queryClient.invalidateQueries({ queryKey }) - return queryClient.ensureQueryData({ queryKey, queryFn }) + return queryClient.ensureQueryData(singleQuery) } export const sessionLoader = - (queryClient: QueryClient) => async (params: Params) => { - const sessid = params.sessid - if (!sessid) return null - - const queryBuilder = (sessid: string = '0') => { - return { - queryKey: ['sessionId', sessid], - queryFn: () => getSessionData(sessid), - } + (queryClient: QueryClient) => + async ({ params }: { params: Params }) => { + const sessionId = params.sessid + if (!sessionId) return null + const queryKey = ['sessionInfo', sessionId] + const queryFn = () => getSessionData(sessionId) + const singleQuery = { + queryKey: queryKey, + queryFn: queryFn, + staleTime: 60000, } - const singleQuery = queryBuilder(params.sessid) - const queryKey = singleQuery.queryKey - await queryClient.invalidateQueries({ queryKey }) - return await queryClient.fetchQuery(singleQuery) + return queryClient.ensureQueryData(singleQuery) } diff --git a/src/loaders/visits.tsx b/src/loaders/visits.tsx index 3fed75e..e381ab0 100644 --- a/src/loaders/visits.tsx +++ b/src/loaders/visits.tsx @@ -23,22 +23,19 @@ const getVisitData = async (instrumentName: string) => { return response.data } -const query = (instrumentName: string) => { - return { - queryKey: ['visits', instrumentName], - queryFn: () => getVisitData(instrumentName), - staleTime: 60000, - } -} - export const visitLoader = - (queryClient: QueryClient) => async (params: Params) => { - if (params.instrumentName) { - const singleQuery = query(params.instrumentName) - return ( - (await queryClient.getQueryData(singleQuery.queryKey)) ?? - (await queryClient.fetchQuery(singleQuery)) - ) + (queryClient: QueryClient) => + async ({ params }: { params: Params }) => { + const instrumentName = params.instrumentName + if (!instrumentName) return null + + const queryKey = ['visits', instrumentName] + const queryFn = () => getVisitData(instrumentName) + const singleQuery = { + queryKey: queryKey, + queryFn: queryFn, + staleTime: 60000, } - return null + + return queryClient.ensureQueryData(singleQuery) } From 79b7a74f25871fa82d4f3e43d04165023c9b1c00 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Thu, 24 Jul 2025 15:09:50 +0000 Subject: [PATCH 20/27] Added logic to the instrument server connectivity checker function to trigger a query refetch when there is a change in connectivity status --- src/components/navbar.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index 37cad01..f5411f3 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -9,6 +9,7 @@ import { BoxProps, Icon, } from '@chakra-ui/react' +import { useQueryClient } from '@tanstack/react-query' import { getInstrumentConnectionStatus } from 'loaders/general' import React from 'react' import { @@ -46,13 +47,20 @@ export const Navbar = ({ React.useState(false) const navigate = useNavigate() const { isOpen, onOpen, onClose } = useDisclosure() + const queryClient = useQueryClient() + const instrumentName = sessionStorage.getItem('instrumentName') // Check connectivity every few seconds React.useEffect(() => { const resolveConnectionStatus = async () => { try { const status: boolean = await getInstrumentConnectionStatus() - setInsrumentConnectionStatus(status) + if (status !== instrumentConnectionStatus) { + setInsrumentConnectionStatus(status) + queryClient.refetchQueries({ + queryKey: ['instrumentServerConnection', instrumentName], + }) + } } catch (err) { console.error('Error checking connection status:', err) setInsrumentConnectionStatus(false) @@ -63,7 +71,7 @@ export const Navbar = ({ // Set it to run every 10s const interval = setInterval(resolveConnectionStatus, 10000) return () => clearInterval(interval) - }, []) + }, [instrumentName, instrumentConnectionStatus, queryClient]) return ( From 6fda141b1ccef34d7c1d602584d345b3717cd64c Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Thu, 24 Jul 2025 15:10:54 +0000 Subject: [PATCH 21/27] Removed need for passing in RSyncer finalisation and removal logic from parent component now that we are using queries to update the data --- src/components/rsyncCard.tsx | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/components/rsyncCard.tsx b/src/components/rsyncCard.tsx index 582d6b3..4c9e98f 100644 --- a/src/components/rsyncCard.tsx +++ b/src/components/rsyncCard.tsx @@ -42,15 +42,7 @@ import { components } from 'schema/main' type RSyncerInfo = components['schemas']['RSyncerInfo'] -export const RsyncCard = ({ - rsyncer, - onRemove, - onFinalise, -}: { - rsyncer: RSyncerInfo - onRemove: (id: number, source: string) => void - onFinalise: (id: number, source: string) => void -}) => { +export const RsyncCard = ({ rsyncer }: { rsyncer: RSyncerInfo }) => { const { isOpen, onOpen, onClose } = useDisclosure() const [action, setAction] = React.useState('finalise') @@ -67,12 +59,8 @@ export const RsyncCard = ({ const handleRsyncerAction = async () => { if (action === 'finalise') { await finaliseRsyncer(rsyncer.session_id, rsyncer.source) - // Run the function passed in from 'Session' - onFinalise(rsyncer.session_id, rsyncer.source) } else if (action === 'remove') { await removeRsyncer(rsyncer.session_id, rsyncer.source) - // Run the function passed in from 'Session' - onRemove(rsyncer.session_id, rsyncer.source) } onClose() } From d396247f4fdf2aebf3e822cd47f193fe7ed0d093 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Thu, 24 Jul 2025 15:12:10 +0000 Subject: [PATCH 22/27] Added logic to the 'SessionRow' component to update itself when a loss in instrument connectivity is detected --- src/components/sessionRow.tsx | 37 +++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/components/sessionRow.tsx b/src/components/sessionRow.tsx index 1ee0b27..e057364 100644 --- a/src/components/sessionRow.tsx +++ b/src/components/sessionRow.tsx @@ -21,7 +21,8 @@ import { VStack, useDisclosure, } from '@chakra-ui/react' -import { useQueryClient } from '@tanstack/react-query' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { getInstrumentConnectionStatus } from 'loaders/general' import { sessionTokenCheck } from 'loaders/jwt' import { finaliseSession } from 'loaders/rsyncers' import { deleteSessionData } from 'loaders/sessionClients' @@ -48,6 +49,8 @@ export const SessionRow = ({ // Set up React states const [sessionActive, setSessionActive] = React.useState(false) const [sessionFinalising, setSessionFinalising] = React.useState(false) + const [instrumentServerConnected, setInstrumentServerConnected] = + React.useState(false) // Set up utility hooks const { @@ -66,15 +69,35 @@ export const SessionRow = ({ if (response.success) { setSessionFinalising(true) } + console.log(`Session ${sessid} marked for cleanup`) onCloseCleanup() } + // Query for probing instrument connection status + const { data: instrmentServerConnectionStatus } = useQuery({ + queryKey: ['instrumentServerConnection', instrumentName], + queryFn: () => getInstrumentConnectionStatus(), + enabled: !!instrumentName, + initialData: sessionActive, + staleTime: 0, + }) + useEffect(() => { + console.log( + `Instrument server is connected:`, + instrmentServerConnectionStatus + ) + setInstrumentServerConnected(instrmentServerConnectionStatus) + }, [instrmentServerConnectionStatus]) + + // Run checks on the state of the session if there is + // a change in instrument server connection status useEffect(() => { sessionTokenCheck(session.id).then((active) => setSessionActive(active)) - checkMultigridControllerStatus(session.id.toString()).then((status) => + checkMultigridControllerStatus(session.id.toString()).then((status) => { setSessionFinalising(status.finalising) - ) - }, [session]) + console.log(`Session ${session.id} finalising:`, status.finalising) + }) + }, [session, instrumentServerConnected]) return ( @@ -186,9 +209,7 @@ export const SessionRow = ({ left="50%" transform="translate(-50%, -50%)" sx={{ - animation: - 'spin 2s linear infinite, glow 2s ease-in-out infinite', - // 'spin 2s linear infinite', + animation: `spin 2s linear infinite, glow-${session.id} 2s ease-in-out infinite`, filter: `drop-shadow(0 0 0px ${sessionFinalising ? 'red' : 'green'})`, '@keyframes spin': { from: { @@ -200,7 +221,7 @@ export const SessionRow = ({ 'translate(-50%, -50%) rotate(0deg)', }, }, - '@keyframes glow': { + [`@keyframes glow-${session.id}`]: { '0%': { filter: `drop-shadow(0 0 2px ${sessionFinalising ? 'red' : 'green'})`, }, From 57a3b1af42286fd5f703f2a75da10b8842b57cd9 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Thu, 24 Jul 2025 15:14:19 +0000 Subject: [PATCH 23/27] Added logs to the web socket handler, and use query refetch to update RSyncer information for a given session --- src/components/webSocketHandler.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/webSocketHandler.tsx b/src/components/webSocketHandler.tsx index 6077f46..8210867 100644 --- a/src/components/webSocketHandler.tsx +++ b/src/components/webSocketHandler.tsx @@ -30,12 +30,17 @@ export const WebSocketHandler = () => { if (parsedMessage.target === 'rsyncer') { let sessionID: string | null = parsedMessage.session_id if (!sessionID) return null - queryClient.refetchQueries({ queryKey: ['sessid', sessionID] }) + console.log( + `Received message to update rsyncer data for session`, + sessionID + ) + queryClient.refetchQueries({ queryKey: ['rsyncers', sessionID] }) } // Update instrument name queries when a change to sessions is detected if (parsedMessage.target === 'sessions') { let instrumentName: string | null = parsedMessage.instrument_name if (!instrumentName) return null + console.log(`Received message to re-fetch data for`, instrumentName) queryClient.refetchQueries({ queryKey: ['homepageSessions', instrumentName], }) From b44a9a4e27f4cfb92af79907c09f76549a310068 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Thu, 24 Jul 2025 15:17:16 +0000 Subject: [PATCH 24/27] Tidied up messages in 'GridSquares' --- src/routes/GridSquares.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/GridSquares.tsx b/src/routes/GridSquares.tsx index 50bd8d2..74934df 100644 --- a/src/routes/GridSquares.tsx +++ b/src/routes/GridSquares.tsx @@ -6,10 +6,10 @@ import { components } from 'schema/main' type GridSquare = components['schemas']['GridSquare'] export const GridSquares = () => { - console.log('gather grid squares') + console.log('Getting grid squares') const gridSquares = useLoaderData() as GridSquare[] console.log( - 'grid squares', + 'Grid squares:', gridSquares, typeof gridSquares, gridSquares.length From 08cee5bf4238dd99d90c75428a81cf6bfbe91a49 Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Thu, 24 Jul 2025 15:19:23 +0000 Subject: [PATCH 25/27] Use loader data for 'Home' route to provide initial data for its query --- src/routes/Home.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index 3d4092d..f68e4eb 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -11,7 +11,7 @@ import { useQuery } from '@tanstack/react-query' import { InstrumentCard } from 'components/instrumentCard' import { SessionRow } from 'components/sessionRow' import { getAllSessionsData } from 'loaders/sessionClients' -import { Link as LinkRouter } from 'react-router-dom' +import { Link as LinkRouter, useLoaderData } from 'react-router-dom' import { components } from 'schema/main' type Session = components['schemas']['Session'] @@ -20,9 +20,11 @@ export const Home = () => { const queryKey = ['homepageSessions', instrumentName] const queryFn = () => getAllSessionsData(instrumentName ? instrumentName : '') + const preloadedData = useLoaderData() const { data, isLoading, isError } = useQuery({ queryKey, queryFn, + initialData: preloadedData, enabled: !!instrumentName, staleTime: 0, // Always refetch on mount unless preloaded }) From 3e3ccdeb49205ef79fc6e3c97f4c6c2869b9582c Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Thu, 24 Jul 2025 15:20:19 +0000 Subject: [PATCH 26/27] Load session parameters for a given session via query instead --- src/routes/SessionParameters.tsx | 34 +++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/routes/SessionParameters.tsx b/src/routes/SessionParameters.tsx index b1506c7..b82ff11 100644 --- a/src/routes/SessionParameters.tsx +++ b/src/routes/SessionParameters.tsx @@ -15,7 +15,11 @@ import { } from '@chakra-ui/react' import { useDisclosure } from '@chakra-ui/react' import { Table } from '@diamondlightsource/ui-components' -import { updateSessionProcessingParameters } from 'loaders/processingParameters' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { + updateSessionProcessingParameters, + getSessionProcessingParameterData, +} from 'loaders/processingParameters' import React from 'react' import { Link as LinkRouter, useLoaderData, useParams } from 'react-router-dom' import { components } from 'schema/main' @@ -40,10 +44,26 @@ const nameLabelMap: Map = new Map([ ['eer_fractionation_file', 'EER fractionation file (for motion correction)'], ]) -const SessionParameters = () => { - const { isOpen, onOpen, onClose } = useDisclosure() +export const SessionParameters = () => { + // Load necessary data const { sessid } = useParams() - const sessionParams = useLoaderData() as EditableSessionParameters | null + const preloadedData = useLoaderData() + const queryKey = ['processingParameters', sessid] + const queryFn = () => getSessionProcessingParameterData(sessid) + const { data, isLoading, isError } = useQuery({ + queryKey, + queryFn, + initialData: preloadedData, + staleTime: 0, + }) + const sessionParams = data as EditableSessionParameters | null + + const queryClient = useQueryClient() + + // Set component hooks + const { isOpen, onOpen, onClose } = useDisclosure() + + // Construct parameters table to display let tableRows = [] as ProcessingRow[] type EditableParameter = | 'gain_ref' @@ -73,8 +93,8 @@ const SessionParameters = () => { symmetry: paramKey === 'symmetry' ? paramValue : '', } await updateSessionProcessingParameters(sessid ?? '0', data) + queryClient.refetchQueries({ queryKey: ['processingParameters', sessid] }) onClose() - window.location.reload() } const editParameterDialogue = async ( @@ -88,6 +108,8 @@ const SessionParameters = () => { onOpen() } + if (isLoading) return

Loading processing parameters for session...

+ if (isError) return

Error loading processing parameters for session.

return (
@@ -154,5 +176,3 @@ const SessionParameters = () => {
) } - -export { SessionParameters } From f7b2df516f70dfb4c79b46cc0fe42960b634095f Mon Sep 17 00:00:00 2001 From: Eu Pin Tien Date: Thu, 24 Jul 2025 15:21:39 +0000 Subject: [PATCH 27/27] Updated data fetching logic in 'Session' page * Use a query to fetch and update RSyncer data from the backend * Remove the need to pass functions to the RSyncCard components now that we are using queries * Added logic to stop polling when the instrument server connection is lost --- src/routes/Session.tsx | 110 +++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 58 deletions(-) diff --git a/src/routes/Session.tsx b/src/routes/Session.tsx index a64bd71..d2a6227 100644 --- a/src/routes/Session.tsx +++ b/src/routes/Session.tsx @@ -24,9 +24,11 @@ import { Switch, VStack, } from '@chakra-ui/react' +import { useQuery } from '@tanstack/react-query' import { InstrumentCard } from 'components/instrumentCard' import { RsyncCard } from 'components/rsyncCard' import { UpstreamVisitCard } from 'components/upstreamVisitsCard' +import { getInstrumentConnectionStatus } from 'loaders/general' import { sessionTokenCheck, sessionHandshake } from 'loaders/jwt' import { getMachineConfigData } from 'loaders/machineConfig' import { @@ -34,13 +36,7 @@ import { setupMultigridWatcher, } from 'loaders/multigridSetup' import { getSessionProcessingParameterData } from 'loaders/processingParameters' -import { - getRsyncerData, - pauseRsyncer, - removeRsyncer, - finaliseRsyncer, - finaliseSession, -} from 'loaders/rsyncers' +import { getRsyncerData, pauseRsyncer, finaliseSession } from 'loaders/rsyncers' import { updateVisitEndTime, getSessionData } from 'loaders/sessionClients' import { checkMultigridControllerStatus } from 'loaders/sessionSetup' import React, { useEffect, useCallback } from 'react' @@ -63,12 +59,16 @@ type MultigridWatcherSpec = components['schemas']['MultigridWatcherSetup'] export const Session = () => { // ---------------------------------------------------------------------------------- // Routing and loader context - const { sessid } = useParams() - const rsyncerLoaderData = useLoaderData() as RSyncerInfo[] | null + const { sessid } = useParams() as { sessid: string } + const instrumentName = sessionStorage.getItem('instrumentName') const navigate = useNavigate() // ---------------------------------------------------------------------------------- // State hooks + // Instrument server connection + const [instrumentServerConnected, setInstrumentServerConnected] = + React.useState(false) + // Session information const [session, setSession] = React.useState() const [sessionActive, setSessionActive] = React.useState(false) @@ -89,11 +89,45 @@ export const Session = () => { const [machineConfig, setMachineConfig] = React.useState() // Rsyncer information - const [rsyncers, setRsyncers] = React.useState( - rsyncerLoaderData ?? [] - ) + const [rsyncers, setRsyncers] = React.useState([]) const [rsyncersPaused, setRsyncersPaused] = React.useState(false) + // ---------------------------------------------------------------------------------- + // Load Rsyncer data via a polling query + const preloadedRsyncerData = useLoaderData() as RSyncerInfo[] | null + const { + data: rsyncerData, + isLoading, + isError, + } = useQuery({ + queryKey: ['rsyncers', sessid], + queryFn: () => getRsyncerData(sessid), + enabled: !!sessid, + initialData: preloadedRsyncerData, + staleTime: 0, + refetchInterval: sessionActive ? 5000 : false, + }) + useEffect(() => { + if (!rsyncerData) return + setRsyncers(rsyncerData) + }, [rsyncerData]) + + // Set up a query to probe the instrument server connection status + const { data: instrmentServerConnectionStatus } = useQuery({ + queryKey: ['instrumentServerConnection', instrumentName], + queryFn: () => getInstrumentConnectionStatus(), + enabled: !!sessid, + initialData: sessionActive, + staleTime: 0, + }) + useEffect(() => { + console.log( + `Instrument server is connected:`, + instrmentServerConnectionStatus + ) + setInstrumentServerConnected(instrmentServerConnectionStatus) + }, [instrmentServerConnectionStatus]) + // ---------------------------------------------------------------------------------- // UI utility hooks const { isOpen, onOpen, onClose } = useDisclosure() @@ -184,49 +218,7 @@ export const Session = () => { loadSession() }, [sessid, loadSession]) - // Poll Rsyncer every few seconds - useEffect(() => { - // Don't run it if a session is inactive or its session ID is not set - if (!sessid || !sessionActive) return - - const fetchData = async () => { - try { - const data = await getRsyncerData(sessid) - setRsyncers(data) - } catch (err) { - console.error('Error polling rsyncers:', err) - } - } - fetchData() // Fetch data once - - // Set it to run every 5s - const interval = setInterval(fetchData, 5000) - return () => clearInterval(interval) - }, [sessid, sessionActive]) - // Other Rsync-related functions - const handleRemoveRsyncer = async (sessionId: number, source: string) => { - // Safely update the displayed Rsync cards after a 'remove' call is made - try { - await removeRsyncer(sessionId, source) - const updatedData = await getRsyncerData(String(sessionId)) - setRsyncers(updatedData) - } catch (err) { - console.error('Failed to remove rsyncer:', err) - } - } - - const handleFinaliseRsyncer = async (sessionId: number, source: string) => { - // Safely update the displayed Rsync cards after a 'finalise' call is made - try { - await finaliseRsyncer(sessionId, source) - const updatedData = await getRsyncerData(String(sessionId)) - setRsyncers(updatedData) - } catch (err) { - console.error('Failed to finalise rsyncer:', err) - } - } - const finaliseAll = async () => { if (sessid) await finaliseSession(parseInt(sessid)) onClose() @@ -251,15 +243,18 @@ export const Session = () => { return r.transferring } + // Update the state of the session when a change in + // instrument server connection status occurs const checkSessionActivationState = useCallback(async () => { if (sessid !== undefined) { const activationState = await sessionTokenCheck(parseInt(sessid)) setSessionActive(activationState) + console.log(`Session is active:`, activationState) } }, [sessid]) useEffect(() => { checkSessionActivationState() - }, [checkSessionActivationState]) + }, [checkSessionActivationState, instrumentServerConnected]) // Set the default visit end time (in UTC) if none was provided const defaultVisitEndTime = session?.visit_end_time @@ -336,6 +331,8 @@ export const Session = () => { } } + if (isLoading) return

Loading RSyncer data...

+ if (isError) return

Error loading RSyncer data

return (
@@ -540,9 +537,6 @@ export const Session = () => { ) )