diff --git a/frontend/src/components/NetworkDisplay.jsx b/frontend/src/components/NetworkDisplay.jsx index 6aec4db0d..fd1b2b1c7 100644 --- a/frontend/src/components/NetworkDisplay.jsx +++ b/frontend/src/components/NetworkDisplay.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, useMemo } from 'react'; import useSocket from '../hooks/useSocket'; import { Box, @@ -204,6 +204,32 @@ const StatusIndicator = ({ status }) => { ); }; +// Add keyboard shortcut tooltip component +const KeyboardShortcut = ({ shortcut, sx = {} }) => ( + + {shortcut} + +); + const NetworkDisplay = () => { const { isConnected, @@ -597,6 +623,28 @@ const NetworkDisplay = () => { const downloadColumnRef = React.useRef(null); const uploadColumnRef = React.useRef(null); + // Memoize getSortedAndFilteredData to optimize performance + const sortedAndFilteredData = useMemo( + () => getSortedAndFilteredData(guestData), + [guestData, sortConfig, filters, showStopped, searchTerm, activeSearchTerms, displayMetrics] + ); + + // Add keyboard shortcut handler for 'F' key to toggle filters + useEffect(() => { + const handleKeyDown = (e) => { + // If 'F' is pressed and no input/textarea is focused, toggle filters + if (e.key.toLowerCase() === 'f' && + !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName) && + !document.activeElement.isContentEditable) { + e.preventDefault(); + setShowFilters(prev => !prev); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, []); + if (error) { return ( { )} + + + + @@ -739,7 +798,14 @@ const NetworkDisplay = () => { }}> {/* Filter controls */} - + + {showFilters ? "Hide filters" : "Show filters"} + + Press F to toggle + + + }> setShowFilters(!showFilters)} sx={{ @@ -770,7 +836,7 @@ const NetworkDisplay = () => { {(Object.values(filters).some(val => val > 0) || searchTerm) ? ( - {`${getSortedAndFilteredData(guestData).length}/${guestData.length}`} + {`${sortedAndFilteredData.length}/${guestData.length}`} ) : ( @@ -931,7 +997,11 @@ const NetworkDisplay = () => { )} {/* Filter Panel that shows when filters are active */} - + { - + + val > 0) ? 'primary.main' : 'text.secondary' + } sx={{ fontWeight: 500 }}> + {Object.values(filters).some(val => val > 0) ? + `Showing ${sortedAndFilteredData.length} of ${guestData.length} systems` : + ''} + val > 0) ? "filled" : "outlined"} size="small" color="primary" - sx={{ height: 28 }} + sx={{ + height: 28, + transition: 'all 0.2s ease', + fontWeight: Object.values(filters).some(val => val > 0) ? 600 : 400 + }} /> @@ -1165,7 +1246,10 @@ const NetworkDisplay = () => { > alpha(theme.palette.grey[50], 0.5), + backgroundColor: theme => alpha(theme.palette.grey[50], 0.8), + }, + '& tbody tr:nth-of-type(even)': { + backgroundColor: theme => alpha(theme.palette.background.paper, 1), }, '& tbody tr': { transition: 'background-color 0.15s ease-in-out', @@ -1206,7 +1290,19 @@ const NetworkDisplay = () => { Guest Name {sortConfig.key === 'name' && ( - + alpha(theme.palette.primary.main, 0.1), + color: 'primary.main', + fontSize: '0.8rem', + fontWeight: 'bold' + }}> {sortConfig.direction === 'asc' ? '↑' : '↓'} )} @@ -1223,6 +1319,9 @@ const NetworkDisplay = () => { minHeight: '48px', position: 'relative', cursor: 'pointer', + borderBottom: theme => filters.cpu > 0 ? + `2px solid ${theme.palette.primary.main}` : undefined, + color: theme => filters.cpu > 0 ? 'primary.main' : 'inherit', '&:hover': { backgroundColor: theme => alpha(theme.palette.primary.main, 0.08) } @@ -1232,7 +1331,19 @@ const NetworkDisplay = () => { CPU {sortConfig.key === 'cpu' && ( - + alpha(theme.palette.primary.main, 0.1), + color: 'primary.main', + fontSize: '0.8rem', + fontWeight: 'bold' + }}> {sortConfig.direction === 'asc' ? '↑' : '↓'} )} @@ -1249,6 +1360,9 @@ const NetworkDisplay = () => { minHeight: '48px', position: 'relative', cursor: 'pointer', + borderBottom: theme => filters.memory > 0 ? + `2px solid ${theme.palette.primary.main}` : undefined, + color: theme => filters.memory > 0 ? 'primary.main' : 'inherit', '&:hover': { backgroundColor: theme => alpha(theme.palette.primary.main, 0.08) } @@ -1258,7 +1372,19 @@ const NetworkDisplay = () => { Memory {sortConfig.key === 'memory' && ( - + alpha(theme.palette.primary.main, 0.1), + color: 'primary.main', + fontSize: '0.8rem', + fontWeight: 'bold' + }}> {sortConfig.direction === 'asc' ? '↑' : '↓'} )} @@ -1275,6 +1401,9 @@ const NetworkDisplay = () => { minHeight: '48px', position: 'relative', cursor: 'pointer', + borderBottom: theme => filters.disk > 0 ? + `2px solid ${theme.palette.primary.main}` : undefined, + color: theme => filters.disk > 0 ? 'primary.main' : 'inherit', '&:hover': { backgroundColor: theme => alpha(theme.palette.primary.main, 0.08) } @@ -1284,7 +1413,19 @@ const NetworkDisplay = () => { Disk {sortConfig.key === 'disk' && ( - + alpha(theme.palette.primary.main, 0.1), + color: 'primary.main', + fontSize: '0.8rem', + fontWeight: 'bold' + }}> {sortConfig.direction === 'asc' ? '↑' : '↓'} )} @@ -1301,6 +1442,9 @@ const NetworkDisplay = () => { minHeight: '48px', position: 'relative', cursor: 'pointer', + borderBottom: theme => filters.download > 0 ? + `2px solid ${theme.palette.primary.main}` : undefined, + color: theme => filters.download > 0 ? 'primary.main' : 'inherit', '&:hover': { backgroundColor: theme => alpha(theme.palette.primary.main, 0.08) } @@ -1310,7 +1454,19 @@ const NetworkDisplay = () => { Download {sortConfig.key === 'download' && ( - + alpha(theme.palette.primary.main, 0.1), + color: 'primary.main', + fontSize: '0.8rem', + fontWeight: 'bold' + }}> {sortConfig.direction === 'asc' ? '↑' : '↓'} )} @@ -1327,6 +1483,9 @@ const NetworkDisplay = () => { minHeight: '48px', position: 'relative', cursor: 'pointer', + borderBottom: theme => filters.upload > 0 ? + `2px solid ${theme.palette.secondary.main}` : undefined, + color: theme => filters.upload > 0 ? 'secondary.main' : 'inherit', '&:hover': { backgroundColor: theme => alpha(theme.palette.primary.main, 0.08) } @@ -1336,7 +1495,19 @@ const NetworkDisplay = () => { Upload {sortConfig.key === 'upload' && ( - + alpha(theme.palette.secondary.main, 0.1), + color: 'secondary.main', + fontSize: '0.8rem', + fontWeight: 'bold' + }}> {sortConfig.direction === 'asc' ? '↑' : '↓'} )} @@ -1346,7 +1517,7 @@ const NetworkDisplay = () => { {guestData.length > 0 ? ( - getSortedAndFilteredData(guestData) + sortedAndFilteredData .map((guest) => { const metrics = getMetricsForGuest(guest.id); const networkMetrics = metrics?.metrics?.network; @@ -1374,6 +1545,42 @@ const NetworkDisplay = () => { opacity: isRunning ? 1 : 0.8, '& > td': { py: 1.5 }, transition: 'all 0.2s ease-in-out', + // Highlight rows that match active filters with subtle indicators + ...(filters.cpu > 0 && cpuUsage > filters.cpu && { + '& td:nth-of-type(2)': { + borderLeft: '2px solid', + borderLeftColor: theme => alpha(theme.palette.primary.main, 0.4), + pl: 1.5 // Add some padding to account for the border + } + }), + ...(filters.memory > 0 && memoryUsage > filters.memory && { + '& td:nth-of-type(3)': { + borderLeft: '2px solid', + borderLeftColor: theme => alpha(theme.palette.primary.main, 0.4), + pl: 1.5 + } + }), + ...(filters.disk > 0 && diskUsage > filters.disk && { + '& td:nth-of-type(4)': { + borderLeft: '2px solid', + borderLeftColor: theme => alpha(theme.palette.primary.main, 0.4), + pl: 1.5 + } + }), + ...(filters.download > 0 && networkMetrics?.inRate >= sliderValueToNetworkRate(filters.download) && { + '& td:nth-of-type(5)': { + borderLeft: '2px solid', + borderLeftColor: theme => alpha(theme.palette.primary.main, 0.4), + pl: 1.5 + } + }), + ...(filters.upload > 0 && networkMetrics?.outRate >= sliderValueToNetworkRate(filters.upload) && { + '& td:nth-of-type(6)': { + borderLeft: '2px solid', + borderLeftColor: theme => alpha(theme.palette.secondary.main, 0.4), + pl: 1.5 + } + }) }}> @@ -1461,7 +1668,7 @@ const NetworkDisplay = () => { )} - {guestData.length > 0 && getSortedAndFilteredData(guestData).length === 0 && ( + {guestData.length > 0 && sortedAndFilteredData.length === 0 && ( @@ -1470,14 +1677,23 @@ const NetworkDisplay = () => { No Matching Systems - No systems match the current filters + Try adjusting your filters or search terms - + + + setShowStopped(true)} + sx={{ mt: 1, display: !showStopped ? 'flex' : 'none' }} + /> +