From a897312ce7936d67f5f768263912d3fbb2b22af9 Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Sat, 1 Mar 2025 14:03:01 +0000 Subject: [PATCH] v1.0.10 --- frontend/index.html | 14 +- frontend/public/favicon.svg | 40 ++ frontend/src/App.jsx | 432 ++++++++++++++++++--- frontend/src/components/NetworkDisplay.jsx | 125 ++++-- 4 files changed, 500 insertions(+), 111 deletions(-) create mode 100644 frontend/public/favicon.svg diff --git a/frontend/index.html b/frontend/index.html index d8bf89fd0..9ae85050a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,15 +2,13 @@ - + - - System Monitor - - + + Pulse + + +
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 000000000..b0d308278 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7401806c7..9a61b496c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,27 +1,128 @@ -import React from 'react'; -import { Container, Box, Typography, AppBar, Toolbar, Paper } from '@mui/material'; +import React, { useState, useEffect } from 'react'; +import { + Container, + Box, + Typography, + AppBar, + Toolbar, + Paper, + IconButton, + Tooltip, + alpha, + Select, + MenuItem, + FormControl, + InputLabel, + Chip, + ListItemIcon, + ListItemText, + Divider +} from '@mui/material'; +import Brightness4Icon from '@mui/icons-material/Brightness4'; +import Brightness7Icon from '@mui/icons-material/Brightness7'; +import ComputerIcon from '@mui/icons-material/Computer'; +import DnsIcon from '@mui/icons-material/Dns'; +import ViewListIcon from '@mui/icons-material/ViewList'; +import CheckIcon from '@mui/icons-material/Check'; import NetworkDisplay from './components/NetworkDisplay'; -import { AppThemeProvider } from './context/ThemeContext'; +import { AppThemeProvider, useThemeContext } from './context/ThemeContext'; +import useSocket from './hooks/useSocket'; -function App() { +function AppContent() { + const { darkMode, toggleDarkMode } = useThemeContext(); + const [selectedNode, setSelectedNode] = useState('all'); + const { nodeData, guestData } = useSocket(); + + // Transform the node data from the API into the format needed for the dropdown + const availableNodes = React.useMemo(() => { + // Start with the "All Nodes" option + const nodes = [ + { id: 'all', name: 'All Nodes', count: 0 } + ]; + + // Add nodes from the API + if (nodeData && nodeData.length > 0) { + // Count guests for each node + const nodeCounts = {}; + + // Initialize counts for each node + nodeData.forEach(node => { + // Extract node number from the id (e.g., "node-1" -> "node1") + const nodeId = node.id.replace('-', ''); + nodeCounts[nodeId] = 0; + }); + + // Count guests for each node + if (guestData && guestData.length > 0) { + guestData.forEach(guest => { + if (guest.node) { + // Convert "node-1" to "node1" format + const normalizedNodeId = guest.node.replace('-', ''); + if (nodeCounts[normalizedNodeId] !== undefined) { + nodeCounts[normalizedNodeId]++; + } + } + }); + } + + // Add nodes to the list with their counts + nodeData.forEach(node => { + // Extract node number from the id (e.g., "node-1" -> "node1") + const nodeId = node.id.replace('-', ''); + + // Add the node to the list + nodes.push({ + id: nodeId, + name: node.name, + count: nodeCounts[nodeId] || 0 + }); + }); + + // Update the count for "All Nodes" + nodes[0].count = guestData ? guestData.length : 0; + } + + return nodes; + }, [nodeData, guestData]); + + // Handle node selection change + const handleNodeChange = (event) => { + setSelectedNode(event.target.value); + }; + return ( - - - - - + + + + {/* Enhanced Logo - Made clickable to refresh page */} + { + window.location.reload(); + // Reset to default state if needed + setSelectedNode('all'); + }} sx={{ - flexGrow: 1, display: 'flex', alignItems: 'center', - fontWeight: 600, - letterSpacing: '0.02em', + cursor: 'pointer', + '&:hover': { + opacity: 0.9, + }, }} > + {/* Pulse Animation Rings */} + - System Monitor - - - - - - - {/* Network Display Component */} - - - - - - - System Monitor © {new Date().getFullYear()} + + Pulse + { + e.stopPropagation(); // Prevent triggering the parent onClick (page refresh) + window.open('https://github.com/rcourtman/pulse', '_blank'); + }} + sx={{ + ml: 1, + opacity: 0.8, + fontWeight: 400, + fontSize: '0.7rem', + bgcolor: 'rgba(255,255,255,0.15)', + px: 0.8, + py: 0.2, + borderRadius: 1, + letterSpacing: '0.02em', + cursor: 'pointer', + '&:hover': { + opacity: 1, + bgcolor: 'rgba(255,255,255,0.25)', + }, + transition: 'all 0.2s ease' + }} + > + v1.0.10 + + + - - + + {/* Node Selection Dropdown */} + + + + + {/* Dark Mode Toggle Button */} + + + {darkMode ? : } + + + + + + + + {/* Network Display Component - pass the selected node as a prop */} + + + + + + + Pulse © {new Date().getFullYear()} + + + + ); +} + +function App() { + return ( + + ); } diff --git a/frontend/src/components/NetworkDisplay.jsx b/frontend/src/components/NetworkDisplay.jsx index fe003411a..38fb0323c 100644 --- a/frontend/src/components/NetworkDisplay.jsx +++ b/frontend/src/components/NetworkDisplay.jsx @@ -43,6 +43,7 @@ import PersonIcon from '@mui/icons-material/Person'; import Brightness4Icon from '@mui/icons-material/Brightness4'; import Brightness7Icon from '@mui/icons-material/Brightness7'; import InputBase from '@mui/material/InputBase'; +import DnsIcon from '@mui/icons-material/Dns'; // Define pulse animation const pulseAnimation = keyframes` @@ -242,13 +243,14 @@ const KeyboardShortcut = ({ shortcut, sx = {} }) => ( ); -const NetworkDisplay = () => { +const NetworkDisplay = ({ selectedNode = 'all' }) => { const { isConnected, guestData, metricsData, error, - isDebugMode + isDebugMode, + nodeData } = useSocket(); const theme = useTheme(); @@ -311,11 +313,11 @@ const NetworkDisplay = () => { try { const saved = localStorage.getItem(STORAGE_KEY_FILTERS); return saved ? JSON.parse(saved) : { - cpu: 0, - memory: 0, - disk: 0, - download: 0, - upload: 0 + cpu: 0, + memory: 0, + disk: 0, + download: 0, + upload: 0 }; } catch (e) { console.error('Error loading filter preferences:', e); @@ -329,6 +331,30 @@ const NetworkDisplay = () => { } }); + // Filter guests based on selected node + const getNodeFilteredGuests = useCallback((guests) => { + if (selectedNode === 'all') { + return guests; + } + + // Filter guests based on the node property from the API + return guests.filter(guest => { + // Extract the node ID from the guest's node property + // The node property from the API is in the format "node-1", "node-2", etc. + // The selectedNode from the dropdown is in the format "node1", "node2", etc. + // We need to convert between these formats + const nodeIdFromApi = guest.node; + + // If the node property doesn't exist, include the guest in all nodes + if (!nodeIdFromApi) return true; + + // Convert "node-1" to "node1" format + const normalizedNodeId = nodeIdFromApi.replace('-', ''); + + return normalizedNodeId === selectedNode; + }); + }, [selectedNode]); + const [activeSlider, setActiveSlider] = useState(null); // Save sort preferences whenever they change @@ -573,9 +599,12 @@ const NetworkDisplay = () => { // Sort and filter data before displaying const getSortedAndFilteredData = (data) => { - const sortableData = [...data]; + // First filter by selected node + const nodeFilteredData = getNodeFilteredGuests(data); - // First filter the data + const sortableData = [...nodeFilteredData]; + + // Then filter the data const filteredData = sortableData.filter(guest => { // If not showing stopped and guest is not running, filter it out if (!showStopped && guest.status !== 'running') { @@ -713,7 +742,7 @@ const NetworkDisplay = () => { // Memoize getSortedAndFilteredData to optimize performance const sortedAndFilteredData = useMemo( () => getSortedAndFilteredData(guestData), - [guestData, sortConfig, filters, showStopped, searchTerm, activeSearchTerms, displayMetrics] + [guestData, sortConfig, filters, showStopped, searchTerm, activeSearchTerms, displayMetrics, selectedNode, getNodeFilteredGuests] ); // Add keyboard shortcut handler for 'F' key to toggle filters @@ -796,7 +825,9 @@ const NetworkDisplay = () => { mb: { xs: 1.5, sm: 2 }, gap: { xs: 1.5, md: 0 } }}> - {/* Search box */} + {/* Dark Mode Toggle removed - now in App header */} + + {/* Search box - moved to the left */} { '&:focus-within': { borderColor: theme => theme.palette.primary.main, boxShadow: theme => `0 0 0 2px ${alpha(theme.palette.primary.main, 0.2)}` - } + }, + mr: { md: 2 } // Add margin to the right on medium+ screens }}> { + + {/* Node indicator */} + {selectedNode !== 'all' && ( + } + label={(() => { + // Find the actual node name from nodeData + if (nodeData && nodeData.length > 0) { + // Convert selectedNode format (e.g., "node1") to API format (e.g., "node-1") + const nodeIdForApi = selectedNode.replace(/^node(\d+)$/, "node-$1"); + const node = nodeData.find(n => n.id === nodeIdForApi); + if (node) { + return `Node: ${node.name}`; + } + } + // Fallback to the node ID if we can't find the name + return `Node: ${selectedNode}`; + })()} + size="small" + color="primary" + variant="outlined" + sx={{ + height: 28, + mr: 2, + fontWeight: 500, + borderRadius: 1.5, + '& .MuiChip-icon': { + color: 'primary.main' + } + }} + /> + )} @@ -884,32 +948,9 @@ const NetworkDisplay = () => { gap: { xs: 1, sm: 1.5 }, ml: { xs: 0, md: 'auto' }, width: { xs: '100%', md: 'auto' }, - justifyContent: { xs: 'space-between', md: 'flex-start' } + justifyContent: { xs: 'space-between', md: 'flex-start' }, + pr: { md: 5 } // Add right padding to avoid overlap with dark mode toggle }}> - {/* Dark Mode Toggle */} - - - {darkMode ? : } - - - {/* Filter controls - updated for better mobile experience */} { val > 0) ? 'primary.main' : 'text.secondary'} + color={Object.values(filters).some(val => val > 0) || selectedNode !== 'all' ? 'primary.main' : 'text.secondary'} sx={{ fontWeight: 500 }} aria-live="polite" // Announce when this changes > - {Object.values(filters).some(val => val > 0) ? - `Showing ${sortedAndFilteredData.length} of ${guestData.length} systems` : + {Object.values(filters).some(val => val > 0) || selectedNode !== 'all' ? + `Showing ${sortedAndFilteredData.length} of ${getNodeFilteredGuests(guestData).length} systems${selectedNode !== 'all' ? ` on ${selectedNode === 'node1' ? 'Production' : selectedNode === 'node2' ? 'Development' : 'Testing'}` : ''}` : ''} { onClick={resetFilters} variant={Object.values(filters).some(val => val > 0) ? "filled" : "outlined"} size="small" - color="primary" - sx={{ + color="primary" + sx={{ height: 28, transition: 'all 0.2s ease', fontWeight: Object.values(filters).some(val => val > 0) ? 600 : 400,