From 71c83f6147c6a52e09fd6172332d336bd7d021ec Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 30 Aug 2025 23:38:51 +0100 Subject: [PATCH] Fixed ws updating. --- .../Dashboard/common/ObjectDisplay.jsx | 52 +++++++- .../Dashboard/common/ObjectTable.jsx | 100 +++++++++------ .../Dashboard/common/SpotlightTooltip.jsx | 114 ++++++++++-------- .../Dashboard/context/ApiServerContext.jsx | 35 ++++-- 4 files changed, 199 insertions(+), 102 deletions(-) diff --git a/src/components/Dashboard/common/ObjectDisplay.jsx b/src/components/Dashboard/common/ObjectDisplay.jsx index d0722dc..71b215a 100644 --- a/src/components/Dashboard/common/ObjectDisplay.jsx +++ b/src/components/Dashboard/common/ObjectDisplay.jsx @@ -1,19 +1,61 @@ 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 { ApiServerContext } from '../context/ApiServerContext' +import { AuthContext } from '../context/AuthContext' +import merge from 'lodash/merge' const { Text } = Typography 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 n/a } + const model = getModelByName(objectType) const Icon = model.icon return ( - }> - {object?.name ? object.name : null} - + + + {objectData?.name ? objectData.name : null} + ) } diff --git a/src/components/Dashboard/common/ObjectTable.jsx b/src/components/Dashboard/common/ObjectTable.jsx index 84d4896..fca65c4 100644 --- a/src/components/Dashboard/common/ObjectTable.jsx +++ b/src/components/Dashboard/common/ObjectTable.jsx @@ -87,8 +87,10 @@ const ObjectTable = forwardRef( const [loading, setLoading] = useState(true) const [lazyLoading, setLazyLoading] = useState(false) - const [subscribedIds, setSubscribedIds] = useState([]) + const subscribedIdsRef = useRef([]) const [typeSubscribed, setTypeSubscribed] = useState(false) + const unsubscribesRef = useRef([]) + const updateEventHandlerRef = useRef() const rowActions = model.actions?.filter((action) => action.row == true) || [] @@ -287,6 +289,7 @@ const ObjectTable = forwardRef( // Update event handler for real-time updates const updateEventHandler = useCallback((id, updatedData) => { + console.log('GOT UPDATE FOR', id) setPages((prevPages) => prevPages.map((page) => ({ ...page, @@ -297,6 +300,9 @@ const ObjectTable = forwardRef( ) }, []) + // Store the latest updateEventHandler in a ref + updateEventHandlerRef.current = updateEventHandler + const newEventHandler = useCallback(() => { console.log('GOT NEW EVENT') reload() @@ -304,44 +310,68 @@ const ObjectTable = forwardRef( // Subscribe to real-time updates for all items useEffect(() => { - if (pages.length > 0 && connected) { - const unsubscribes = [] - // Subscribe to each item in all pages - pages.forEach((page) => { - if (page?.items && page?.items?.length > 0) { - page.items.forEach((item) => { - if (!item.isSkeleton && !subscribedIds.includes(item?._id)) { - const unsubscribe = subscribeToObjectUpdates( - item._id, - type, - (updateData) => { - updateEventHandler(item._id, updateData) - } - ) - setSubscribedIds((prev) => [...prev, item._id]) - if (unsubscribe) { - unsubscribes.push(unsubscribe) - } - } - }) + if (pages.length > 0 && connected == true) { + // Get all non-skeleton item IDs from all pages + const allItemIds = pages + .flatMap((page) => page.items || []) + .filter((item) => !item.isSkeleton) + .map((item) => item._id) + .filter(Boolean) + + // Find new items that need subscription + const newItemIds = allItemIds.filter( + (id) => !subscribedIdsRef.current.includes(id) + ) + + // Subscribe to new items only + newItemIds.forEach((itemId) => { + console.log('SUB', itemId) + const unsubscribe = subscribeToObjectUpdates( + itemId, + type, + (updateData) => { + updateEventHandlerRef.current(itemId, updateData) + } + ) + subscribedIdsRef.current.push(itemId) + if (unsubscribe) { + unsubscribesRef.current.push(unsubscribe) } }) - return () => { - // Clean up all subscriptions - unsubscribes.forEach((unsubscribe) => { - if (unsubscribe) unsubscribe() - }) - } + // Clean up subscriptions for items that are no longer in the pages + const currentSubscribedIds = subscribedIdsRef.current + const itemsToUnsubscribe = currentSubscribedIds.filter( + (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, - updateEventHandler, - connected, - subscribedIds - ]) + }, [pages, type, subscribeToObjectUpdates, connected]) + + // Cleanup effect for component unmount + useEffect(() => { + return () => { + // Clean up all subscriptions when component unmounts + unsubscribesRef.current.forEach((unsubscribe) => { + console.log('CALLING UNSUB on unmount') + if (unsubscribe) unsubscribe() + }) + unsubscribesRef.current = [] + subscribedIdsRef.current = [] + } + }, []) useEffect(() => { if (connected == true && typeSubscribed == false) { diff --git a/src/components/Dashboard/common/SpotlightTooltip.jsx b/src/components/Dashboard/common/SpotlightTooltip.jsx index d5430c9..43ccfa1 100644 --- a/src/components/Dashboard/common/SpotlightTooltip.jsx +++ b/src/components/Dashboard/common/SpotlightTooltip.jsx @@ -7,6 +7,8 @@ import { AuthContext } from '../context/AuthContext' import ObjectProperty from './ObjectProperty' import InfoCircleIcon from '../../Icons/InfoCircleIcon' import { ApiServerContext } from '../context/ApiServerContext' +import merge from 'lodash/merge' +import { getModelByName } from '../../../database/ObjectModels' const { Text } = Typography @@ -16,7 +18,17 @@ const SpotlightTooltip = ({ query, type }) => { const [initialized, setInitialized] = useState(false) 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 () => { setLoading(true) @@ -33,6 +45,28 @@ const SpotlightTooltip = ({ query, type }) => { } }, [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) { return ( @@ -51,29 +85,9 @@ const SpotlightTooltip = ({ query, type }) => { ) } - // Helper to determine property type based on key and value - const getPropertyType = (key, value) => { - if (key === '_id') { - 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 + // Helper to get nested property value + const getNestedValue = (obj, path) => { + return path.split('.').reduce((current, key) => current?.[key], obj) } return ( @@ -105,33 +119,31 @@ const SpotlightTooltip = ({ query, type }) => { ) : ( - Object.entries(spotlightData).map(([key, value]) => - value !== undefined && - value !== null && - value !== '' && - key !== 'objectType' ? ( - - - - ) : null - ) + modelProperties + .filter((prop) => { + const value = getNestedValue(spotlightData, prop.name) + return value !== undefined && value !== null && value !== '' + }) + .map((prop) => { + const value = getNestedValue(spotlightData, prop.name) + return ( + + + + ) + }) )} diff --git a/src/components/Dashboard/context/ApiServerContext.jsx b/src/components/Dashboard/context/ApiServerContext.jsx index 1009cfb..004bb43 100644 --- a/src/components/Dashboard/context/ApiServerContext.jsx +++ b/src/components/Dashboard/context/ApiServerContext.jsx @@ -285,20 +285,33 @@ const ApiServerProvider = ({ children }) => { if (!subscribedCallbacksRef.current.has(callbacksRefKey)) { subscribedCallbacksRef.current.set(callbacksRefKey, []) } - subscribedCallbacksRef.current.get(callbacksRefKey).push(callback) - socketRef.current.emit( - 'subscribeToObjectUpdate', - { - _id: id, - objectType: objectType - }, - (result) => { - if (result.success) { - logger.info('Subscribed to id:', id, 'objectType:', objectType) + 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)