diff --git a/src/components/Dashboard/context/ApiServerContext.js b/src/components/Dashboard/context/ApiServerContext.js index 9d47033..8eadaf7 100644 --- a/src/components/Dashboard/context/ApiServerContext.js +++ b/src/components/Dashboard/context/ApiServerContext.js @@ -4,7 +4,8 @@ import React, { useEffect, useState, useContext, - useRef + useRef, + useCallback } from 'react' import io from 'socket.io-client' import { message, notification, Modal, Space, Button } from 'antd' @@ -22,8 +23,9 @@ logger.setLevel(config.logLevel) const ApiServerContext = createContext() const ApiServerProvider = ({ children }) => { - const { token, userProfile } = useContext(AuthContext) + 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() @@ -32,9 +34,43 @@ const ApiServerProvider = ({ children }) => { const [showErrorModal, setShowErrorModal] = useState(false) const [errorModalContent, setErrorModalContent] = useState('') const [retryCallback, setRetryCallback] = useState(null) + const subscribedCallbacksRef = useRef(new Map()) + const subscribedLockCallbacksRef = useRef(new Map()) - useEffect(() => { - if (token) { + 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, { @@ -48,18 +84,25 @@ const ApiServerProvider = ({ children }) => { 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) => { @@ -75,21 +118,27 @@ const ApiServerProvider = ({ children }) => { }) socketRef.current = newSocket + } + }, [token, authenticated, messageApi, notificationApi, notifyLockUpdate]) - // Clean up function - return () => { - if (socketRef.current) { - logger.debug('Cleaning up api server connection...') - socketRef.current.disconnect() - socketRef.current = null - } - } + 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 } - }, [token, messageApi]) + + // 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) @@ -118,8 +167,12 @@ const ApiServerProvider = ({ children }) => { type: type }, (lockEvent) => { - logger.debug('Received lock event for object:', id, lockEvent) - resolve(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) @@ -127,65 +180,186 @@ const ApiServerProvider = ({ children }) => { } } - const onLockEvent = (id, callback) => { + 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) { - const eventHandler = (data) => { - if (data._id === id && data?.user !== userProfile._id) { - logger.debug( - 'Lock update received for object:', - id, - 'locked:', - data.locked - ) - callback(data) + // 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) } } - 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) => { + 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) @@ -200,8 +374,8 @@ const ApiServerProvider = ({ children }) => { setRetryCallback(null) } - // Generalized fetchObjectInfo function - const fetchObjectInfo = async (id, type) => { + // Generalized fetchObject function + const fetchObject = async (id, type) => { const fetchUrl = `${config.backendUrl}/${type}s/${id}` setFetchLoading(true) logger.debug('Fetching from ' + fetchUrl) @@ -214,62 +388,17 @@ const ApiServerProvider = ({ children }) => { }) 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 + showError(err, () => { + fetchObject(id, type) + }) + return {} } 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 fetchObjects = async (type, params = {}) => { const { page = 1, limit = 25, @@ -319,9 +448,122 @@ const ApiServerProvider = ({ children }) => { hasMore, page } - } catch (error) { - logger.error('Failed to fetch table data:', error) - throw error + } 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 {} } } @@ -348,27 +590,35 @@ const ApiServerProvider = ({ children }) => { document.body.appendChild(fileLink) fileLink.click() fileLink.parentNode.removeChild(fileLink) - } catch (error) { - logger.error('Failed to download GCode file content:', error) + } catch (err) { + showError(err, () => { + fetchObjectContent(id, type, fileName) + }) + } + } - 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) - ) - } + // 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 } } @@ -378,19 +628,24 @@ const ApiServerProvider = ({ children }) => { apiServer: socketRef.current, error, connecting, + connected, lockObject, unlockObject, fetchObjectLock, - updateObjectInfo, + updateObject, createObject, - onLockEvent, - onUpdateEvent, + deleteObject, + subscribeToObject, + subscribeToType, + subscribeToLock, offUpdateEvent, - fetchObjectInfo, - fetchTableData, + fetchObject, + fetchObjects, + fetchObjectsByProperty, fetchLoading, showError, - fetchObjectContent + fetchObjectContent, + fetchNotes }} > {contextHolder} diff --git a/src/components/Dashboard/context/AuthContext.js b/src/components/Dashboard/context/AuthContext.js index 41f4f6f..f313b87 100644 --- a/src/components/Dashboard/context/AuthContext.js +++ b/src/components/Dashboard/context/AuthContext.js @@ -53,8 +53,7 @@ const AuthProvider = ({ children }) => { }) if (response.status === 200 && response.data) { - logger.debug('User is authenticated!') - setAuthenticated(true) + logger.debug('Got auth token!') setToken(response.data.access_token) setExpiresAt(response.data.expires_at) setUserProfile(response.data) @@ -89,75 +88,6 @@ const AuthProvider = ({ children }) => { } }, []) - const showTokenExpirationMessage = useCallback( - (expiresAt) => { - const now = new Date() - const expirationDate = new Date(expiresAt) - const timeRemaining = expirationDate - now - - if (timeRemaining <= 0) { - if (authenticated) { - setShowSessionExpiredModal(true) - setAuthenticated(false) - notificationApi.destroy('token-expiration') - } - } else { - const minutes = Math.floor(timeRemaining / 60000) - const seconds = Math.floor((timeRemaining % 60000) / 1000) - - // Only show notification in the final minute - if (minutes === 0) { - const totalSeconds = 60 - const remainingSeconds = totalSeconds - seconds - const progress = (remainingSeconds / totalSeconds) * 100 - - notificationApi.info({ - message: 'Session Expiring Soon', - description: ( -