Moved more common functions into ApiServerContext

This commit is contained in:
Tom Butcher 2025-07-14 23:04:18 +01:00
parent b71537dc64
commit 3ad0002bbb
2 changed files with 477 additions and 225 deletions

View File

@ -4,7 +4,8 @@ import React, {
useEffect, useEffect,
useState, useState,
useContext, useContext,
useRef useRef,
useCallback
} from 'react' } from 'react'
import io from 'socket.io-client' import io from 'socket.io-client'
import { message, notification, Modal, Space, Button } from 'antd' import { message, notification, Modal, Space, Button } from 'antd'
@ -22,8 +23,9 @@ logger.setLevel(config.logLevel)
const ApiServerContext = createContext() const ApiServerContext = createContext()
const ApiServerProvider = ({ children }) => { const ApiServerProvider = ({ children }) => {
const { token, userProfile } = useContext(AuthContext) const { token, userProfile, authenticated } = useContext(AuthContext)
const socketRef = useRef(null) const socketRef = useRef(null)
const [connected, setConnected] = useState(false)
const [connecting, setConnecting] = useState(false) const [connecting, setConnecting] = useState(false)
const [error, setError] = useState(null) const [error, setError] = useState(null)
const [messageApi, contextHolder] = message.useMessage() const [messageApi, contextHolder] = message.useMessage()
@ -32,9 +34,43 @@ const ApiServerProvider = ({ children }) => {
const [showErrorModal, setShowErrorModal] = useState(false) const [showErrorModal, setShowErrorModal] = useState(false)
const [errorModalContent, setErrorModalContent] = useState('') const [errorModalContent, setErrorModalContent] = useState('')
const [retryCallback, setRetryCallback] = useState(null) const [retryCallback, setRetryCallback] = useState(null)
const subscribedCallbacksRef = useRef(new Map())
const subscribedLockCallbacksRef = useRef(new Map())
useEffect(() => { const notifyLockUpdate = useCallback(
if (token) { async (lockData) => {
logger.debug('Notifying lock update:', lockData)
const objectId = lockData._id || lockData.id
if (
objectId &&
subscribedLockCallbacksRef.current.has(objectId) &&
lockData.user != userProfile?._id
) {
const callbacks = subscribedLockCallbacksRef.current.get(objectId)
logger.debug(
`Calling ${callbacks.length} lock callbacks for object:`,
objectId
)
callbacks.forEach((callback) => {
try {
callback(lockData)
} catch (error) {
logger.error('Error in lock update callback:', error)
}
})
} else {
logger.debug(
`No lock callbacks found for object: ${objectId}, subscribed lock callbacks:`,
Array.from(subscribedLockCallbacksRef.current.keys())
)
}
},
[userProfile?._id]
)
const connectToServer = useCallback(() => {
if (token && authenticated == true) {
logger.debug('Token is available, connecting to api server...') logger.debug('Token is available, connecting to api server...')
const newSocket = io(config.apiServerUrl, { const newSocket = io(config.apiServerUrl, {
@ -48,18 +84,25 @@ const ApiServerProvider = ({ children }) => {
newSocket.on('connect', () => { newSocket.on('connect', () => {
logger.debug('Api Server connected') logger.debug('Api Server connected')
setConnecting(false) setConnecting(false)
setConnected(true)
setError(null) setError(null)
}) })
newSocket.on('notify_object_update', notifyObjectUpdate)
newSocket.on('notify_object_new', notifyObjectNew)
newSocket.on('notify_lock_update', notifyLockUpdate)
newSocket.on('disconnect', () => { newSocket.on('disconnect', () => {
logger.debug('Api Server disconnected') logger.debug('Api Server disconnected')
setError('Api Server disconnected') setError('Api Server disconnected')
setConnected(false)
}) })
newSocket.on('connect_error', (err) => { newSocket.on('connect_error', (err) => {
logger.error('Api Server connection error:', err) logger.error('Api Server connection error:', err)
messageApi.error('Api Server connection error: ' + err.message) messageApi.error('Api Server connection error: ' + err.message)
setError('Api Server connection error') setError('Api Server connection error')
setConnected(false)
}) })
newSocket.on('bridge.notification', (data) => { newSocket.on('bridge.notification', (data) => {
@ -75,6 +118,17 @@ const ApiServerProvider = ({ children }) => {
}) })
socketRef.current = newSocket socketRef.current = newSocket
}
}, [token, authenticated, messageApi, notificationApi, notifyLockUpdate])
useEffect(() => {
if (token && authenticated == true) {
connectToServer()
} else if (!token && socketRef.current) {
logger.debug('Token not available, disconnecting api server...')
socketRef.current.disconnect()
socketRef.current = null
}
// Clean up function // Clean up function
return () => { return () => {
@ -84,12 +138,7 @@ const ApiServerProvider = ({ children }) => {
socketRef.current = null socketRef.current = null
} }
} }
} else if (!token && socketRef.current) { }, [token, authenticated, connectToServer])
logger.debug('Token not available, disconnecting api server...')
socketRef.current.disconnect()
socketRef.current = null
}
}, [token, messageApi])
const lockObject = (id, type) => { const lockObject = (id, type) => {
logger.debug('Locking ' + id) logger.debug('Locking ' + id)
@ -118,8 +167,12 @@ const ApiServerProvider = ({ children }) => {
type: type type: type
}, },
(lockEvent) => { (lockEvent) => {
logger.debug('Received lock event for object:', id, lockEvent) logger.debug('Received lock status for object:', id, lockEvent)
if (lockEvent.user != userProfile?._id) {
resolve(lockEvent) resolve(lockEvent)
} else {
resolve(null)
}
} }
) )
logger.debug('Sent fetch lock command for object:', id) logger.debug('Sent fetch lock command for object:', id)
@ -127,65 +180,186 @@ const ApiServerProvider = ({ children }) => {
} }
} }
const onLockEvent = (id, callback) => { const notifyObjectUpdate = async (object) => {
if (socketRef.current && socketRef.current.connected == true) { logger.debug('Notifying object update:', object)
const eventHandler = (data) => { const objectId = object._id || object.id
if (data._id === id && data?.user !== userProfile._id) {
if (objectId && subscribedCallbacksRef.current.has(objectId)) {
const callbacks = subscribedCallbacksRef.current.get(objectId)
logger.debug( logger.debug(
'Lock update received for object:', `Calling ${callbacks.length} callbacks for object:`,
id, objectId
'locked:',
data.locked
) )
callback(data) callbacks.forEach((callback) => {
try {
callback(object)
} catch (error) {
logger.error('Error in object update callback:', error)
} }
} })
} else {
socketRef.current.on('notify_lock_update', eventHandler)
logger.debug('Registered lock event listener for object:', id)
// Return cleanup function
return () => offLockEvent(id, eventHandler)
}
}
const offLockEvent = (id, eventHandler) => {
if (socketRef.current && socketRef.current.connected == true) {
socketRef.current.off('notify_lock_update', eventHandler)
logger.debug('Removed lock event listener for object:', id)
}
}
const onUpdateEvent = (id, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
const eventHandler = (data) => {
if (data._id === id && data?.user !== userProfile._id) {
logger.debug( logger.debug(
'Update event received for object:', `No callbacks found for object: ${objectId}, subscribed callbacks:`,
id, Array.from(subscribedCallbacksRef.current.keys())
'updatedAt:',
data.updatedAt
) )
callback(data)
} }
} }
socketRef.current.on('notify_object_update', eventHandler) const notifyObjectNew = async (object) => {
logger.debug('Notifying object new:', object)
const objectType = object.type || 'unknown'
if (objectType && subscribedCallbacksRef.current.has(objectType)) {
const callbacks = subscribedCallbacksRef.current.get(objectType)
logger.debug(
`Calling ${callbacks.length} callbacks for type:`,
objectType
)
callbacks.forEach((callback) => {
try {
callback(object)
} catch (error) {
logger.error('Error in object new callback:', error)
}
})
} else {
logger.debug(
`No callbacks found for object: ${objectType}, subscribed callbacks:`,
Array.from(subscribedCallbacksRef.current.keys())
)
}
}
const offUpdateEvent = useCallback((id, type, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
// Remove callback from the subscribed callbacks map
if (subscribedCallbacksRef.current.has(id)) {
const callbacks = subscribedCallbacksRef.current
.get(id)
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedCallbacksRef.current.delete(id)
socketRef.current.emit('unsubscribe', { id: id, type: type })
} else {
subscribedCallbacksRef.current.set(id, callbacks)
}
}
logger.debug('Removed update event listener for object:', id)
}
}, [])
const offTypeEvent = useCallback((type, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
// Remove callback from the subscribed callbacks map
if (subscribedCallbacksRef.current.has(type)) {
const callbacks = subscribedCallbacksRef.current
.get(type)
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedCallbacksRef.current.delete(type)
socketRef.current.emit('unsubscribe', { type: type })
} else {
subscribedCallbacksRef.current.set(type, callbacks)
}
}
logger.debug('Removed new event listener for type:', type)
}
}, [])
const subscribeToObject = useCallback(
(id, type, callback) => {
logger.debug('Subscribing to object:', id, 'type:', type)
if (socketRef.current && socketRef.current.connected == true) {
// Add callback to the subscribed callbacks map immediately
if (!subscribedCallbacksRef.current.has(id)) {
subscribedCallbacksRef.current.set(id, [])
}
subscribedCallbacksRef.current.get(id).push(callback)
logger.debug(
`Added callback for object ${id}, total callbacks: ${subscribedCallbacksRef.current.get(id).length}`
)
socketRef.current.emit('subscribe', { id: id, type: type })
logger.debug('Registered update event listener for object:', id) logger.debug('Registered update event listener for object:', id)
// Return cleanup function // Return cleanup function
return () => offUpdateEvent(id, eventHandler) return () => offUpdateEvent(id, type, callback)
}
} }
},
[offUpdateEvent]
)
const offUpdateEvent = (id, eventHandler) => { const subscribeToType = useCallback(
(type, callback) => {
logger.debug('Subscribing to type:', type)
if (socketRef.current && socketRef.current.connected == true) { if (socketRef.current && socketRef.current.connected == true) {
socketRef.current.off('notify_update', eventHandler) // Add callback to the subscribed callbacks map immediately
logger.debug('Removed update event listener for object:', id) if (!subscribedCallbacksRef.current.has(type)) {
subscribedCallbacksRef.current.set(type, [])
}
subscribedCallbacksRef.current.get(type).push(callback)
logger.debug(
`Added callback for type ${type}, total callbacks: ${subscribedCallbacksRef.current.get(type).length}`
)
socketRef.current.emit('subscribe', { type: type })
logger.debug('Registered update event listener for object:', type)
// Return cleanup function
return () => offTypeEvent(type, callback)
}
},
[offTypeEvent]
)
const offLockEvent = useCallback((id, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
// Remove callback from the subscribed lock callbacks map
if (subscribedLockCallbacksRef.current.has(id)) {
const callbacks = subscribedLockCallbacksRef.current
.get(id)
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedLockCallbacksRef.current.delete(id)
} else {
subscribedLockCallbacksRef.current.set(id, callbacks)
} }
} }
const showError = (content, callback = null) => { logger.debug('Removed lock event listener for object:', id)
}
}, [])
const subscribeToLock = useCallback(
(id, type, callback) => {
logger.debug('Subscribing to lock for object:', id, 'type:', type)
if (socketRef.current && socketRef.current.connected == true) {
// Add callback to the subscribed lock callbacks map immediately
if (!subscribedLockCallbacksRef.current.has(id)) {
subscribedLockCallbacksRef.current.set(id, [])
}
subscribedLockCallbacksRef.current.get(id).push(callback)
logger.debug(
`Added lock callback for object ${id}, total lock callbacks: ${subscribedLockCallbacksRef.current.get(id).length}`
)
socketRef.current.emit('subscribe_lock', { id: id, type: type })
logger.debug('Registered lock event listener for object:', id)
// Return cleanup function
return () => offLockEvent(id, callback)
}
},
[offLockEvent]
)
const showError = (error, callback = null) => {
var content = `Error ${error.code} (${error.status}): ${error.message}`
if (error.response?.data?.error) {
content = `${error.response?.data?.error} (${error.status})`
}
setErrorModalContent(content) setErrorModalContent(content)
setRetryCallback(() => callback) setRetryCallback(() => callback)
setShowErrorModal(true) setShowErrorModal(true)
@ -200,8 +374,8 @@ const ApiServerProvider = ({ children }) => {
setRetryCallback(null) setRetryCallback(null)
} }
// Generalized fetchObjectInfo function // Generalized fetchObject function
const fetchObjectInfo = async (id, type) => { const fetchObject = async (id, type) => {
const fetchUrl = `${config.backendUrl}/${type}s/${id}` const fetchUrl = `${config.backendUrl}/${type}s/${id}`
setFetchLoading(true) setFetchLoading(true)
logger.debug('Fetching from ' + fetchUrl) logger.debug('Fetching from ' + fetchUrl)
@ -214,62 +388,17 @@ const ApiServerProvider = ({ children }) => {
}) })
return response.data return response.data
} catch (err) { } catch (err) {
logger.error('Failed to fetch object information:', err) showError(err, () => {
// Don't automatically show error - let the component handle it fetchObject(id, type)
throw err })
return {}
} finally { } finally {
setFetchLoading(false) setFetchLoading(false)
} }
} }
// Update filament information
const updateObjectInfo = async (id, type, value) => {
const updateUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}`
logger.debug('Updating info for ' + id)
try {
const response = await axios.put(updateUrl, value, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
logger.debug('Filament updated successfully')
if (socketRef.current && socketRef.current.connected == true) {
await socketRef.current.emit('update', {
_id: id,
type: type,
updatedAt: response.data.updatedAt
})
}
return response.data
} catch (err) {
logger.error('Failed to update filament information:', err)
// Don't automatically show error - let the component handle it
throw err
}
}
// Update filament information
const createObject = async (type, value) => {
const createUrl = `${config.backendUrl}/${type.toLowerCase()}s`
logger.debug('Creating object...')
try {
const response = await axios.post(createUrl, value, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
return response.data
} catch (err) {
logger.error('Failed to update filament information:', err)
// Don't automatically show error - let the component handle it
throw err
}
}
// Fetch table data with pagination, filtering, and sorting // Fetch table data with pagination, filtering, and sorting
const fetchTableData = async (type, params = {}) => { const fetchObjects = async (type, params = {}) => {
const { const {
page = 1, page = 1,
limit = 25, limit = 25,
@ -319,9 +448,122 @@ const ApiServerProvider = ({ children }) => {
hasMore, hasMore,
page page
} }
} catch (error) { } catch (err) {
logger.error('Failed to fetch table data:', error) showError(err, () => {
throw error fetchObjects(type, params)
})
return []
}
}
// Fetch table data with pagination, filtering, and sorting
const fetchObjectsByProperty = async (type, params = {}) => {
const { filter = {}, properties = [] } = params
logger.debug('Fetching property object data from:', type, {
properties,
filter
})
try {
const response = await axios.get(
`${config.backendUrl}/${type.toLowerCase()}s/properties`,
{
params: {
...filter,
properties: properties.join(',') // Convert array to comma-separated string
},
headers: {
Accept: 'application/json'
},
withCredentials: true
}
)
const newData = response.data
return newData
} catch (err) {
showError(err, () => {
fetchObjectsByProperty(type, params)
})
return []
}
}
// Update filament information
const updateObject = async (id, type, value) => {
const updateUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}`
logger.debug('Updating info for ' + id)
try {
const response = await axios.put(updateUrl, value, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
logger.debug('Object updated successfully')
if (socketRef.current && socketRef.current.connected == true) {
await socketRef.current.emit('update', {
_id: id,
type: type,
updatedAt: response.data.updatedAt
})
}
return response.data
} catch (err) {
setError(err, () => {
updateObject(id, type, value)
})
return {}
}
}
// Update filament information
const deleteObject = async (id, type) => {
const deleteUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}`
logger.debug('Deleting object ID: ' + id)
try {
const response = await axios.delete(deleteUrl, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
logger.debug('Object deleted successfully')
if (socketRef.current && socketRef.current.connected == true) {
await socketRef.current.emit('update', {
_id: id,
type: type,
updatedAt: response.data.updatedAt
})
}
return response.data
} catch (err) {
showError(err, () => {
deleteObject(id, type)
})
return {}
}
}
// Update filament information
const createObject = async (type, value) => {
const createUrl = `${config.backendUrl}/${type.toLowerCase()}s`
logger.debug('Creating object...')
try {
const response = await axios.post(createUrl, value, {
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
})
return response.data
} catch (err) {
showError(err, () => {
createObject(type, value)
})
return {}
} }
} }
@ -348,27 +590,35 @@ const ApiServerProvider = ({ children }) => {
document.body.appendChild(fileLink) document.body.appendChild(fileLink)
fileLink.click() fileLink.click()
fileLink.parentNode.removeChild(fileLink) fileLink.parentNode.removeChild(fileLink)
} catch (error) { } catch (err) {
logger.error('Failed to download GCode file content:', error) showError(err, () => {
fetchObjectContent(id, type, fileName)
})
}
}
if (error.response) { // Fetch notes for a specific parent
if (error.response.status === 404) { const fetchNotes = async (parentId) => {
showError( logger.debug('Fetching notes for parent:', parentId)
`The ${type} file "${fileName}" was not found on the server. It may have been deleted or moved.`, try {
() => fetchObjectContent(id, type, fileName) const response = await axios.get(`${config.backendUrl}/notes`, {
) params: {
} else { parent: parentId,
showError( sort: 'createdAt',
`Error downloading ${type} file: ${error.response.status} - ${error.response.statusText}`, order: 'ascend'
() => fetchObjectContent(id, type, fileName) },
) headers: {
} Accept: 'application/json'
} else { },
showError( withCredentials: true
'An unexpected error occurred while downloading. Please check your connection and try again.', })
() => fetchObjectContent(id, type, fileName)
) const notesData = response.data
} logger.debug('Fetched notes:', notesData.length)
return notesData
} catch (error) {
logger.error('Failed to fetch notes:', error)
throw error
} }
} }
@ -378,19 +628,24 @@ const ApiServerProvider = ({ children }) => {
apiServer: socketRef.current, apiServer: socketRef.current,
error, error,
connecting, connecting,
connected,
lockObject, lockObject,
unlockObject, unlockObject,
fetchObjectLock, fetchObjectLock,
updateObjectInfo, updateObject,
createObject, createObject,
onLockEvent, deleteObject,
onUpdateEvent, subscribeToObject,
subscribeToType,
subscribeToLock,
offUpdateEvent, offUpdateEvent,
fetchObjectInfo, fetchObject,
fetchTableData, fetchObjects,
fetchObjectsByProperty,
fetchLoading, fetchLoading,
showError, showError,
fetchObjectContent fetchObjectContent,
fetchNotes
}} }}
> >
{contextHolder} {contextHolder}

View File

@ -53,8 +53,7 @@ const AuthProvider = ({ children }) => {
}) })
if (response.status === 200 && response.data) { if (response.status === 200 && response.data) {
logger.debug('User is authenticated!') logger.debug('Got auth token!')
setAuthenticated(true)
setToken(response.data.access_token) setToken(response.data.access_token)
setExpiresAt(response.data.expires_at) setExpiresAt(response.data.expires_at)
setUserProfile(response.data) setUserProfile(response.data)
@ -89,19 +88,32 @@ const AuthProvider = ({ children }) => {
} }
}, []) }, [])
const showTokenExpirationMessage = useCallback( const handleSessionExpiredModalOk = () => {
(expiresAt) => { setShowSessionExpiredModal(false)
loginWithSSO()
}
// Initialize on component mount
useEffect(() => {
let intervalId
const tokenRefresh = () => {
if (expiresAt) {
const now = new Date() const now = new Date()
const expirationDate = new Date(expiresAt) const expirationDate = new Date(expiresAt)
const timeRemaining = expirationDate - now const timeRemaining = expirationDate - now
if (timeRemaining <= 0) { if (timeRemaining <= 0) {
if (authenticated) { if (authenticated == true) {
setShowSessionExpiredModal(true)
setAuthenticated(false) setAuthenticated(false)
notificationApi.destroy('token-expiration')
} }
setShowSessionExpiredModal(true)
notificationApi.destroy('token-expiration')
} else { } else {
if (authenticated == false) {
setAuthenticated(true)
}
const minutes = Math.floor(timeRemaining / 60000) const minutes = Math.floor(timeRemaining / 60000)
const seconds = Math.floor((timeRemaining % 60000) / 1000) const seconds = Math.floor((timeRemaining % 60000) / 1000)
@ -154,35 +166,20 @@ const AuthProvider = ({ children }) => {
notificationApi.destroy('token-expiration') notificationApi.destroy('token-expiration')
} }
} }
},
[authenticated, notificationApi]
)
const handleSessionExpiredModalOk = () => {
setShowSessionExpiredModal(false)
loginWithSSO()
}
// Initialize on component mount
useEffect(() => {
let intervalId
const tokenRefreshInterval = () => {
if (expiresAt) {
showTokenExpirationMessage(expiresAt)
} }
} }
if (authenticated) { intervalId = setInterval(tokenRefresh, 1000)
intervalId = setInterval(tokenRefreshInterval, 1000)
} console.log('fresh', authenticated)
tokenRefresh()
return () => { return () => {
if (intervalId) { if (intervalId) {
clearInterval(intervalId) clearInterval(intervalId)
} }
} }
}, [expiresAt, authenticated, showTokenExpirationMessage]) }, [expiresAt, authenticated, notificationApi, refreshToken])
useEffect(() => { useEffect(() => {
checkAuthStatus() checkAuthStatus()