436 lines
12 KiB
JavaScript
436 lines
12 KiB
JavaScript
// src/contexts/ApiServerContext.js
|
|
import React, {
|
|
createContext,
|
|
useEffect,
|
|
useState,
|
|
useContext,
|
|
useRef
|
|
} 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 } = useContext(AuthContext)
|
|
const socketRef = useRef(null)
|
|
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)
|
|
|
|
useEffect(() => {
|
|
if (token) {
|
|
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)
|
|
setError(null)
|
|
})
|
|
|
|
newSocket.on('disconnect', () => {
|
|
logger.debug('Api Server disconnected')
|
|
setError('Api Server disconnected')
|
|
})
|
|
|
|
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')
|
|
})
|
|
|
|
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
|
|
|
|
// Clean up function
|
|
return () => {
|
|
if (socketRef.current) {
|
|
logger.debug('Cleaning up api server connection...')
|
|
socketRef.current.disconnect()
|
|
socketRef.current = null
|
|
}
|
|
}
|
|
} else if (!token && socketRef.current) {
|
|
logger.debug('Token not available, disconnecting api server...')
|
|
socketRef.current.disconnect()
|
|
socketRef.current = null
|
|
}
|
|
}, [token, messageApi])
|
|
|
|
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 event for object:', id, lockEvent)
|
|
resolve(lockEvent)
|
|
}
|
|
)
|
|
logger.debug('Sent fetch lock command for object:', id)
|
|
})
|
|
}
|
|
}
|
|
|
|
const onLockEvent = (id, callback) => {
|
|
if (socketRef.current && socketRef.current.connected == true) {
|
|
const eventHandler = (data) => {
|
|
if (data._id === id && data?.user !== userProfile._id) {
|
|
logger.debug(
|
|
'Lock update received for object:',
|
|
id,
|
|
'locked:',
|
|
data.locked
|
|
)
|
|
callback(data)
|
|
}
|
|
}
|
|
|
|
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(
|
|
'Update event received for object:',
|
|
id,
|
|
'updatedAt:',
|
|
data.updatedAt
|
|
)
|
|
callback(data)
|
|
}
|
|
}
|
|
|
|
socketRef.current.on('notify_object_update', eventHandler)
|
|
logger.debug('Registered update event listener for object:', id)
|
|
|
|
// Return cleanup function
|
|
return () => offUpdateEvent(id, eventHandler)
|
|
}
|
|
}
|
|
|
|
const offUpdateEvent = (id, eventHandler) => {
|
|
if (socketRef.current && socketRef.current.connected == true) {
|
|
socketRef.current.off('notify_update', eventHandler)
|
|
logger.debug('Removed update event listener for object:', id)
|
|
}
|
|
}
|
|
|
|
const showError = (content, callback = null) => {
|
|
setErrorModalContent(content)
|
|
setRetryCallback(() => callback)
|
|
setShowErrorModal(true)
|
|
}
|
|
|
|
const handleRetry = () => {
|
|
setShowErrorModal(false)
|
|
setErrorModalContent('')
|
|
if (retryCallback) {
|
|
retryCallback()
|
|
}
|
|
setRetryCallback(null)
|
|
}
|
|
|
|
// Generalized fetchObjectInfo function
|
|
const fetchObjectInfo = 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) {
|
|
logger.error('Failed to fetch object information:', err)
|
|
// Don't automatically show error - let the component handle it
|
|
throw err
|
|
} 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 {
|
|
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 (error) {
|
|
logger.error('Failed to fetch table data:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// 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 (error) {
|
|
logger.error('Failed to download GCode file content:', error)
|
|
|
|
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)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<ApiServerContext.Provider
|
|
value={{
|
|
apiServer: socketRef.current,
|
|
error,
|
|
connecting,
|
|
lockObject,
|
|
unlockObject,
|
|
fetchObjectLock,
|
|
updateObjectInfo,
|
|
createObject,
|
|
onLockEvent,
|
|
onUpdateEvent,
|
|
offUpdateEvent,
|
|
fetchObjectInfo,
|
|
fetchTableData,
|
|
fetchLoading,
|
|
showError,
|
|
fetchObjectContent
|
|
}}
|
|
>
|
|
{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 }
|