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 }