Compare commits

..

4 Commits

Author SHA1 Message Date
be2109505f Fixed filament number data types. 2025-08-30 23:39:27 +01:00
2b092cb846 Removed test action. 2025-08-30 23:39:10 +01:00
c38ed74f21 Fixed auth not removing session token. 2025-08-30 23:39:01 +01:00
71c83f6147 Fixed ws updating. 2025-08-30 23:38:51 +01:00
7 changed files with 211 additions and 115 deletions

View File

@ -1,6 +1,6 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useContext, useEffect, useState, useRef } from 'react' import { useContext, useEffect, useState, useRef } from 'react'
import { Input, Result, Typography, Flex, Progress, Button } from 'antd' import { Input, Result, Typography, Flex, Progress } from 'antd'
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import { ApiServerContext } from '../../context/ApiServerContext' import { ApiServerContext } from '../../context/ApiServerContext'
import CopyButton from '../../common/CopyButton' import CopyButton from '../../common/CopyButton'
@ -9,7 +9,7 @@ import OTPIcon from '../../../Icons/OTPIcon'
const { Text } = Typography const { Text } = Typography
const HostOTP = ({ id }) => { const HostOTP = ({ id }) => {
const { fetchHostOTP, sendObjectAction } = useContext(ApiServerContext) const { fetchHostOTP } = useContext(ApiServerContext)
const [hostObject, setHostObject] = useState(null) const [hostObject, setHostObject] = useState(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [timeRemaining, setTimeRemaining] = useState(0) const [timeRemaining, setTimeRemaining] = useState(0)
@ -35,13 +35,6 @@ const HostOTP = ({ id }) => {
}) })
} }
const sendTestAction = () => {
console.log('Sending test action...')
sendObjectAction(id, 'host', { method: 'testMethod' }, (result) => {
console.log('Got callback', result)
})
}
useEffect(() => { useEffect(() => {
if (hostObject === null && initialized == false) { if (hostObject === null && initialized == false) {
setInitialized(true) setInitialized(true)
@ -85,7 +78,6 @@ const HostOTP = ({ id }) => {
return ( return (
<Flex vertical align='center'> <Flex vertical align='center'>
<Button onClick={sendTestAction}>Test Action</Button>
<Result <Result
title={'Connect a Host.'} title={'Connect a Host.'}
subTitle={<Text>Enter the following one time passcode.</Text>} subTitle={<Text>Enter the following one time passcode.</Text>}

View File

@ -1,19 +1,61 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Tag, Typography } from 'antd' import { Typography, Flex } from 'antd'
import { useState, useEffect, useContext, useCallback } from 'react'
import { getModelByName } from '../../../database/ObjectModels' import { getModelByName } from '../../../database/ObjectModels'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import merge from 'lodash/merge'
const { Text } = Typography const { Text } = Typography
const ObjectDisplay = ({ object, objectType }) => { const ObjectDisplay = ({ object, objectType }) => {
if (!object) { const [objectData, setObjectData] = useState(object)
const { subscribeToObjectUpdates, connected } = useContext(ApiServerContext)
const { token } = useContext(AuthContext)
// Update event handler
const updateObjectEventHandler = useCallback((value) => {
setObjectData((prev) => merge({}, prev, value))
}, [])
// Subscribe to object updates when component mounts
useEffect(() => {
if (object?._id && objectType && connected && token) {
const objectUpdatesUnsubscribe = subscribeToObjectUpdates(
object._id,
objectType,
updateObjectEventHandler
)
return () => {
if (objectUpdatesUnsubscribe) objectUpdatesUnsubscribe()
}
}
}, [
object?._id,
objectType,
subscribeToObjectUpdates,
connected,
token,
updateObjectEventHandler
])
// Update local state when object prop changes
useEffect(() => {
setObjectData(object)
}, [object])
if (!objectData) {
return <Text type='secondary'>n/a</Text> return <Text type='secondary'>n/a</Text>
} }
const model = getModelByName(objectType) const model = getModelByName(objectType)
const Icon = model.icon const Icon = model.icon
return ( return (
<Tag color='default' style={{ margin: 0 }} icon={<Icon />}> <Flex gap={'small'}>
{object?.name ? object.name : null} <Icon />
</Tag> <Text>{objectData?.name ? objectData.name : null}</Text>
</Flex>
) )
} }

View File

@ -87,8 +87,10 @@ const ObjectTable = forwardRef(
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [lazyLoading, setLazyLoading] = useState(false) const [lazyLoading, setLazyLoading] = useState(false)
const [subscribedIds, setSubscribedIds] = useState([]) const subscribedIdsRef = useRef([])
const [typeSubscribed, setTypeSubscribed] = useState(false) const [typeSubscribed, setTypeSubscribed] = useState(false)
const unsubscribesRef = useRef([])
const updateEventHandlerRef = useRef()
const rowActions = const rowActions =
model.actions?.filter((action) => action.row == true) || [] model.actions?.filter((action) => action.row == true) || []
@ -287,6 +289,7 @@ const ObjectTable = forwardRef(
// Update event handler for real-time updates // Update event handler for real-time updates
const updateEventHandler = useCallback((id, updatedData) => { const updateEventHandler = useCallback((id, updatedData) => {
console.log('GOT UPDATE FOR', id)
setPages((prevPages) => setPages((prevPages) =>
prevPages.map((page) => ({ prevPages.map((page) => ({
...page, ...page,
@ -297,6 +300,9 @@ const ObjectTable = forwardRef(
) )
}, []) }, [])
// Store the latest updateEventHandler in a ref
updateEventHandlerRef.current = updateEventHandler
const newEventHandler = useCallback(() => { const newEventHandler = useCallback(() => {
console.log('GOT NEW EVENT') console.log('GOT NEW EVENT')
reload() reload()
@ -304,44 +310,68 @@ const ObjectTable = forwardRef(
// Subscribe to real-time updates for all items // Subscribe to real-time updates for all items
useEffect(() => { useEffect(() => {
if (pages.length > 0 && connected) { if (pages.length > 0 && connected == true) {
const unsubscribes = [] // Get all non-skeleton item IDs from all pages
// Subscribe to each item in all pages const allItemIds = pages
pages.forEach((page) => { .flatMap((page) => page.items || [])
if (page?.items && page?.items?.length > 0) { .filter((item) => !item.isSkeleton)
page.items.forEach((item) => { .map((item) => item._id)
if (!item.isSkeleton && !subscribedIds.includes(item?._id)) { .filter(Boolean)
const unsubscribe = subscribeToObjectUpdates(
item._id, // Find new items that need subscription
type, const newItemIds = allItemIds.filter(
(updateData) => { (id) => !subscribedIdsRef.current.includes(id)
updateEventHandler(item._id, updateData) )
}
) // Subscribe to new items only
setSubscribedIds((prev) => [...prev, item._id]) newItemIds.forEach((itemId) => {
if (unsubscribe) { console.log('SUB', itemId)
unsubscribes.push(unsubscribe) const unsubscribe = subscribeToObjectUpdates(
} itemId,
} type,
}) (updateData) => {
updateEventHandlerRef.current(itemId, updateData)
}
)
subscribedIdsRef.current.push(itemId)
if (unsubscribe) {
unsubscribesRef.current.push(unsubscribe)
} }
}) })
return () => { // Clean up subscriptions for items that are no longer in the pages
// Clean up all subscriptions const currentSubscribedIds = subscribedIdsRef.current
unsubscribes.forEach((unsubscribe) => { const itemsToUnsubscribe = currentSubscribedIds.filter(
if (unsubscribe) unsubscribe() (id) => !allItemIds.includes(id)
}) )
}
itemsToUnsubscribe.forEach((itemId) => {
const index = subscribedIdsRef.current.indexOf(itemId)
if (index > -1) {
subscribedIdsRef.current.splice(index, 1)
const unsubscribe = unsubscribesRef.current[index]
if (unsubscribe) {
console.log('UNSUB', itemId)
unsubscribe()
}
unsubscribesRef.current.splice(index, 1)
}
})
} }
}, [ }, [pages, type, subscribeToObjectUpdates, connected])
pages,
type, // Cleanup effect for component unmount
subscribeToObjectUpdates, useEffect(() => {
updateEventHandler, return () => {
connected, // Clean up all subscriptions when component unmounts
subscribedIds unsubscribesRef.current.forEach((unsubscribe) => {
]) console.log('CALLING UNSUB on unmount')
if (unsubscribe) unsubscribe()
})
unsubscribesRef.current = []
subscribedIdsRef.current = []
}
}, [])
useEffect(() => { useEffect(() => {
if (connected == true && typeSubscribed == false) { if (connected == true && typeSubscribed == false) {

View File

@ -7,6 +7,8 @@ import { AuthContext } from '../context/AuthContext'
import ObjectProperty from './ObjectProperty' import ObjectProperty from './ObjectProperty'
import InfoCircleIcon from '../../Icons/InfoCircleIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import { ApiServerContext } from '../context/ApiServerContext' import { ApiServerContext } from '../context/ApiServerContext'
import merge from 'lodash/merge'
import { getModelByName } from '../../../database/ObjectModels'
const { Text } = Typography const { Text } = Typography
@ -16,7 +18,17 @@ const SpotlightTooltip = ({ query, type }) => {
const [initialized, setInitialized] = useState(false) const [initialized, setInitialized] = useState(false)
const { token } = useContext(AuthContext) const { token } = useContext(AuthContext)
const { fetchSpotlightData } = useContext(ApiServerContext) const { fetchSpotlightData, subscribeToObjectUpdates, connected } =
useContext(ApiServerContext)
// Get the model for this type
const model = getModelByName(type)
const modelProperties = model.properties || []
// Update event handler
const updateObjectEventHandler = useCallback((value) => {
setSpotlightData((prev) => merge({}, prev, value))
}, [])
const fetchSpotlight = useCallback(async () => { const fetchSpotlight = useCallback(async () => {
setLoading(true) setLoading(true)
@ -33,6 +45,28 @@ const SpotlightTooltip = ({ query, type }) => {
} }
}, [token, fetchSpotlight, initialized]) }, [token, fetchSpotlight, initialized])
// Subscribe to object updates when component mounts
useEffect(() => {
if (spotlightData?._id && type && connected && token) {
const objectUpdatesUnsubscribe = subscribeToObjectUpdates(
spotlightData._id,
type,
updateObjectEventHandler
)
return () => {
if (objectUpdatesUnsubscribe) objectUpdatesUnsubscribe()
}
}
}, [
spotlightData?._id,
type,
subscribeToObjectUpdates,
connected,
token,
updateObjectEventHandler
])
if (!spotlightData && !loading) { if (!spotlightData && !loading) {
return ( return (
<Card className='spotlight-tooltip'> <Card className='spotlight-tooltip'>
@ -51,29 +85,9 @@ const SpotlightTooltip = ({ query, type }) => {
) )
} }
// Helper to determine property type based on key and value // Helper to get nested property value
const getPropertyType = (key, value) => { const getNestedValue = (obj, path) => {
if (key === '_id') { return path.split('.').reduce((current, key) => current?.[key], obj)
return 'id'
}
if (key === 'createdAt' || key === 'updatedAt') {
return 'dateTime'
}
if (typeof value === 'boolean') {
return 'bool'
}
return key
}
// Map of property names to user-friendly labels
const LABEL_MAP = {
name: 'Name',
state: 'State',
tags: 'Tags',
email: 'Email',
updatedAt: 'Updated At',
_id: 'ID'
// Add more mappings as needed
} }
return ( return (
@ -105,33 +119,31 @@ const SpotlightTooltip = ({ query, type }) => {
</Descriptions.Item> </Descriptions.Item>
</> </>
) : ( ) : (
Object.entries(spotlightData).map(([key, value]) => modelProperties
value !== undefined && .filter((prop) => {
value !== null && const value = getNestedValue(spotlightData, prop.name)
value !== '' && return value !== undefined && value !== null && value !== ''
key !== 'objectType' ? ( })
<Descriptions.Item .map((prop) => {
key={key} const value = getNestedValue(spotlightData, prop.name)
label={ return (
LABEL_MAP[key] || key.charAt(0).toUpperCase() + key.slice(1) <Descriptions.Item key={prop.name} label={prop.label}>
} <ObjectProperty
> type={prop.type}
<ObjectProperty value={value}
type={getPropertyType(key)} objectData={spotlightData}
value={value} objectType={prop.objectType || type}
objectData={spotlightData} isEditing={false}
objectType={type} longId={false}
isEditing={false} showSpotlight={false}
longId={false} showLabel={false}
showSpotlight={false} showName={false}
showLabel={false} showId={false}
showName={false} showQuantity={false}
showId={false} />
showQuantity={false} </Descriptions.Item>
/> )
</Descriptions.Item> })
) : null
)
)} )}
</Descriptions> </Descriptions>
</Spin> </Spin>

View File

@ -285,20 +285,33 @@ const ApiServerProvider = ({ children }) => {
if (!subscribedCallbacksRef.current.has(callbacksRefKey)) { if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
subscribedCallbacksRef.current.set(callbacksRefKey, []) subscribedCallbacksRef.current.set(callbacksRefKey, [])
} }
subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)
socketRef.current.emit( const callbacksLength =
'subscribeToObjectUpdate', subscribedCallbacksRef.current.get(callbacksRefKey).length
{
_id: id, if (callbacksLength <= 0) {
objectType: objectType socketRef.current.emit(
}, 'subscribeToObjectUpdate',
(result) => { {
if (result.success) { _id: id,
logger.info('Subscribed to id:', id, 'objectType:', objectType) 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 cleanup function
return () => offObjectUpdatesEvent(id, objectType, callback) return () => offObjectUpdatesEvent(id, objectType, callback)

View File

@ -209,7 +209,12 @@ const AuthProvider = ({ children }) => {
}, [token]) }, [token])
const setUnauthenticated = () => { const setUnauthenticated = () => {
setAuthenticated(false) setToken(null)
setExpiresAt(null)
setUserProfile(null)
sessionStorage.removeItem('authToken')
sessionStorage.removeItem('authExpiresAt')
sessionStorage.removeItem('user')
setShowUnauthorizedModal(true) setShowUnauthorizedModal(true)
} }

View File

@ -126,14 +126,16 @@ export const Filament = {
label: 'Diameter', label: 'Diameter',
columnWidth: 150, columnWidth: 150,
required: true, required: true,
type: 'mm' type: 'number',
suffix: 'mm'
}, },
{ {
name: 'density', name: 'density',
label: 'Density', label: 'Density',
columnWidth: 150, columnWidth: 150,
required: true, required: true,
type: 'density' type: 'number',
suffix: 'g/cm³'
}, },
{ {
name: 'emptySpoolWeight', name: 'emptySpoolWeight',