Tom Butcher f743f9b892
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
Refactor TemplateEditor and TemplatePreview components for improved performance and error handling
- Updated TemplateEditor to use useCallback for handlePreviewMessage, optimizing performance.
- Enhanced TemplatePreview by adding default no-op functions for onTestObjectOpen and onPreviewMessage props.
- Improved error handling in fetchTemplatePDF and fetchTemplatePreview methods to ensure callbacks are only invoked if they are functions.
- Refactored preview loading logic to prevent unnecessary calls when template ID is not available.
2026-07-28 01:50:16 +01:00

2245 lines
64 KiB
JavaScript

// src/contexts/ApiServerContext.js
import {
createContext,
useEffect,
useState,
useContext,
useRef,
useCallback
} from 'react'
import io from 'socket.io-client'
import { message, Modal, Space, Button } from 'antd'
import PropTypes from 'prop-types'
import { AuthContext } from './AuthContext'
import { useLocation, useNavigate } from 'react-router-dom'
import axios from 'axios'
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import config from '../../../config'
import loglevel from 'loglevel'
import { getModelByName } from '../../../database/ObjectModels'
const logger = loglevel.getLogger('ApiServerContext')
logger.setLevel(config.logLevel)
const SPOTLIGHT_CACHE_TTL_MS = 10_000
const spotlightCache = new Map()
const runningSpotlightFetches = new Map()
const createEmptyUserSettings = () => ({
viewMode: {},
filterSidebarVisibility: {},
columnVisibility: {},
collapseState: {}
})
const normalizeUserSettingsCategory = (category) =>
category && typeof category === 'object' && !Array.isArray(category)
? category
: {}
const normalizeUserSettings = (settings = {}) => ({
viewMode: normalizeUserSettingsCategory(settings?.viewMode),
filterSidebarVisibility: normalizeUserSettingsCategory(
settings?.filterSidebarVisibility
),
columnVisibility: normalizeUserSettingsCategory(settings?.columnVisibility),
collapseState: normalizeUserSettingsCategory(settings?.collapseState)
})
const emitWithAcknowledgement = (socket, eventName, data) =>
new Promise((resolve) => socket.emit(eventName, data, resolve))
const stableStringify = (value) => {
if (Array.isArray(value)) {
return `[${value.map(stableStringify).join(',')}]`
}
if (value && typeof value === 'object') {
return `{${Object.keys(value)
.sort()
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
.join(',')}}`
}
return JSON.stringify(value)
}
const getObjectTypeSubscriptionKey = (objectType, filter = {}) =>
`${objectType}:${stableStringify(filter || {})}`
const getObjectTypeSubscriptionArgs = (filterOrCallback, callback) => {
if (typeof filterOrCallback === 'function') {
return { filter: {}, callback: filterOrCallback }
}
return { filter: filterOrCallback || {}, callback }
}
const getObjectEndpoint = (type) =>
getModelByName(type)?.endpoint || `${type.toLowerCase()}s`
const ApiServerContext = createContext()
const ApiServerProvider = ({ children }) => {
const location = useLocation()
const navigate = useNavigate()
const {
token,
userProfile,
setUserProfile,
authenticated,
setUnauthenticated
} = 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 [fetchLoading, setFetchLoading] = useState(false)
const [showErrorModal, setShowErrorModal] = useState(false)
const [errorModalContent, setErrorModalContent] = useState('')
const [retryCallback, setRetryCallback] = useState(null)
const [userSettings, setUserSettings] = useState(createEmptyUserSettings)
const [userSettingsLoaded, setUserSettingsLoaded] = useState(false)
const subscribedCallbacksRef = useRef(new Map())
const subscribedActivityCallbacksRef = useRef(new Map())
const subscribedActivityServerSubscriptionsRef = useRef(new Set())
const notificationListenersRef = useRef(new Set())
const completedLaunchSessionsRef = useRef(new Set())
const handleActivityUpdate = useCallback(async (data) => {
logger.debug('Notifying activity update:', data)
const objectId = data._id || data.id
const objectType = data.objectType
const activity = data.activity
const activities = data.activities
const callbacksRefKey = `${objectType}:${objectId}`
if (
objectId &&
objectType &&
(activity || Array.isArray(activities)) &&
subscribedActivityCallbacksRef.current.has(callbacksRefKey)
) {
const callbacks =
subscribedActivityCallbacksRef.current.get(callbacksRefKey)
logger.debug(
`Calling ${callbacks.length} activity callbacks for object:`,
objectId
)
callbacks.forEach((callback) => {
try {
callback(activity, activities)
} catch (error) {
logger.error('Error in activity update callback:', error)
}
})
}
}, [])
const clearSubscriptions = useCallback(() => {
subscribedCallbacksRef.current.clear()
subscribedActivityServerSubscriptionsRef.current.clear()
notificationListenersRef.current.clear()
}, [])
const resubscribeActivityListeners = useCallback((socket = socketRef.current) => {
if (!socket?.connected) {
return
}
subscribedActivityCallbacksRef.current.forEach((callbacks, callbacksRefKey) => {
if (!callbacks?.length) {
return
}
if (subscribedActivityServerSubscriptionsRef.current.has(callbacksRefKey)) {
return
}
const colonIndex = callbacksRefKey.indexOf(':')
if (colonIndex === -1) {
return
}
const objectType = callbacksRefKey.slice(0, colonIndex)
const objectId = callbacksRefKey.slice(colonIndex + 1)
socket.emit(
'subscribeToObjectActivity',
{ _id: objectId, objectType },
(result) => {
if (result?.success) {
subscribedActivityServerSubscriptionsRef.current.add(callbacksRefKey)
if (Array.isArray(result.activities)) {
callbacks.forEach((callback) => {
try {
callback(null, result.activities)
} catch (error) {
logger.error('Error in activity resubscribe callback:', error)
}
})
}
}
}
)
})
}, [])
const registerNotificationListener = useCallback((callback) => {
notificationListenersRef.current.add(callback)
return () => notificationListenersRef.current.delete(callback)
}, [])
const unregisterNotificationListener = useCallback((callback) => {
notificationListenersRef.current.delete(callback)
}, [])
const getUserSettings = useCallback(async (socket = socketRef.current) => {
if (!socket?.connected) {
return null
}
try {
const result = await emitWithAcknowledgement(
socket,
'getUserSettings',
{}
)
if (!result?.success) {
throw new Error(result?.error || 'Unable to get user settings')
}
const settings = normalizeUserSettings(result.settings)
setUserSettings(settings)
return settings
} catch (err) {
logger.error('Failed to get user settings:', err)
setUserSettings(createEmptyUserSettings())
return null
} finally {
setUserSettingsLoaded(true)
}
}, [])
const updateUserSettings = useCallback(
async (category, key, value) => {
setUserSettings((currentSettings) => ({
...currentSettings,
[category]: {
...currentSettings[category],
[key]: value
}
}))
const socket = socketRef.current
if (!socket?.connected) {
return null
}
try {
const result = await emitWithAcknowledgement(
socket,
'updateUserSettings',
{ category, key, value }
)
if (!result?.success) {
throw new Error(result?.error || 'Unable to update user settings')
}
return result.settings
} catch (err) {
logger.error('Failed to update user settings:', err)
messageApi.error('Failed to save user settings')
await getUserSettings(socket)
return null
}
},
[getUserSettings, messageApi]
)
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: { type: 'user' }
})
setConnecting(true)
newSocket.on('connect', () => {
logger.debug('Api Server connected')
newSocket.emit('authenticate', { token: token }, async (result) => {
if (result?.valid !== true) {
setConnecting(false)
setError('Api Server authentication failed')
newSocket.disconnect()
return
}
setUserSettings(createEmptyUserSettings())
setUserSettingsLoaded(false)
await getUserSettings(newSocket)
if (!newSocket.connected) {
return
}
setConnecting(false)
setConnected(true)
setError(null)
resubscribeActivityListeners(newSocket)
})
})
newSocket.on('objectUpdate', handleObjectUpdate)
newSocket.on('objectEvent', handleObjectEvent)
newSocket.on('objectNew', handleObjectNew)
newSocket.on('objectDelete', handleObjectDelete)
newSocket.on('activityUpdate', handleActivityUpdate)
newSocket.on('modelStats', handleModelStats)
newSocket.on('notification', (data) => {
let notification
try {
notification = typeof data === 'string' ? JSON.parse(data) : data
} catch (err) {
logger.error('Failed to parse notification:', err)
return
}
if (!notification) return
logger.debug('Notification received:', notification)
notificationListenersRef.current.forEach((cb) => {
try {
cb(notification)
} catch (err) {
logger.error('Error in notification callback:', err)
}
})
})
newSocket.on('disconnect', () => {
logger.debug('Api Server disconnected')
setError('Api Server disconnected')
clearSubscriptions()
setConnected(false)
setUserSettings(createEmptyUserSettings())
setUserSettingsLoaded(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')
clearSubscriptions()
setConnected(false)
setUserSettings(createEmptyUserSettings())
setUserSettingsLoaded(false)
})
newSocket.on('error', (err) => {
logger.error('Api Server error:', err)
setError('Api Server error')
})
socketRef.current = newSocket
}
}, [
token,
authenticated,
messageApi,
handleActivityUpdate,
clearSubscriptions,
getUserSettings,
resubscribeActivityListeners
])
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
setUserSettings(createEmptyUserSettings())
setUserSettingsLoaded(false)
}
// Clean up function
return () => {
if (socketRef.current) {
logger.debug('Cleaning up api server connection...')
socketRef.current.disconnect()
socketRef.current = null
}
}
}, [token, authenticated, connectToServer])
useEffect(() => {
const handlePageVisible = () => {
if (document.visibilityState !== 'visible') {
return
}
const socket = socketRef.current
if (!socket || !token || authenticated !== true) {
return
}
if (!socket.connected) {
logger.debug('Page visible with disconnected socket, reconnecting...')
socket.connect()
return
}
if (subscribedActivityCallbacksRef.current.size > 0) {
logger.debug('Page visible, resubscribing activity listeners...')
subscribedActivityServerSubscriptionsRef.current.clear()
resubscribeActivityListeners(socket)
}
}
const handlePageShow = (event) => {
if (!event.persisted) {
return
}
logger.debug('Page restored from bfcache, reconnecting socket...')
subscribedActivityServerSubscriptionsRef.current.clear()
setConnected(false)
if (socketRef.current) {
socketRef.current.disconnect()
socketRef.current = null
}
connectToServer()
}
document.addEventListener('visibilitychange', handlePageVisible)
window.addEventListener('pageshow', handlePageShow)
return () => {
document.removeEventListener('visibilitychange', handlePageVisible)
window.removeEventListener('pageshow', handlePageShow)
}
}, [token, authenticated, connectToServer, resubscribeActivityListeners])
const setObjectActivity = (id, type, mode) => {
logger.debug('Setting activity for', id, mode)
if (socketRef.current && socketRef.current.connected) {
return new Promise((resolve) => {
socketRef.current.emit(
'setActivity',
{ _id: id, type, mode },
(result) => {
logger.debug('Received setActivity result for object:', id, result)
resolve(result || { success: true })
}
)
})
}
return Promise.resolve({ success: false, error: 'not_connected' })
}
const clearObjectActivity = (id, type) => {
logger.debug('Clearing activity for', id)
if (socketRef.current && socketRef.current.connected == true) {
socketRef.current.emit('clearActivity', { _id: id, type })
logger.debug('Sent clearActivity command for object:', id)
}
}
const fetchObjectActivities = async (id, type) => {
if (socketRef.current && socketRef.current.connected == true) {
logger.debug('Fetching activities for ' + id)
return new Promise((resolve) => {
socketRef.current.emit(
'getActivities',
{
_id: id,
type: type
},
(result) => {
logger.debug('Received activities for object:', id, result)
resolve(result?.activities || [])
}
)
})
}
return []
}
const handleObjectUpdate = async (data) => {
logger.debug('Notifying object update:', data)
const id = data._id
const objectType = data.objectType
const callbacksRefKey = `${objectType}:${id}`
if (
id &&
objectType &&
subscribedCallbacksRef.current.has(callbacksRefKey)
) {
const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
logger.debug(
`Calling ${callbacks.length} callbacks for object:`,
callbacksRefKey
)
callbacks.forEach((callback) => {
try {
callback(data.object)
} catch (error) {
logger.error('Error in object update callback:', error)
}
})
} else {
logger.debug(
`No callbacks found for object: ${callbacksRefKey}, subscribed callbacks:`,
Array.from(subscribedCallbacksRef.current.keys())
)
}
}
const handleObjectEvent = async (data) => {
const id = data._id
const objectType = data.objectType
const callbacksRefKey = `${objectType}:${id}:events:${data.event.type}`
logger.debug('Notifying object event:', data)
if (
id &&
objectType &&
subscribedCallbacksRef.current.has(callbacksRefKey)
) {
const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
logger.debug(
`Calling ${callbacks.length} callbacks for object:`,
callbacksRefKey
)
callbacks.forEach((callback) => {
try {
callback(data.event)
} catch (error) {
logger.error('Error in object event callback:', error)
}
})
} else {
logger.debug(
`No callbacks found for object: ${callbacksRefKey}, subscribed callbacks:`,
Array.from(subscribedCallbacksRef.current.keys())
)
}
}
const handleModelStats = async (data) => {
const objectType = data.objectType || 'unknown'
const callbacksRefKey = `modelStats:${objectType}`
logger.debug('Notifying model stats update:', data)
if (objectType && subscribedCallbacksRef.current.has(callbacksRefKey)) {
const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
logger.debug(
`Calling ${callbacks.length} callbacks for model stats:`,
callbacksRefKey
)
callbacks.forEach((callback) => {
try {
callback(data.stats)
} catch (error) {
logger.error('Error in model stats callback:', error)
}
})
} else {
logger.debug(
`No callbacks found for model stats: ${callbacksRefKey}, subscribed callbacks:`,
Array.from(subscribedCallbacksRef.current.keys())
)
}
}
const handleObjectNew = async (data) => {
logger.debug('Notifying object new:', data)
const objectType = data.objectType || 'unknown'
const callbacksRefKey = getObjectTypeSubscriptionKey(
objectType,
data.filter || {}
)
if (objectType && subscribedCallbacksRef.current.has(callbacksRefKey)) {
const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
logger.debug(
`Calling ${callbacks.length} callbacks for type:`,
callbacksRefKey
)
callbacks.forEach((callback) => {
try {
callback(data.object)
} catch (error) {
logger.error('Error in object new callback:', error)
}
})
} else {
logger.debug(
`No callbacks found for object: ${callbacksRefKey}, subscribed callbacks:`,
Array.from(subscribedCallbacksRef.current.keys())
)
}
}
const handleObjectDelete = async (data) => {
logger.debug('Notifying object delete:', data)
const objectType = data.objectType || 'unknown'
const callbacksRefKey = getObjectTypeSubscriptionKey(
objectType,
data.filter || {}
)
if (objectType && subscribedCallbacksRef.current.has(callbacksRefKey)) {
const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
logger.debug(
`Calling ${callbacks.length} callbacks for type:`,
callbacksRefKey
)
callbacks.forEach((callback) => {
try {
callback(data.object)
} catch (error) {
logger.error('Error in object new callback:', error)
}
})
} else {
logger.debug(
`No callbacks found for object: ${callbacksRefKey}, subscribed callbacks:`,
Array.from(subscribedCallbacksRef.current.keys())
)
}
}
const offObjectUpdatesEvent = useCallback((id, objectType, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
const callbacksRefKey = `${objectType}:${id}`
// Remove callback from the subscribed callbacks map
if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
const callbacks = subscribedCallbacksRef.current
.get(callbacksRefKey)
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
logger.debug(
'No callbacks found for object:',
callbacksRefKey,
'unsubscribing from object update...'
)
subscribedCallbacksRef.current.delete(callbacksRefKey)
socketRef.current.emit('unsubscribeObjectUpdate', {
_id: id,
objectType: objectType
})
} else {
subscribedCallbacksRef.current.set(callbacksRefKey, callbacks)
}
}
}
}, [])
const offObjectTypeUpdatesEvent = useCallback(
(objectType, filter, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
const callbacksRefKey = getObjectTypeSubscriptionKey(objectType, filter)
// Remove callback from the subscribed callbacks map
if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
const callbacks = subscribedCallbacksRef.current
.get(callbacksRefKey)
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedCallbacksRef.current.delete(callbacksRefKey)
socketRef.current.emit('unsubscribeObjectTypeUpdate', {
objectType: objectType,
filter: filter
})
} else {
subscribedCallbacksRef.current.set(callbacksRefKey, callbacks)
}
}
}
},
[]
)
const subscribeToObjectUpdates = useCallback(
(id, objectType, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
const callbacksRefKey = `${objectType}:${id}`
// Add callback to the subscribed callbacks map immediately
if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
subscribedCallbacksRef.current.set(callbacksRefKey, [])
}
const callbacksLength =
subscribedCallbacksRef.current.get(callbacksRefKey).length
if (callbacksLength <= 0) {
socketRef.current.emit(
'subscribeToObjectUpdate',
{
_id: id,
objectType: objectType
},
(result) => {
if (result.success) {
logger.info('Subscribed to id:', id, 'objectType:', objectType)
}
}
)
}
logger.info(
'Adding callback id:',
id,
'objectType:',
objectType,
'callbacks length:',
callbacksLength + 1
)
subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)
// Return cleanup function
return () => offObjectUpdatesEvent(id, objectType, callback)
}
},
[offObjectUpdatesEvent]
)
// Subscribe to user profile updates when WebSocket is connected and userProfile._id exists
useEffect(() => {
if (connected && userProfile?._id) {
const unsubscribe = subscribeToObjectUpdates(
userProfile._id,
'user',
(updatedUser) => {
logger.debug('Notifying user profile update:', updatedUser)
setUserProfile((prev) =>
prev && updatedUser ? { ...prev, ...updatedUser } : prev
)
}
)
return () => {
if (unsubscribe) unsubscribe()
}
}
}, [connected, userProfile?._id, subscribeToObjectUpdates, setUserProfile])
const subscribeToObjectTypeUpdates = useCallback(
(objectType, filterOrCallback = {}, maybeCallback) => {
const { filter, callback } = getObjectTypeSubscriptionArgs(
filterOrCallback,
maybeCallback
)
const callbacksRefKey = getObjectTypeSubscriptionKey(objectType, filter)
logger.debug('Subscribing to type updates:', objectType, filter)
if (socketRef.current && socketRef.current.connected == true) {
// Add callback to the subscribed callbacks map immediately
if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
subscribedCallbacksRef.current.set(callbacksRefKey, [])
}
const callbacksLength =
subscribedCallbacksRef.current.get(callbacksRefKey).length
if (callbacksLength <= 0) {
socketRef.current.emit(
'subscribeToObjectTypeUpdate',
{ objectType: objectType, filter: filter },
(result) => {
if (result.success) {
logger.info('Subscribed to objectType:', objectType, filter)
}
}
)
}
subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)
logger.debug(
`Added callback for type ${callbacksRefKey}, total callbacks: ${callbacksLength + 1}`
)
logger.debug(
'Registered type event listener for object:',
callbacksRefKey
)
// Return cleanup function
return () => offObjectTypeUpdatesEvent(objectType, filter, callback)
}
},
[offObjectTypeUpdatesEvent]
)
const offActivityEvent = useCallback((id, objectType, callback) => {
const callbacksRefKey = `${objectType}:${id}`
if (!subscribedActivityCallbacksRef.current.has(callbacksRefKey)) {
return
}
const callbacks = subscribedActivityCallbacksRef.current
.get(callbacksRefKey)
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedActivityCallbacksRef.current.delete(callbacksRefKey)
subscribedActivityServerSubscriptionsRef.current.delete(callbacksRefKey)
if (socketRef.current?.connected) {
socketRef.current.emit('unsubscribeObjectActivity', {
_id: id,
objectType
})
}
} else {
subscribedActivityCallbacksRef.current.set(callbacksRefKey, callbacks)
}
logger.debug('Removed activity event listener for object:', id)
}, [])
const offObjectEventEvent = useCallback(
(id, objectType, eventType, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
const callbacksRefKey = `${objectType}:${id}:events:${eventType}`
// Remove callback from the subscribed callbacks map
if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
const callbacks = subscribedCallbacksRef.current
.get(callbacksRefKey)
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedCallbacksRef.current.delete(callbacksRefKey)
socketRef.current.emit('unsubscribeObjectEvent', {
_id: id,
objectType,
eventType
})
} else {
subscribedCallbacksRef.current.set(callbacksRefKey, callbacks)
}
}
}
},
[]
)
const subscribeToObjectEvent = useCallback(
(id, objectType, eventType, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
const callbacksRefKey = `${objectType}:${id}:events:${eventType}`
// Add callback to the subscribed callbacks map immediately
if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
subscribedCallbacksRef.current.set(callbacksRefKey, [])
}
const callbacksLength =
subscribedCallbacksRef.current.get(callbacksRefKey).length
if (callbacksLength <= 0) {
socketRef.current.emit(
'subscribeToObjectEvent',
{
_id: id,
objectType: objectType,
eventType: eventType
},
(result) => {
if (result.success) {
logger.info(
'Subscribed to event id:',
id,
'objectType:',
objectType,
'eventType:',
eventType
)
}
}
)
}
logger.info(
'Adding event callback id:',
id,
'objectType:',
objectType,
'eventType:',
eventType,
'callbacks length:',
callbacksLength + 1
)
subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)
// Return cleanup function
return () => offObjectEventEvent(id, objectType, eventType, callback)
}
},
[offObjectEventEvent]
)
const offModelStats = useCallback((objectType, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
const callbacksRefKey = `modelStats:${objectType}`
// Remove callback from the subscribed callbacks map
if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
const callbacks = subscribedCallbacksRef.current
.get(callbacksRefKey)
.filter((cb) => cb !== callback)
if (callbacks.length === 0) {
subscribedCallbacksRef.current.delete(callbacksRefKey)
socketRef.current.emit('unsubscribeModelStats', {
objectType
})
} else {
subscribedCallbacksRef.current.set(callbacksRefKey, callbacks)
}
}
}
}, [])
const subscribeToModelStats = useCallback(
(objectType, callback) => {
if (socketRef.current && socketRef.current.connected == true) {
const callbacksRefKey = `modelStats:${objectType}`
// Add callback to the subscribed callbacks map immediately
if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
subscribedCallbacksRef.current.set(callbacksRefKey, [])
}
const callbacksLength =
subscribedCallbacksRef.current.get(callbacksRefKey).length
if (callbacksLength <= 0) {
socketRef.current.emit(
'subscribeToModelStats',
{
objectType: objectType
},
(result) => {
if (result.success) {
logger.info('Subscribed to model stats:', objectType)
}
}
)
}
logger.info(
'Adding model stats callback:',
objectType,
'callbacks length:',
callbacksLength + 1
)
subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)
// Return cleanup function
return () => offModelStats(objectType, callback)
}
},
[offModelStats]
)
const subscribeToObjectActivity = useCallback(
(id, type, callback) => {
logger.debug('Subscribing to activity for object:', id, 'type:', type)
if (!id || !type) {
return undefined
}
const callbacksRefKey = `${type}:${id}`
if (!subscribedActivityCallbacksRef.current.has(callbacksRefKey)) {
subscribedActivityCallbacksRef.current.set(callbacksRefKey, [])
}
subscribedActivityCallbacksRef.current.get(callbacksRefKey).push(callback)
const socket = socketRef.current
if (
socket?.connected &&
!subscribedActivityServerSubscriptionsRef.current.has(callbacksRefKey)
) {
socket.emit(
'subscribeToObjectActivity',
{ _id: id, objectType: type },
(result) => {
if (result?.success) {
subscribedActivityServerSubscriptionsRef.current.add(callbacksRefKey)
if (Array.isArray(result.activities)) {
subscribedActivityCallbacksRef.current
.get(callbacksRefKey)
?.forEach((activityCallback) => {
try {
activityCallback(null, result.activities)
} catch (error) {
logger.error('Error in activity subscribe callback:', error)
}
})
}
}
}
)
}
logger.debug('Registered activity event listener for object:', id)
return () => offActivityEvent(id, type, callback)
},
[offActivityEvent]
)
const showError = useCallback(
(error, callback = null) => {
const code = error.response.data.code || 'UNKNOWN'
if (code == 'UNAUTHORIZED') {
setUnauthenticated()
return
}
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)
},
[setUnauthenticated]
)
const handleRetry = () => {
setShowErrorModal(false)
setErrorModalContent('')
if (retryCallback) {
retryCallback()
}
setRetryCallback(null)
}
// Generalized fetchObject function
const fetchObject = async (id, type) => {
const fetchUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
setFetchLoading(true)
logger.debug('Fetching from ' + fetchUrl)
try {
const response = await axios.get(fetchUrl, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
setFetchLoading(false)
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
fetchObject(id, type)
})
return {}
}
}
// Fetch table data with pagination, filtering, and sorting
const fetchObjects = async (type, params = {}) => {
const {
page = 1,
limit = 25,
filter = {},
sorter = {},
onDataChange
} = params
let newFilter = { ...filter }
if (filter != null && Object.keys(filter).length > 0) {
const model = getModelByName(type)
if (model.name !== 'unknown') {
for (const key of Object.keys(filter)) {
const property = model?.properties?.find((p) => p.name === key)
if (property && property.type === 'object') {
const value = filter[key]
newFilter[`${key}._id`] = value?._id ?? value
delete newFilter[key]
}
}
}
}
logger.debug('Fetching table data from:', type, {
page,
limit,
newFilter,
sorter
})
try {
const response = await axios.get(
`${config.backendUrl}/${getObjectEndpoint(type)}`,
{
params: {
page,
limit,
...newFilter,
sort: sorter.field,
order: sorter.order
},
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
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) {
console.error(err)
showError(err, () => {
fetchObjects(type, params)
})
return []
}
}
// Fetch table data with pagination, filtering, and sorting
const fetchObjectsByProperty = async (type, params = {}) => {
const { filter = {}, properties = [], masterFilter = {} } = params
logger.debug('Fetching property object data from:', type, {
properties,
filter
})
try {
const response = await axios.get(
`${config.backendUrl}/${getObjectEndpoint(type)}/properties`,
{
params: {
...Object.keys(filter).reduce((acc, key) => {
acc[key] = Array.isArray(filter[key])
? filter[key].join(',')
: filter[key]
return acc
}, {}),
properties: properties.join(','), // Convert array to comma-separated string
masterFilter: JSON.stringify(masterFilter)
},
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
const newData = response.data
return newData
} catch (err) {
console.error(err)
showError(err, () => {
fetchObjects(type, params)
})
return []
}
}
const searchObjects = async (type, search) => {
const searchUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/search`
logger.debug('Searching objects for ' + type, { search })
try {
const response = await axios.get(searchUrl, {
params: { search },
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
searchObjects(type, search)
})
return []
}
}
// Update filament information
const updateObject = async (id, type, value) => {
const updateUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
logger.debug('Updating info for ' + id)
try {
const response = await axios.put(updateUrl, value, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
logger.debug('Object updated successfully')
return response.data
} catch (err) {
console.error(err)
setError(err, () => {
updateObject(id, type, value)
})
return {}
}
}
// Update multiple objects
const updateMultipleObjects = async (type, objects) => {
const updateUrl = `${config.backendUrl}/${getObjectEndpoint(type)}`
logger.debug('Updating multiple objects for ' + type)
try {
const response = await axios.put(updateUrl, objects, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
logger.debug('Objects updated successfully')
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
updateMultipleObjects(type, objects)
})
return []
}
}
// Update filament information
const deleteObject = async (id, type) => {
const deleteUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
logger.debug('Deleting object ID: ' + id)
try {
const response = await axios.delete(deleteUrl, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
logger.debug('Object deleted successfully')
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
deleteObject(id, type)
})
return {}
}
}
// Update filament information
const createObject = async (type, value) => {
const createUrl = `${config.backendUrl}/${getObjectEndpoint(type)}`
logger.debug('Creating object...')
try {
const response = await axios.post(createUrl, value, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
createObject(type, value)
})
return {}
}
}
// Call a function on an object
const sendObjectFunction = async (id, type, functionName, value = {}) => {
const url = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}/${functionName}`
logger.debug(`Calling object function ${functionName} for ${id} at ${url}`)
try {
const response = await axios.post(url, value, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
sendObjectFunction(id, type, functionName, value)
})
return {}
}
}
const getObjectFunction = async (id, type, functionName, params = {}) => {
const url = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}/${functionName}`
logger.debug(`Fetching object function ${functionName} for ${id} at ${url}`)
try {
const response = await axios.get(url, {
params,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
getObjectFunction(id, type, functionName, params)
})
return {}
}
}
const getMarketplaceAuthUrl = async (marketplaceId, state) => {
return getObjectFunction(marketplaceId, 'Marketplace', 'auth/url', {
state
})
}
const refreshMarketplaceAuth = async (marketplaceId) => {
return sendObjectFunction(marketplaceId, 'Marketplace', 'auth/refresh')
}
// Export list data to CSV and download
const exportToCsv = async (objectType) => {
try {
const response = await axios.get(
`${config.backendUrl}/csv/${objectType}`,
{
responseType: 'blob',
headers: {
Accept: 'text/csv',
Authorization: `Bearer ${token}`
}
}
)
const blob = new Blob([response.data], { type: 'text/csv' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${objectType}-export-${new Date().toISOString().slice(0, 10)}.csv`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
message.success('CSV file downloaded')
} catch (err) {
console.error(err)
showError(err, () => exportToCsv(objectType))
}
}
// Export list data to Excel and download or open in Excel
const exportToExcel = async (objectType, mode = 'download') => {
try {
if (mode === 'open') {
// Open in Excel: get a temporary https URL (Excel fetches it when opening)
const response = await axios.post(
`${config.backendUrl}/excel/${objectType}/open`,
{},
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
const url = response.data?.url
if (!url || typeof url !== 'string') {
throw new Error('No URL received from server')
}
const excelUri = `ms-excel:ofe|u|${url}`
window.location.href = excelUri
message.success('Opening in Excel')
} else if (mode === 'download') {
const response = await axios.get(
`${config.backendUrl}/excel/${objectType}`,
{
responseType: 'blob',
headers: {
Accept:
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
Authorization: `Bearer ${token}`
}
}
)
const blob = new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${objectType}-export-${new Date().toISOString().slice(0, 10)}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
message.success('Excel file downloaded')
} else {
throw new Error('Invalid mode')
}
} catch (err) {
console.error(err)
showError(err, () => exportToExcel(objectType))
}
}
// Download GCode file content
const fetchFileContent = async (file, download = false) => {
try {
const response = await axios.get(
`${config.backendUrl}/files/${file._id}/content`,
{
headers: {
Accept: '*/*',
Authorization: `Bearer ${token}`
},
responseType: 'blob'
}
)
const blob = new Blob([response.data], {
type: response.headers['content-type']
})
const fileURL = window.URL.createObjectURL(blob)
if (download == true) {
const fileLink = document.createElement('a')
fileLink.href = fileURL
fileLink.setAttribute('download', `${file.name}${file.extension}`)
document.body.appendChild(fileLink)
fileLink.click()
fileLink.parentNode.removeChild(fileLink)
return
}
return fileURL
} catch (err) {
console.error(err)
showError(err, () => {
fetchFileContent(file, download)
})
}
}
const fetchFileThumbnail = async (file, size) => {
try {
const response = await axios.get(
`${config.backendUrl}/files/${file._id}/thumbnail`,
{
params: { size },
headers: {
Accept: 'image/jpeg',
Authorization: `Bearer ${token}`
},
responseType: 'blob'
}
)
const blob = new Blob([response.data], {
type: response.headers['content-type'] || 'image/jpeg'
})
return window.URL.createObjectURL(blob)
} catch (err) {
console.error(err)
showError(err, () => {
fetchFileThumbnail(file, size)
})
}
}
// 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._id': parentId,
sort: 'createdAt',
order: 'ascend'
},
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
const notesData = response.data || []
logger.debug('Fetched notes:', notesData.length)
return notesData
} catch (err) {
console.error(err)
showError(err, () => {
fetchNotes(parentId)
})
}
}
const fetchSpotlightData = async (query) => {
const cached = spotlightCache.get(query)
if (cached && Date.now() - cached.timestamp < SPOTLIGHT_CACHE_TTL_MS) {
logger.debug('Returning cached spotlight data for query:', query)
return cached.data
}
const existing = runningSpotlightFetches.get(query)
if (existing) {
return existing
}
const fetchPromise = (async () => {
try {
logger.debug('Fetching spotlight data with query:', query)
const response = await axios.get(
`${config.backendUrl}/spotlight/${query}`,
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
spotlightCache.set(query, {
data: response.data,
timestamp: Date.now()
})
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
fetchSpotlightData(query)
})
} finally {
runningSpotlightFetches.delete(query)
}
})()
runningSpotlightFetches.set(query, fetchPromise)
return fetchPromise
}
const getModelStats = async (objectType) => {
logger.debug('Fetching stats for model type:', objectType)
try {
let statsUrl
if (objectType === 'history') {
statsUrl = `${config.backendUrl}/stats/history`
} else {
statsUrl = `${config.backendUrl}/${getObjectEndpoint(objectType)}/stats`
}
const response = await axios.get(statsUrl, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
logger.debug('Fetched stats for model type:', objectType)
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
getModelStats(objectType)
})
return null
}
}
const getModelPropertyValues = async (objectType, property) => {
logger.debug(
'Fetching property values for model type:',
objectType,
property
)
try {
const response = await axios.get(
`${config.backendUrl}/${getObjectEndpoint(objectType)}/values`,
{
params: { property },
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
logger.debug(
'Fetched property values for model type:',
objectType,
property
)
return Array.isArray(response.data) ? response.data : []
} catch (err) {
console.error(err)
showError(err, () => {
getModelPropertyValues(objectType, property)
})
return []
}
}
const getModelHistory = async (objectType, startDate, endDate) => {
logger.debug('Fetching history for model type:', objectType)
const encodedStartDate = encodeURIComponent(startDate.toISOString())
const encodedEndDate = encodeURIComponent(endDate.toISOString())
try {
const historyUrl = `${config.backendUrl}/${getObjectEndpoint(objectType)}/history?from=${encodedStartDate}&to=${encodedEndDate}`
const response = await axios.get(historyUrl, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
logger.debug('Fetched history for model type:', objectType)
// Calculate time range in milliseconds
const timeRangeMs = endDate.getTime() - startDate.getTime()
const oneHourMs = 60 * 60 * 1000
const twelveHoursMs = 12 * 60 * 60 * 1000
const oneDayMs = 24 * 60 * 60 * 1000
const threeDaysMs = 3 * 24 * 60 * 60 * 1000
// Determine interval based on time range
let intervalMinutes = 1 // Default: 1 minute
if (timeRangeMs > threeDaysMs) {
intervalMinutes = 60 // Over 1 day: 60 minutes
} else if (timeRangeMs > oneDayMs) {
intervalMinutes = 30 // Over 2 days: 30 minutes
} else if (timeRangeMs > twelveHoursMs) {
intervalMinutes = 10 // Over 12 hours: 10 minutes
} else if (timeRangeMs > oneHourMs) {
intervalMinutes = 5 // Over 1 hour: 5 minutes
}
const intervalMs = intervalMinutes * 60 * 1000
// Filter data to only include points at the specified intervals
if (response.data && Array.isArray(response.data)) {
const filteredData = []
const seenIntervals = new Set()
response.data.forEach((point) => {
const pointDate = new Date(point.date)
// Round down to the nearest interval
const roundedTime =
Math.floor(pointDate.getTime() / intervalMs) * intervalMs
const roundedDate = new Date(roundedTime)
const intervalKey = roundedDate.toISOString()
// Only include if we haven't seen this interval yet
if (!seenIntervals.has(intervalKey)) {
seenIntervals.add(intervalKey)
// Update the point's date to the rounded interval
filteredData.push({
...point,
date: roundedDate.toISOString()
})
}
})
// Sort by date to ensure chronological order
filteredData.sort((a, b) => new Date(a.date) - new Date(b.date))
logger.debug(
`Filtered history data: ${response.data.length} -> ${filteredData.length} points (${intervalMinutes} min intervals)`
)
return filteredData
}
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
getModelHistory(objectType)
})
return null
}
}
const fetchTemplatePreview = async (
id,
content,
testObject,
scale,
callback
) => {
logger.debug('Fetching preview...')
if (socketRef.current && socketRef.current.connected) {
return socketRef.current.emit(
'previewTemplate',
{
_id: id,
content: content,
testObject: testObject,
scale: scale
},
callback
)
}
if (typeof callback === 'function') {
callback({ error: 'Api Server disconnected' })
}
}
const fetchTemplatePDF = async (id, content, testObject, callback) => {
logger.debug('Fetching pdf template...')
if (socketRef.current && socketRef.current.connected) {
return socketRef.current.emit(
'renderTemplatePDF',
{
_id: id,
content: content,
object: testObject
},
callback
)
}
if (typeof callback === 'function') {
callback({ error: 'Api Server disconnected' })
}
}
const downloadTemplatePDF = async (
id,
content,
object,
filename,
callback
) => {
logger.debug('Downloading template PDF...')
fetchTemplatePDF(id, content, object, (result) => {
logger.debug('Downloading template PDF result:', result)
if (result?.error) {
console.error(result.error)
if (callback) {
callback(result.error)
}
} else {
const pdfBlob = new Blob([result.pdf], { type: 'application/pdf' })
const pdfUrl = URL.createObjectURL(pdfBlob)
const fileLink = document.createElement('a')
fileLink.href = pdfUrl
fileLink.setAttribute('download', `${filename}.pdf`)
document.body.appendChild(fileLink)
fileLink.click()
fileLink.parentNode.removeChild(fileLink)
if (callback) {
callback()
}
}
})
}
const fetchHostOTP = async (id, callback) => {
logger.debug('Fetching host OTP...')
if (socketRef.current && socketRef.current.connected) {
return socketRef.current.emit(
'generateHostOtp',
{
_id: id
},
callback
)
}
}
const sendObjectAction = async (id, objectType, action, callback) => {
logger.debug('Sending object action...')
if (socketRef.current && socketRef.current.connected) {
return socketRef.current.emit(
'objectAction',
{
_id: id,
objectType: objectType,
action: action
},
callback
)
}
}
// Upload file to the API
const uploadFile = async (
file,
additionalData = {},
progressCallback = null
) => {
const uploadUrl = `${config.backendUrl}/files`
logger.debug('Uploading file:', file.name, 'to:', uploadUrl)
try {
const formData = new FormData()
formData.append('file', file)
// Add any additional data to the form
Object.keys(additionalData).forEach((key) => {
formData.append(key, additionalData[key])
})
const response = await axios.post(uploadUrl, formData, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
)
logger.debug(`Upload progress: ${percentCompleted}%`)
if (progressCallback) {
progressCallback(percentCompleted)
}
}
})
logger.debug('File uploaded successfully:', response.data)
return response.data
} catch (err) {
console.error('File upload error:', err)
showError(err, () => {
uploadFile(file, additionalData, progressCallback)
})
return null
}
}
const createUserNotifier = async (objectId, objectType) => {
if (!userProfile?._id) return null
return createObject('userNotifier', {
user: userProfile._id,
object: objectId,
objectType
})
}
const deleteUserNotifier = async (userNotifierId) => {
return deleteObject(userNotifierId, 'userNotifier')
}
const fetchUserNotifiersForObject = async (objectId, objectType) => {
if (!userProfile?._id) return { data: [] }
const result = await fetchObjects('userNotifier', {
filter: {
user: userProfile._id,
object: objectId,
objectType
},
limit: 1
})
return result || { data: [] }
}
const fetchAllUserNotifiersForObject = async (objectId, objectType) => {
const result = await fetchObjects('userNotifier', {
filter: {
object: objectId,
objectType
},
limit: 100
})
return result || { data: [] }
}
const toggleUserNotifier = async (objectId, objectType) => {
const { data } = await fetchUserNotifiersForObject(objectId, objectType)
const existing = data?.[0]
if (existing) {
await deleteUserNotifier(existing._id)
return false
} else {
await createUserNotifier(objectId, objectType)
return true
}
}
const editUserNotifier = async (userNotifierId, updates) => {
try {
const result = await updateObject(userNotifierId, 'userNotifier', updates)
return result
} catch (err) {
console.error(err)
showError(err, () => editUserNotifier(userNotifierId, updates))
return null
}
}
const fetchNotificationsApi = useCallback(async () => {
const response = await axios.get(`${config.backendUrl}/notifications`, {
params: { limit: 100, sort: 'createdAt', order: 'descend' },
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
return Array.isArray(response.data) ? response.data : []
}, [token])
const markNotificationAsReadApi = useCallback(
async (notificationId) => {
await axios.put(
`${config.backendUrl}/notifications/${notificationId}/read`,
{},
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
},
[token]
)
const markAllNotificationsAsReadApi = useCallback(async () => {
await axios.put(
`${config.backendUrl}/notifications/read-all`,
{},
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
}, [token])
const deleteNotificationApi = useCallback(
async (notificationId) => {
await axios.delete(
`${config.backendUrl}/notifications/${notificationId}`,
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
},
[token]
)
const deleteAllNotificationsApi = useCallback(async () => {
await axios.delete(`${config.backendUrl}/notifications`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
}, [token])
const completeAppLaunchSession = useCallback(
async (launchSession) => {
const response = await axios.post(
`${config.backendUrl}/applaunch/${launchSession}`,
{},
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
return response.data
},
[token]
)
useEffect(() => {
const launchSession = new URLSearchParams(location.search).get(
'launchSession'
)
if (
authenticated !== true ||
!token ||
!launchSession ||
completedLaunchSessionsRef.current.has(launchSession)
) {
return
}
completedLaunchSessionsRef.current.add(launchSession)
completeAppLaunchSession(launchSession)
.then(() => {
const searchParams = new URLSearchParams(location.search)
searchParams.delete('launchSession')
const newSearch = searchParams.toString()
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
navigate(newPath, { replace: true })
})
.catch((err) => {
logger.error('Failed to complete app launch session:', err)
completedLaunchSessionsRef.current.delete(launchSession)
})
}, [
token,
authenticated,
completeAppLaunchSession,
location.search,
location.pathname,
navigate
])
const getAppLaunchSession = useCallback(async (launchSession) => {
const response = await axios.get(
`${config.backendUrl}/applaunch/${launchSession}`,
{
headers: {
Accept: 'application/json'
}
}
)
return response.data
}, [])
const fetchApiServerVersion = useCallback(async () => {
try {
const response = await axios.get(`${config.backendUrl}/server/version`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
fetchApiServerVersion()
})
return null
}
}, [token, showError])
const fetchWsServerVersion = useCallback(async () => {
try {
if (socketRef.current && socketRef.current.connected) {
return await new Promise((resolve) => {
socketRef.current.emit('getServerVersion', {}, resolve)
})
}
return null
} catch (err) {
console.error(err)
showError(err, () => {
fetchWsServerVersion()
})
return null
}
}, [showError])
const fetchAppUpdateBranches = useCallback(async () => {
try {
const response = await axios.get(
`${config.backendUrl}/appupdate/branches`,
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
return Array.isArray(response.data?.branches)
? response.data.branches
: []
} catch (err) {
console.error(err)
showError(err, () => {
fetchAppUpdateBranches()
})
return []
}
}, [token, showError])
const fetchAppUpdateCurrent = useCallback(
async (branch) => {
try {
const response = await axios.get(
`${config.backendUrl}/appupdate/current`,
{
params: { branch },
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
fetchAppUpdateCurrent(branch)
})
return null
}
},
[token, showError]
)
const flushFile = async (id) => {
logger.debug('Flushing file...')
try {
const response = await axios.delete(
`${config.backendUrl}/files/${id}/flush`,
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
}
)
logger.debug('Flushed file:', response.data)
return true
} catch (err) {
console.error(err)
showError(err, () => {
flushFile(id)
})
}
}
// Sanitize a string so it is safe to use as a filename on most file systems
const formatFileName = (name) => {
if (!name || typeof name !== 'string') {
return ''
}
// Remove characters that are problematic on most common file systems
const cleaned = name.replace(/[^a-zA-Z0-9.\-_\s]/g, '')
// Normalize whitespace to single underscores
const normalized = cleaned.trim().replace(/\s+/g, '_')
// Most file systems limit filenames to 255 characters
return normalized.slice(0, 255)
}
return (
<ApiServerContext.Provider
value={{
apiServer: socketRef.current,
error,
connecting,
connected,
userSettings,
userSettingsLoaded,
getUserSettings,
updateUserSettings,
setObjectActivity,
clearObjectActivity,
fetchObjectActivities,
updateObject,
updateMultipleObjects,
createObject,
getObjectFunction,
sendObjectFunction,
deleteObject,
subscribeToObjectUpdates,
subscribeToObjectEvent,
subscribeToObjectTypeUpdates,
subscribeToObjectActivity,
subscribeToModelStats,
fetchObject,
fetchObjects,
fetchObjectsByProperty,
searchObjects,
fetchSpotlightData,
getModelStats,
getModelPropertyValues,
getModelHistory,
fetchLoading,
showError,
fetchFileContent,
fetchFileThumbnail,
exportToExcel,
exportToCsv,
fetchTemplatePreview,
fetchTemplatePDF,
fetchNotes,
downloadTemplatePDF,
fetchHostOTP,
sendObjectAction,
uploadFile,
flushFile,
formatFileName,
createUserNotifier,
deleteUserNotifier,
fetchUserNotifiersForObject,
fetchAllUserNotifiersForObject,
toggleUserNotifier,
editUserNotifier,
fetchNotificationsApi,
markNotificationAsReadApi,
markAllNotificationsAsReadApi,
deleteNotificationApi,
deleteAllNotificationsApi,
registerNotificationListener,
unregisterNotificationListener,
getMarketplaceAuthUrl,
refreshMarketplaceAuth,
completeAppLaunchSession,
getAppLaunchSession,
fetchAppUpdateBranches,
fetchAppUpdateCurrent,
fetchWsServerVersion,
fetchApiServerVersion
}}
>
{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 }