diff --git a/src/App.tsx b/src/App.tsx index 656e35a..258069d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useShallow } from 'zustand/shallow'; import { ToastContainer, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; @@ -18,17 +18,18 @@ function App() { ); const [showConnectionDialog, setShowConnectionDialog] = useState(false); + const autoConnectAttempted = useRef(false); // Auto-connect on mount if we have a stored URL useEffect(() => { - if (serverUrl && !isConnected) { - Promise.resolve(connect(serverUrl, baseEndpoint)) - .catch((err) => { - toast.error( - `Auto-connect failed: ${err?.message || 'Please check your server settings.'}` - ); + if (serverUrl && !isConnected && !autoConnectAttempted.current) { + autoConnectAttempted.current = true; + connect(serverUrl, baseEndpoint).then((success) => { + if (!success) { + toast.error('Auto-connect failed. Please check your server settings.'); setShowConnectionDialog(true); - }); + } + }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/EntityDetailPanel.tsx b/src/components/EntityDetailPanel.tsx index f7e427d..be6f299 100644 --- a/src/components/EntityDetailPanel.tsx +++ b/src/components/EntityDetailPanel.tsx @@ -55,10 +55,18 @@ export function EntityDetailPanel({ onConnectClick }: EntityDetailPanelProps) { return; } + // Parse JSON before setting publishing state to avoid stuck state on parse error + let data: unknown; + try { + data = JSON.parse(inputData); + } catch { + toast.error('Invalid JSON format. Please check your message data.'); + return; + } + setPublishingTopics(prev => new Set(prev).add(topic.topic)); try { - const data = JSON.parse(inputData); const messageType = inferMessageType(topic.data); await client.publishToComponentTopic(selectedEntity.id, topicName, { @@ -129,156 +137,151 @@ export function EntityDetailPanel({ onConnectClick }: EntityDetailPanelProps) { // Entity detail view if (selectedEntity) { - // Component with topics - if (selectedEntity.type === 'component' && selectedEntity.topics && selectedEntity.topics.length > 0) { - return ( -
-
- {/* Component Header */} + const hasTopics = selectedEntity.type === 'component' && selectedEntity.topics && selectedEntity.topics.length > 0; + const hasError = !!selectedEntity.error; + + return ( +
+
+ {/* Component Header */} + + +
+
+ {selectedEntity.name} + + {selectedEntity.type} • {selectedPath} + +
+ +
+
+
+ + {/* Content */} + {hasError ? ( - -
-
- {selectedEntity.name} - - {selectedEntity.type} • {selectedPath} - -
- + +
+

Failed to load entity details

+

The server might be unreachable or the entity might not exist.

- +
+ ) : hasTopics ? ( +
+ {/* Topics List */} + {selectedEntity.topics!.map((topic: ComponentTopic) => { + const topicName = topic.topic.split('/').pop() || topic.topic; + const isExpanded = expandedTopics.has(topic.topic); + const isPublishing = publishingTopics.has(topic.topic); + const hasNoData = topic.data === null || topic.data === undefined; - {/* Topics List */} - {selectedEntity.topics.map((topic: ComponentTopic) => { - const topicName = topic.topic.split('/').pop() || topic.topic; - const isExpanded = expandedTopics.has(topic.topic); - const isPublishing = publishingTopics.has(topic.topic); - const hasNoData = topic.data === null || topic.data === undefined; - - return ( - - -
-
- -
-
- {topic.topic} - {hasNoData && ( - - No Data - - )} + return ( + + +
+
+ +
+
+ {topic.topic} + {hasNoData && ( + + No Data + + )} +
+ + {hasNoData + ? 'No messages received (topic may be inactive)' + : `Last update: ${new Date(topic.timestamp / 1000000).toLocaleString()}` + } +
- - {hasNoData - ? 'No messages received (topic may be inactive)' - : `Last update: ${new Date(topic.timestamp / 1000000).toLocaleString()}` - } -
+
- -
- - {isExpanded && ( - - {/* Latest Data */} -
-
Latest Message
- {hasNoData ? ( -
- No data available - topic exists but is not publishing messages -
- ) : ( -
-                                                        {JSON.stringify(topic.data, null, 2)}
-                                                    
- )} -
- - {/* Publish Form - only show if we have data to infer type */} - {!hasNoData && ( -
-
Publish Message
-