691 lines
20 KiB
JavaScript
691 lines
20 KiB
JavaScript
// src/contexts/ApiServerContext.js
|
|
import React, {
|
|
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 } = 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 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, {
|
|
reconnectionAttempts: 3,
|
|
timeout: 3000,
|
|
auth: { token: token }
|
|
})
|
|
|
|
setConnecting(true)
|
|
|
|
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) => {
|
|
notificationApi[data.type]({
|
|
title: data.title,
|
|
message: data.message
|
|
})
|
|
})
|
|
|
|
newSocket.on('error', (err) => {
|
|
logger.error('Api Server error:', err)
|
|
setError('Api Server error')
|
|
})
|
|
|
|
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 () => {
|
|
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 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(
|
|
`Calling ${callbacks.length} callbacks for object:`,
|
|
objectId
|
|
)
|
|
callbacks.forEach((callback) => {
|
|
try {
|
|
callback(object)
|
|
} catch (error) {
|
|
logger.error('Error in object update callback:', error)
|
|
}
|
|
})
|
|
} else {
|
|
logger.debug(
|
|
`No callbacks found for object: ${objectId}, subscribed callbacks:`,
|
|
Array.from(subscribedCallbacksRef.current.keys())
|
|
)
|
|
}
|
|
}
|
|
|
|
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, type, callback)
|
|
}
|
|
},
|
|
[offUpdateEvent]
|
|
)
|
|
|
|
const subscribeToType = useCallback(
|
|
(type, callback) => {
|
|
logger.debug('Subscribing to type:', type)
|
|
if (socketRef.current && socketRef.current.connected == true) {
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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'
|
|
},
|
|
withCredentials: true
|
|
})
|
|
return response.data
|
|
} catch (err) {
|
|
showError(err, () => {
|
|
fetchObject(id, type)
|
|
})
|
|
return {}
|
|
} finally {
|
|
setFetchLoading(false)
|
|
}
|
|
}
|
|
|
|
// 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'
|
|
},
|
|
withCredentials: true
|
|
}
|
|
)
|
|
|
|
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) {
|
|
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 {}
|
|
}
|
|
}
|
|
|
|
// Download GCode file content
|
|
const fetchObjectContent = async (id, type, fileName) => {
|
|
if (!token) {
|
|
return
|
|
}
|
|
try {
|
|
const response = await axios.get(
|
|
`${config.backendUrl}/${type.toLowerCase()}s/${id}/content`,
|
|
{
|
|
headers: {
|
|
Accept: 'application/json'
|
|
},
|
|
withCredentials: true
|
|
}
|
|
)
|
|
|
|
const fileURL = window.URL.createObjectURL(new Blob([response.data]))
|
|
const fileLink = document.createElement('a')
|
|
fileLink.href = fileURL
|
|
fileLink.setAttribute('download', fileName)
|
|
document.body.appendChild(fileLink)
|
|
fileLink.click()
|
|
fileLink.parentNode.removeChild(fileLink)
|
|
} catch (err) {
|
|
showError(err, () => {
|
|
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
|
|
}
|
|
}
|
|
|
|
return (
|
|
<ApiServerContext.Provider
|
|
value={{
|
|
apiServer: socketRef.current,
|
|
error,
|
|
connecting,
|
|
connected,
|
|
lockObject,
|
|
unlockObject,
|
|
fetchObjectLock,
|
|
updateObject,
|
|
createObject,
|
|
deleteObject,
|
|
subscribeToObject,
|
|
subscribeToType,
|
|
subscribeToLock,
|
|
offUpdateEvent,
|
|
fetchObject,
|
|
fetchObjects,
|
|
fetchObjectsByProperty,
|
|
fetchLoading,
|
|
showError,
|
|
fetchObjectContent,
|
|
fetchNotes
|
|
}}
|
|
>
|
|
{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 }
|