- Implemented updateMultipleObjects for batch updates of objects via PUT request. - Added sendObjectFunction to invoke specific functions on objects with POST requests. - Enhanced error handling for both methods to retry on failure.
1336 lines
39 KiB
JavaScript
1336 lines
39 KiB
JavaScript
// src/contexts/ApiServerContext.js
|
|
import {
|
|
createContext,
|
|
useEffect,
|
|
useState,
|
|
useContext,
|
|
useRef,
|
|
useCallback
|
|
} from 'react'
|
|
import io from 'socket.io-client'
|
|
import { message, notification, Modal, Space, Button } from 'antd'
|
|
import PropTypes from 'prop-types'
|
|
import { AuthContext } from './AuthContext'
|
|
|
|
import axios from 'axios'
|
|
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
|
|
import ReloadIcon from '../../Icons/ReloadIcon'
|
|
import config from '../../../config'
|
|
import loglevel from 'loglevel'
|
|
const logger = loglevel.getLogger('ApiServerContext')
|
|
logger.setLevel(config.logLevel)
|
|
|
|
const ApiServerContext = createContext()
|
|
|
|
const ApiServerProvider = ({ children }) => {
|
|
const { token, userProfile, authenticated, setUnauthenticated } =
|
|
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()
|
|
const [notificationApi] = notification.useNotification()
|
|
const [fetchLoading, setFetchLoading] = useState(false)
|
|
const [showErrorModal, setShowErrorModal] = useState(false)
|
|
const [errorModalContent, setErrorModalContent] = useState('')
|
|
const [retryCallback, setRetryCallback] = useState(null)
|
|
const subscribedCallbacksRef = useRef(new Map())
|
|
const subscribedLockCallbacksRef = useRef(new Map())
|
|
|
|
const handleLockUpdate = 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 clearSubscriptions = useCallback(() => {
|
|
subscribedCallbacksRef.current.clear()
|
|
subscribedLockCallbacksRef.current.clear()
|
|
}, [])
|
|
|
|
const connectToServer = useCallback(() => {
|
|
if (token && authenticated == true) {
|
|
logger.debug('Token is available, connecting to api server...')
|
|
|
|
const newSocket = io(config.apiServerUrl, {
|
|
reconnectionAttempts: 3,
|
|
timeout: 3000,
|
|
auth: { type: 'user' }
|
|
})
|
|
|
|
setConnecting(true)
|
|
|
|
newSocket.on('connect', () => {
|
|
logger.debug('Api Server connected')
|
|
newSocket.emit('authenticate', { token: token }, () => {
|
|
setConnecting(false)
|
|
setConnected(true)
|
|
setError(null)
|
|
})
|
|
})
|
|
|
|
newSocket.on('objectUpdate', handleObjectUpdate)
|
|
newSocket.on('objectEvent', handleObjectEvent)
|
|
newSocket.on('objectNew', handleObjectNew)
|
|
newSocket.on('objectDelete', handleObjectDelete)
|
|
newSocket.on('lockUpdate', handleLockUpdate)
|
|
newSocket.on('modelStats', handleModelStats)
|
|
|
|
newSocket.on('disconnect', () => {
|
|
logger.debug('Api Server disconnected')
|
|
setError('Api Server disconnected')
|
|
clearSubscriptions()
|
|
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')
|
|
clearSubscriptions()
|
|
setConnected(false)
|
|
})
|
|
|
|
newSocket.on('error', (err) => {
|
|
logger.error('Api Server error:', err)
|
|
setError('Api Server error')
|
|
})
|
|
|
|
socketRef.current = newSocket
|
|
}
|
|
}, [token, authenticated, messageApi, notificationApi, handleLockUpdate])
|
|
|
|
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 () => {
|
|
if (socketRef.current) {
|
|
logger.debug('Cleaning up api server connection...')
|
|
socketRef.current.disconnect()
|
|
socketRef.current = null
|
|
}
|
|
}
|
|
}, [token, authenticated, connectToServer])
|
|
|
|
const lockObject = (id, type) => {
|
|
logger.debug('Locking ' + id)
|
|
if (socketRef.current && socketRef.current.connected) {
|
|
socketRef.current.emit('lock', { _id: id, type: type })
|
|
logger.debug('Sent lock command for object:', id)
|
|
}
|
|
}
|
|
|
|
const unlockObject = (id, type) => {
|
|
logger.debug('Unlocking ' + id)
|
|
if (socketRef.current && socketRef.current.connected == true) {
|
|
socketRef.current.emit('unlock', { _id: id, type: type })
|
|
logger.debug('Sent unlock command for object:', id)
|
|
}
|
|
}
|
|
|
|
const fetchObjectLock = async (id, type) => {
|
|
if (socketRef.current && socketRef.current.connected == true) {
|
|
logger.debug('Fetching lock status for ' + id)
|
|
return new Promise((resolve) => {
|
|
socketRef.current.emit(
|
|
'getLock',
|
|
{
|
|
_id: id,
|
|
type: type
|
|
},
|
|
(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)
|
|
})
|
|
}
|
|
}
|
|
|
|
const handleObjectUpdate = async (data) => {
|
|
logger.debug('Notifying object update:', data)
|
|
const id = data._id
|
|
const objectType = data.objectType
|
|
|
|
const callbacksRefKey = `${objectType}:${id}`
|
|
|
|
if (
|
|
id &&
|
|
objectType &&
|
|
subscribedCallbacksRef.current.has(callbacksRefKey)
|
|
) {
|
|
const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
|
|
logger.debug(
|
|
`Calling ${callbacks.length} callbacks for object:`,
|
|
callbacksRefKey
|
|
)
|
|
callbacks.forEach((callback) => {
|
|
try {
|
|
callback(data.object)
|
|
} catch (error) {
|
|
logger.error('Error in object update callback:', error)
|
|
}
|
|
})
|
|
} else {
|
|
logger.debug(
|
|
`No callbacks found for object: ${callbacksRefKey}, subscribed callbacks:`,
|
|
Array.from(subscribedCallbacksRef.current.keys())
|
|
)
|
|
}
|
|
}
|
|
|
|
const handleObjectEvent = async (data) => {
|
|
const id = data._id
|
|
const objectType = data.objectType
|
|
|
|
const callbacksRefKey = `${objectType}:${id}:events:${data.event.type}`
|
|
logger.debug('Notifying object event:', data)
|
|
if (
|
|
id &&
|
|
objectType &&
|
|
subscribedCallbacksRef.current.has(callbacksRefKey)
|
|
) {
|
|
const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
|
|
logger.debug(
|
|
`Calling ${callbacks.length} callbacks for object:`,
|
|
callbacksRefKey
|
|
)
|
|
callbacks.forEach((callback) => {
|
|
try {
|
|
callback(data.event)
|
|
} catch (error) {
|
|
logger.error('Error in object event callback:', error)
|
|
}
|
|
})
|
|
} else {
|
|
logger.debug(
|
|
`No callbacks found for object: ${callbacksRefKey}, subscribed callbacks:`,
|
|
Array.from(subscribedCallbacksRef.current.keys())
|
|
)
|
|
}
|
|
}
|
|
|
|
const handleModelStats = async (data) => {
|
|
const objectType = data.objectType || 'unknown'
|
|
|
|
const callbacksRefKey = `modelStats:${objectType}`
|
|
logger.debug('Notifying model stats update:', data)
|
|
console.log('handleModelStats', data)
|
|
if (objectType && subscribedCallbacksRef.current.has(callbacksRefKey)) {
|
|
const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
|
|
logger.debug(
|
|
`Calling ${callbacks.length} callbacks for model stats:`,
|
|
callbacksRefKey
|
|
)
|
|
callbacks.forEach((callback) => {
|
|
try {
|
|
callback(data.stats)
|
|
} catch (error) {
|
|
logger.error('Error in model stats callback:', error)
|
|
}
|
|
})
|
|
} else {
|
|
logger.debug(
|
|
`No callbacks found for model stats: ${callbacksRefKey}, subscribed callbacks:`,
|
|
Array.from(subscribedCallbacksRef.current.keys())
|
|
)
|
|
}
|
|
}
|
|
|
|
const handleObjectNew = async (data) => {
|
|
logger.debug('Notifying object new:', data)
|
|
const objectType = data.objectType || '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(data.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 handleObjectDelete = async (data) => {
|
|
logger.debug('Notifying object delete:', data)
|
|
const objectType = data.objectType || '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(data.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 offObjectUpdatesEvent = useCallback((id, objectType, callback) => {
|
|
if (socketRef.current && socketRef.current.connected == true) {
|
|
const callbacksRefKey = `${objectType}:${id}`
|
|
// Remove callback from the subscribed callbacks map
|
|
if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
|
|
const callbacks = subscribedCallbacksRef.current
|
|
.get(callbacksRefKey)
|
|
.filter((cb) => cb !== callback)
|
|
if (callbacks.length === 0) {
|
|
logger.debug(
|
|
'No callbacks found for object:',
|
|
callbacksRefKey,
|
|
'unsubscribing from object update...'
|
|
)
|
|
subscribedCallbacksRef.current.delete(callbacksRefKey)
|
|
socketRef.current.emit('unsubscribeObjectUpdate', {
|
|
_id: id,
|
|
objectType: objectType
|
|
})
|
|
} else {
|
|
subscribedCallbacksRef.current.set(callbacksRefKey, callbacks)
|
|
}
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
const offObjectTypeUpdatesEvent = useCallback((objectType, callback) => {
|
|
if (socketRef.current && socketRef.current.connected == true) {
|
|
// Remove callback from the subscribed callbacks map
|
|
if (subscribedCallbacksRef.current.has(objectType)) {
|
|
const callbacks = subscribedCallbacksRef.current
|
|
.get(objectType)
|
|
.filter((cb) => cb !== callback)
|
|
if (callbacks.length === 0) {
|
|
subscribedCallbacksRef.current.delete(objectType)
|
|
socketRef.current.emit('unsubscribeObjectTypeUpdate', {
|
|
objectType: objectType
|
|
})
|
|
} else {
|
|
subscribedCallbacksRef.current.set(objectType, callbacks)
|
|
}
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
const subscribeToObjectUpdates = useCallback(
|
|
(id, objectType, callback) => {
|
|
if (socketRef.current && socketRef.current.connected == true) {
|
|
const callbacksRefKey = `${objectType}:${id}`
|
|
// Add callback to the subscribed callbacks map immediately
|
|
if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
|
|
subscribedCallbacksRef.current.set(callbacksRefKey, [])
|
|
}
|
|
|
|
const callbacksLength =
|
|
subscribedCallbacksRef.current.get(callbacksRefKey).length
|
|
|
|
if (callbacksLength <= 0) {
|
|
socketRef.current.emit(
|
|
'subscribeToObjectUpdate',
|
|
{
|
|
_id: id,
|
|
objectType: objectType
|
|
},
|
|
(result) => {
|
|
if (result.success) {
|
|
logger.info('Subscribed to id:', id, 'objectType:', objectType)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
logger.info(
|
|
'Adding callback id:',
|
|
id,
|
|
'objectType:',
|
|
objectType,
|
|
'callbacks length:',
|
|
callbacksLength + 1
|
|
)
|
|
subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)
|
|
|
|
// Return cleanup function
|
|
return () => offObjectUpdatesEvent(id, objectType, callback)
|
|
}
|
|
},
|
|
[offObjectUpdatesEvent]
|
|
)
|
|
|
|
const subscribeToObjectTypeUpdates = useCallback(
|
|
(objectType, callback) => {
|
|
logger.debug('Subscribing to type updates:', objectType)
|
|
if (socketRef.current && socketRef.current.connected == true) {
|
|
// Add callback to the subscribed callbacks map immediately
|
|
if (!subscribedCallbacksRef.current.has(objectType)) {
|
|
subscribedCallbacksRef.current.set(objectType, [])
|
|
}
|
|
subscribedCallbacksRef.current.get(objectType).push(callback)
|
|
logger.debug(
|
|
`Added callback for type ${objectType}, total callbacks: ${subscribedCallbacksRef.current.get(objectType).length}`
|
|
)
|
|
|
|
socketRef.current.emit(
|
|
'subscribeToObjectTypeUpdate',
|
|
{ objectType: objectType },
|
|
(result) => {
|
|
if (result.success) {
|
|
logger.info('Subscribed to objectType:', objectType)
|
|
}
|
|
}
|
|
)
|
|
logger.debug('Registered type event listener for object:', objectType)
|
|
|
|
// Return cleanup function
|
|
return () => offObjectTypeUpdatesEvent(objectType, callback)
|
|
}
|
|
},
|
|
[offObjectTypeUpdatesEvent]
|
|
)
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
logger.debug('Removed lock event listener for object:', id)
|
|
}
|
|
}, [])
|
|
|
|
const offObjectEventEvent = useCallback(
|
|
(id, objectType, eventType, callback) => {
|
|
if (socketRef.current && socketRef.current.connected == true) {
|
|
const callbacksRefKey = `${objectType}:${id}:events:${eventType}`
|
|
|
|
// Remove callback from the subscribed callbacks map
|
|
if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
|
|
const callbacks = subscribedCallbacksRef.current
|
|
.get(callbacksRefKey)
|
|
.filter((cb) => cb !== callback)
|
|
if (callbacks.length === 0) {
|
|
subscribedCallbacksRef.current.delete(callbacksRefKey)
|
|
socketRef.current.emit('unsubscribeObjectEvent', {
|
|
_id: id,
|
|
objectType,
|
|
eventType
|
|
})
|
|
} else {
|
|
subscribedCallbacksRef.current.set(callbacksRefKey, callbacks)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
[]
|
|
)
|
|
|
|
const subscribeToObjectEvent = useCallback(
|
|
(id, objectType, eventType, callback) => {
|
|
if (socketRef.current && socketRef.current.connected == true) {
|
|
const callbacksRefKey = `${objectType}:${id}:events:${eventType}`
|
|
// Add callback to the subscribed callbacks map immediately
|
|
if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
|
|
subscribedCallbacksRef.current.set(callbacksRefKey, [])
|
|
}
|
|
|
|
const callbacksLength =
|
|
subscribedCallbacksRef.current.get(callbacksRefKey).length
|
|
|
|
if (callbacksLength <= 0) {
|
|
socketRef.current.emit(
|
|
'subscribeToObjectEvent',
|
|
{
|
|
_id: id,
|
|
objectType: objectType,
|
|
eventType: eventType
|
|
},
|
|
(result) => {
|
|
if (result.success) {
|
|
logger.info(
|
|
'Subscribed to event id:',
|
|
id,
|
|
'objectType:',
|
|
objectType,
|
|
'eventType:',
|
|
eventType
|
|
)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
logger.info(
|
|
'Adding event callback id:',
|
|
id,
|
|
'objectType:',
|
|
objectType,
|
|
'eventType:',
|
|
eventType,
|
|
'callbacks length:',
|
|
callbacksLength + 1
|
|
)
|
|
subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)
|
|
|
|
// Return cleanup function
|
|
return () => offObjectEventEvent(id, objectType, eventType, callback)
|
|
}
|
|
},
|
|
[offObjectUpdatesEvent]
|
|
)
|
|
|
|
const offModelStats = useCallback((objectType, callback) => {
|
|
if (socketRef.current && socketRef.current.connected == true) {
|
|
const callbacksRefKey = `modelStats:${objectType}`
|
|
|
|
// Remove callback from the subscribed callbacks map
|
|
if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
|
|
const callbacks = subscribedCallbacksRef.current
|
|
.get(callbacksRefKey)
|
|
.filter((cb) => cb !== callback)
|
|
if (callbacks.length === 0) {
|
|
subscribedCallbacksRef.current.delete(callbacksRefKey)
|
|
socketRef.current.emit('unsubscribeModelStats', {
|
|
objectType
|
|
})
|
|
} else {
|
|
subscribedCallbacksRef.current.set(callbacksRefKey, callbacks)
|
|
}
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
const subscribeToModelStats = useCallback(
|
|
(objectType, callback) => {
|
|
if (socketRef.current && socketRef.current.connected == true) {
|
|
const callbacksRefKey = `modelStats:${objectType}`
|
|
// Add callback to the subscribed callbacks map immediately
|
|
if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
|
|
subscribedCallbacksRef.current.set(callbacksRefKey, [])
|
|
}
|
|
|
|
const callbacksLength =
|
|
subscribedCallbacksRef.current.get(callbacksRefKey).length
|
|
|
|
if (callbacksLength <= 0) {
|
|
socketRef.current.emit(
|
|
'subscribeToModelStats',
|
|
{
|
|
objectType: objectType
|
|
},
|
|
(result) => {
|
|
if (result.success) {
|
|
logger.info('Subscribed to model stats:', objectType)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
logger.info(
|
|
'Adding model stats callback:',
|
|
objectType,
|
|
'callbacks length:',
|
|
callbacksLength + 1
|
|
)
|
|
subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)
|
|
|
|
// Return cleanup function
|
|
return () => offModelStats(objectType, callback)
|
|
}
|
|
},
|
|
[offModelStats]
|
|
)
|
|
|
|
const subscribeToObjectLock = 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, objectType: type })
|
|
logger.debug('Registered lock event listener for object:', id)
|
|
|
|
// Return cleanup function
|
|
return () => offLockEvent(id, callback)
|
|
}
|
|
},
|
|
[offLockEvent]
|
|
)
|
|
|
|
const showError = (error, callback = null) => {
|
|
const code = error.response.data.code || 'UNKNOWN'
|
|
if (code == 'UNAUTHORIZED') {
|
|
setUnauthenticated()
|
|
return
|
|
}
|
|
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)
|
|
}
|
|
|
|
const handleRetry = () => {
|
|
setShowErrorModal(false)
|
|
setErrorModalContent('')
|
|
if (retryCallback) {
|
|
retryCallback()
|
|
}
|
|
setRetryCallback(null)
|
|
}
|
|
|
|
// Generalized fetchObject function
|
|
const fetchObject = async (id, type) => {
|
|
const fetchUrl = `${config.backendUrl}/${type}s/${id}`
|
|
setFetchLoading(true)
|
|
logger.debug('Fetching from ' + fetchUrl)
|
|
try {
|
|
const response = await axios.get(fetchUrl, {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
})
|
|
setFetchLoading(false)
|
|
return response.data
|
|
} catch (err) {
|
|
console.error(err)
|
|
showError(err, () => {
|
|
fetchObject(id, type)
|
|
})
|
|
return {}
|
|
}
|
|
}
|
|
|
|
// Fetch table data with pagination, filtering, and sorting
|
|
const fetchObjects = async (type, params = {}) => {
|
|
const {
|
|
page = 1,
|
|
limit = 25,
|
|
filter = {},
|
|
sorter = {},
|
|
onDataChange
|
|
} = params
|
|
logger.debug('Fetching table data from:', type, {
|
|
page,
|
|
limit,
|
|
filter,
|
|
sorter
|
|
})
|
|
|
|
try {
|
|
const response = await axios.get(
|
|
`${config.backendUrl}/${type.toLowerCase()}s`,
|
|
{
|
|
params: {
|
|
page,
|
|
limit,
|
|
...filter,
|
|
sort: sorter.field,
|
|
order: sorter.order
|
|
},
|
|
headers: {
|
|
Accept: 'application/json',
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
}
|
|
)
|
|
|
|
const newData = response.data
|
|
const totalCount = parseInt(response.headers['x-total-count'] || '0', 10)
|
|
const totalPages = Math.ceil(totalCount / limit)
|
|
const hasMore = newData.length >= limit
|
|
|
|
if (onDataChange) {
|
|
onDataChange(newData)
|
|
}
|
|
|
|
return {
|
|
data: newData,
|
|
totalCount,
|
|
totalPages,
|
|
hasMore,
|
|
page
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
showError(err, () => {
|
|
fetchObjects(type, params)
|
|
})
|
|
return []
|
|
}
|
|
}
|
|
|
|
// Fetch table data with pagination, filtering, and sorting
|
|
const fetchObjectsByProperty = async (type, params = {}) => {
|
|
const { filter = {}, properties = [], masterFilter = {} } = params
|
|
|
|
logger.debug('Fetching property object data from:', type, {
|
|
properties,
|
|
filter
|
|
})
|
|
|
|
try {
|
|
const response = await axios.get(
|
|
`${config.backendUrl}/${type.toLowerCase()}s/properties`,
|
|
{
|
|
params: {
|
|
...Object.keys(filter).reduce((acc, key) => {
|
|
acc[key] = Array.isArray(filter[key])
|
|
? filter[key].join(',')
|
|
: filter[key]
|
|
return acc
|
|
}, {}),
|
|
properties: properties.join(','), // Convert array to comma-separated string
|
|
masterFilter: JSON.stringify(masterFilter)
|
|
},
|
|
headers: {
|
|
Accept: 'application/json',
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
}
|
|
)
|
|
|
|
const newData = response.data
|
|
|
|
return newData
|
|
} catch (err) {
|
|
console.error(err)
|
|
showError(err, () => {
|
|
fetchObjects(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: {
|
|
Accept: 'application/json',
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
})
|
|
logger.debug('Object updated successfully')
|
|
return response.data
|
|
} catch (err) {
|
|
console.error(err)
|
|
setError(err, () => {
|
|
updateObject(id, type, value)
|
|
})
|
|
return {}
|
|
}
|
|
}
|
|
|
|
// Update multiple objects
|
|
const updateMultipleObjects = async (type, objects) => {
|
|
const updateUrl = `${config.backendUrl}/${type.toLowerCase()}s`
|
|
logger.debug('Updating multiple objects for ' + type)
|
|
try {
|
|
const response = await axios.put(updateUrl, objects, {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
})
|
|
logger.debug('Objects updated successfully')
|
|
return response.data
|
|
} catch (err) {
|
|
console.error(err)
|
|
showError(err, () => {
|
|
updateMultipleObjects(type, objects)
|
|
})
|
|
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: {
|
|
Accept: 'application/json',
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
})
|
|
logger.debug('Object deleted successfully')
|
|
return response.data
|
|
} catch (err) {
|
|
console.error(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: {
|
|
Accept: 'application/json',
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
})
|
|
return response.data
|
|
} catch (err) {
|
|
console.error(err)
|
|
showError(err, () => {
|
|
createObject(type, value)
|
|
})
|
|
return {}
|
|
}
|
|
}
|
|
|
|
// Call a function on an object
|
|
const sendObjectFunction = async (id, type, functionName, value = {}) => {
|
|
const url = `${config.backendUrl}/${type.toLowerCase()}s/${id}/${functionName}`
|
|
logger.debug(`Calling object function ${functionName} for ${id} at ${url}`)
|
|
try {
|
|
const response = await axios.post(url, value, {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
})
|
|
return response.data
|
|
} catch (err) {
|
|
console.error(err)
|
|
showError(err, () => {
|
|
sendObjectFunction(id, type, functionName, value)
|
|
})
|
|
return {}
|
|
}
|
|
}
|
|
|
|
// Download GCode file content
|
|
const fetchFileContent = async (file, download = false) => {
|
|
try {
|
|
const response = await axios.get(
|
|
`${config.backendUrl}/files/${file._id}/content`,
|
|
{
|
|
headers: {
|
|
Accept: '*/*',
|
|
Authorization: `Bearer ${token}`
|
|
},
|
|
responseType: 'blob'
|
|
}
|
|
)
|
|
const blob = new Blob([response.data], {
|
|
type: response.headers['content-type']
|
|
})
|
|
const fileURL = window.URL.createObjectURL(blob)
|
|
if (download == true) {
|
|
const fileLink = document.createElement('a')
|
|
fileLink.href = fileURL
|
|
fileLink.setAttribute('download', `${file.name}${file.extension}`)
|
|
document.body.appendChild(fileLink)
|
|
fileLink.click()
|
|
fileLink.parentNode.removeChild(fileLink)
|
|
return
|
|
}
|
|
return fileURL
|
|
} catch (err) {
|
|
console.error(err)
|
|
showError(err, () => {
|
|
fetchFileContent(file, download)
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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._id': parentId,
|
|
sort: 'createdAt',
|
|
order: 'ascend'
|
|
},
|
|
headers: {
|
|
Accept: 'application/json',
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
})
|
|
|
|
const notesData = response.data || []
|
|
logger.debug('Fetched notes:', notesData.length)
|
|
return notesData
|
|
} catch (err) {
|
|
console.error(err)
|
|
showError(err, () => {
|
|
fetchNotes(parentId)
|
|
})
|
|
}
|
|
}
|
|
|
|
const fetchSpotlightData = async (query) => {
|
|
logger.debug('Fetching spotlight data with query:', query)
|
|
try {
|
|
const response = await axios.get(
|
|
`${config.backendUrl}/spotlight/${query}`,
|
|
{
|
|
headers: {
|
|
Accept: 'application/json',
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
}
|
|
)
|
|
return response.data
|
|
} catch (err) {
|
|
console.error(err)
|
|
showError(err, () => {
|
|
fetchSpotlightData(query)
|
|
})
|
|
}
|
|
}
|
|
|
|
const getModelStats = async (objectType) => {
|
|
logger.debug('Fetching stats for model type:', objectType)
|
|
try {
|
|
let statsUrl
|
|
if (objectType === 'history') {
|
|
statsUrl = `${config.backendUrl}/stats/history`
|
|
} else {
|
|
statsUrl = `${config.backendUrl}/${objectType.toLowerCase()}s/stats`
|
|
}
|
|
|
|
const response = await axios.get(statsUrl, {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
})
|
|
logger.debug('Fetched stats for model type:', objectType)
|
|
return response.data
|
|
} catch (err) {
|
|
console.error(err)
|
|
showError(err, () => {
|
|
getModelStats(objectType)
|
|
})
|
|
return null
|
|
}
|
|
}
|
|
|
|
const getModelHistory = async (objectType, startDate, endDate) => {
|
|
logger.debug('Fetching history for model type:', objectType)
|
|
const encodedStartDate = encodeURIComponent(startDate.toISOString())
|
|
const encodedEndDate = encodeURIComponent(endDate.toISOString())
|
|
try {
|
|
const historyUrl = `${config.backendUrl}/${objectType.toLowerCase()}s/history?from=${encodedStartDate}&to=${encodedEndDate}`
|
|
|
|
const response = await axios.get(historyUrl, {
|
|
headers: {
|
|
Accept: 'application/json',
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
})
|
|
logger.debug('Fetched history for model type:', objectType)
|
|
|
|
// Calculate time range in milliseconds
|
|
const timeRangeMs = endDate.getTime() - startDate.getTime()
|
|
const oneHourMs = 60 * 60 * 1000
|
|
const twelveHoursMs = 12 * 60 * 60 * 1000
|
|
const oneDayMs = 24 * 60 * 60 * 1000
|
|
const threeDaysMs = 3 * 24 * 60 * 60 * 1000
|
|
|
|
// Determine interval based on time range
|
|
let intervalMinutes = 1 // Default: 1 minute
|
|
if (timeRangeMs > threeDaysMs) {
|
|
intervalMinutes = 60 // Over 1 day: 60 minutes
|
|
} else if (timeRangeMs > oneDayMs) {
|
|
intervalMinutes = 30 // Over 2 days: 30 minutes
|
|
} else if (timeRangeMs > twelveHoursMs) {
|
|
intervalMinutes = 10 // Over 12 hours: 10 minutes
|
|
} else if (timeRangeMs > oneHourMs) {
|
|
intervalMinutes = 5 // Over 1 hour: 5 minutes
|
|
}
|
|
|
|
const intervalMs = intervalMinutes * 60 * 1000
|
|
|
|
// Filter data to only include points at the specified intervals
|
|
if (response.data && Array.isArray(response.data)) {
|
|
const filteredData = []
|
|
const seenIntervals = new Set()
|
|
|
|
response.data.forEach((point) => {
|
|
const pointDate = new Date(point.date)
|
|
// Round down to the nearest interval
|
|
const roundedTime =
|
|
Math.floor(pointDate.getTime() / intervalMs) * intervalMs
|
|
const roundedDate = new Date(roundedTime)
|
|
const intervalKey = roundedDate.toISOString()
|
|
|
|
// Only include if we haven't seen this interval yet
|
|
if (!seenIntervals.has(intervalKey)) {
|
|
seenIntervals.add(intervalKey)
|
|
// Update the point's date to the rounded interval
|
|
filteredData.push({
|
|
...point,
|
|
date: roundedDate.toISOString()
|
|
})
|
|
}
|
|
})
|
|
|
|
// Sort by date to ensure chronological order
|
|
filteredData.sort((a, b) => new Date(a.date) - new Date(b.date))
|
|
|
|
logger.debug(
|
|
`Filtered history data: ${response.data.length} -> ${filteredData.length} points (${intervalMinutes} min intervals)`
|
|
)
|
|
return filteredData
|
|
}
|
|
|
|
return response.data
|
|
} catch (err) {
|
|
console.error(err)
|
|
showError(err, () => {
|
|
getModelHistory(objectType)
|
|
})
|
|
return null
|
|
}
|
|
}
|
|
|
|
const fetchTemplatePreview = async (
|
|
id,
|
|
content,
|
|
testObject,
|
|
scale,
|
|
callback
|
|
) => {
|
|
logger.debug('Fetching preview...')
|
|
if (socketRef.current && socketRef.current.connected) {
|
|
return socketRef.current.emit(
|
|
'previewTemplate',
|
|
{
|
|
_id: id,
|
|
content: content,
|
|
testObject: testObject,
|
|
scale: scale
|
|
},
|
|
callback
|
|
)
|
|
}
|
|
}
|
|
|
|
const fetchTemplatePDF = async (id, content, testObject, callback) => {
|
|
logger.debug('Fetching pdf template...')
|
|
if (socketRef.current && socketRef.current.connected) {
|
|
return socketRef.current.emit(
|
|
'renderTemplatePDF',
|
|
{
|
|
_id: id,
|
|
content: content,
|
|
object: testObject
|
|
},
|
|
callback
|
|
)
|
|
}
|
|
}
|
|
|
|
const downloadTemplatePDF = async (
|
|
id,
|
|
content,
|
|
object,
|
|
filename,
|
|
callback
|
|
) => {
|
|
logger.debug('Downloading template PDF...')
|
|
|
|
fetchTemplatePDF(id, content, object, (result) => {
|
|
logger.debug('Downloading template PDF result:', result)
|
|
if (result?.error) {
|
|
console.error(result.error)
|
|
if (callback) {
|
|
callback(result.error)
|
|
}
|
|
} else {
|
|
const pdfBlob = new Blob([result.pdf], { type: 'application/pdf' })
|
|
const pdfUrl = URL.createObjectURL(pdfBlob)
|
|
const fileLink = document.createElement('a')
|
|
fileLink.href = pdfUrl
|
|
fileLink.setAttribute('download', `${filename}.pdf`)
|
|
document.body.appendChild(fileLink)
|
|
fileLink.click()
|
|
fileLink.parentNode.removeChild(fileLink)
|
|
if (callback) {
|
|
callback()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const fetchHostOTP = async (id, callback) => {
|
|
logger.debug('Fetching host OTP...')
|
|
if (socketRef.current && socketRef.current.connected) {
|
|
return socketRef.current.emit(
|
|
'generateHostOtp',
|
|
{
|
|
_id: id
|
|
},
|
|
callback
|
|
)
|
|
}
|
|
}
|
|
|
|
const sendObjectAction = async (id, objectType, action, callback) => {
|
|
logger.debug('Sending object action...')
|
|
if (socketRef.current && socketRef.current.connected) {
|
|
return socketRef.current.emit(
|
|
'objectAction',
|
|
{
|
|
_id: id,
|
|
objectType: objectType,
|
|
action: action
|
|
},
|
|
callback
|
|
)
|
|
}
|
|
}
|
|
|
|
// Upload file to the API
|
|
const uploadFile = async (
|
|
file,
|
|
additionalData = {},
|
|
progressCallback = null
|
|
) => {
|
|
const uploadUrl = `${config.backendUrl}/files`
|
|
logger.debug('Uploading file:', file.name, 'to:', uploadUrl)
|
|
|
|
try {
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
|
|
// Add any additional data to the form
|
|
Object.keys(additionalData).forEach((key) => {
|
|
formData.append(key, additionalData[key])
|
|
})
|
|
|
|
const response = await axios.post(uploadUrl, formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
Authorization: `Bearer ${token}`
|
|
},
|
|
onUploadProgress: (progressEvent) => {
|
|
const percentCompleted = Math.round(
|
|
(progressEvent.loaded * 100) / progressEvent.total
|
|
)
|
|
logger.debug(`Upload progress: ${percentCompleted}%`)
|
|
if (progressCallback) {
|
|
progressCallback(percentCompleted)
|
|
}
|
|
}
|
|
})
|
|
|
|
logger.debug('File uploaded successfully:', response.data)
|
|
return response.data
|
|
} catch (err) {
|
|
console.error('File upload error:', err)
|
|
showError(err, () => {
|
|
uploadFile(file, additionalData, progressCallback)
|
|
})
|
|
return null
|
|
}
|
|
}
|
|
|
|
const flushFile = async (id) => {
|
|
logger.debug('Flushing file...')
|
|
try {
|
|
const response = await axios.delete(
|
|
`${config.backendUrl}/files/${id}/flush`,
|
|
{
|
|
headers: {
|
|
Accept: 'application/json',
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
}
|
|
)
|
|
|
|
logger.debug('Flushed file:', response.data)
|
|
return true
|
|
} catch (err) {
|
|
console.error(err)
|
|
showError(err, () => {
|
|
flushFile(id)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Sanitize a string so it is safe to use as a filename on most file systems
|
|
const formatFileName = (name) => {
|
|
if (!name || typeof name !== 'string') {
|
|
return ''
|
|
}
|
|
|
|
// Remove characters that are problematic on most common file systems
|
|
const cleaned = name.replace(/[^a-zA-Z0-9.\-_\s]/g, '')
|
|
|
|
// Normalize whitespace to single underscores
|
|
const normalized = cleaned.trim().replace(/\s+/g, '_')
|
|
|
|
// Most file systems limit filenames to 255 characters
|
|
return normalized.slice(0, 255)
|
|
}
|
|
|
|
return (
|
|
<ApiServerContext.Provider
|
|
value={{
|
|
apiServer: socketRef.current,
|
|
error,
|
|
connecting,
|
|
connected,
|
|
lockObject,
|
|
unlockObject,
|
|
fetchObjectLock,
|
|
updateObject,
|
|
updateMultipleObjects,
|
|
createObject,
|
|
sendObjectFunction,
|
|
deleteObject,
|
|
subscribeToObjectUpdates,
|
|
subscribeToObjectEvent,
|
|
subscribeToObjectTypeUpdates,
|
|
subscribeToObjectLock,
|
|
subscribeToModelStats,
|
|
fetchObject,
|
|
fetchObjects,
|
|
fetchObjectsByProperty,
|
|
fetchSpotlightData,
|
|
getModelStats,
|
|
getModelHistory,
|
|
fetchLoading,
|
|
showError,
|
|
fetchFileContent,
|
|
fetchTemplatePreview,
|
|
fetchTemplatePDF,
|
|
fetchNotes,
|
|
downloadTemplatePDF,
|
|
fetchHostOTP,
|
|
sendObjectAction,
|
|
uploadFile,
|
|
flushFile,
|
|
formatFileName
|
|
}}
|
|
>
|
|
{contextHolder}
|
|
{children}
|
|
<Modal
|
|
title={
|
|
<Space size={'middle'}>
|
|
<ExclamationOctagonIcon />
|
|
Error
|
|
</Space>
|
|
}
|
|
open={showErrorModal}
|
|
okText='OK'
|
|
style={{ maxWidth: 430 }}
|
|
closable={false}
|
|
centered
|
|
maskClosable={true}
|
|
footer={[
|
|
<Button
|
|
key='retry'
|
|
onClick={() => {
|
|
setShowErrorModal(false)
|
|
}}
|
|
>
|
|
Close
|
|
</Button>,
|
|
<Button key='retry' icon={<ReloadIcon />} onClick={handleRetry}>
|
|
Retry
|
|
</Button>
|
|
]}
|
|
>
|
|
{errorModalContent}
|
|
</Modal>
|
|
</ApiServerContext.Provider>
|
|
)
|
|
}
|
|
|
|
ApiServerProvider.propTypes = {
|
|
children: PropTypes.node.isRequired
|
|
}
|
|
|
|
export { ApiServerContext, ApiServerProvider }
|