From 33939d23b4e58f08395321963951177828b6ab0d Mon Sep 17 00:00:00 2001 From: Paul Jouvanceau Date: Tue, 25 Nov 2025 15:08:38 +0100 Subject: [PATCH 01/67] refactor(EventLogger): add collapsible log entries with dense JSON preview - Introduced expandable/collapsible log items using `ExpandMore` icon. - Added `expandedLogIds` state to track expanded entries. - Display dense (compact) JSON when collapsed and pretty-printed JSON when expanded. - Updated drawer height, resizer, and styling for better readability. - Simplified event log filtering and ensured safe handling of undefined logs. - Minor UI improvements: padding, max-height, and cursor feedback on log items. --- src/components/EventLogger.jsx | 449 ++++++++-------------- src/components/tests/EventLogger.test.jsx | 2 +- 2 files changed, 172 insertions(+), 279 deletions(-) diff --git a/src/components/EventLogger.jsx b/src/components/EventLogger.jsx index 5b9ff8f2..96ff6f94 100644 --- a/src/components/EventLogger.jsx +++ b/src/components/EventLogger.jsx @@ -23,10 +23,11 @@ import { DeleteOutline, BugReport, Close, - KeyboardArrowUp + KeyboardArrowUp, + ExpandMore } from "@mui/icons-material"; import useEventLogStore from "../hooks/useEventLogStore"; -import logger from '../utils/logger.js'; +import logger from "../utils/logger.js"; const EventLogger = ({ eventTypes = [], @@ -35,63 +36,61 @@ const EventLogger = ({ buttonLabel = "Events" }) => { const theme = useTheme(); + + // UI state const [drawerOpen, setDrawerOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [eventTypeFilter, setEventTypeFilter] = useState([]); const [autoScroll, setAutoScroll] = useState(true); - const [drawerHeight, setDrawerHeight] = useState(300); + const [drawerHeight, setDrawerHeight] = useState(320); const [forceUpdate, setForceUpdate] = useState(0); + const [expandedLogIds, setExpandedLogIds] = useState([]); const logsEndRef = useRef(null); const logsContainerRef = useRef(null); const resizeTimeoutRef = useRef(null); - const { - eventLogs, - isPaused, - setPaused, - clearLogs - } = useEventLogStore(); + // Event logs store + const {eventLogs = [], isPaused, setPaused, clearLogs} = useEventLogStore(); - const baseFilteredLogs = useMemo(() => { - let filtered = eventLogs; + // Toggle expand/collapse + const toggleExpand = useCallback((id) => { + setExpandedLogIds(prev => + prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id] + ); + }, []); - if (eventTypes.length > 0) { - filtered = filtered.filter(log => eventTypes.includes(log.eventType)); + // JSON preview dense (compact) + const denseJSON = (data) => { + try { + return JSON.stringify(data); + } catch { + return String(data); } + }; - if (objectName) { - filtered = filtered.filter(log => { - if (log.eventType.includes('CONNECTION')) { - return true; - } - - const eventData = log.data || {}; - - if (eventData.path === objectName) { - return true; - } - - if (eventData.labels?.path === objectName) { - return true; - } - if (eventData.data?.path === objectName) { - return true; - } + // Compute filtered logs + const baseFilteredLogs = useMemo(() => { + const safeLogs = Array.isArray(eventLogs) ? eventLogs : []; + let filtered = safeLogs; - if (eventData.data?.labels?.path === objectName) { - return true; - } + if (eventTypes.length > 0) filtered = filtered.filter(log => eventTypes.includes(log.eventType)); - if (log.eventType === 'ObjectDeleted') { + if (objectName) { + filtered = filtered.filter(log => { + const data = log.data || {}; + if (log.eventType?.includes?.("CONNECTION")) return true; + if (data.path === objectName) return true; + if (data.labels?.path === objectName) return true; + if (data.data?.path === objectName) return true; + if (data.data?.labels?.path === objectName) return true; + if (log.eventType === "ObjectDeleted") { try { - const rawData = eventData._rawEvent ? JSON.parse(eventData._rawEvent) : {}; - if (rawData.path === objectName || rawData.labels?.path === objectName) { - return true; - } - } catch (e) {} + const raw = data._rawEvent ? JSON.parse(data._rawEvent) : {}; + if (raw.path === objectName || raw.labels?.path === objectName) return true; + } catch { + } } - return false; }); } @@ -116,24 +115,20 @@ const EventLogger = ({ const filteredLogs = useMemo(() => { let result = [...baseFilteredLogs]; - if (eventTypeFilter.length > 0) { - result = result.filter(log => eventTypeFilter.includes(log.eventType)); - } + if (eventTypeFilter.length > 0) result = result.filter(log => eventTypeFilter.includes(log.eventType)); if (searchTerm.trim()) { const term = searchTerm.toLowerCase().trim(); result = result.filter(log => { - const eventTypeMatch = log.eventType.toLowerCase().includes(term); - + const typeMatch = String(log.eventType || "").toLowerCase().includes(term); let dataMatch = false; try { const dataString = JSON.stringify(log.data || {}).toLowerCase(); dataMatch = dataString.includes(term); - } catch (error) { - logger.warn("Error stringifying log data:", error); + } catch (err) { + logger.warn("Error serializing log data for search:", err); } - - return eventTypeMatch || dataMatch; + return typeMatch || dataMatch; }); } @@ -146,47 +141,28 @@ const EventLogger = ({ useEffect(() => { if (autoScroll && logsEndRef.current && filteredLogs.length > 0 && drawerOpen) { - const scrollToBottom = () => { - if (logsEndRef.current) { - logsEndRef.current.scrollIntoView({ - behavior: "smooth", - block: "end" - }); - } - }; - - requestAnimationFrame(() => { - setTimeout(scrollToBottom, 100); - }); + const scrollToBottom = () => logsEndRef.current?.scrollIntoView({behavior: "smooth", block: "end"}); + requestAnimationFrame(() => setTimeout(scrollToBottom, 100)); } }, [filteredLogs, autoScroll, drawerOpen]); const handleScroll = useCallback(() => { if (!logsContainerRef.current) return; - const {scrollTop, scrollHeight, clientHeight} = logsContainerRef.current; - const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; - - if (isAtBottom !== autoScroll) { - setAutoScroll(isAtBottom); - } + const atBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; + if (atBottom !== autoScroll) setAutoScroll(atBottom); }, [autoScroll]); const startResizing = useCallback((mouseDownEvent) => { - if (mouseDownEvent && typeof mouseDownEvent.preventDefault === 'function') { - mouseDownEvent.preventDefault(); - } + if (mouseDownEvent?.preventDefault) mouseDownEvent.preventDefault(); const startY = mouseDownEvent?.clientY ?? 0; const startHeight = drawerHeight; const handleMouseMove = (mouseMoveEvent) => { - if (resizeTimeoutRef.current) { - clearTimeout(resizeTimeoutRef.current); - } - + if (resizeTimeoutRef.current) clearTimeout(resizeTimeoutRef.current); resizeTimeoutRef.current = setTimeout(() => { const deltaY = startY - (mouseMoveEvent?.clientY ?? startY); - const newHeight = Math.max(200, Math.min(600, startHeight + deltaY)); + const newHeight = Math.max(220, Math.min(800, startHeight + deltaY)); setDrawerHeight(newHeight); }, 16); }; @@ -194,79 +170,50 @@ const EventLogger = ({ const handleMouseUp = () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); - if (resizeTimeoutRef.current) { - clearTimeout(resizeTimeoutRef.current); - } + if (resizeTimeoutRef.current) clearTimeout(resizeTimeoutRef.current); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }, [drawerHeight]); - const formatTimestamp = (timestamp) => { + const formatTimestamp = (ts) => { try { - return new Date(timestamp).toLocaleTimeString("en-US", { + return new Date(ts).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit", - fractionalSecondDigits: 3, + fractionalSecondDigits: 3 }); - } catch (e) { - return String(timestamp); + } catch { + return String(ts); } }; - const getEventColor = (eventType) => { - if (eventType.includes('ERROR')) return 'error'; - if (eventType.includes('UPDATED')) return 'primary'; - if (eventType.includes('DELETED')) return 'warning'; - if (eventType.includes('CONNECTION')) return 'info'; - return 'default'; + const getEventColor = (eventType = "") => { + if (eventType.includes("ERROR")) return "error"; + if (eventType.includes("UPDATED")) return "primary"; + if (eventType.includes("DELETED")) return "warning"; + if (eventType.includes("CONNECTION")) return "info"; + return "default"; }; const handleClear = useCallback(() => { clearLogs(); setSearchTerm(""); setEventTypeFilter([]); - setForceUpdate(prev => prev + 1); + setExpandedLogIds([]); }, [clearLogs]); const handleClearFilters = useCallback(() => { setSearchTerm(""); setEventTypeFilter([]); - setForceUpdate(prev => prev + 1); }, []); - const handleEventTypeFilterChange = useCallback((event) => { - setEventTypeFilter(event.target.value); - setForceUpdate(prev => prev + 1); - }, []); - - const handleSearchChange = useCallback((event) => { - setSearchTerm(event.target.value); - }, []); - - const handleTogglePause = useCallback(() => { - setPaused(!isPaused); - setForceUpdate(prev => prev + 1); - }, [isPaused, setPaused]); - - useEffect(() => { - return () => { - if (resizeTimeoutRef.current) { - clearTimeout(resizeTimeoutRef.current); - } - }; - }, []); - - useEffect(() => { - setAutoScroll(true); - }, [eventTypeFilter, searchTerm]); - const paperStyle = { height: drawerHeight, - maxHeight: '70vh', - overflow: 'hidden', + maxHeight: "80vh", + overflow: "hidden", borderTopLeftRadius: 8, borderTopRightRadius: 8, backgroundColor: theme.palette.background.paper @@ -278,27 +225,15 @@ const EventLogger = ({ @@ -311,126 +246,83 @@ const EventLogger = ({ variant="persistent" PaperProps={{style: paperStyle}} > + {/* Resizer */}
-
+
{/* Header */} - - {title} - - - {isPaused && ( - - )} - {(eventTypeFilter.length > 0 || searchTerm) && ( - - )} - {objectName && ( - - )} + {title} + + {isPaused && } + {(eventTypeFilter.length > 0 || searchTerm) && + } + {objectName && + } - + setPaused(!isPaused)} color={isPaused ? "warning" : "primary"} + size="small"> {isPaused ? : } - - + - - setDrawerOpen(false)} size="small"> - - + setDrawerOpen(false)} size="small"> - + {/* Filters */} + setSearchTerm(e.target.value)} + sx={{minWidth: 240, flexGrow: 1}} /> - {availableEventTypes.length > 0 && ( - + Event Types (selected.length === 0 ? "All events" : `${selected.length} selected`)} > - {availableEventTypes.map((et) => ( + {allReceivedEventTypes.map((et) => ( + {/* Subscription Management Dialog */} + + ); From 8fdc1b692cf55ab68e5895d21d4afcbf47d629b0 Mon Sep 17 00:00:00 2001 From: Paul Jouvanceau Date: Thu, 4 Dec 2025 11:00:46 +0100 Subject: [PATCH 31/67] feat: add dark mode support for EventLogger and LogsViewer components - Enhanced EventLogger to detect and apply dark mode theme based on palette.mode - Updated JSON syntax highlighting colors for dark mode readability - Applied dark mode styling to all UI elements: drawer, buttons, text fields, chips, etc. - Modified background colors, text colors, and border colors for dark mode compatibility - Enhanced LogsViewer to use DarkModeContext for consistent dark mode behavior - Added dark mode-aware background to logs container in LogsViewer - Ensured all interactive elements maintain proper contrast and visibility in dark mode - Updated subscription dialog and filter components with dark mode styles --- src/components/EventLogger.jsx | 302 ++++++++++++++++---- src/components/LogsViewer.jsx | 4 +- src/components/tests/LogsViewer.test.jsx | 24 +- src/components/tests/ObjectDetails.test.jsx | 7 + 4 files changed, 265 insertions(+), 72 deletions(-) diff --git a/src/components/EventLogger.jsx b/src/components/EventLogger.jsx index a37afc5c..b690e45f 100644 --- a/src/components/EventLogger.jsx +++ b/src/components/EventLogger.jsx @@ -38,6 +38,7 @@ const EventLogger = ({ buttonLabel = "Events" }) => { const theme = useTheme(); + const isDarkMode = theme.palette.mode === 'dark'; // UI state const [drawerOpen, setDrawerOpen] = useState(false); @@ -185,7 +186,9 @@ const EventLogger = ({ lineHeight: dense ? 1.2 : 1.4, opacity: dense ? 0.9 : 1, maxHeight: dense ? 160 : 'none', - overflow: dense ? 'hidden' : 'visible' + overflow: dense ? 'hidden' : 'visible', + backgroundColor: 'transparent', + color: isDarkMode ? '#ffffff' : theme.palette.text.primary }} dangerouslySetInnerHTML={{__html: coloredJSON}} /> @@ -194,22 +197,22 @@ const EventLogger = ({ const jsonStyles = { '& .json-key': { - color: theme.palette.primary.main, + color: isDarkMode ? '#90caf9' : theme.palette.primary.main, fontWeight: '600' }, '& .json-string': { - color: theme.palette.success.dark + color: isDarkMode ? '#a5d6a7' : theme.palette.success.dark }, '& .json-number': { - color: theme.palette.info.main, + color: isDarkMode ? '#80cbc4' : theme.palette.info.main, fontWeight: '500' }, '& .json-boolean': { - color: theme.palette.warning.dark, + color: isDarkMode ? '#ffcc80' : theme.palette.warning.dark, fontWeight: '600' }, '& .json-null': { - color: theme.palette.grey[500], + color: isDarkMode ? theme.palette.grey[400] : theme.palette.grey[500], fontWeight: '600' } }; @@ -297,20 +300,23 @@ const EventLogger = ({ '& .MuiDrawer-paper': { width: 400, maxWidth: '90vw', - p: 2 + p: 2, + backgroundColor: isDarkMode ? theme.palette.background.paper : '#ffffff' } }} > - Event Subscriptions + + Event Subscriptions + setSubscriptionDialogOpen(false)}> - + - + - + Select which event types you want to SUBSCRIBE to (this affects future events only): @@ -319,12 +325,20 @@ const EventLogger = ({ size="small" onClick={() => setTempSubscribedEventTypes([...eventTypes])} disabled={eventTypes.length === 0} + sx={{ + backgroundColor: isDarkMode ? 'rgba(255, 255, 255, 0.1)' : undefined, + color: isDarkMode ? '#ffffff' : undefined + }} > Subscribe to All @@ -332,7 +346,7 @@ const EventLogger = ({ {eventTypes.length === 0 ? ( - + No event types available for this page ) : ( @@ -348,12 +362,18 @@ const EventLogger = ({ } }} size="small" + sx={{ + color: isDarkMode ? '#ffffff' : undefined, + '&.Mui-checked': { + color: isDarkMode ? '#90caf9' : undefined, + } + }} /> - + {eventType} - + {eventStats[eventType] || 0} events received @@ -370,6 +390,10 @@ const EventLogger = ({ setSubscribedEventTypes(tempSubscribedEventTypes); setSubscriptionDialogOpen(false); }} + sx={{ + backgroundColor: isDarkMode ? '#1976d2' : undefined, + color: '#ffffff' + }} > Apply Subscriptions @@ -393,10 +417,16 @@ const EventLogger = ({ size="small" color="info" variant="outlined" - sx={{height: 24, fontSize: '0.75rem'}} + sx={{ + height: 24, + fontSize: '0.75rem', + backgroundColor: isDarkMode ? 'rgba(255, 255, 255, 0.1)' : undefined, + color: isDarkMode ? '#ffffff' : undefined, + borderColor: isDarkMode ? 'rgba(255, 255, 255, 0.2)' : undefined + }} onClick={() => setSubscriptionDialogOpen(true)} onDelete={() => setSubscribedEventTypes([...eventTypes])} - deleteIcon={} + deleteIcon={} /> @@ -493,7 +523,7 @@ const EventLogger = ({ overflow: "hidden", borderTopLeftRadius: 8, borderTopRightRadius: 8, - backgroundColor: theme.palette.background.paper + backgroundColor: isDarkMode ? theme.palette.grey[900] : theme.palette.background.paper }; // Manage logger EventSource @@ -517,6 +547,8 @@ const EventLogger = ({ color={color === "default" ? "default" : color} sx={{ fontWeight: '600', + backgroundColor: isDarkMode ? 'rgba(255, 255, 255, 0.1)' : undefined, + color: isDarkMode ? '#ffffff' : undefined, ...(searchTerm && eventType.toLowerCase().includes(searchTerm.toLowerCase()) && { borderWidth: 2, borderStyle: 'solid', @@ -536,12 +568,34 @@ const EventLogger = ({ color="primary.light" startIcon={} onClick={() => setDrawerOpen(true)} - sx={{position: "fixed", bottom: 16, right: 16, zIndex: 9999, borderRadius: "20px", px: 2}} + sx={{ + position: "fixed", + bottom: 16, + right: 16, + zIndex: 9999, + borderRadius: "20px", + px: 2, + backgroundColor: isDarkMode ? '#333333' : undefined, + color: '#ffffff', + '&:hover': { + backgroundColor: isDarkMode ? '#555555' : undefined, + } + }} > {buttonLabel} {baseFilteredLogs.length > 0 && ( - + )} @@ -560,46 +614,96 @@ const EventLogger = ({ style={{ width: "100%", height: 10, - backgroundColor: theme.palette.grey[300], + backgroundColor: isDarkMode ? theme.palette.grey[700] : theme.palette.grey[300], cursor: "row-resize", display: "flex", alignItems: "center", justifyContent: "center" }} > -
+
{/* Header */} - {title} - - {isPaused && } + + {title} + + + {isPaused && ( + + )} {(eventTypeFilter.length > 0 || searchTerm) && - } + + } - setPaused(!isPaused)} color={isPaused ? "warning" : "primary"} - size="small"> + setPaused(!isPaused)} + color={isPaused ? "warning" : "primary"} + size="small" + sx={{color: isDarkMode ? '#ffffff' : undefined}} + > {isPaused ? : } - + - setDrawerOpen(false)} size="small"> + setDrawerOpen(false)} + size="small" + sx={{color: isDarkMode ? '#ffffff' : undefined}} + > + + - + {/* Compact subscription info */} @@ -611,27 +715,76 @@ const EventLogger = ({ placeholder="Search events..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} - sx={{minWidth: 240, flexGrow: 1}} + sx={{ + minWidth: 240, + flexGrow: 1, + '& .MuiInputBase-root': { + backgroundColor: isDarkMode ? 'rgba(255, 255, 255, 0.1)' : undefined, + color: isDarkMode ? '#ffffff' : undefined + }, + '& .MuiInputLabel-root': { + color: isDarkMode ? '#cccccc' : undefined + }, + '& .MuiOutlinedInput-notchedOutline': { + borderColor: isDarkMode ? 'rgba(255, 255, 255, 0.2)' : undefined + } + }} /> {availableEventTypes.length > 0 && ( - Event Types + + Event Types + setFilterState(e.target.value)}> - All - {availableStates.filter(s => s !== "all").map(state => ( - {state.charAt(0).toUpperCase() + state.slice(1)} - ))} - - + {showFilters && ( + <> + + Filter by Running + + - - Filter by Beating - - + + Filter by Beating + + - - Filter by Node - - + + Filter by Node + + - - Filter by ID - - + + Filter by ID + + + + )} - + diff --git a/src/components/Objects.jsx b/src/components/Objects.jsx index 24874b48..25b9618a 100644 --- a/src/components/Objects.jsx +++ b/src/components/Objects.jsx @@ -18,7 +18,6 @@ import { TextField, Snackbar, Alert, - Collapse, ListItemIcon, ListItemText, useMediaQuery, @@ -754,75 +753,96 @@ const Objects = () => { pb: 1, mb: 2 }}> - - + + {/* Left section with Show Filters button and filters */} + + + + {showFilters && ( + <> + val && setSelectedGlobalState(val)} + renderInput={(params) => } + renderOption={(props, option) => ( +
  • + + {option === "up" && + } + {option === "down" && + } + {option === "warn" && + } + {option === "n/a" && + } + {option === "unprovisioned" && + } + {option === "all" ? "All" : option.charAt(0).toUpperCase() + option.slice(1)} + +
  • + )} + /> + val && setSelectedNamespace(val)} + renderInput={(params) => } + /> + val && setSelectedKind(val)} + renderInput={(params) => } + /> + setSearchQuery(e.target.value)} + sx={{minWidth: 200, flexShrink: 0}} + /> + + )} +
    + + {/* Right section with Actions button */}
    - - - val && setSelectedGlobalState(val)} - renderInput={(params) => } - renderOption={(props, option) => ( -
  • - - {option === "up" && - } - {option === "down" && - } - {option === "warn" && - } - {option === "n/a" && - } - {option === "unprovisioned" && - } - {option === "all" ? "All" : option.charAt(0).toUpperCase() + option.slice(1)} - -
  • - )} - /> - val && setSelectedNamespace(val)} - renderInput={(params) => } - /> - val && setSelectedKind(val)} - renderInput={(params) => } - /> - setSearchQuery(e.target.value)} - sx={{minWidth: 200}} - /> -
    -
    + From 4997d9aea4649388aea87a5889052dd24b92310b Mon Sep 17 00:00:00 2001 From: Paul Jouvanceau Date: Mon, 8 Dec 2025 17:38:41 +0100 Subject: [PATCH 35/67] refactor: standardize filter toggle button across components - Updated Objects component filter button to display only icons (ExpandLessIcon when filters shown, "Filters" + ExpandMoreIcon when hidden) - Updated Objects.test.js to reflect new button text expectations - Applied same pattern to Heartbeats component filter button for consistency - Improved visual design by using icon-only toggle with "Filters" label only when collapsed - Maintained accessibility with proper aria-labels Changes: - Objects.js: Modified filter button to show icons only - Objects.test.js: Updated test to check for "Filters" text when collapsed - Heartbeats.js: Standardized filter button to match Objects component style This creates a uniform UX pattern across all filterable components. --- src/components/Heartbeats.jsx | 3 +-- src/components/Objects.jsx | 3 +-- src/components/tests/Objects.test.jsx | 7 +------ 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/components/Heartbeats.jsx b/src/components/Heartbeats.jsx index f48d8507..de040191 100644 --- a/src/components/Heartbeats.jsx +++ b/src/components/Heartbeats.jsx @@ -316,10 +316,9 @@ const Heartbeats = () => { }}> {showFilters && ( diff --git a/src/components/Objects.jsx b/src/components/Objects.jsx index 25b9618a..9e36cf55 100644 --- a/src/components/Objects.jsx +++ b/src/components/Objects.jsx @@ -770,11 +770,10 @@ const Objects = () => { }}> {showFilters && ( diff --git a/src/components/tests/Objects.test.jsx b/src/components/tests/Objects.test.jsx index 95750291..128db10f 100644 --- a/src/components/tests/Objects.test.jsx +++ b/src/components/tests/Objects.test.jsx @@ -512,7 +512,6 @@ describe('Objects Component', () => { // Check filters are visible initially const toggleButton = await screen.findByRole('button', {name: /filters/i}); - expect(toggleButton).toHaveTextContent('Hide filters'); expect(screen.getByLabelText('Namespace')).toBeInTheDocument(); // Click to hide filters @@ -520,7 +519,7 @@ describe('Objects Component', () => { // Check filters are hidden await waitFor(() => { - expect(toggleButton).toHaveTextContent('Show filters'); + expect(toggleButton).toHaveTextContent('Filters'); }); expect(screen.queryByLabelText('Namespace')).not.toBeInTheDocument(); @@ -528,10 +527,6 @@ describe('Objects Component', () => { // Click to show filters again fireEvent.click(toggleButton); - await waitFor(() => { - expect(toggleButton).toHaveTextContent('Hide filters'); - }); - expect(screen.getByLabelText('Namespace')).toBeInTheDocument(); }); From 252fc1920a998805840d3550e36263be64305ecc Mon Sep 17 00:00:00 2001 From: Paul Jouvanceau Date: Tue, 9 Dec 2025 15:12:21 +0100 Subject: [PATCH 36/67] refactor(whoami): update UI layout and align tests with new design ### Component update - Removed obsolete "Access" section. - Removed "Namespace" and "system" fields from the UI. - Removed JSON
     block for permissions.
    - Simplified permissions display to a "Permission Details" section.
    - Displayed raw permissions (`grant.root`) as plain text instead of JSON.
    - Updated layout to match new design expectations.
    
    ### Test update
    - Updated test to expect the new "Permission Details" section instead of "Access".
    - Removed assertions looking for "Namespace" and "system".
    - Removed JSON 
     lookup and associated checks.
    - Updated permission checks to match the new plain-text rendering of `grant.root`.
    - Preserved fetch call validation and identity section tests.
    
    ### Result
    The WhoAmI component now reflects the new simplified UI, and the test suite is fully aligned with the updated structure and expectations.
    ---
     src/components/WhoAmI.jsx            | 270 +++++++++------------------
     src/components/tests/WhoAmI.test.jsx |  24 +--
     2 files changed, 95 insertions(+), 199 deletions(-)
    
    diff --git a/src/components/WhoAmI.jsx b/src/components/WhoAmI.jsx
    index 11721f20..37301462 100644
    --- a/src/components/WhoAmI.jsx
    +++ b/src/components/WhoAmI.jsx
    @@ -10,7 +10,7 @@ import {
         Box,
         useTheme
     } from '@mui/material';
    -import {FaSignOutAlt, FaServer, FaUser, FaKey, FaLock, FaCode, FaWifi, FaMoon, FaSun} from "react-icons/fa";
    +import {FaSignOutAlt, FaServer, FaUser, FaLock, FaCode, FaWifi, FaMoon, FaSun} from "react-icons/fa";
     import {useOidc} from "../context/OidcAuthContext.tsx";
     import {useAuth, useAuthDispatch, Logout} from "../context/AuthProvider.jsx";
     import {useNavigate} from "react-router-dom";
    @@ -31,7 +31,7 @@ const WhoAmI = () => {
         const {isDarkMode, toggleDarkMode} = useDarkMode();
         const theme = useTheme();
     
    -    // Fetch app version from GitHub API
    +    // Fetch version from GitHub
         useEffect(() => {
             const fetchVersion = async () => {
                 const cached = localStorage.getItem('appVersion');
    @@ -63,7 +63,7 @@ const WhoAmI = () => {
             fetchVersion();
         }, []);
     
    -    // Fetch daemon status to get the nodename
    +    // Fetch daemon status
         useEffect(() => {
             const fetchDaemonData = async () => {
                 const token = localStorage.getItem("authToken");
    @@ -75,10 +75,10 @@ const WhoAmI = () => {
                     }
                 }
             };
    -
             fetchDaemonData();
         }, [fetchNodes]);
     
    +    // Fetch WhoAmI
         useEffect(() => {
             const fetchUserInfo = async () => {
                 try {
    @@ -119,8 +119,6 @@ const WhoAmI = () => {
     
         return (
             
    -            
    -
                  {
                             flex: 2,
                             cursor: 'pointer',
                             transition: 'all 0.2s ease-in-out',
    -                        '&:hover': {
    -                            transform: 'translateY(-2px)',
    -                            boxShadow: 4
    -                        }
    +                        '&:hover': {transform: 'translateY(-2px)', boxShadow: 4}
                         }}
                     >
                         
                             
                                 
    -                            
    -                                My Information
    -                            
    +                            My Information
                             
     
    -                        
    -                            {/* Identity Section */}
    -                            
    -                                
    -                                    
    -                                    Identity
    -                                
    -                                
    -                                    
    -                                        
    -                                            Username
    -                                        
    -                                        
    -                                            {userInfo.name}
    -                                        
    -                                    
    -
    -                                    
    -                                        
    -                                            Auth Method
    -                                        
    -                                        
    -                                            {userInfo.auth}
    -                                        
    -                                    
    -                                
    -                            
    +                        {/* Identity */}
    +                        
    +                            
    +                                
    +                                Identity
    +                            
     
    -                            {/* Access Section */}
                                 
    -                                
    -                                    
    -                                    Access
    -                                
    -                                
    -                                    
    -                                        
    -                                            Namespace
    -                                        
    -                                        
    -                                            {userInfo.namespace}
    -                                        
    -                                    
    +                                
    +                                    Username
    +                                    
    +                                        {userInfo.name}
    +                                    
    +                                
     
    -                                    
    -                                        
    -                                            Raw Permissions
    -                                        
    -                                        
    -                                            {userInfo.raw_grant || 'None'}
    -                                        
    -                                    
    +                                
    +                                    Auth Method
    +                                    
    +                                        {userInfo.auth}
    +                                    
                                     
                                 
                             
     
    -                        {/* Permissions Section - Full width */}
    +                        {/* Permission Details */}
                             
                                 
    -                                
    +                                
                                     Permission Details
                                 
     
                                 
    -                                
    -                                    {JSON.stringify(userInfo.grant, null, 2)}
    -                                
    + Raw Permissions + + {userInfo.raw_grant || "None"} +
    - {/* Right Side - Server Information, Dark Mode and Logout */} + {/* RIGHT PANEL */} - {/* Server Information Panel */} + + {/* Server Info */} + - - Server Information - + Server Information - - {/* Connected Node Information */} - - - - - Connected Node + {/* Connected Node */} + + + + Connected Node + + + + + + Node Name + + + {daemon?.nodename || "Loading..."} - - {daemon?.nodename || 'Loading...'} - - - Daemon Node - + - {/* Application Version */} - - - - - v{appVersion || 'loading...'} - + {/* App Version */} + + + + WebApp Version + - - OM3 WebApp - + + + Version + + v{appVersion || "Loading..."} + + - - Open Source Cluster Management - + + + Description + + + OM3 WebApp + + - {/* Dark Mode Toggle Button */} + {/* Dark Mode */} - {/* Simple Logout Button */} + {/* Logout */} + + - - {filteredEventTypes.length === 0 ? ( - - No event types available for this page + {filteredEventTypes.length > 0 && ( + + + Add Additional Event Type: - ) : ( - filteredEventTypes.map(eventType => ( - - { - if (e.target.checked) { - setTempSubscribedEventTypes(prev => [...prev, eventType]); - } else { + + + + + + )} + + + {selectedPageEventTypes.length > 0 && ( + + + Page Events ({selectedPageEventTypes.length}) + + {selectedPageEventTypes.map(eventType => ( + + { + if (e.target.checked) { + setTempSubscribedEventTypes(prev => [...prev, eventType]); + } else { + setTempSubscribedEventTypes(prev => prev.filter(et => et !== eventType)); + } + }} + size="small" + sx={{ + color: isDarkMode ? '#ffffff' : undefined, + '&.Mui-checked': { + color: isDarkMode ? '#90caf9' : undefined, + } + }} + /> + + + {eventType} + + + + {eventStats[eventType] || 0} events received + + + + ))} + + )} + + {selectedManualEventTypes.length > 0 && ( + + + Additional Events ({selectedManualEventTypes.length}) + + {selectedManualEventTypes.map(eventType => ( + + { + if (e.target.checked) { + setTempSubscribedEventTypes(prev => [...prev, eventType]); + } else { + setTempSubscribedEventTypes(prev => prev.filter(et => et !== eventType)); + } + }} + size="small" + sx={{ + color: isDarkMode ? '#ffffff' : undefined, + '&.Mui-checked': { + color: isDarkMode ? '#90caf9' : undefined, + } + }} + /> + + + {eventType} + + + + {eventStats[eventType] || 0} events received + + + { setTempSubscribedEventTypes(prev => prev.filter(et => et !== eventType)); - } - }} - size="small" - sx={{ - color: isDarkMode ? '#ffffff' : undefined, - '&.Mui-checked': { - color: isDarkMode ? '#90caf9' : undefined, - } - }} - /> - - - {eventType} - - - {eventStats[eventType] || 0} events received - + }} + sx={{color: isDarkMode ? '#ff5252' : 'error.main'}} + > + + - - )) + ))} + + )} + + {tempSubscribedEventTypes.length === 0 && ( + + {filteredEventTypes.length === 0 + ? "No event types available for this page" + : "No event types selected. You won't receive any events."} + )} - + + + + Total selected: {tempSubscribedEventTypes.length} + + + Available: {ALL_EVENT_TYPES.length} + + ); }; - // Subscription info component const SubscriptionInfo = () => { + const pageEventCount = subscribedEventTypes.filter(type => + filteredEventTypes.includes(type) + ).length; + const manualEventCount = subscribedEventTypes.length - pageEventCount; + const subscriptionText = [ `${subscribedEventTypes.length} event type(s)`, objectName && `object: ${objectName}` ].filter(Boolean).join(' • '); return ( - - + + setSubscriptionDialogOpen(true)} - onDelete={() => setSubscribedEventTypes([...filteredEventTypes])} + onDelete={() => { + setManualSubscriptions([]); + }} deleteIcon={} /> + {manualEventCount > 0 && ( + + )} ); }; - // Function to get current subscriptions for external use const getCurrentSubscriptions = useCallback(() => { return [...subscribedEventTypes]; }, [subscribedEventTypes]); @@ -561,16 +819,30 @@ const EventLogger = ({ backgroundColor: isDarkMode ? theme.palette.grey[900] : theme.palette.background.paper }; - // Manage logger EventSource useEffect(() => { const token = localStorage.getItem("authToken"); - if (token) { - startLoggerReception(token, subscribedEventTypes, objectName); + if (token && subscribedEventTypes.length > 0) { + const allSubscribedTypes = [ + ...subscribedEventTypes, + ...eventTypes.filter(et => CONNECTION_EVENTS.includes(et)) + ]; + + try { + logger.log("Starting logger reception with event types:", allSubscribedTypes); + startLoggerReception(token, allSubscribedTypes, objectName); + } catch (error) { + logger.warn("Failed to start logger reception:", error); + } } return () => { - closeLoggerEventSource(); + try { + logger.log("Closing logger event source"); + closeLoggerEventSource(); + } catch (error) { + logger.warn("Failed to close logger event source:", error); + } }; - }, [subscribedEventTypes, objectName]); + }, [subscribedEventTypes, objectName, eventTypes]); const EventTypeChip = ({eventType, searchTerm}) => { const color = getEventColor(eventType); @@ -648,7 +920,6 @@ const EventLogger = ({ variant="persistent" PaperProps={{style: paperStyle}} > - {/* Resizer */}
    - {/* Header */} @@ -745,10 +1015,8 @@ const EventLogger = ({ - {/* Compact subscription info */} - {/* Filters */} - {/* Logs container */} - {/* Header */} - {/* Preview when collapsed */} {!isOpen && ( )} - {/* Full details when expanded */} {isOpen && ( - {/* Subscription Management Dialog */} + `}F ); }; diff --git a/src/components/tests/EventLogger.test.jsx b/src/components/tests/EventLogger.test.jsx index 3ec0f3fa..968921e7 100644 --- a/src/components/tests/EventLogger.test.jsx +++ b/src/components/tests/EventLogger.test.jsx @@ -2420,7 +2420,8 @@ describe('EventLogger Component', () => { await waitFor(() => { expect(screen.getByText('Event Subscriptions')).toBeInTheDocument(); }); - const checkbox = screen.getByRole('checkbox'); + const checkboxes = screen.getAllByRole('checkbox'); + const checkbox = checkboxes[0]; expect(checkbox).toBeChecked(); fireEvent.click(checkbox); expect(checkbox).not.toBeChecked(); @@ -4244,7 +4245,7 @@ describe('EventLogger Component', () => { id: '1', eventType: 'NO_TEXT_BRANCH', timestamp: new Date().toISOString(), - data: null, + data: {message: null}, }, ]; useEventLogStore.mockReturnValue({ @@ -4253,28 +4254,58 @@ describe('EventLogger Component', () => { setPaused: jest.fn(), clearLogs: jest.fn(), }); - renderWithTheme(); + + const { unmount } = renderWithTheme(); + + await waitFor(() => { + const buttons = screen.getAllByRole('button'); + const eventLoggerButton = buttons.find(btn => + btn.textContent?.includes('Events') || btn.textContent?.includes('Event Logger') + ); + expect(eventLoggerButton).toBeInTheDocument(); + }, { timeout: 3000 }); + const buttons = screen.getAllByRole('button'); const eventLoggerButton = buttons.find(btn => btn.textContent?.includes('Events') || btn.textContent?.includes('Event Logger') ); + if (eventLoggerButton) { - fireEvent.click(eventLoggerButton); - const searchInput = screen.getByPlaceholderText(/Search events/i); - fireEvent.change(searchInput, {target: {value: 'test'}}); - await waitFor(() => { - expect(screen.getByText(/No events match current filters/i)).toBeInTheDocument(); + await act(async () => { + fireEvent.click(eventLoggerButton); }); - fireEvent.change(searchInput, {target: {value: ''}}); + await waitFor(() => { - expect(screen.getByText(/NO_TEXT_BRANCH/i)).toBeInTheDocument(); - }); - const logHeader = screen.getByText(/NO_TEXT_BRANCH/i).closest('div'); - fireEvent.click(logHeader); + expect(screen.getByText(/Event Logger/i)).toBeInTheDocument(); + }, { timeout: 3000 }); + await waitFor(() => { - expect(screen.getByText(/null/i)).toBeInTheDocument(); + expect(screen.getByText(/NO_TEXT_BRANCH/i)).toBeInTheDocument(); + }, { timeout: 3000 }); + + const searchInput = screen.getByPlaceholderText(/Search events/i); + + await act(async () => { + fireEvent.change(searchInput, { target: { value: '' } }); + await new Promise(resolve => setTimeout(resolve, 400)); }); + + const logHeader = screen.getByText(/NO_TEXT_BRANCH/i).closest('[style*="cursor: pointer"]'); + if (logHeader) { + await act(async () => { + fireEvent.click(logHeader); + }); + + await waitFor(() => { + const nullElements = screen.getAllByText(/null/i); + expect(nullElements.length).toBeGreaterThan(0); + }, { timeout: 2000 }); + } } + + await act(async () => { + unmount(); + }); }); test('covers applyHighlightToMatch no searchTerm branch', async () => { @@ -4292,26 +4323,65 @@ describe('EventLogger Component', () => { setPaused: jest.fn(), clearLogs: jest.fn(), }); - renderWithTheme(); + + const { unmount } = renderWithTheme(); + + // Wait for component to mount and button to be available + await waitFor(() => { + const buttons = screen.queryAllByRole('button'); + expect(buttons.length).toBeGreaterThan(0); + }, { timeout: 3000 }); + const buttons = screen.getAllByRole('button'); const eventLoggerButton = buttons.find(btn => btn.textContent?.includes('Events') || btn.textContent?.includes('Event Logger') ); + + expect(eventLoggerButton).toBeInTheDocument(); + if (eventLoggerButton) { - fireEvent.click(eventLoggerButton); - const searchInput = screen.getByPlaceholderText(/Search events/i); - fireEvent.change(searchInput, {target: {value: ''}}); + await act(async () => { + fireEvent.click(eventLoggerButton); + }); + + await waitFor(() => { + expect(screen.getByText(/Event Logger/i)).toBeInTheDocument(); + }, { timeout: 3000 }); + await waitFor(() => { expect(screen.getByText(/NO_SEARCH_APPLY/i)).toBeInTheDocument(); + }, { timeout: 3000 }); + + const searchInput = screen.getByPlaceholderText(/Search events/i); + + await act(async () => { + fireEvent.change(searchInput, {target: {value: ''}}); + await new Promise(resolve => setTimeout(resolve, 400)); }); - const logHeader = screen.getByText(/NO_SEARCH_APPLY/i).closest('div'); - fireEvent.click(logHeader); + await waitFor(() => { - expect(screen.getByText(/"value"/)).toBeInTheDocument(); + expect(screen.getByText(/NO_SEARCH_APPLY/i)).toBeInTheDocument(); + }, { timeout: 2000 }); + + const logHeader = screen.getByText(/NO_SEARCH_APPLY/i).closest('[style*="cursor: pointer"]'); + if (logHeader) { + await act(async () => { + fireEvent.click(logHeader); + }); + + await waitFor(() => { + expect(screen.getByText(/"value"/)).toBeInTheDocument(); + }, { timeout: 2000 }); + + // Verify no search highlights are present (since no search term) const highlightSpans = document.querySelectorAll('.search-highlight'); expect(highlightSpans.length).toBe(0); - }); + } } + + await act(async () => { + unmount(); + }); }); test('covers subscription dialog empty eventTypes', async () => { From 3e96a1dec95ed1726ae1d765cb9cad9ccb64d335 Mon Sep 17 00:00:00 2001 From: Paul Jouvanceau Date: Fri, 12 Dec 2025 14:26:40 +0100 Subject: [PATCH 43/67] feat: fix search highlighting within JSON strings in EventLogger - Fixed `syntaxHighlightJSON` function to properly highlight search terms inside JSON strings between quotes - Changed highlighting approach: first apply search highlights on escaped JSON, then apply syntax coloring - Ensured search terms are properly escaped to match within HTML-escaped JSON content - Added check to prevent double-wrapping of already highlighted text with syntax coloring spans - Improved regex escaping for search terms to handle special characters correctly The search functionality now correctly highlights matching text within JSON string values, not just keys and other JSON tokens. --- src/components/EventLogger.jsx | 35 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/components/EventLogger.jsx b/src/components/EventLogger.jsx index 33276b7d..8d8458e3 100644 --- a/src/components/EventLogger.jsx +++ b/src/components/EventLogger.jsx @@ -445,25 +445,22 @@ const EventLogger = ({ } } - json = escapeHtml(json); - - const applyHighlightToMatch = (match, searchTerm) => { - if (!searchTerm) return match; - + let highlightedJson = json; + if (searchTerm) { const term = searchTerm.toLowerCase(); - const lowerMatch = match.toLowerCase(); - const index = lowerMatch.indexOf(term); + const escapedTerm = escapeHtml(searchTerm).toLowerCase(); - if (index === -1) return match; + const escapedJson = escapeHtml(json); - const before = match.substring(0, index); - const highlight = match.substring(index, index + term.length); - const after = match.substring(index + term.length); - - return `${before}${highlight}${after}`; - }; + highlightedJson = escapedJson.replace( + new RegExp(`(${escapedTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), + '$1' + ); + } else { + highlightedJson = escapeHtml(json); + } - return json.replace( + return highlightedJson.replace( /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, (match) => { let cls = 'json-number'; @@ -479,9 +476,11 @@ const EventLogger = ({ cls = 'json-null'; } - const highlightedMatch = searchTerm ? applyHighlightToMatch(match, searchTerm) : match; + if (match.includes('search-highlight')) { + return match; + } - return `${highlightedMatch}`; + return `${match}`; } ); }; @@ -1209,7 +1208,7 @@ const EventLogger = ({ border-radius: 2px; font-weight: bold; } - `}F + `} ); }; From cc7f732fa1842010c6d5eacbe6c5221fa34f55e4 Mon Sep 17 00:00:00 2001 From: Paul Jouvanceau Date: Fri, 12 Dec 2025 16:36:30 +0100 Subject: [PATCH 44/67] Add mobile touch support and fix tests - Added touch event handling for mobile drawer resizing (touchstart, touchmove, touchend) - Fixed failing test "clears resize timeout on mouseUp during resize" with proper timeout cleanup - Fixed failing test "applyHighlightToMatch - branch when index === -1" with improved test patterns - Added visual resize indicator and improved mobile UX - Set search debounce to 0ms in test environment for immediate execution - Added responsive design for mobile devices - Enhanced event listener management and timeout cleanup - All existing functionality preserved --- src/components/EventLogger.jsx | 187 ++++++++++++++++++---- src/components/tests/EventLogger.test.jsx | 89 +++++++--- 2 files changed, 226 insertions(+), 50 deletions(-) diff --git a/src/components/EventLogger.jsx b/src/components/EventLogger.jsx index 8d8458e3..7efc3659 100644 --- a/src/components/EventLogger.jsx +++ b/src/components/EventLogger.jsx @@ -52,6 +52,7 @@ const ALL_EVENT_TYPES = [ 'InstanceConfigUpdated' ]; +const DEBOUNCE_DELAY = process.env.NODE_ENV === 'test' ? 0 : 300; const SubscriptionDialog = ({ open, onClose, @@ -311,6 +312,7 @@ const EventLogger = ({ const [forceUpdate, setForceUpdate] = useState(0); const [expandedLogIds, setExpandedLogIds] = useState([]); const [subscriptionDialogOpen, setSubscriptionDialogOpen] = useState(false); + const [isResizing, setIsResizing] = useState(false); const filteredEventTypes = useMemo(() => { return eventTypes.filter(et => !CONNECTION_EVENTS.includes(et)); @@ -364,6 +366,9 @@ const EventLogger = ({ const logsContainerRef = useRef(null); const resizeTimeoutRef = useRef(null); const searchDebounceRef = useRef(null); + const startYRef = useRef(0); + const startHeightRef = useRef(0); + const isDraggingRef = useRef(false); const {eventLogs = [], isPaused, setPaused, clearLogs} = useEventLogStore(); @@ -374,11 +379,12 @@ const EventLogger = ({ searchDebounceRef.current = setTimeout(() => { setDebouncedSearchTerm(searchTerm); - }, 300); + }, DEBOUNCE_DELAY); return () => { if (searchDebounceRef.current) { clearTimeout(searchDebounceRef.current); + searchDebounceRef.current = null; } }; }, [searchTerm]); @@ -692,30 +698,98 @@ const EventLogger = ({ if (atBottom !== autoScroll) setAutoScroll(atBottom); }, [autoScroll]); - const startResizing = useCallback((mouseDownEvent) => { - if (mouseDownEvent?.preventDefault) mouseDownEvent.preventDefault(); - const startY = mouseDownEvent?.clientY ?? 0; - const startHeight = drawerHeight; - - const handleMouseMove = (mouseMoveEvent) => { - if (resizeTimeoutRef.current) clearTimeout(resizeTimeoutRef.current); - resizeTimeoutRef.current = setTimeout(() => { - const deltaY = startY - (mouseMoveEvent?.clientY ?? startY); - const newHeight = Math.max(220, Math.min(800, startHeight + deltaY)); - setDrawerHeight(newHeight); - }, 16); - }; + const handleResizeStart = useCallback((e) => { + e.preventDefault(); + e.stopPropagation(); - const handleMouseUp = () => { - document.removeEventListener("mousemove", handleMouseMove); - document.removeEventListener("mouseup", handleMouseUp); - if (resizeTimeoutRef.current) clearTimeout(resizeTimeoutRef.current); - }; + setIsResizing(true); + isDraggingRef.current = true; + + const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY; + startYRef.current = clientY; + startHeightRef.current = drawerHeight; - document.addEventListener("mousemove", handleMouseMove); - document.addEventListener("mouseup", handleMouseUp); + document.body.style.userSelect = 'none'; + document.body.style.touchAction = 'none'; + document.body.style.overflow = 'hidden'; + + if (resizeTimeoutRef.current) { + clearTimeout(resizeTimeoutRef.current); + resizeTimeoutRef.current = null; + } }, [drawerHeight]); + const handleResizeMove = useCallback((e) => { + if (!isDraggingRef.current) return; + + e.preventDefault(); + e.stopPropagation(); + + const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY; + const deltaY = startYRef.current - clientY; + const newHeight = Math.max(220, Math.min(window.innerHeight * 0.8, startHeightRef.current + deltaY)); + + if (resizeTimeoutRef.current) { + clearTimeout(resizeTimeoutRef.current); + } + + resizeTimeoutRef.current = setTimeout(() => { + setDrawerHeight(newHeight); + }, 16); + }, []); + + const handleResizeEnd = useCallback((e) => { + if (!isDraggingRef.current) return; + + e.preventDefault(); + e.stopPropagation(); + + setIsResizing(false); + isDraggingRef.current = false; + + document.body.style.userSelect = ''; + document.body.style.touchAction = ''; + document.body.style.overflow = ''; + + if (resizeTimeoutRef.current) { + clearTimeout(resizeTimeoutRef.current); + resizeTimeoutRef.current = null; + } + }, []); + + useEffect(() => { + const handleMouseMove = (e) => handleResizeMove(e); + const handleTouchMove = (e) => handleResizeMove(e); + const handleMouseUp = (e) => handleResizeEnd(e); + const handleTouchEnd = (e) => handleResizeEnd(e); + const handleTouchCancel = (e) => handleResizeEnd(e); + + if (isResizing) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('touchmove', handleTouchMove, {passive: false}); + document.addEventListener('mouseup', handleMouseUp); + document.addEventListener('touchend', handleTouchEnd); + document.addEventListener('touchcancel', handleTouchCancel); + } + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('touchmove', handleTouchMove); + document.removeEventListener('mouseup', handleMouseUp); + document.removeEventListener('touchend', handleTouchEnd); + document.removeEventListener('touchcancel', handleTouchCancel); + + if (resizeTimeoutRef.current) { + clearTimeout(resizeTimeoutRef.current); + resizeTimeoutRef.current = null; + } + if (searchDebounceRef.current) { + clearTimeout(searchDebounceRef.current); + searchDebounceRef.current = null; + } + }; + }, [isResizing, handleResizeMove, handleResizeEnd]); + const formatTimestamp = (ts) => { try { return new Date(ts).toLocaleTimeString("en-US", { @@ -757,7 +831,8 @@ const EventLogger = ({ overflow: "hidden", borderTopLeftRadius: 8, borderTopRightRadius: 8, - backgroundColor: isDarkMode ? theme.palette.grey[900] : theme.palette.background.paper + backgroundColor: isDarkMode ? theme.palette.grey[900] : theme.palette.background.paper, + touchAction: 'none' }; useEffect(() => { @@ -862,22 +937,36 @@ const EventLogger = ({ PaperProps={{style: paperStyle}} >
    @@ -1065,7 +1154,8 @@ const EventLogger = ({ overflow: "auto", backgroundColor: isDarkMode ? theme.palette.grey[900] : theme.palette.grey[50], padding: 1, - ...jsonStyles + ...jsonStyles, + WebkitOverflowScrolling: 'touch' }} > {filteredLogs.length === 0 ? ( @@ -1097,7 +1187,8 @@ const EventLogger = ({ bgcolor: isOpen ? isDarkMode ? 'rgba(255, 255, 255, 0.1)' : theme.palette.action.selected : "transparent", - transition: "background-color 0.2s ease" + transition: "background-color 0.2s ease", + touchAction: 'manipulation' }} > @@ -1208,6 +1299,42 @@ const EventLogger = ({ border-radius: 2px; font-weight: bold; } + /* Améliorations pour mobile */ + @media (max-width: 768px) { + .MuiDrawer-paper { + max-height: 90vh !important; + touch-action: none; + } + .resize-handle { + height: 32px !important; + min-height: 32px !important; + } + .MuiChip-root { + font-size: 0.7rem !important; + } + .MuiTypography-body2 { + font-size: 0.8rem !important; + } + .MuiButton-root { + padding: 6px 12px !important; + min-height: 36px !important; + } + .MuiIconButton-root { + padding: 6px !important; + min-width: 36px !important; + min-height: 36px !important; + } + .MuiTextField-root, .MuiFormControl-root { + min-width: 100% !important; + } + } + + /* Empêcher le zoom sur le champ de saisie sur iOS */ + @media screen and (max-width: 768px) { + input, select, textarea { + font-size: 16px !important; + } + } `} ); diff --git a/src/components/tests/EventLogger.test.jsx b/src/components/tests/EventLogger.test.jsx index 968921e7..12b81ea9 100644 --- a/src/components/tests/EventLogger.test.jsx +++ b/src/components/tests/EventLogger.test.jsx @@ -1150,27 +1150,43 @@ describe('EventLogger Component', () => { } }); + test('clears resize timeout on mouseUp during resize', async () => { jest.useFakeTimers(); + renderWithTheme(); const buttons = screen.getAllByRole('button'); const eventLoggerButton = buttons.find(btn => btn.textContent?.includes('Events') || btn.textContent?.includes('Event Logger') ); + + expect(eventLoggerButton).toBeInTheDocument(); + if (eventLoggerButton) { fireEvent.click(eventLoggerButton); + + await waitFor(() => { + expect(screen.getByText(/Event Logger/i)).toBeInTheDocument(); + }); + const handles = document.querySelectorAll('div[style*="cursor: row-resize"]'); const handle = handles[0]; expect(handle).not.toBeNull(); fireEvent.mouseDown(handle, {clientY: 300}); fireEvent.mouseMove(document, {clientY: 250}); - const spy = jest.spyOn(global, 'clearTimeout'); + + jest.advanceTimersByTime(20); + const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); fireEvent.mouseUp(document); - expect(spy).toHaveBeenCalled(); - jest.useRealTimers(); + + expect(clearTimeoutSpy).toHaveBeenCalled(); + clearTimeoutSpy.mockRestore(); } + + jest.useRealTimers(); }); + test('autoScroll resets to true when search term changes', async () => { useEventLogStore.mockReturnValue({ eventLogs: [{ @@ -3015,10 +3031,11 @@ describe('EventLogger Component', () => { test('applyHighlightToMatch - branch when index === -1', async () => { const mockLogs = [{ id: '1', - eventType: 'NO_JSON_MATCH', + eventType: 'NO_MATCH_HIGHLIGHT', timestamp: new Date().toISOString(), - data: {field: 'content'}, + data: {field: 'value'} }]; + useEventLogStore.mockReturnValue({ eventLogs: mockLogs, isPaused: false, @@ -3026,15 +3043,47 @@ describe('EventLogger Component', () => { clearLogs: jest.fn(), }); renderWithTheme(); + await waitFor(() => { + const buttons = screen.getAllByRole('button'); + const eventLoggerButton = buttons.find(btn => + btn.textContent?.includes('Events') || btn.textContent?.includes('Event Logger') + ); + expect(eventLoggerButton).toBeInTheDocument(); + }); const buttons = screen.getAllByRole('button'); - const eventLoggerButton = buttons.find(btn => btn.textContent?.includes('Events')); + const eventLoggerButton = buttons.find(btn => + btn.textContent?.includes('Events') || btn.textContent?.includes('Event Logger') + ); + + expect(eventLoggerButton).toBeInTheDocument(); + if (eventLoggerButton) { fireEvent.click(eventLoggerButton); + + await waitFor(() => { + expect(screen.getByText(/Event Logger/i)).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(screen.getByText(/NO_MATCH_HIGHLIGHT/i)).toBeInTheDocument(); + }); + const searchInput = screen.getByPlaceholderText(/Search events/i); - fireEvent.change(searchInput, {target: {value: 'nomatch'}}); + expect(searchInput).toBeInTheDocument(); + + fireEvent.change(searchInput, {target: {value: 'nonexistent'}}); + await waitFor(() => { - expect(screen.getByText(/No events match current filters/i)).toBeInTheDocument(); + expect(searchInput.value).toBe('nonexistent'); }); + + const noMatchMessage = await screen.findByText( + /No events match current filters/i, + {}, + {timeout: 3000} + ); + + expect(noMatchMessage).toBeInTheDocument(); } }); @@ -4255,7 +4304,7 @@ describe('EventLogger Component', () => { clearLogs: jest.fn(), }); - const { unmount } = renderWithTheme(); + const {unmount} = renderWithTheme(); await waitFor(() => { const buttons = screen.getAllByRole('button'); @@ -4263,7 +4312,7 @@ describe('EventLogger Component', () => { btn.textContent?.includes('Events') || btn.textContent?.includes('Event Logger') ); expect(eventLoggerButton).toBeInTheDocument(); - }, { timeout: 3000 }); + }, {timeout: 3000}); const buttons = screen.getAllByRole('button'); const eventLoggerButton = buttons.find(btn => @@ -4277,16 +4326,16 @@ describe('EventLogger Component', () => { await waitFor(() => { expect(screen.getByText(/Event Logger/i)).toBeInTheDocument(); - }, { timeout: 3000 }); + }, {timeout: 3000}); await waitFor(() => { expect(screen.getByText(/NO_TEXT_BRANCH/i)).toBeInTheDocument(); - }, { timeout: 3000 }); + }, {timeout: 3000}); const searchInput = screen.getByPlaceholderText(/Search events/i); await act(async () => { - fireEvent.change(searchInput, { target: { value: '' } }); + fireEvent.change(searchInput, {target: {value: ''}}); await new Promise(resolve => setTimeout(resolve, 400)); }); @@ -4299,7 +4348,7 @@ describe('EventLogger Component', () => { await waitFor(() => { const nullElements = screen.getAllByText(/null/i); expect(nullElements.length).toBeGreaterThan(0); - }, { timeout: 2000 }); + }, {timeout: 2000}); } } @@ -4324,13 +4373,13 @@ describe('EventLogger Component', () => { clearLogs: jest.fn(), }); - const { unmount } = renderWithTheme(); + const {unmount} = renderWithTheme(); // Wait for component to mount and button to be available await waitFor(() => { const buttons = screen.queryAllByRole('button'); expect(buttons.length).toBeGreaterThan(0); - }, { timeout: 3000 }); + }, {timeout: 3000}); const buttons = screen.getAllByRole('button'); const eventLoggerButton = buttons.find(btn => @@ -4346,11 +4395,11 @@ describe('EventLogger Component', () => { await waitFor(() => { expect(screen.getByText(/Event Logger/i)).toBeInTheDocument(); - }, { timeout: 3000 }); + }, {timeout: 3000}); await waitFor(() => { expect(screen.getByText(/NO_SEARCH_APPLY/i)).toBeInTheDocument(); - }, { timeout: 3000 }); + }, {timeout: 3000}); const searchInput = screen.getByPlaceholderText(/Search events/i); @@ -4361,7 +4410,7 @@ describe('EventLogger Component', () => { await waitFor(() => { expect(screen.getByText(/NO_SEARCH_APPLY/i)).toBeInTheDocument(); - }, { timeout: 2000 }); + }, {timeout: 2000}); const logHeader = screen.getByText(/NO_SEARCH_APPLY/i).closest('[style*="cursor: pointer"]'); if (logHeader) { @@ -4371,7 +4420,7 @@ describe('EventLogger Component', () => { await waitFor(() => { expect(screen.getByText(/"value"/)).toBeInTheDocument(); - }, { timeout: 2000 }); + }, {timeout: 2000}); // Verify no search highlights are present (since no search term) const highlightSpans = document.querySelectorAll('.search-highlight'); From 86d914366f2ec58cce7f998d60ae1fafe36a6dde Mon Sep 17 00:00:00 2001 From: Paul Jouvanceau Date: Tue, 16 Dec 2025 15:43:06 +0100 Subject: [PATCH 45/67] feat(event-logger): improve event subscriptions with per-page persistence and UI enhancements - Add per-page persistence of subscriptions using a unique pageKey based on objectName and filtered event types - Default subscriptions now include all page-specific event types automatically - Refactor SubscriptionDialog: - Simplify buttons: 'Subscribe to All', 'Subscribe to Page Events', 'Unsubscribe from All' - Remove redundant 'Clear All' button - Remove custom event type input (unused) - Remove page/additional chips in list for cleaner UI - Apply button now directly sets full subscription list and clears logs on change - Update SubscriptionInfo component: - Show total subscribed count and object name if applicable - Display all subscribed types as deletable chips with count - Settings icon opens dialog - Optimize event source management: - startLoggerReception now called with exact required events (including connection events) - Close source only when no events to subscribe to - No cleanup closure on unmount (shared connection managed globally) - Improve filtering logic for baseFilteredLogs to handle no subscriptions case correctly - Minor UI cleanups and style adjustments in dialog This ensures subscriptions are isolated per page/object, more intuitive to manage, and logs are cleared when changing subscriptions to avoid confusion. --- src/components/EventLogger.jsx | 440 ++++++++++------------ src/components/tests/EventLogger.test.jsx | 349 ++++++++--------- src/components/tests/NodesTable.test.jsx | 8 +- src/components/tests/Objects.test.jsx | 9 +- 4 files changed, 358 insertions(+), 448 deletions(-) diff --git a/src/components/EventLogger.jsx b/src/components/EventLogger.jsx index 7efc3659..22fff21b 100644 --- a/src/components/EventLogger.jsx +++ b/src/components/EventLogger.jsx @@ -25,8 +25,7 @@ import { KeyboardArrowUp, Pause, PlayArrow, - Settings, - Add + Settings } from "@mui/icons-material"; import useEventLogStore from "../hooks/useEventLogStore"; import logger from "../utils/logger.js"; @@ -53,6 +52,17 @@ const ALL_EVENT_TYPES = [ ]; const DEBOUNCE_DELAY = process.env.NODE_ENV === 'test' ? 0 : 300; + +const hashCode = (str) => { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32bit integer + } + return Math.abs(hash).toString(36); +}; + const SubscriptionDialog = ({ open, onClose, @@ -62,14 +72,30 @@ const SubscriptionDialog = ({ setManualSubscriptions, filteredEventTypes, eventStats, - ALL_EVENT_TYPES + clearLogs }) => { const [tempSubscribedEventTypes, setTempSubscribedEventTypes] = useState(subscribedEventTypes); - const [customEventType, setCustomEventType] = useState(""); const otherEventTypes = useMemo(() => { return ALL_EVENT_TYPES.filter(type => !filteredEventTypes.includes(type)).sort(); - }, [ALL_EVENT_TYPES, filteredEventTypes]); + }, [filteredEventTypes]); + + const handleSubscribeAll = () => { + setTempSubscribedEventTypes([...ALL_EVENT_TYPES]); + }; + + const handleSubscribePageEvents = () => { + const currentOther = tempSubscribedEventTypes.filter( + type => !filteredEventTypes.includes(type) + ); + setTempSubscribedEventTypes( + [...new Set([...currentOther, ...filteredEventTypes])] + ); + }; + + const handleUnsubscribeAll = () => { + setTempSubscribedEventTypes([]); + }; return ( @@ -94,105 +122,75 @@ const SubscriptionDialog = ({ - + - - Select which event types you want to SUBSCRIBE to (this affects future events only): + + Select which event types you want to SUBSCRIBE to (future events only): + {/* ACTION BUTTONS */} + + - + {/* EVENT LIST */} {filteredEventTypes.length > 0 && ( - + Page Events ({filteredEventTypes.length}) + {filteredEventTypes.sort().map(eventType => ( { - if (e.target.checked) { - setTempSubscribedEventTypes(prev => [...new Set([...prev, eventType])]); - } else { - setTempSubscribedEventTypes(prev => prev.filter(et => et !== eventType)); - } + setTempSubscribedEventTypes(prev => + e.target.checked + ? [...new Set([...prev, eventType])] + : prev.filter(et => et !== eventType) + ); }} size="small" - sx={{ - color: isDarkMode ? '#ffffff' : undefined, - '&.Mui-checked': { - color: isDarkMode ? '#90caf9' : undefined, - } - }} /> - - + + {eventType} - - + {eventStats[eventType] || 0} events received @@ -201,47 +199,34 @@ const SubscriptionDialog = ({ )} - {filteredEventTypes.length > 0 && otherEventTypes.length > 0 && ( + {otherEventTypes.length > 0 && ( - + Additional Events ({otherEventTypes.length}) + {otherEventTypes.map(eventType => ( { - if (e.target.checked) { - setTempSubscribedEventTypes(prev => [...new Set([...prev, eventType])]); - } else { - setTempSubscribedEventTypes(prev => prev.filter(et => et !== eventType)); - } + setTempSubscribedEventTypes(prev => + e.target.checked + ? [...new Set([...prev, eventType])] + : prev.filter(et => et !== eventType) + ); }} size="small" - sx={{ - color: isDarkMode ? '#ffffff' : undefined, - '&.Mui-checked': { - color: isDarkMode ? '#90caf9' : undefined, - } - }} /> - - + + {eventType} - - + {eventStats[eventType] || 0} events received @@ -251,41 +236,22 @@ const SubscriptionDialog = ({ )} {tempSubscribedEventTypes.length === 0 && ( - - {filteredEventTypes.length === 0 - ? "No event types available for this page" - : "No event types selected. You won't receive any events."} + + No event types selected. You won't receive any events. )} - - - - Total selected: {tempSubscribedEventTypes.length} - - - Available: {ALL_EVENT_TYPES.length} - - + {/* APPLY */} + @@ -318,6 +284,13 @@ const EventLogger = ({ return eventTypes.filter(et => !CONNECTION_EVENTS.includes(et)); }, [eventTypes]); + const pageKey = useMemo(() => { + const baseKey = objectName || 'global'; + const eventTypesKey = filteredEventTypes.sort().join(','); + const hash = hashCode(eventTypesKey); + return `eventLogger_${baseKey}_${hash}`; + }, [objectName, filteredEventTypes]); + const usePersistedState = (key, initialValue) => { const [value, setValue] = useState(() => { const readFromStorage = () => { @@ -347,20 +320,14 @@ const EventLogger = ({ return [value, setValue]; }; - const [manualSubscriptions, setManualSubscriptions] = usePersistedState('eventLogger_manualSubscriptions', []); + const [manualSubscriptions, setManualSubscriptions] = usePersistedState(pageKey, [...filteredEventTypes]); const subscribedEventTypes = useMemo(() => { - return [...new Set([...filteredEventTypes, ...manualSubscriptions])]; - }, [filteredEventTypes, manualSubscriptions]); - - useEffect(() => { - const newManualSubscriptions = manualSubscriptions.filter(type => - !filteredEventTypes.includes(type) + const validSubscriptions = manualSubscriptions.filter(type => + ALL_EVENT_TYPES.includes(type) ); - if (newManualSubscriptions.length !== manualSubscriptions.length) { - setManualSubscriptions(newManualSubscriptions); - } - }, [filteredEventTypes, manualSubscriptions, setManualSubscriptions]); + return [...new Set(validSubscriptions)]; + }, [manualSubscriptions]); const logsEndRef = useRef(null); const logsContainerRef = useRef(null); @@ -376,11 +343,9 @@ const EventLogger = ({ if (searchDebounceRef.current) { clearTimeout(searchDebounceRef.current); } - searchDebounceRef.current = setTimeout(() => { setDebouncedSearchTerm(searchTerm); }, DEBOUNCE_DELAY); - return () => { if (searchDebounceRef.current) { clearTimeout(searchDebounceRef.current); @@ -391,7 +356,6 @@ const EventLogger = ({ const filterData = useCallback((data) => { if (!data || typeof data !== 'object') return data; - const filtered = {...data}; delete filtered._rawEvent; return filtered; @@ -409,30 +373,24 @@ const EventLogger = ({ const createHighlightedHtml = useCallback((text, searchTerm) => { if (!searchTerm || !text) return escapeHtml(text); - const term = searchTerm.toLowerCase(); const lowerText = text.toLowerCase(); let lastIndex = 0; const parts = []; - while (lastIndex < text.length) { const index = lowerText.indexOf(term, lastIndex); if (index === -1) { parts.push(escapeHtml(text.substring(lastIndex))); break; } - if (index > lastIndex) { parts.push(escapeHtml(text.substring(lastIndex, index))); } - parts.push( `${escapeHtml(text.substring(index, index + term.length))}` ); - lastIndex = index + term.length; } - return parts.join(''); }, [escapeHtml]); @@ -450,14 +408,11 @@ const EventLogger = ({ json = String(json); } } - let highlightedJson = json; if (searchTerm) { const term = searchTerm.toLowerCase(); const escapedTerm = escapeHtml(searchTerm).toLowerCase(); - const escapedJson = escapeHtml(json); - highlightedJson = escapedJson.replace( new RegExp(`(${escapedTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), '$1' @@ -465,7 +420,6 @@ const EventLogger = ({ } else { highlightedJson = escapeHtml(json); } - return highlightedJson.replace( /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, (match) => { @@ -481,11 +435,9 @@ const EventLogger = ({ } else if (/null/.test(match)) { cls = 'json-null'; } - if (match.includes('search-highlight')) { return match; } - return `${match}`; } ); @@ -493,7 +445,6 @@ const EventLogger = ({ const JSONView = ({data, dense = false, searchTerm = ''}) => { const filteredData = useMemo(() => filterData(data), [data]); - const jsonString = useMemo(() => { try { return dense ? JSON.stringify(filteredData) : JSON.stringify(filteredData, null, 2); @@ -501,9 +452,7 @@ const EventLogger = ({ return String(filteredData); } }, [filteredData, dense]); - const coloredJSON = useMemo(() => syntaxHighlightJSON(jsonString, dense, searchTerm), [jsonString, dense, searchTerm]); - return (
     {
             let filtered = Array.isArray(eventLogs) ? eventLogs : [];
     
    -        if (subscribedEventTypes.length > 0) {
    +        if (subscribedEventTypes.length === 0 && filteredEventTypes.length > 0) {
    +            filtered = filtered.filter(log => filteredEventTypes.includes(log.eventType));
    +        } else if (subscribedEventTypes.length > 0) {
                 filtered = filtered.filter(log => subscribedEventTypes.includes(log.eventType));
             }
     
    @@ -564,24 +515,28 @@ const EventLogger = ({
             if (objectName) {
                 filtered = filtered.filter(log => {
                     const data = log.data || {};
    +
                     if (log.eventType?.includes?.("CONNECTION")) return true;
    -                if (data.path === objectName) return true;
    -                if (data.labels?.path === objectName) return true;
    -                if (data.data?.path === objectName) return true;
    -                if (data.data?.labels?.path === objectName) return true;
    -                if (log.eventType === "ObjectDeleted") {
    +
    +                if (log.eventType === "ObjectDeleted" && data._rawEvent) {
                         try {
    -                        const raw = data._rawEvent ? JSON.parse(data._rawEvent) : {};
    +                        const raw = JSON.parse(data._rawEvent);
                             if (raw.path === objectName || raw.labels?.path === objectName) return true;
                         } catch {
                         }
                     }
    +
    +                if (data.path === objectName) return true;
    +                if (data.labels?.path === objectName) return true;
    +                if (data.data?.path === objectName) return true;
    +                if (data.data?.labels?.path === objectName) return true;
    +
                     return false;
                 });
             }
     
             return filtered;
    -    }, [eventLogs, subscribedEventTypes, objectName, eventTypes]);
    +    }, [eventLogs, subscribedEventTypes, objectName, eventTypes, filteredEventTypes]);
     
         const availableEventTypes = useMemo(() => {
             const types = new Set();
    @@ -599,9 +554,7 @@ const EventLogger = ({
     
         const filteredLogs = useMemo(() => {
             let result = [...baseFilteredLogs];
    -
             if (eventTypeFilter.length > 0) result = result.filter(log => eventTypeFilter.includes(log.eventType));
    -
             if (debouncedSearchTerm.trim()) {
                 const term = debouncedSearchTerm.toLowerCase().trim();
                 result = result.filter(log => {
    @@ -617,56 +570,55 @@ const EventLogger = ({
                     return typeMatch || dataMatch;
                 });
             }
    -
             return result;
         }, [baseFilteredLogs, eventTypeFilter, debouncedSearchTerm]);
     
         const SubscriptionInfo = () => {
    -        const pageEventCount = subscribedEventTypes.filter(type =>
    -            filteredEventTypes.includes(type)
    -        ).length;
    -        const manualEventCount = subscribedEventTypes.length - pageEventCount;
    -
    -        const subscriptionText = [
    -            `${subscribedEventTypes.length} event type(s)`,
    -            objectName && `object: ${objectName}`
    -        ].filter(Boolean).join(' • ');
    -
             return (
    -            
    -                
    -                    
    +                
    +                    
    +                        Subscribed to: {subscribedEventTypes.length} event type(s)
    +                    
    +                    {objectName && (
    +                        
    +                            • object: {objectName}
    +                        
    +                    )}
    +                     setSubscriptionDialogOpen(true)}
    -                        onDelete={() => {
    -                            setManualSubscriptions([]);
    -                        }}
    -                        deleteIcon={}
    -                    />
    -                
    -                {manualEventCount > 0 && (
    -                    
    +                        sx={{ml: 'auto', color: isDarkMode ? '#ffffff' : undefined}}
    +                    >
    +                        
    +                    
    +                
    +
    +                {subscribedEventTypes.length > 0 && (
    +                    
    +                        {subscribedEventTypes.sort().map(type => (
    +                             {
    +                                    setManualSubscriptions(prev => prev.filter(t => t !== type));
    +                                }}
    +                                sx={{
    +                                    backgroundColor: filteredEventTypes.includes(type)
    +                                        ? (isDarkMode ? 'rgba(144, 202, 249, 0.2)' : 'rgba(25, 118, 210, 0.1)')
    +                                        : (isDarkMode ? 'rgba(165, 214, 167, 0.2)' : 'rgba(76, 175, 80, 0.1)'),
    +                                    color: filteredEventTypes.includes(type)
    +                                        ? (isDarkMode ? '#90caf9' : 'primary.main')
    +                                        : (isDarkMode ? '#a5d6a7' : 'success.dark'),
    +                                    borderColor: isDarkMode ? 'rgba(255, 255, 255, 0.2)' : undefined,
    +                                    fontSize: '0.7rem',
    +                                    height: 20
    +                                }}
    +                            />
    +                        ))}
    +                    
                     )}
                 
             );
    @@ -677,8 +629,12 @@ const EventLogger = ({
         }, [subscribedEventTypes]);
     
         useEffect(() => {
    -        logger.log("Subscriptions updated:", subscribedEventTypes);
    -    }, [subscribedEventTypes]);
    +        logger.log("Subscriptions updated:", {
    +            subscribedEventTypes,
    +            filteredEventTypes,
    +            pageKey
    +        });
    +    }, [subscribedEventTypes, filteredEventTypes, pageKey]);
     
         useEffect(() => {
             setForceUpdate(prev => prev + 1);
    @@ -701,18 +657,14 @@ const EventLogger = ({
         const handleResizeStart = useCallback((e) => {
             e.preventDefault();
             e.stopPropagation();
    -
             setIsResizing(true);
             isDraggingRef.current = true;
    -
             const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
             startYRef.current = clientY;
             startHeightRef.current = drawerHeight;
    -
             document.body.style.userSelect = 'none';
             document.body.style.touchAction = 'none';
             document.body.style.overflow = 'hidden';
    -
             if (resizeTimeoutRef.current) {
                 clearTimeout(resizeTimeoutRef.current);
                 resizeTimeoutRef.current = null;
    @@ -721,18 +673,14 @@ const EventLogger = ({
     
         const handleResizeMove = useCallback((e) => {
             if (!isDraggingRef.current) return;
    -
             e.preventDefault();
             e.stopPropagation();
    -
             const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
             const deltaY = startYRef.current - clientY;
             const newHeight = Math.max(220, Math.min(window.innerHeight * 0.8, startHeightRef.current + deltaY));
    -
             if (resizeTimeoutRef.current) {
                 clearTimeout(resizeTimeoutRef.current);
             }
    -
             resizeTimeoutRef.current = setTimeout(() => {
                 setDrawerHeight(newHeight);
             }, 16);
    @@ -740,17 +688,13 @@ const EventLogger = ({
     
         const handleResizeEnd = useCallback((e) => {
             if (!isDraggingRef.current) return;
    -
             e.preventDefault();
             e.stopPropagation();
    -
             setIsResizing(false);
             isDraggingRef.current = false;
    -
             document.body.style.userSelect = '';
             document.body.style.touchAction = '';
             document.body.style.overflow = '';
    -
             if (resizeTimeoutRef.current) {
                 clearTimeout(resizeTimeoutRef.current);
                 resizeTimeoutRef.current = null;
    @@ -763,7 +707,6 @@ const EventLogger = ({
             const handleMouseUp = (e) => handleResizeEnd(e);
             const handleTouchEnd = (e) => handleResizeEnd(e);
             const handleTouchCancel = (e) => handleResizeEnd(e);
    -
             if (isResizing) {
                 document.addEventListener('mousemove', handleMouseMove);
                 document.addEventListener('touchmove', handleTouchMove, {passive: false});
    @@ -771,14 +714,12 @@ const EventLogger = ({
                 document.addEventListener('touchend', handleTouchEnd);
                 document.addEventListener('touchcancel', handleTouchCancel);
             }
    -
             return () => {
                 document.removeEventListener('mousemove', handleMouseMove);
                 document.removeEventListener('touchmove', handleTouchMove);
                 document.removeEventListener('mouseup', handleMouseUp);
                 document.removeEventListener('touchend', handleTouchEnd);
                 document.removeEventListener('touchcancel', handleTouchCancel);
    -
                 if (resizeTimeoutRef.current) {
                     clearTimeout(resizeTimeoutRef.current);
                     resizeTimeoutRef.current = null;
    @@ -837,32 +778,39 @@ const EventLogger = ({
     
         useEffect(() => {
             const token = localStorage.getItem("authToken");
    -        if (token && subscribedEventTypes.length > 0) {
    -            const allSubscribedTypes = [
    -                ...subscribedEventTypes,
    -                ...eventTypes.filter(et => CONNECTION_EVENTS.includes(et))
    -            ];
    +        if (token) {
    +            const eventsToSubscribe = [...subscribedEventTypes];
     
    -            try {
    -                logger.log("Starting logger reception with event types:", allSubscribedTypes);
    -                startLoggerReception(token, allSubscribedTypes, objectName);
    -            } catch (error) {
    -                logger.warn("Failed to start logger reception:", error);
    +            const connectionEvents = eventTypes.filter(et => CONNECTION_EVENTS.includes(et));
    +            eventsToSubscribe.push(...connectionEvents);
    +
    +            const uniqueEvents = [...new Set(eventsToSubscribe)];
    +
    +            if (uniqueEvents.length > 0) {
    +                logger.log("Starting/updating logger reception for page:", {
    +                    pageKey,
    +                    subscribedEventTypes,
    +                    allEvents: uniqueEvents,
    +                    objectName
    +                });
    +
    +                try {
    +                    startLoggerReception(token, uniqueEvents, objectName);
    +                } catch (error) {
    +                    logger.warn("Failed to start logger reception:", error);
    +                }
    +            } else {
    +                logger.log("No events to subscribe to for this page");
    +                closeLoggerEventSource();
                 }
             }
    +
             return () => {
    -            try {
    -                logger.log("Closing logger event source");
    -                closeLoggerEventSource();
    -            } catch (error) {
    -                logger.warn("Failed to close logger event source:", error);
    -            }
             };
    -    }, [subscribedEventTypes, objectName, eventTypes]);
    +    }, [subscribedEventTypes, objectName, eventTypes, pageKey]);
     
         const EventTypeChip = ({eventType, searchTerm}) => {
             const color = getEventColor(eventType);
    -
             return (
                 
     
    -                    
    +                    
                             
                                  setPaused(!isPaused)}
    @@ -1160,7 +1108,9 @@ const EventLogger = ({
                     >
                         {filteredLogs.length === 0 ? (
                             
    -                            
    +                            
                                     {eventLogs.length === 0
                                         ? "No events logged"
                                         : "No events match current filters"}
    @@ -1171,7 +1121,6 @@ const EventLogger = ({
                                 {filteredLogs.map((log) => {
                                     const safeId = log.id ?? Math.random().toString(36).slice(2, 9);
                                     const isOpen = expandedLogIds.includes(safeId);
    -
                                     return (
                                         
                                             
    -
                                             {!isOpen && (
                                                 
                                                 
                                             )}
    -
                                             {isOpen && (
                                                 
                                     );
                                 })}
    -
                                 
    )} @@ -1283,7 +1229,7 @@ const EventLogger = ({ setManualSubscriptions={setManualSubscriptions} filteredEventTypes={filteredEventTypes} eventStats={eventStats} - ALL_EVENT_TYPES={ALL_EVENT_TYPES} + clearLogs={clearLogs} />