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,
useState,
useContext,
useRef
useRef,
useCallback
} from 'react'
import io from 'socket.io-client'
import { message, notification, Modal, Space, Button } from 'antd'
@ -22,8 +23,9 @@ logger.setLevel(config.logLevel)
const ApiServerContext = createContext()
const ApiServerProvider = ({ children }) => {
const { token, userProfile } = useContext(AuthContext)
const { token, userProfile, authenticated } = useContext(AuthContext)
const socketRef = useRef(null)
const [connected, setConnected] = useState(false)
const [connecting, setConnecting] = useState(false)
const [error, setError] = useState(null)
const [messageApi, contextHolder] = message.useMessage()
@ -32,9 +34,43 @@ const ApiServerProvider = ({ children }) => {
const [showErrorModal, setShowErrorModal] = useState(false)
const [errorModalContent, setErrorModalContent] = useState('')
const [retryCallback, setRetryCallback] = useState(null)
const subscribedCallbacksRef = useRef(new Map())
const subscribedLockCallbacksRef = useRef(new Map())
useEffect(() => {
if (token) {
const notifyLockUpdate = useCallback(
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...')
const newSocket = io(config.apiServerUrl, {
@ -48,18 +84,25 @@ const ApiServerProvider = ({ children }) => {
newSocket.on('connect', () => {
logger.debug('Api Server connected')
setConnecting(false)
setConnected(true)
setError(null)
})
newSocket.on('notify_object_update', notifyObjectUpdate)
newSocket.on('notify_object_new', notifyObjectNew)
newSocket.on('notify_lock_update', notifyLockUpdate)
newSocket.on('disconnect', () => {
logger.debug('Api Server disconnected')
setError('Api Server disconnected')
setConnected(false)
})
newSocket.on('connect_error', (err) => {
logger.error('Api Server connection error:', err)
messageApi.error('Api Server connection error: ' + err.message)
setError('Api Server connection error')
setConnected(false)
})
newSocket.on('bridge.notification', (data) => {
@ -75,6 +118,17 @@ const ApiServerProvider = ({ children }) => {
})
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
return () => {
@ -84,12 +138,7 @@ const ApiServerProvider = ({ children }) => {
socketRef.current = null
}
}
} else if (!token && socketRef.current) {
logger.debug('Token not available, disconnecting api server...')
socketRef.current.disconnect()
socketRef.current = null
}
}, [token, messageApi])
}, [token, authenticated, connectToServer])
const lockObject = (id, type) => {
logger.debug('Locking ' + id)
@ -118,8 +167,12 @@ const ApiServerProvider = ({ children }) => {
type: type
},
(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)
} else {
resolve(null)
}
}
)
logger.debug('Sent fetch lock command for object:', id)
@ -127,65 +180,186 @@ const ApiServerProvider = ({ children }) => {
}
}
const onLockEvent = (id, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
const eventHandler = (data) => {
if (data._id === id && data?.user !== userProfile._id) {
const notifyObjectUpdate = async (object) => {
logger.debug('Notifying object update:', object)
const objectId = object._id || object.id
if (objectId && subscribedCallbacksRef.current.has(objectId)) {
const callbacks = subscribedCallbacksRef.current.get(objectId)
logger.debug(
'Lock update received for object:',
id,
'locked:',
data.locked
`Calling ${callbacks.length} callbacks for object:`,
objectId
)
callback(data)
callbacks.forEach((callback) => {
try {
callback(object)
} catch (error) {
logger.error('Error in object update callback:', error)
}
}
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) {
})
} else {
logger.debug(
'Update event received for object:',
id,
'updatedAt:',
data.updatedAt
`No callbacks found for object: ${objectId}, subscribed callbacks:`,
Array.from(subscribedCallbacksRef.current.keys())
)
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)
// 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) {
socketRef.current.off('notify_update', eventHandler)
logger.debug('Removed update event listener for object:', id)
// Add callback to the subscribed callbacks map immediately
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)
setRetryCallback(() => callback)
setShowErrorModal(true)
@ -200,8 +374,8 @@ const ApiServerProvider = ({ children }) => {
setRetryCallback(null)
}
// Generalized fetchObjectInfo function
const fetchObjectInfo = async (id, type) => {
// Generalized fetchObject function
const fetchObject = async (id, type) => {
const fetchUrl = `${config.backendUrl}/${type}s/${id}`
setFetchLoading(true)
logger.debug('Fetching from ' + fetchUrl)
@ -214,62 +388,17 @@ const ApiServerProvider = ({ children }) => {
})
return response.data
} catch (err) {
logger.error('Failed to fetch object information:', err)
// Don't automatically show error - let the component handle it
throw err
showError(err, () => {
fetchObject(id, type)
})
return {}
} finally {
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
const fetchTableData = async (type, params = {}) => {
const fetchObjects = async (type, params = {}) => {
const {
page = 1,
limit = 25,
@ -319,9 +448,122 @@ const ApiServerProvider = ({ children }) => {
hasMore,
page
}
} catch (error) {
logger.error('Failed to fetch table data:', error)
throw error
} catch (err) {
showError(err, () => {
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)
fileLink.click()
fileLink.parentNode.removeChild(fileLink)
} catch (error) {
logger.error('Failed to download GCode file content:', error)
} catch (err) {
showError(err, () => {
fetchObjectContent(id, type, fileName)
})
}
}
if (error.response) {
if (error.response.status === 404) {
showError(
`The ${type} file "${fileName}" was not found on the server. It may have been deleted or moved.`,
() => fetchObjectContent(id, type, fileName)
)
} else {
showError(
`Error downloading ${type} file: ${error.response.status} - ${error.response.statusText}`,
() => fetchObjectContent(id, type, fileName)
)
}
} else {
showError(
'An unexpected error occurred while downloading. Please check your connection and try again.',
() => fetchObjectContent(id, type, fileName)
)
}
// Fetch notes for a specific parent
const fetchNotes = async (parentId) => {
logger.debug('Fetching notes for parent:', parentId)
try {
const response = await axios.get(`${config.backendUrl}/notes`, {
params: {
parent: parentId,
sort: 'createdAt',
order: 'ascend'
},
headers: {
Accept: 'application/json'
},
withCredentials: true
})
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,
error,
connecting,
connected,
lockObject,
unlockObject,
fetchObjectLock,
updateObjectInfo,
updateObject,
createObject,
onLockEvent,
onUpdateEvent,
deleteObject,
subscribeToObject,
subscribeToType,
subscribeToLock,
offUpdateEvent,
fetchObjectInfo,
fetchTableData,
fetchObject,
fetchObjects,
fetchObjectsByProperty,
fetchLoading,
showError,
fetchObjectContent
fetchObjectContent,
fetchNotes
}}
>
{contextHolder}

View File

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