// 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 ( {contextHolder} {children} Error } open={showErrorModal} okText='OK' style={{ maxWidth: 430 }} closable={false} centered maskClosable={true} footer={[ , ]} > {errorModalContent} ) } ApiServerProvider.propTypes = { children: PropTypes.node.isRequired } export { ApiServerContext, ApiServerProvider }