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 }