fix(ui): optimize react state, performance, and ux logic

- Refactored useFilteredNodes to memoize logic and prevent re-rendering.
- Optimized LeftPanel useMemo dependencies and HostChart O(N) traversal.
- Fixed polling interval desync and added WebSocket throttling/React Strict Mode transport patch.
- Fixed Graph layout dependencies and 'ghost' children chevrons on collapsed nodes.
- Fixed RightPanel tab persistence UI bug and resolved React Hooks order crash.

No remaining known issues from the UI analysis.
This commit is contained in:
2026-02-23 15:20:25 -08:00
parent d40be883fe
commit df02542c26
9 changed files with 337 additions and 175 deletions

View File

@@ -86,10 +86,15 @@ function App() {
const pollIntervalRef = useRef(pollInterval);
pollIntervalRef.current = pollInterval;
const loadData = useCallback(async () => {
const loadData = useCallback(async (isBackgroundPoll = false) => {
if (isLoadingRef.current) return;
setIsLoading(true);
// Only set loading state if there are no existing nodes (initial fresh load)
// or if this is an explicit user refresh (not background poll).
const isInitialLoad = useTopologyStore.getState().nodes.length === 0;
if (isInitialLoad && !isBackgroundPoll) {
setIsLoading(true);
}
try {
const response = await fetch(`${API_BASE_URL}/api/discover`, {
@@ -184,18 +189,20 @@ function App() {
}, [toggleCommandPalette]);
useEffect(() => {
loadData();
const isInitialLoad = useTopologyStore.getState().nodes.length === 0;
loadData(!isInitialLoad); // Poll in background if we already have nodes
}, []);
useEffect(() => {
const intervalId = setInterval(loadData, pollIntervalRef.current);
// Rely on pollInterval from store state instead of ref
const intervalId = setInterval(() => loadData(true), pollInterval);
return () => clearInterval(intervalId);
}, [loadData]);
}, [loadData, pollInterval]);
// --- WebSocket connection (websocket-engineer skill) ---
useEffect(() => {
const socket: Socket = ioClient(API_BASE_URL, {
transports: ['websocket', 'polling'],
transports: ['polling', 'websocket'],
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: Infinity,
@@ -214,7 +221,14 @@ function App() {
});
// Listen for real-time topology updates
let lastWsUpdate = 0;
socket.on('topology:update', (data: ApiDiscoveryResponse) => {
// Throttle updates to max 1 per second to prevent UI freezes
const now = Date.now();
if (now - lastWsUpdate < 1000) return;
lastWsUpdate = now;
if (data?.hosts) {
const discoveredHosts: DiscoveredHost[] = data.hosts.map((h: ApiHost) => ({
name: h.name,
@@ -288,9 +302,10 @@ const Footer = memo(function Footer() {
const pollIntervalRef = useRef(pollInterval);
pollIntervalRef.current = pollInterval;
const formatTime = (date: Date | null) => {
const formatTime = (date: Date | string | null) => {
if (!date) return 'Never';
return date.toLocaleTimeString();
const d = new Date(date);
return isNaN(d.getTime()) ? 'Never' : d.toLocaleTimeString();
};
useEffect(() => {