diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index 2d9560f..f5411f3 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -2,7 +2,6 @@ import { Box, Flex, HStack, - Link, IconButton, useDisclosure, Image, @@ -10,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 { @@ -19,7 +19,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,16 +43,24 @@ 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() + 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) @@ -60,10 +68,10 @@ 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) - }, []) + }, [instrumentName, instrumentConnectionStatus, queryClient]) return ( @@ -95,51 +103,55 @@ 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' }} + /> + - + + + 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/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() } diff --git a/src/components/sessionRow.tsx b/src/components/sessionRow.tsx index 6e2dda9..e057364 100644 --- a/src/components/sessionRow.tsx +++ b/src/components/sessionRow.tsx @@ -1,8 +1,10 @@ import { + Box, Button, GridItem, Heading, HStack, + Icon, IconButton, Link, Modal, @@ -19,18 +21,38 @@ import { VStack, useDisclosure, } from '@chakra-ui/react' +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' +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'] -export const SessionRow = (session: Session) => { +type SessionRowProps = { + session: Session + instrumentName: string | null +} +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) + const [instrumentServerConnected, setInstrumentServerConnected] = + React.useState(false) + + // Set up utility hooks const { isOpen: isOpenDelete, onOpen: onOpenDelete, @@ -43,15 +65,39 @@ export const SessionRow = (session: Session) => { } = useDisclosure() const cleanupSession = async (sessid: number) => { - await finaliseSession(sessid) + const response = await finaliseSession(sessid) + if (response.success) { + setSessionFinalising(true) + } + console.log(`Session ${sessid} marked for cleanup`) onCloseCleanup() } - const [sessionActive, setSessionActive] = React.useState(false) + // 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)) - }, [session]) + checkMultigridControllerStatus(session.id.toString()).then((status) => { + setSessionFinalising(status.finalising) + console.log(`Session ${session.id} finalising:`, status.finalising) + }) + }, [session, instrumentServerConnected]) return ( @@ -78,7 +124,10 @@ export const SessionRow = (session: Session) => { variant="ghost" onClick={() => { deleteSessionData(session.id).then(() => - window.location.reload() + // Refetch session information for this instrument + queryClient.refetchQueries({ + queryKey: ['homepageSessions', instrumentName], + }) ) }} > @@ -141,11 +190,63 @@ export const SessionRow = (session: Session) => { > {session.name}: {session.id} - {sessionActive ? ( - - ) : ( - <> - )} + + {sessionActive ? ( + // Show a pulsing spinning sync icon when running + + ) : ( + // Show a sync error icon when disconnected + + )} + @@ -155,7 +256,7 @@ export const SessionRow = (session: Session) => { aria-label="Delete session" icon={} onClick={onOpenDelete} - isDisabled={sessionActive} + isDisabled={sessionActive || sessionFinalising} /> @@ -163,7 +264,7 @@ export const SessionRow = (session: Session) => { aria-label="Clean up session" icon={} onClick={onOpenCleanup} - isDisabled={!sessionActive} + isDisabled={!sessionActive || sessionFinalising} /> diff --git a/src/components/webSocketHandler.tsx b/src/components/webSocketHandler.tsx new file mode 100644 index 0000000..8210867 --- /dev/null +++ b/src/components/webSocketHandler.tsx @@ -0,0 +1,79 @@ +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 + 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], + }) + } + } + } + + // 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/index.tsx b/src/index.tsx index 4e3e39c..32efdb1 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,13 +65,13 @@ const router = createBrowserRouter([ path: '/home', element: , errorElement: , - loader: sessionsLoader(queryClient), + loader: allSessionsLoader(queryClient), }, { 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 4051a8a..e088e00 100644 --- a/src/loaders/rsyncers.tsx +++ b/src/loaders/rsyncers.tsx @@ -105,19 +105,20 @@ 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 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 + + // Construct the queryKey and queryFn + const queryKey = ['rsyncers', sessionId] + const queryFn = () => getRsyncerData(sessionId) + const singleQuery = { + queryKey: queryKey, + queryFn: queryFn, + staleTime: 60000, + } + + return queryClient.ensureQueryData(singleQuery) } diff --git a/src/loaders/sessionClients.tsx b/src/loaders/sessionClients.tsx index 535dee5..2e5faa0 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 getSessionsData = 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 { @@ -107,46 +107,35 @@ 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') + if (!instrumentName) return null -const queryBuilder = (sessid: string = '0') => { - return { - queryKey: ['sessionId', sessid], - queryFn: () => getSessionData(sessid), + // Construct the query key and query function + const queryKey = ['homepageSessions', instrumentName] + const queryFn = () => getAllSessionsData(instrumentName) + const singleQuery = { + queryKey: queryKey, + queryFn: queryFn, staleTime: 60000, } + + return queryClient.ensureQueryData(singleQuery) } export const sessionLoader = - (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 = ['sessionInfo', sessionId] + const queryFn = () => getSessionData(sessionId) + const singleQuery = { + queryKey: queryKey, + queryFn: queryFn, + staleTime: 60000, + } + + return queryClient.ensureQueryData(singleQuery) } 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/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) } 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 diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index d659e9f..f68e4eb 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -7,74 +7,34 @@ import { Link, VStack, } from '@chakra-ui/react' +import { useQuery } from '@tanstack/react-query' 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 useWebSocket from 'react-use-websocket' +import { getAllSessionsData } from 'loaders/sessionClients' +import { Link as LinkRouter, useLoaderData } from 'react-router-dom' 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 - const sessions = useLoaderData() as { - current: Session[] - } | null + const instrumentName = sessionStorage.getItem('instrumentName') + const queryKey = ['homepageSessions', instrumentName] + const queryFn = () => getAllSessionsData(instrumentName ? instrumentName : '') - // Clean the URL after loading the page - const [searchParams] = useSearchParams() - const navigate = useNavigate() - useEffect(() => { - if (searchParams.has('instrumentName')) { - navigate('/home', { replace: true }) - } - }, [searchParams, navigate]) + const preloadedData = useLoaderData() + const { data, isLoading, isError } = useQuery({ + queryKey, + queryFn, + initialData: preloadedData, + enabled: !!instrumentName, + staleTime: 0, // Always refetch on mount unless preloaded + }) - const [UUID, setUUID] = React.useState('') - const baseUrl = - sessionStorage.getItem('murfeyServerURL') ?? - process.env.REACT_APP_API_ENDPOINT - const url = baseUrl ? baseUrl.replace('http', 'ws') : 'ws://localhost:8000' - const parseWebsocketMessage = (message: any) => { - let parsedMessage: any = {} - try { - parsedMessage = JSON.parse(message) - } catch (err) { - return - } - if (parsedMessage.message === 'refresh') { - window.location.reload() - } - } + if (isLoading) return

Loading sessions...

+ if (isError || !data) return

Failed to load sessions.

- // 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 - ) + const sessions = data as { + current: Session[] + } | null return (
@@ -101,7 +61,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`} > @@ -117,7 +77,10 @@ export const Home = () => { sessions.current.map((current) => { return ( - {SessionRow(current)} + ) }) 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') } diff --git a/src/routes/Session.tsx b/src/routes/Session.tsx index 7c3e1bf..d2a6227 100644 --- a/src/routes/Session.tsx +++ b/src/routes/Session.tsx @@ -23,11 +23,12 @@ import { Stack, Switch, VStack, - useToast, } 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 { @@ -35,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' @@ -53,10 +48,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'] @@ -66,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) @@ -91,15 +88,46 @@ 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 ?? [] - ) + 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() @@ -116,64 +144,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) => { @@ -199,7 +169,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 && @@ -248,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() @@ -315,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 @@ -395,9 +326,13 @@ export const Session = () => { parseInt(sessid) ) await startMultigridWatcher(parseInt(sessid)) + await checkSessionActivationState() + onCloseReconnect() } } + if (isLoading) return

Loading RSyncer data...

+ if (isError) return

Error loading RSyncer data

return (
@@ -602,9 +537,6 @@ export const Session = () => { ) ) 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 }