diff --git a/frontend/src/components/network/NetworkDisplay.jsx b/frontend/src/components/network/NetworkDisplay.jsx
index 27db1721b..e99f0a893 100644
--- a/frontend/src/components/network/NetworkDisplay.jsx
+++ b/frontend/src/components/network/NetworkDisplay.jsx
@@ -1,69 +1,30 @@
-import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
+import React, { useEffect, useMemo } from 'react';
import useSocket from '../../hooks/useSocket';
import useFormattedMetrics from '../../hooks/useFormattedMetrics';
import useMockMetrics from '../../hooks/useMockMetrics';
import { useThemeContext } from '../../context/ThemeContext';
-import {
- Box,
- Card,
- CardContent,
- Typography,
- CircularProgress,
- Table,
- TableContainer,
- Paper,
- IconButton,
- Badge,
- ClickAwayListener,
- useTheme,
- Snackbar,
- Alert,
- Popover,
- InputBase,
- Chip,
- Button,
- ToggleButtonGroup,
- ToggleButton,
- MenuItem,
- Switch,
- Slider,
- TextField,
- Autocomplete
-} from '@mui/material';
-import FilterAltIcon from '@mui/icons-material/FilterAlt';
-import FilterAltOffIcon from '@mui/icons-material/FilterAltOff';
-import ViewColumnIcon from '@mui/icons-material/ViewColumn';
-import SearchIcon from '@mui/icons-material/Search';
-import ComputerIcon from '@mui/icons-material/Computer';
-import DnsIcon from '@mui/icons-material/Dns';
-import ViewListIcon from '@mui/icons-material/ViewList';
-import VisibilityIcon from '@mui/icons-material/Visibility';
-import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
-import TuneIcon from '@mui/icons-material/Tune';
-import ClearIcon from '@mui/icons-material/Clear';
+import { Box, CircularProgress, useTheme } from '@mui/material';
-// Import constants
+// Import hooks
import {
- STORAGE_KEY_FILTERS,
- STORAGE_KEY_SORT,
- STORAGE_KEY_SHOW_STOPPED,
- STORAGE_KEY_SHOW_FILTERS,
- STORAGE_KEY_SEARCH_TERMS,
- STORAGE_KEY_COLUMN_VISIBILITY,
- STORAGE_KEY_GUEST_TYPE_FILTER,
- DEFAULT_COLUMN_CONFIG,
- STORAGE_KEY_COLUMN_ORDER,
- STORAGE_KEY_COLUMN_DRAG_ENABLED
-} from '../../constants/networkConstants';
+ useNetworkFilters,
+ useColumnManagement,
+ usePopoverManagement,
+ useNotifications,
+ useSortManagement,
+ useKeyboardShortcuts,
+ useActiveFilteredColumns,
+ useDataProcessing
+} from './hooks';
// Import components
import ConnectionErrorDisplay from './ConnectionErrorDisplay';
-import NetworkFilters from './NetworkFilters';
-import NetworkTableHeader from './NetworkTableHeader';
-import NetworkTableBody from './NetworkTableBody';
-
-// Import utilities
-import { getSortedAndFilteredData, getNodeFilteredGuests as nodeFilteredGuestsUtil, getNodeName as getNodeNameUtil, extractNumericId as extractNumericIdUtil } from '../../utils/networkUtils';
+import {
+ NetworkHeader,
+ NetworkPopovers,
+ NetworkNotification,
+ NetworkTable
+} from './components';
const NetworkDisplay = ({ selectedNode = 'all' }) => {
const {
@@ -110,689 +71,124 @@ const NetworkDisplay = ({ selectedNode = 'all' }) => {
const theme = useTheme();
const { darkMode } = useThemeContext();
- // State for filter menu
- const [filterAnchorEl, setFilterAnchorEl] = useState(null);
- const openFilters = Boolean(filterAnchorEl);
+ // Use notification hook
+ const {
+ snackbarOpen,
+ snackbarMessage,
+ snackbarSeverity,
+ handleSnackbarClose,
+ showNotification
+ } = useNotifications();
- // State for search popover
- const [searchAnchorEl, setSearchAnchorEl] = useState(null);
- const openSearch = Boolean(searchAnchorEl);
- const [searchTerm, setSearchTerm] = useState('');
- const [activeSearchTerms, setActiveSearchTerms] = useState(() => {
- try {
- const saved = localStorage.getItem(STORAGE_KEY_SEARCH_TERMS);
- return saved ? JSON.parse(saved) : [];
- } catch (e) {
- console.error('Error loading active search terms:', e);
- return [];
- }
- });
- const searchInputRef = useRef(null);
+ // Use column management hook
+ const {
+ columnVisibility,
+ columnOrder,
+ setColumnOrder,
+ columnMenuAnchorEl,
+ openColumnMenu,
+ forceUpdateCounter,
+ toggleColumnVisibility,
+ resetColumnVisibility,
+ handleColumnMenuOpen,
+ handleColumnMenuClose
+ } = useColumnManagement(showNotification);
- // State for type popover
- const [typeAnchorEl, setTypeAnchorEl] = useState(null);
- const openType = Boolean(typeAnchorEl);
+ // Use sort management hook
+ const {
+ sortConfig,
+ requestSort
+ } = useSortManagement();
- // State for visibility popover
- const [visibilityAnchorEl, setVisibilityAnchorEl] = useState(null);
- const openVisibility = Boolean(visibilityAnchorEl);
-
- // State for tracking which slider is being dragged
- const [sliderDragging, setSliderDragging] = useState(null);
-
- // Helper function to extract numeric ID from strings like "node-1-ct-105"
- const extractNumericId = useCallback((fullId) => {
- return extractNumericIdUtil(fullId);
- }, []);
-
- // Helper function to get the node name from the node ID
- const getNodeName = useCallback((nodeId) => {
- return getNodeNameUtil(nodeId, nodeData);
- }, [nodeData]);
-
- // Sort state
- const [sortConfig, setSortConfig] = useState(() => {
- try {
- const saved = localStorage.getItem(STORAGE_KEY_SORT);
- return saved ? JSON.parse(saved) : { key: 'id', direction: 'asc' };
- } catch (e) {
- console.error('Error loading sort preferences:', e);
- return { key: 'id', direction: 'asc' };
- }
- });
-
- // Filter states
- const [filters, setFilters] = useState(() => {
- try {
- const saved = localStorage.getItem(STORAGE_KEY_FILTERS);
- return saved ? JSON.parse(saved) : {
- cpu: 0,
- memory: 0,
- disk: 0,
- download: 0,
- upload: 0
- };
- } catch (e) {
- console.error('Error loading filter preferences:', e);
- return {
- cpu: 0,
- memory: 0,
- disk: 0,
- download: 0,
- upload: 0
- };
- }
- });
-
- // Column visibility state
- const [columnVisibility, setColumnVisibility] = useState(() => {
- try {
- console.log('Initializing column visibility state');
- const saved = localStorage.getItem(STORAGE_KEY_COLUMN_VISIBILITY);
- if (saved) {
- console.log('Found saved column visibility:', saved);
- const parsed = JSON.parse(saved);
- // Ensure all columns from DEFAULT_COLUMN_CONFIG exist in the saved config
- const merged = { ...DEFAULT_COLUMN_CONFIG };
- Object.keys(parsed).forEach(key => {
- if (merged[key]) {
- merged[key].visible = parsed[key].visible;
- }
- });
-
- // Ensure at least one column is visible
- const hasVisibleColumn = Object.values(merged).some(col => col.visible);
- if (!hasVisibleColumn) {
- console.warn('No visible columns in saved config, resetting to defaults');
- return JSON.parse(JSON.stringify(DEFAULT_COLUMN_CONFIG));
- }
-
- console.log('Using merged column visibility:', merged);
- return merged;
- }
- console.log('No saved column visibility, using defaults');
- return JSON.parse(JSON.stringify(DEFAULT_COLUMN_CONFIG));
- } catch (e) {
- console.error('Error loading column visibility preferences:', e);
- return JSON.parse(JSON.stringify(DEFAULT_COLUMN_CONFIG));
- }
- });
-
- // Column order state
- const [columnOrder, setColumnOrder] = useState(() => {
- try {
- console.log('Initializing column order state');
- const defaultOrder = Object.keys(DEFAULT_COLUMN_CONFIG);
-
- const saved = localStorage.getItem(STORAGE_KEY_COLUMN_ORDER);
- if (saved) {
- console.log('Found saved column order:', saved);
- try {
- const parsed = JSON.parse(saved);
-
- // Validate the saved order - it should contain all columns from DEFAULT_COLUMN_CONFIG
- const isValid =
- Array.isArray(parsed) &&
- parsed.length === defaultOrder.length &&
- defaultOrder.every(col => parsed.includes(col));
-
- if (isValid) {
- console.log('Using saved column order:', parsed);
- return parsed;
- } else {
- console.warn('Invalid saved column order, using default order');
- }
- } catch (parseError) {
- console.error('Error parsing saved column order:', parseError);
- }
- }
-
- console.log('Using default column order:', defaultOrder);
- return defaultOrder;
- } catch (e) {
- console.error('Error loading column order preferences:', e);
- const fallbackOrder = Object.keys(DEFAULT_COLUMN_CONFIG || {});
- console.log('Using fallback column order:', fallbackOrder);
- return fallbackOrder;
- }
- });
-
- // Column settings menu state
- const [columnMenuAnchorEl, setColumnMenuAnchorEl] = useState(null);
- const openColumnMenu = Boolean(columnMenuAnchorEl);
-
- // Snackbar state for notifications
- const [snackbarOpen, setSnackbarOpen] = useState(false);
- const [snackbarMessage, setSnackbarMessage] = useState('');
- const [snackbarSeverity, setSnackbarSeverity] = useState('info');
-
- // Handle snackbar close
- const handleSnackbarClose = (event, reason) => {
- if (reason === 'clickaway') {
- return;
- }
- setSnackbarOpen(false);
- };
-
- // Show a notification
- const showNotification = (message, severity = 'info') => {
- setSnackbarMessage(message);
- setSnackbarSeverity(severity);
- setSnackbarOpen(true);
- };
-
- // Toggle column visibility
- const toggleColumnVisibility = (columnId) => {
- try {
- setColumnVisibility(prev => {
- if (!prev || !prev[columnId]) {
- console.error('Invalid column configuration:', prev);
- return prev;
- }
-
- // Check if this is the last visible column and we're trying to hide it
- const isLastVisibleColumn =
- Object.values(prev).filter(col => col.visible).length === 1 &&
- prev[columnId].visible;
-
- // If this is the last visible column and we're trying to hide it, don't allow it
- if (isLastVisibleColumn) {
- // Show a console warning
- console.warn('Cannot hide the last visible column');
- // Show a notification to the user
- setTimeout(() => {
- showNotification('At least one column must remain visible', 'warning');
- }, 0);
- // Return the previous state unchanged
- return prev;
- }
-
- const updated = {
- ...prev,
- [columnId]: {
- ...prev[columnId],
- visible: !prev[columnId].visible
- }
- };
- return updated;
- });
- } catch (error) {
- console.error('Error toggling column visibility:', error);
- }
- };
-
- // State for forcing re-renders
- const [forceUpdateCounter, setForceUpdateCounter] = useState(0);
-
- // Function to force a re-render
- const forceUpdate = () => {
- setForceUpdateCounter(prev => prev + 1);
- };
-
- // Reset column visibility to defaults
- const resetColumnVisibility = () => {
- console.log('Resetting column visibility to defaults');
-
- // Create a new configuration with all columns set to visible
- const allVisibleConfig = {};
- if (DEFAULT_COLUMN_CONFIG) {
- Object.keys(DEFAULT_COLUMN_CONFIG).forEach(key => {
- allVisibleConfig[key] = {
- ...DEFAULT_COLUMN_CONFIG[key],
- visible: true // Force all columns to be visible
- };
- });
- }
-
- console.log('New config with all columns visible:', allVisibleConfig);
-
- // Clear the localStorage entries to ensure a clean state
- try {
- localStorage.removeItem(STORAGE_KEY_COLUMN_VISIBILITY);
- localStorage.removeItem(STORAGE_KEY_COLUMN_ORDER);
- console.log('Cleared column preferences from localStorage');
- } catch (error) {
- console.error('Error clearing column preferences from localStorage:', error);
- }
-
- // Set the state with the new config where all columns are visible
- setColumnVisibility(allVisibleConfig);
-
- // Reset column order to default
- if (DEFAULT_COLUMN_CONFIG) {
- setColumnOrder(Object.keys(DEFAULT_COLUMN_CONFIG));
- }
-
- // Force a re-render of the table
- setTimeout(() => {
- forceUpdate();
- console.log('Forced table re-render');
- }, 50);
-
- // Show a notification to confirm the action
- showNotification('All columns are now visible', 'success');
-
- // Close the column menu if it's open
- if (openColumnMenu) {
- handleColumnMenuClose();
- }
- };
-
- // Handle column menu open
- const handleColumnMenuOpen = (event) => {
- setColumnMenuAnchorEl(event.currentTarget);
- };
-
- // Handle column menu close
- const handleColumnMenuClose = () => {
- setColumnMenuAnchorEl(null);
- };
-
- // Save column visibility preferences whenever they change
- useEffect(() => {
- try {
- console.log('Column visibility changed, saving to localStorage');
-
- // Check if columnVisibility is valid
- if (!columnVisibility || Object.keys(columnVisibility).length === 0) {
- console.error('Invalid column visibility state, resetting to defaults');
- setColumnVisibility(JSON.parse(JSON.stringify(DEFAULT_COLUMN_CONFIG)));
- return;
- }
-
- // Ensure at least one column is visible
- const hasVisibleColumn = Object.values(columnVisibility).some(col => col.visible);
- if (!hasVisibleColumn) {
- console.error('No visible columns in state, not saving to localStorage');
- return;
- }
-
- // Save to localStorage
- localStorage.setItem(STORAGE_KEY_COLUMN_VISIBILITY, JSON.stringify(columnVisibility));
- console.log('Saved column visibility to localStorage');
- } catch (error) {
- console.error('Error saving column visibility:', error);
- }
- }, [columnVisibility]);
-
- // Save column order preferences whenever they change
- useEffect(() => {
- try {
- console.log('Column order changed, saving to localStorage');
-
- // Check if columnOrder is valid
- if (!columnOrder || !Array.isArray(columnOrder) || columnOrder.length === 0) {
- console.error('Invalid column order state, resetting to defaults');
- setColumnOrder(Object.keys(DEFAULT_COLUMN_CONFIG));
- return;
- }
-
- // Save to localStorage
- localStorage.setItem(STORAGE_KEY_COLUMN_ORDER, JSON.stringify(columnOrder));
- console.log('Saved column order to localStorage');
- } catch (error) {
- console.error('Error saving column order:', error);
- }
- }, [columnOrder]);
-
- // UI state
- const [showStopped, setShowStopped] = useState(() => {
- try {
- const saved = localStorage.getItem(STORAGE_KEY_SHOW_STOPPED);
- return saved ? JSON.parse(saved) === true : false; // Default: false (show only running systems)
- } catch (e) {
- console.error('Error loading show stopped preference:', e);
- return false; // Default to showing only running systems
- }
- });
-
- const [showFilters, setShowFilters] = useState(() => {
- try {
- const saved = localStorage.getItem(STORAGE_KEY_SHOW_FILTERS);
- return saved ? JSON.parse(saved) === true : false;
- } catch (e) {
- console.error('Error loading show filters preference:', e);
- return false;
- }
- });
-
- const [escRecentlyPressed, setEscRecentlyPressed] = useState(false);
-
- // Add a ref for the filter button
- const filterButtonRef = useRef(null);
-
- // Add a ref for the search button
- const searchButtonRef = useRef(null);
-
- // Filter popover handlers
- const handleFilterButtonClick = (event) => {
- setFilterAnchorEl(event.currentTarget);
- };
-
- const handleCloseFilterPopover = () => {
- setFilterAnchorEl(null);
- };
-
- // Search popover handlers
- const handleSearchButtonClick = (event) => {
- setSearchAnchorEl(event.currentTarget);
- // Focus the search input after a short delay to ensure the popover is open
- setTimeout(() => {
- if (searchInputRef.current) {
- searchInputRef.current.focus();
- }
- }, 100);
- };
-
- const handleCloseSearchPopover = () => {
- setSearchAnchorEl(null);
- };
-
- // System type popover handlers
- const handleTypeButtonClick = (event) => {
- setTypeAnchorEl(event.currentTarget);
- };
-
- const handleCloseTypePopover = () => {
- setTypeAnchorEl(null);
- };
-
- // Visibility popover handlers
- const handleVisibilityButtonClick = (event) => {
- setVisibilityAnchorEl(event.currentTarget);
- };
-
- const handleCloseVisibilityPopover = () => {
- setVisibilityAnchorEl(null);
- };
-
- // Count active filters
- const activeFilterCount = useMemo(() => {
- return activeSearchTerms.length +
- (searchTerm && !activeSearchTerms.includes(searchTerm) ? 1 : 0) +
- Object.values(filters).filter(val => val > 0).length;
- }, [activeSearchTerms, searchTerm, filters]);
-
- // Add guest type filter state - load from localStorage or use default
- const [guestTypeFilter, setGuestTypeFilter] = useState(() => {
- try {
- const saved = localStorage.getItem(STORAGE_KEY_GUEST_TYPE_FILTER);
- return saved ? JSON.parse(saved) : 'all'; // Default: 'all' (show both VMs and LXCs)
- } catch (e) {
- console.error('Error loading guest type filter preference:', e);
- return 'all'; // Default to showing all guest types
- }
- });
-
- // Filter guests based on selected node
- const getNodeFilteredGuests = useCallback((guests) => {
- return nodeFilteredGuestsUtil(guests, selectedNode);
- }, [selectedNode]);
-
- // Save guest type filter preference whenever it changes
- useEffect(() => {
- try {
- localStorage.setItem(STORAGE_KEY_GUEST_TYPE_FILTER, JSON.stringify(guestTypeFilter));
- } catch (e) {
- console.error('Error saving guest type filter preference:', e);
- }
- }, [guestTypeFilter]);
-
- // Save sort preferences whenever they change
- useEffect(() => {
- localStorage.setItem(STORAGE_KEY_SORT, JSON.stringify(sortConfig));
- }, [sortConfig]);
-
- // Save show stopped preference whenever it changes
- useEffect(() => {
- localStorage.setItem(STORAGE_KEY_SHOW_STOPPED, JSON.stringify(showStopped));
- }, [showStopped]);
-
- // Save show filters preference whenever it changes
- useEffect(() => {
- localStorage.setItem(STORAGE_KEY_SHOW_FILTERS, JSON.stringify(showFilters));
- }, [showFilters]);
-
- // Save active search terms whenever they change
- useEffect(() => {
- localStorage.setItem(STORAGE_KEY_SEARCH_TERMS, JSON.stringify(activeSearchTerms));
- }, [activeSearchTerms]);
-
- // Save filter preferences whenever they change
- useEffect(() => {
- localStorage.setItem(STORAGE_KEY_FILTERS, JSON.stringify(filters));
- }, [filters]);
-
- // Function to reset all filters - wrap in useCallback to prevent infinite renders
- const resetFilters = useCallback(() => {
- setFilters({
- cpu: 0,
- memory: 0,
- disk: 0,
- download: 0,
- upload: 0
- });
- setActiveSearchTerms([]);
- setSearchTerm('');
- setShowStopped(false);
- setGuestTypeFilter('all');
- }, []);
-
- // Set up keyboard shortcuts
- useEffect(() => {
- const handleGlobalKeyDown = (e) => {
- // Immediately return if search popover is open
- if (openSearch) {
- return;
- }
-
- // Don't trigger shortcuts if typing in an input field
- if (
- e.target.tagName === 'INPUT' ||
- e.target.tagName === 'TEXTAREA' ||
- e.target.isContentEditable
- ) {
- return;
- }
-
- // Escape key to close filters or clear search
- if (e.key === 'Escape') {
- // Close any open popovers
- if (openFilters) {
- setFilterAnchorEl(null);
- }
- if (openSearch) {
- setSearchAnchorEl(null);
- }
- if (openType) {
- setTypeAnchorEl(null);
- }
- if (openVisibility) {
- setVisibilityAnchorEl(null);
- }
- if (openColumnMenu) {
- setColumnMenuAnchorEl(null);
- }
-
- // Reset all filters
- resetFilters();
-
- // Set flag to prevent other shortcuts from triggering
- setEscRecentlyPressed(true);
- setTimeout(() => setEscRecentlyPressed(false), 300);
-
- // Show notification
- showNotification('All filters have been cleared', 'info');
- return;
- }
-
- // Ctrl+F or Cmd+F to focus search
- if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
- e.preventDefault();
- handleSearchButtonClick({ currentTarget: searchButtonRef.current });
- return;
- }
-
- // / to focus search
- if (e.key === '/' && !escRecentlyPressed) {
- e.preventDefault();
- handleSearchButtonClick({ currentTarget: searchButtonRef.current });
- return;
- }
-
- // Capture typing for search (only if it's a single printable character)
- const isPrintableChar =
- e.key.length === 1 &&
- !e.ctrlKey &&
- !e.metaKey &&
- !e.altKey &&
- !e.key.match(/^F\d+$/); // Exclude function keys like F1-F12
-
- if (isPrintableChar && !openSearch) {
- // Open search popover and set the search term to the pressed key
- handleSearchButtonClick({ currentTarget: searchButtonRef.current });
- // Set a small timeout to ensure the search input is ready
- setTimeout(() => {
- setSearchTerm(e.key);
- }, 50);
- }
- };
-
- window.addEventListener('keydown', handleGlobalKeyDown);
- return () => {
- window.removeEventListener('keydown', handleGlobalKeyDown);
- };
- }, [
- showFilters,
- searchTerm,
- activeSearchTerms,
- escRecentlyPressed,
- openFilters,
- openSearch,
- openType,
- openVisibility,
- openColumnMenu,
+ // Use network filters hook
+ const {
+ filters,
+ showStopped,
+ setShowStopped,
+ showFilters,
+ searchTerm,
+ setSearchTerm,
+ activeSearchTerms,
+ guestTypeFilter,
+ setGuestTypeFilter,
+ addSearchTerm,
+ removeSearchTerm,
+ updateFilter,
+ handleSliderDragStart,
+ handleSliderDragEnd,
+ clearFilter,
resetFilters,
- handleSearchButtonClick
- ]);
+ activeFilterCount
+ } = useNetworkFilters();
- // Get sorted and filtered data
- const sortedAndFilteredData = useMemo(() => {
- // First filter by node
- const nodeFilteredData = selectedNode === 'all'
- ? guestData
- : getNodeFilteredGuests(guestData);
-
- // Then apply all other filters and sorting
- return getSortedAndFilteredData(
- nodeFilteredData,
- sortConfig,
- filters,
- showStopped,
- activeSearchTerms,
- searchTerm,
- combinedMetrics,
- guestTypeFilter,
- nodeData
- );
- }, [
- guestData,
- combinedMetrics,
- sortConfig,
- filters,
- showStopped,
- activeSearchTerms,
- searchTerm,
- selectedNode,
- getNodeFilteredGuests,
+ // Use popover management hook
+ const {
+ filterAnchorEl,
+ openFilters,
+ searchAnchorEl,
+ openSearch,
+ searchInputRef,
+ typeAnchorEl,
+ openType,
+ visibilityAnchorEl,
+ openVisibility,
+ filterButtonRef,
+ searchButtonRef,
+ handleFilterButtonClick,
+ handleCloseFilterPopover,
+ handleSearchButtonClick,
+ handleCloseSearchPopover,
+ handleTypeButtonClick,
+ handleCloseTypePopover,
+ handleVisibilityButtonClick,
+ handleCloseVisibilityPopover,
+ closeAllPopovers
+ } = usePopoverManagement();
+
+ // Use keyboard shortcuts hook
+ useKeyboardShortcuts({
+ openFilters,
+ openSearch,
+ openType,
+ openVisibility,
+ openColumnMenu,
+ resetFilters,
+ closeAllPopovers,
+ handleSearchButtonClick,
+ searchButtonRef,
+ setSearchTerm,
+ showNotification
+ });
+
+ // Use data processing hook
+ const {
+ extractNumericId,
+ getNodeName,
+ sortedAndFilteredData,
+ formatPercentage,
+ formatNetworkRateForFilter
+ } = useDataProcessing({
+ guestData,
+ combinedMetrics,
+ sortConfig,
+ filters,
+ showStopped,
+ activeSearchTerms,
+ searchTerm,
+ selectedNode,
guestTypeFilter,
nodeData
- ]);
+ });
- // Format percentage for display
- const formatPercentage = (value) => {
- return `${value}%`;
- };
-
- // Format network rate for filter display
- const formatNetworkRateForFilter = (value) => {
- if (value === 0) return '0 KB/s';
- if (value <= 10) return `${value * 10} KB/s`;
- if (value <= 50) return `${(value - 10) * 20 + 100} KB/s`;
- if (value <= 80) return `${(value - 50) * 50 + 900} KB/s`;
- return `${(value - 80) * 500 + 2400} KB/s`;
- };
-
- // Determine which columns have active filters
- const activeFilteredColumns = useMemo(() => {
- const result = {};
-
- // Resource filters
- if (filters.cpu > 0) result.cpu = true;
- if (filters.memory > 0) result.memory = true;
- if (filters.disk > 0) result.disk = true;
- if (filters.download > 0) result.netIn = true;
- if (filters.upload > 0) result.netOut = true;
-
- // Search terms - determine which column to highlight based on the search term
- if (activeSearchTerms.length > 0 || searchTerm) {
- const allTerms = [...activeSearchTerms];
- if (searchTerm) allTerms.push(searchTerm);
-
- // Check each term and determine which column(s) to highlight
- allTerms.forEach(term => {
- const termLower = term.trim().toLowerCase();
-
- // Check for exact type matches first
- if (termLower === 'ct' || termLower === 'container') {
- result.type = true;
- return; // Skip other checks for this term
- }
-
- if (termLower === 'vm' || termLower === 'virtual machine') {
- result.type = true;
- return; // Skip other checks for this term
- }
-
- // Check if term is a numeric ID
- if (/^\d+$/.test(termLower)) {
- result.id = true;
- }
- // Check if term matches type-related keywords
- else if (['qemu', 'lxc'].includes(termLower)) {
- result.type = true;
- }
- // Check if term matches status-related keywords
- else if (['running', 'stopped', 'online', 'offline', 'active', 'inactive'].includes(termLower)) {
- result.status = true;
- }
- // Check if term might be a node name
- else if (nodeData && nodeData.some(node =>
- (node.name && node.name.toLowerCase().includes(termLower)) ||
- (node.id && node.id.toLowerCase().includes(termLower))
- )) {
- result.node = true;
- }
- // Default to name column for other terms
- else {
- result.name = true;
- }
- });
- }
-
- // Guest type filter affects the type column
- if (guestTypeFilter !== 'all') {
- result.type = true;
- }
-
- // Only highlight status column for show/hide stopped systems if it's not the default state
- // By default, showStopped is false, so we only highlight when it's true (showing stopped systems)
- if (showStopped) {
- result.status = true;
- }
-
- return result;
- }, [filters, activeSearchTerms, searchTerm, guestTypeFilter, showStopped, nodeData]);
+ // Use active filtered columns hook
+ const activeFilteredColumns = useActiveFilteredColumns({
+ filters,
+ activeSearchTerms,
+ searchTerm,
+ guestTypeFilter,
+ showStopped,
+ nodeData
+ });
// Show loading state if not connected
if (!isConnected && connectionStatus !== 'error' && connectionStatus !== 'disconnected') {
@@ -814,718 +210,104 @@ const NetworkDisplay = ({ selectedNode = 'all' }) => {
);
}
- // Function to add a search term
- const addSearchTerm = (term) => {
- if (!activeSearchTerms.includes(term)) {
- setActiveSearchTerms([...activeSearchTerms, term]);
- }
- };
-
- // Function to remove a search term
- const removeSearchTerm = (term) => {
- setActiveSearchTerms(activeSearchTerms.filter(t => t !== term));
- };
-
- // Update filter value
- const updateFilter = (filterName, newValue) => {
- setFilters(prev => ({
- ...prev,
- [filterName]: newValue
- }));
- };
-
- // Handle slider drag start
- const handleSliderDragStart = (filterName) => {
- setSliderDragging(filterName);
- };
-
- // Handle slider drag end
- const handleSliderDragEnd = () => {
- setSliderDragging(null);
- };
-
- // Clear a specific filter
- const clearFilter = (filterName) => {
- setFilters(prev => ({
- ...prev,
- [filterName]: 0
- }));
- };
-
- // Request sort by key
- const requestSort = (key) => {
- setSortConfig(prev => ({
- key,
- direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc'
- }));
- };
-
return (
-
- {/* Header with buttons */}
-
-
- Systems
-
-
- {/* Column visibility button */}
-
- col.visible).length}
- color="primary"
- invisible={Object.values(columnVisibility).filter(col => col.visible).length === Object.keys(DEFAULT_COLUMN_CONFIG).length}
- >
-
-
-
-
- {/* Search button */}
-
-
-
-
-
-
- {/* System type button */}
-
-
- {guestTypeFilter === 'vm' ? :
- guestTypeFilter === 'ct' ? :
- }
-
-
-
- {/* Visibility button */}
-
- {showStopped ? : }
-
-
- {/* Resource thresholds button */}
-
- 0 ? 1 : 0) +
- (filters.memory > 0 ? 1 : 0) +
- (filters.disk > 0 ? 1 : 0) +
- (filters.download > 0 ? 1 : 0) +
- (filters.upload > 0 ? 1 : 0)
- }
- color="primary"
- invisible={
- filters.cpu === 0 &&
- filters.memory === 0 &&
- filters.disk === 0 &&
- filters.download === 0 &&
- filters.upload === 0
- }
- >
-
-
-
-
-
+ {/* Header with buttons */}
+
+
+ {/* Popovers */}
+
+ // Formatters
+ formatPercentage={formatPercentage}
+ formatNetworkRateForFilter={formatNetworkRateForFilter}
+ />
{/* Main data table */}
-
-
-
-
-
-
-
+
{/* Notification Snackbar */}
-
-
- {snackbarMessage}
-
-
+
);
};
diff --git a/frontend/src/components/network/components/NetworkHeader.jsx b/frontend/src/components/network/components/NetworkHeader.jsx
new file mode 100644
index 000000000..6f659ec6d
--- /dev/null
+++ b/frontend/src/components/network/components/NetworkHeader.jsx
@@ -0,0 +1,178 @@
+import React from 'react';
+import {
+ Box,
+ Typography,
+ IconButton,
+ Badge
+} from '@mui/material';
+import ViewColumnIcon from '@mui/icons-material/ViewColumn';
+import SearchIcon from '@mui/icons-material/Search';
+import ComputerIcon from '@mui/icons-material/Computer';
+import DnsIcon from '@mui/icons-material/Dns';
+import ViewListIcon from '@mui/icons-material/ViewList';
+import VisibilityIcon from '@mui/icons-material/Visibility';
+import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
+import TuneIcon from '@mui/icons-material/Tune';
+import { DEFAULT_COLUMN_CONFIG } from '../../../constants/networkConstants';
+
+const NetworkHeader = ({
+ openColumnMenu,
+ openSearch,
+ openType,
+ openVisibility,
+ openFilters,
+ activeSearchTerms,
+ guestTypeFilter,
+ showStopped,
+ filters,
+ columnVisibility,
+ handleColumnMenuOpen,
+ handleSearchButtonClick,
+ handleTypeButtonClick,
+ handleVisibilityButtonClick,
+ handleFilterButtonClick,
+ searchButtonRef,
+ filterButtonRef
+}) => {
+ return (
+
+
+ Systems
+
+
+ {/* Column visibility button */}
+
+ col.visible).length}
+ color="primary"
+ invisible={Object.values(columnVisibility).filter(col => col.visible).length === Object.keys(DEFAULT_COLUMN_CONFIG).length}
+ >
+
+
+
+
+ {/* Search button */}
+
+
+
+
+
+
+ {/* System type button */}
+
+
+ {guestTypeFilter === 'vm' ? :
+ guestTypeFilter === 'ct' ? :
+ }
+
+
+
+ {/* Visibility button */}
+
+ {showStopped ? : }
+
+
+ {/* Resource thresholds button */}
+
+ 0 ? 1 : 0) +
+ (filters.memory > 0 ? 1 : 0) +
+ (filters.disk > 0 ? 1 : 0) +
+ (filters.download > 0 ? 1 : 0) +
+ (filters.upload > 0 ? 1 : 0)
+ }
+ color="primary"
+ invisible={
+ filters.cpu === 0 &&
+ filters.memory === 0 &&
+ filters.disk === 0 &&
+ filters.download === 0 &&
+ filters.upload === 0
+ }
+ >
+
+
+
+
+
+ );
+};
+
+export default NetworkHeader;
\ No newline at end of file
diff --git a/frontend/src/components/network/components/NetworkNotification.jsx b/frontend/src/components/network/components/NetworkNotification.jsx
new file mode 100644
index 000000000..4cde6a0b0
--- /dev/null
+++ b/frontend/src/components/network/components/NetworkNotification.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { Snackbar, Alert } from '@mui/material';
+
+const NetworkNotification = ({
+ snackbarOpen,
+ snackbarMessage,
+ snackbarSeverity,
+ handleSnackbarClose
+}) => {
+ return (
+
+
+ {snackbarMessage}
+
+
+ );
+};
+
+export default NetworkNotification;
\ No newline at end of file
diff --git a/frontend/src/components/network/components/NetworkPopovers.jsx b/frontend/src/components/network/components/NetworkPopovers.jsx
new file mode 100644
index 000000000..33b6085c1
--- /dev/null
+++ b/frontend/src/components/network/components/NetworkPopovers.jsx
@@ -0,0 +1,531 @@
+import React from 'react';
+import {
+ Popover,
+ Paper,
+ Box,
+ Typography,
+ TextField,
+ IconButton,
+ Chip,
+ Button,
+ ToggleButtonGroup,
+ ToggleButton,
+ MenuItem,
+ Switch,
+ Slider
+} from '@mui/material';
+import SearchIcon from '@mui/icons-material/Search';
+import ClearIcon from '@mui/icons-material/Clear';
+import ComputerIcon from '@mui/icons-material/Computer';
+import DnsIcon from '@mui/icons-material/Dns';
+import ViewListIcon from '@mui/icons-material/ViewList';
+import VisibilityIcon from '@mui/icons-material/Visibility';
+import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
+import FilterAltOffIcon from '@mui/icons-material/FilterAltOff';
+
+const NetworkPopovers = ({
+ // Search popover props
+ searchAnchorEl,
+ openSearch,
+ handleCloseSearchPopover,
+ searchTerm,
+ setSearchTerm,
+ activeSearchTerms,
+ addSearchTerm,
+ removeSearchTerm,
+ searchInputRef,
+
+ // Type popover props
+ typeAnchorEl,
+ openType,
+ handleCloseTypePopover,
+ guestTypeFilter,
+ setGuestTypeFilter,
+
+ // Visibility popover props
+ visibilityAnchorEl,
+ openVisibility,
+ handleCloseVisibilityPopover,
+ showStopped,
+ setShowStopped,
+
+ // Filter popover props
+ filterAnchorEl,
+ openFilters,
+ handleCloseFilterPopover,
+ filters,
+ updateFilter,
+ handleSliderDragStart,
+ handleSliderDragEnd,
+ resetFilters,
+
+ // Formatters
+ formatPercentage,
+ formatNetworkRateForFilter
+}) => {
+ return (
+ <>
+ {/* Search popover */}
+
+
+ {/* System type popover */}
+
+
+ {/* Visibility popover */}
+
+
+ {/* Resource thresholds popover */}
+
+ >
+ );
+};
+
+export default NetworkPopovers;
\ No newline at end of file
diff --git a/frontend/src/components/network/components/NetworkTable.jsx b/frontend/src/components/network/components/NetworkTable.jsx
new file mode 100644
index 000000000..cf10ef391
--- /dev/null
+++ b/frontend/src/components/network/components/NetworkTable.jsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import {
+ Card,
+ CardContent,
+ TableContainer,
+ Table,
+ Paper
+} from '@mui/material';
+import NetworkTableHeader from '../NetworkTableHeader';
+import NetworkTableBody from '../NetworkTableBody';
+
+const NetworkTable = ({
+ sortConfig,
+ requestSort,
+ columnVisibility,
+ toggleColumnVisibility,
+ resetColumnVisibility,
+ columnMenuAnchorEl,
+ handleColumnMenuOpen,
+ handleColumnMenuClose,
+ openColumnMenu,
+ forceUpdateCounter,
+ columnOrder,
+ setColumnOrder,
+ activeFilteredColumns,
+ sortedAndFilteredData,
+ guestData,
+ metricsData,
+ getNodeName,
+ extractNumericId,
+ resetFilters,
+ showStopped,
+ setShowStopped,
+ guestTypeFilter
+}) => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default NetworkTable;
\ No newline at end of file
diff --git a/frontend/src/components/network/components/index.js b/frontend/src/components/network/components/index.js
new file mode 100644
index 000000000..cca9b8e76
--- /dev/null
+++ b/frontend/src/components/network/components/index.js
@@ -0,0 +1,4 @@
+export { default as NetworkHeader } from './NetworkHeader';
+export { default as NetworkPopovers } from './NetworkPopovers';
+export { default as NetworkNotification } from './NetworkNotification';
+export { default as NetworkTable } from './NetworkTable';
\ No newline at end of file
diff --git a/frontend/src/components/network/hooks/index.js b/frontend/src/components/network/hooks/index.js
new file mode 100644
index 000000000..b58225cdb
--- /dev/null
+++ b/frontend/src/components/network/hooks/index.js
@@ -0,0 +1,8 @@
+export { default as useNetworkFilters } from './useNetworkFilters';
+export { default as useColumnManagement } from './useColumnManagement';
+export { default as usePopoverManagement } from './usePopoverManagement';
+export { default as useNotifications } from './useNotifications';
+export { default as useSortManagement } from './useSortManagement';
+export { default as useKeyboardShortcuts } from './useKeyboardShortcuts';
+export { default as useActiveFilteredColumns } from './useActiveFilteredColumns';
+export { default as useDataProcessing } from './useDataProcessing';
\ No newline at end of file
diff --git a/frontend/src/components/network/hooks/useActiveFilteredColumns.js b/frontend/src/components/network/hooks/useActiveFilteredColumns.js
new file mode 100644
index 000000000..f75b686da
--- /dev/null
+++ b/frontend/src/components/network/hooks/useActiveFilteredColumns.js
@@ -0,0 +1,85 @@
+import { useMemo } from 'react';
+
+const useActiveFilteredColumns = ({
+ filters,
+ activeSearchTerms,
+ searchTerm,
+ guestTypeFilter,
+ showStopped,
+ nodeData
+}) => {
+ // Determine which columns have active filters
+ const activeFilteredColumns = useMemo(() => {
+ const result = {};
+
+ // Resource filters
+ if (filters.cpu > 0) result.cpu = true;
+ if (filters.memory > 0) result.memory = true;
+ if (filters.disk > 0) result.disk = true;
+ if (filters.download > 0) result.netIn = true;
+ if (filters.upload > 0) result.netOut = true;
+
+ // Search terms - determine which column to highlight based on the search term
+ if (activeSearchTerms.length > 0 || searchTerm) {
+ const allTerms = [...activeSearchTerms];
+ if (searchTerm) allTerms.push(searchTerm);
+
+ // Check each term and determine which column(s) to highlight
+ allTerms.forEach(term => {
+ const termLower = term.trim().toLowerCase();
+
+ // Check for exact type matches first
+ if (termLower === 'ct' || termLower === 'container') {
+ result.type = true;
+ return; // Skip other checks for this term
+ }
+
+ if (termLower === 'vm' || termLower === 'virtual machine') {
+ result.type = true;
+ return; // Skip other checks for this term
+ }
+
+ // Check if term is a numeric ID
+ if (/^\d+$/.test(termLower)) {
+ result.id = true;
+ }
+ // Check if term matches type-related keywords
+ else if (['qemu', 'lxc'].includes(termLower)) {
+ result.type = true;
+ }
+ // Check if term matches status-related keywords
+ else if (['running', 'stopped', 'online', 'offline', 'active', 'inactive'].includes(termLower)) {
+ result.status = true;
+ }
+ // Check if term might be a node name
+ else if (nodeData && nodeData.some(node =>
+ (node.name && node.name.toLowerCase().includes(termLower)) ||
+ (node.id && node.id.toLowerCase().includes(termLower))
+ )) {
+ result.node = true;
+ }
+ // Default to name column for other terms
+ else {
+ result.name = true;
+ }
+ });
+ }
+
+ // Guest type filter affects the type column
+ if (guestTypeFilter !== 'all') {
+ result.type = true;
+ }
+
+ // Only highlight status column for show/hide stopped systems if it's not the default state
+ // By default, showStopped is false, so we only highlight when it's true (showing stopped systems)
+ if (showStopped) {
+ result.status = true;
+ }
+
+ return result;
+ }, [filters, activeSearchTerms, searchTerm, guestTypeFilter, showStopped, nodeData]);
+
+ return activeFilteredColumns;
+};
+
+export default useActiveFilteredColumns;
\ No newline at end of file
diff --git a/frontend/src/components/network/hooks/useColumnManagement.js b/frontend/src/components/network/hooks/useColumnManagement.js
new file mode 100644
index 000000000..c751b80f8
--- /dev/null
+++ b/frontend/src/components/network/hooks/useColumnManagement.js
@@ -0,0 +1,256 @@
+import { useState, useCallback, useEffect } from 'react';
+import {
+ STORAGE_KEY_COLUMN_VISIBILITY,
+ STORAGE_KEY_COLUMN_ORDER,
+ DEFAULT_COLUMN_CONFIG
+} from '../../../constants/networkConstants';
+
+const useColumnManagement = (showNotification) => {
+ // Column visibility state
+ const [columnVisibility, setColumnVisibility] = useState(() => {
+ try {
+ console.log('Initializing column visibility state');
+ const saved = localStorage.getItem(STORAGE_KEY_COLUMN_VISIBILITY);
+ if (saved) {
+ console.log('Found saved column visibility:', saved);
+ const parsed = JSON.parse(saved);
+ // Ensure all columns from DEFAULT_COLUMN_CONFIG exist in the saved config
+ const merged = { ...DEFAULT_COLUMN_CONFIG };
+ Object.keys(parsed).forEach(key => {
+ if (merged[key]) {
+ merged[key].visible = parsed[key].visible;
+ }
+ });
+
+ // Ensure at least one column is visible
+ const hasVisibleColumn = Object.values(merged).some(col => col.visible);
+ if (!hasVisibleColumn) {
+ console.warn('No visible columns in saved config, resetting to defaults');
+ return JSON.parse(JSON.stringify(DEFAULT_COLUMN_CONFIG));
+ }
+
+ console.log('Using merged column visibility:', merged);
+ return merged;
+ }
+ console.log('No saved column visibility, using defaults');
+ return JSON.parse(JSON.stringify(DEFAULT_COLUMN_CONFIG));
+ } catch (e) {
+ console.error('Error loading column visibility preferences:', e);
+ return JSON.parse(JSON.stringify(DEFAULT_COLUMN_CONFIG));
+ }
+ });
+
+ // Column order state
+ const [columnOrder, setColumnOrder] = useState(() => {
+ try {
+ console.log('Initializing column order state');
+ const defaultOrder = Object.keys(DEFAULT_COLUMN_CONFIG);
+
+ const saved = localStorage.getItem(STORAGE_KEY_COLUMN_ORDER);
+ if (saved) {
+ console.log('Found saved column order:', saved);
+ try {
+ const parsed = JSON.parse(saved);
+
+ // Validate the saved order - it should contain all columns from DEFAULT_COLUMN_CONFIG
+ const isValid =
+ Array.isArray(parsed) &&
+ parsed.length === defaultOrder.length &&
+ defaultOrder.every(col => parsed.includes(col));
+
+ if (isValid) {
+ console.log('Using saved column order:', parsed);
+ return parsed;
+ } else {
+ console.warn('Invalid saved column order, using default order');
+ }
+ } catch (parseError) {
+ console.error('Error parsing saved column order:', parseError);
+ }
+ }
+
+ console.log('Using default column order:', defaultOrder);
+ return defaultOrder;
+ } catch (e) {
+ console.error('Error loading column order preferences:', e);
+ const fallbackOrder = Object.keys(DEFAULT_COLUMN_CONFIG || {});
+ console.log('Using fallback column order:', fallbackOrder);
+ return fallbackOrder;
+ }
+ });
+
+ // Column settings menu state
+ const [columnMenuAnchorEl, setColumnMenuAnchorEl] = useState(null);
+ const openColumnMenu = Boolean(columnMenuAnchorEl);
+
+ // State for forcing re-renders
+ const [forceUpdateCounter, setForceUpdateCounter] = useState(0);
+
+ // Function to force a re-render
+ const forceUpdate = useCallback(() => {
+ setForceUpdateCounter(prev => prev + 1);
+ }, []);
+
+ // Toggle column visibility
+ const toggleColumnVisibility = useCallback((columnId) => {
+ try {
+ setColumnVisibility(prev => {
+ if (!prev || !prev[columnId]) {
+ console.error('Invalid column configuration:', prev);
+ return prev;
+ }
+
+ // Check if this is the last visible column and we're trying to hide it
+ const isLastVisibleColumn =
+ Object.values(prev).filter(col => col.visible).length === 1 &&
+ prev[columnId].visible;
+
+ // If this is the last visible column and we're trying to hide it, don't allow it
+ if (isLastVisibleColumn) {
+ // Show a console warning
+ console.warn('Cannot hide the last visible column');
+ // Show a notification to the user
+ setTimeout(() => {
+ showNotification('At least one column must remain visible', 'warning');
+ }, 0);
+ // Return the previous state unchanged
+ return prev;
+ }
+
+ const updated = {
+ ...prev,
+ [columnId]: {
+ ...prev[columnId],
+ visible: !prev[columnId].visible
+ }
+ };
+ return updated;
+ });
+ } catch (error) {
+ console.error('Error toggling column visibility:', error);
+ }
+ }, [showNotification]);
+
+ // Reset column visibility to defaults
+ const resetColumnVisibility = useCallback(() => {
+ console.log('Resetting column visibility to defaults');
+
+ // Create a new configuration with all columns set to visible
+ const allVisibleConfig = {};
+ if (DEFAULT_COLUMN_CONFIG) {
+ Object.keys(DEFAULT_COLUMN_CONFIG).forEach(key => {
+ allVisibleConfig[key] = {
+ ...DEFAULT_COLUMN_CONFIG[key],
+ visible: true // Force all columns to be visible
+ };
+ });
+ }
+
+ console.log('New config with all columns visible:', allVisibleConfig);
+
+ // Clear the localStorage entries to ensure a clean state
+ try {
+ localStorage.removeItem(STORAGE_KEY_COLUMN_VISIBILITY);
+ localStorage.removeItem(STORAGE_KEY_COLUMN_ORDER);
+ console.log('Cleared column preferences from localStorage');
+ } catch (error) {
+ console.error('Error clearing column preferences from localStorage:', error);
+ }
+
+ // Set the state with the new config where all columns are visible
+ setColumnVisibility(allVisibleConfig);
+
+ // Reset column order to default
+ if (DEFAULT_COLUMN_CONFIG) {
+ setColumnOrder(Object.keys(DEFAULT_COLUMN_CONFIG));
+ }
+
+ // Force a re-render of the table
+ setTimeout(() => {
+ forceUpdate();
+ console.log('Forced table re-render');
+ }, 50);
+
+ // Show a notification to confirm the action
+ showNotification('All columns are now visible', 'success');
+
+ // Close the column menu if it's open
+ if (openColumnMenu) {
+ handleColumnMenuClose();
+ }
+ }, [openColumnMenu, forceUpdate, showNotification]);
+
+ // Handle column menu open
+ const handleColumnMenuOpen = useCallback((event) => {
+ setColumnMenuAnchorEl(event.currentTarget);
+ }, []);
+
+ // Handle column menu close
+ const handleColumnMenuClose = useCallback(() => {
+ setColumnMenuAnchorEl(null);
+ }, []);
+
+ // Save column visibility preferences whenever they change
+ useEffect(() => {
+ try {
+ console.log('Column visibility changed, saving to localStorage');
+
+ // Check if columnVisibility is valid
+ if (!columnVisibility || Object.keys(columnVisibility).length === 0) {
+ console.error('Invalid column visibility state, resetting to defaults');
+ setColumnVisibility(JSON.parse(JSON.stringify(DEFAULT_COLUMN_CONFIG)));
+ return;
+ }
+
+ // Ensure at least one column is visible
+ const hasVisibleColumn = Object.values(columnVisibility).some(col => col.visible);
+ if (!hasVisibleColumn) {
+ console.error('No visible columns in state, not saving to localStorage');
+ return;
+ }
+
+ // Save to localStorage
+ localStorage.setItem(STORAGE_KEY_COLUMN_VISIBILITY, JSON.stringify(columnVisibility));
+ console.log('Saved column visibility to localStorage');
+ } catch (error) {
+ console.error('Error saving column visibility:', error);
+ }
+ }, [columnVisibility]);
+
+ // Save column order preferences whenever they change
+ useEffect(() => {
+ try {
+ console.log('Column order changed, saving to localStorage');
+
+ // Check if columnOrder is valid
+ if (!columnOrder || !Array.isArray(columnOrder) || columnOrder.length === 0) {
+ console.error('Invalid column order state, resetting to defaults');
+ setColumnOrder(Object.keys(DEFAULT_COLUMN_CONFIG));
+ return;
+ }
+
+ // Save to localStorage
+ localStorage.setItem(STORAGE_KEY_COLUMN_ORDER, JSON.stringify(columnOrder));
+ console.log('Saved column order to localStorage');
+ } catch (error) {
+ console.error('Error saving column order:', error);
+ }
+ }, [columnOrder]);
+
+ return {
+ columnVisibility,
+ setColumnVisibility,
+ columnOrder,
+ setColumnOrder,
+ columnMenuAnchorEl,
+ openColumnMenu,
+ forceUpdateCounter,
+ forceUpdate,
+ toggleColumnVisibility,
+ resetColumnVisibility,
+ handleColumnMenuOpen,
+ handleColumnMenuClose
+ };
+};
+
+export default useColumnManagement;
\ No newline at end of file
diff --git a/frontend/src/components/network/hooks/useDataProcessing.js b/frontend/src/components/network/hooks/useDataProcessing.js
new file mode 100644
index 000000000..3ca60b2cd
--- /dev/null
+++ b/frontend/src/components/network/hooks/useDataProcessing.js
@@ -0,0 +1,88 @@
+import { useMemo, useCallback } from 'react';
+import { getSortedAndFilteredData, getNodeFilteredGuests as nodeFilteredGuestsUtil, getNodeName as getNodeNameUtil, extractNumericId as extractNumericIdUtil } from '../../../utils/networkUtils';
+
+const useDataProcessing = ({
+ guestData,
+ combinedMetrics,
+ sortConfig,
+ filters,
+ showStopped,
+ activeSearchTerms,
+ searchTerm,
+ selectedNode,
+ guestTypeFilter,
+ nodeData
+}) => {
+ // Helper function to extract numeric ID from strings like "node-1-ct-105"
+ const extractNumericId = useCallback((fullId) => {
+ return extractNumericIdUtil(fullId);
+ }, []);
+
+ // Helper function to get the node name from the node ID
+ const getNodeName = useCallback((nodeId) => {
+ return getNodeNameUtil(nodeId, nodeData);
+ }, [nodeData]);
+
+ // Filter guests based on selected node
+ const getNodeFilteredGuests = useCallback((guests) => {
+ return nodeFilteredGuestsUtil(guests, selectedNode);
+ }, [selectedNode]);
+
+ // Get sorted and filtered data
+ const sortedAndFilteredData = useMemo(() => {
+ // First filter by node
+ const nodeFilteredData = selectedNode === 'all'
+ ? guestData
+ : getNodeFilteredGuests(guestData);
+
+ // Then apply all other filters and sorting
+ return getSortedAndFilteredData(
+ nodeFilteredData,
+ sortConfig,
+ filters,
+ showStopped,
+ activeSearchTerms,
+ searchTerm,
+ combinedMetrics,
+ guestTypeFilter,
+ nodeData
+ );
+ }, [
+ guestData,
+ combinedMetrics,
+ sortConfig,
+ filters,
+ showStopped,
+ activeSearchTerms,
+ searchTerm,
+ selectedNode,
+ getNodeFilteredGuests,
+ guestTypeFilter,
+ nodeData
+ ]);
+
+ // Format percentage for display
+ const formatPercentage = useCallback((value) => {
+ return `${value}%`;
+ }, []);
+
+ // Format network rate for filter display
+ const formatNetworkRateForFilter = useCallback((value) => {
+ if (value === 0) return '0 KB/s';
+ if (value <= 10) return `${value * 10} KB/s`;
+ if (value <= 50) return `${(value - 10) * 20 + 100} KB/s`;
+ if (value <= 80) return `${(value - 50) * 50 + 900} KB/s`;
+ return `${(value - 80) * 500 + 2400} KB/s`;
+ }, []);
+
+ return {
+ extractNumericId,
+ getNodeName,
+ getNodeFilteredGuests,
+ sortedAndFilteredData,
+ formatPercentage,
+ formatNetworkRateForFilter
+ };
+};
+
+export default useDataProcessing;
\ No newline at end of file
diff --git a/frontend/src/components/network/hooks/useKeyboardShortcuts.js b/frontend/src/components/network/hooks/useKeyboardShortcuts.js
new file mode 100644
index 000000000..fa8c70ca2
--- /dev/null
+++ b/frontend/src/components/network/hooks/useKeyboardShortcuts.js
@@ -0,0 +1,108 @@
+import { useState, useEffect, useCallback } from 'react';
+
+const useKeyboardShortcuts = ({
+ openFilters,
+ openSearch,
+ openType,
+ openVisibility,
+ openColumnMenu,
+ resetFilters,
+ closeAllPopovers,
+ handleSearchButtonClick,
+ searchButtonRef,
+ setSearchTerm,
+ showNotification
+}) => {
+ const [escRecentlyPressed, setEscRecentlyPressed] = useState(false);
+
+ // Set up keyboard shortcuts
+ useEffect(() => {
+ const handleGlobalKeyDown = (e) => {
+ // Immediately return if search popover is open
+ if (openSearch) {
+ return;
+ }
+
+ // Don't trigger shortcuts if typing in an input field
+ if (
+ e.target.tagName === 'INPUT' ||
+ e.target.tagName === 'TEXTAREA' ||
+ e.target.isContentEditable
+ ) {
+ return;
+ }
+
+ // Escape key to close filters or clear search
+ if (e.key === 'Escape') {
+ // Close any open popovers
+ closeAllPopovers();
+
+ // Reset all filters
+ resetFilters();
+
+ // Set flag to prevent other shortcuts from triggering
+ setEscRecentlyPressed(true);
+ setTimeout(() => setEscRecentlyPressed(false), 300);
+
+ // Show notification
+ showNotification('All filters have been cleared', 'info');
+ return;
+ }
+
+ // Ctrl+F or Cmd+F to focus search
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
+ e.preventDefault();
+ handleSearchButtonClick({ currentTarget: searchButtonRef.current });
+ return;
+ }
+
+ // / to focus search
+ if (e.key === '/' && !escRecentlyPressed) {
+ e.preventDefault();
+ handleSearchButtonClick({ currentTarget: searchButtonRef.current });
+ return;
+ }
+
+ // Capture typing for search (only if it's a single printable character)
+ const isPrintableChar =
+ e.key.length === 1 &&
+ !e.ctrlKey &&
+ !e.metaKey &&
+ !e.altKey &&
+ !e.key.match(/^F\d+$/); // Exclude function keys like F1-F12
+
+ if (isPrintableChar && !openSearch) {
+ // Open search popover and set the search term to the pressed key
+ handleSearchButtonClick({ currentTarget: searchButtonRef.current });
+ // Set a small timeout to ensure the search input is ready
+ setTimeout(() => {
+ setSearchTerm(e.key);
+ }, 50);
+ }
+ };
+
+ window.addEventListener('keydown', handleGlobalKeyDown);
+ return () => {
+ window.removeEventListener('keydown', handleGlobalKeyDown);
+ };
+ }, [
+ openFilters,
+ openSearch,
+ openType,
+ openVisibility,
+ openColumnMenu,
+ escRecentlyPressed,
+ resetFilters,
+ closeAllPopovers,
+ handleSearchButtonClick,
+ searchButtonRef,
+ setSearchTerm,
+ showNotification
+ ]);
+
+ return {
+ escRecentlyPressed
+ };
+};
+
+export default useKeyboardShortcuts;
\ No newline at end of file
diff --git a/frontend/src/components/network/hooks/useNetworkFilters.js b/frontend/src/components/network/hooks/useNetworkFilters.js
new file mode 100644
index 000000000..64a8edae0
--- /dev/null
+++ b/frontend/src/components/network/hooks/useNetworkFilters.js
@@ -0,0 +1,193 @@
+import { useState, useCallback, useEffect } from 'react';
+import {
+ STORAGE_KEY_FILTERS,
+ STORAGE_KEY_SHOW_STOPPED,
+ STORAGE_KEY_SHOW_FILTERS,
+ STORAGE_KEY_SEARCH_TERMS,
+ STORAGE_KEY_GUEST_TYPE_FILTER
+} from '../../../constants/networkConstants';
+
+const useNetworkFilters = () => {
+ // Filter states
+ const [filters, setFilters] = useState(() => {
+ try {
+ const saved = localStorage.getItem(STORAGE_KEY_FILTERS);
+ return saved ? JSON.parse(saved) : {
+ cpu: 0,
+ memory: 0,
+ disk: 0,
+ download: 0,
+ upload: 0
+ };
+ } catch (e) {
+ console.error('Error loading filter preferences:', e);
+ return {
+ cpu: 0,
+ memory: 0,
+ disk: 0,
+ download: 0,
+ upload: 0
+ };
+ }
+ });
+
+ // UI state
+ const [showStopped, setShowStopped] = useState(() => {
+ try {
+ const saved = localStorage.getItem(STORAGE_KEY_SHOW_STOPPED);
+ return saved ? JSON.parse(saved) === true : false; // Default: false (show only running systems)
+ } catch (e) {
+ console.error('Error loading show stopped preference:', e);
+ return false; // Default to showing only running systems
+ }
+ });
+
+ const [showFilters, setShowFilters] = useState(() => {
+ try {
+ const saved = localStorage.getItem(STORAGE_KEY_SHOW_FILTERS);
+ return saved ? JSON.parse(saved) === true : false;
+ } catch (e) {
+ console.error('Error loading show filters preference:', e);
+ return false;
+ }
+ });
+
+ // Search state
+ const [searchTerm, setSearchTerm] = useState('');
+ const [activeSearchTerms, setActiveSearchTerms] = useState(() => {
+ try {
+ const saved = localStorage.getItem(STORAGE_KEY_SEARCH_TERMS);
+ return saved ? JSON.parse(saved) : [];
+ } catch (e) {
+ console.error('Error loading active search terms:', e);
+ return [];
+ }
+ });
+
+ // Add guest type filter state - load from localStorage or use default
+ const [guestTypeFilter, setGuestTypeFilter] = useState(() => {
+ try {
+ const saved = localStorage.getItem(STORAGE_KEY_GUEST_TYPE_FILTER);
+ return saved ? JSON.parse(saved) : 'all'; // Default: 'all' (show both VMs and LXCs)
+ } catch (e) {
+ console.error('Error loading guest type filter preference:', e);
+ return 'all'; // Default to showing all guest types
+ }
+ });
+
+ // State for tracking which slider is being dragged
+ const [sliderDragging, setSliderDragging] = useState(null);
+
+ // Save guest type filter preference whenever it changes
+ useEffect(() => {
+ try {
+ localStorage.setItem(STORAGE_KEY_GUEST_TYPE_FILTER, JSON.stringify(guestTypeFilter));
+ } catch (e) {
+ console.error('Error saving guest type filter preference:', e);
+ }
+ }, [guestTypeFilter]);
+
+ // Save show stopped preference whenever it changes
+ useEffect(() => {
+ localStorage.setItem(STORAGE_KEY_SHOW_STOPPED, JSON.stringify(showStopped));
+ }, [showStopped]);
+
+ // Save show filters preference whenever it changes
+ useEffect(() => {
+ localStorage.setItem(STORAGE_KEY_SHOW_FILTERS, JSON.stringify(showFilters));
+ }, [showFilters]);
+
+ // Save active search terms whenever they change
+ useEffect(() => {
+ localStorage.setItem(STORAGE_KEY_SEARCH_TERMS, JSON.stringify(activeSearchTerms));
+ }, [activeSearchTerms]);
+
+ // Save filter preferences whenever they change
+ useEffect(() => {
+ localStorage.setItem(STORAGE_KEY_FILTERS, JSON.stringify(filters));
+ }, [filters]);
+
+ // Function to add a search term
+ const addSearchTerm = useCallback((term) => {
+ if (!activeSearchTerms.includes(term)) {
+ setActiveSearchTerms(prev => [...prev, term]);
+ }
+ }, [activeSearchTerms]);
+
+ // Function to remove a search term
+ const removeSearchTerm = useCallback((term) => {
+ setActiveSearchTerms(prev => prev.filter(t => t !== term));
+ }, []);
+
+ // Update filter value
+ const updateFilter = useCallback((filterName, newValue) => {
+ setFilters(prev => ({
+ ...prev,
+ [filterName]: newValue
+ }));
+ }, []);
+
+ // Handle slider drag start
+ const handleSliderDragStart = useCallback((filterName) => {
+ setSliderDragging(filterName);
+ }, []);
+
+ // Handle slider drag end
+ const handleSliderDragEnd = useCallback(() => {
+ setSliderDragging(null);
+ }, []);
+
+ // Clear a specific filter
+ const clearFilter = useCallback((filterName) => {
+ setFilters(prev => ({
+ ...prev,
+ [filterName]: 0
+ }));
+ }, []);
+
+ // Function to reset all filters
+ const resetFilters = useCallback(() => {
+ setFilters({
+ cpu: 0,
+ memory: 0,
+ disk: 0,
+ download: 0,
+ upload: 0
+ });
+ setActiveSearchTerms([]);
+ setSearchTerm('');
+ setShowStopped(false);
+ setGuestTypeFilter('all');
+ }, []);
+
+ // Count active filters
+ const activeFilterCount = activeSearchTerms.length +
+ (searchTerm && !activeSearchTerms.includes(searchTerm) ? 1 : 0) +
+ Object.values(filters).filter(val => val > 0).length;
+
+ return {
+ filters,
+ setFilters,
+ showStopped,
+ setShowStopped,
+ showFilters,
+ setShowFilters,
+ searchTerm,
+ setSearchTerm,
+ activeSearchTerms,
+ setActiveSearchTerms,
+ guestTypeFilter,
+ setGuestTypeFilter,
+ sliderDragging,
+ addSearchTerm,
+ removeSearchTerm,
+ updateFilter,
+ handleSliderDragStart,
+ handleSliderDragEnd,
+ clearFilter,
+ resetFilters,
+ activeFilterCount
+ };
+};
+
+export default useNetworkFilters;
\ No newline at end of file
diff --git a/frontend/src/components/network/hooks/useNotifications.js b/frontend/src/components/network/hooks/useNotifications.js
new file mode 100644
index 000000000..b571165fc
--- /dev/null
+++ b/frontend/src/components/network/hooks/useNotifications.js
@@ -0,0 +1,33 @@
+import { useState, useCallback } from 'react';
+
+const useNotifications = () => {
+ // Snackbar state for notifications
+ const [snackbarOpen, setSnackbarOpen] = useState(false);
+ const [snackbarMessage, setSnackbarMessage] = useState('');
+ const [snackbarSeverity, setSnackbarSeverity] = useState('info');
+
+ // Handle snackbar close
+ const handleSnackbarClose = useCallback((event, reason) => {
+ if (reason === 'clickaway') {
+ return;
+ }
+ setSnackbarOpen(false);
+ }, []);
+
+ // Show a notification
+ const showNotification = useCallback((message, severity = 'info') => {
+ setSnackbarMessage(message);
+ setSnackbarSeverity(severity);
+ setSnackbarOpen(true);
+ }, []);
+
+ return {
+ snackbarOpen,
+ snackbarMessage,
+ snackbarSeverity,
+ handleSnackbarClose,
+ showNotification
+ };
+};
+
+export default useNotifications;
\ No newline at end of file
diff --git a/frontend/src/components/network/hooks/usePopoverManagement.js b/frontend/src/components/network/hooks/usePopoverManagement.js
new file mode 100644
index 000000000..162818ee1
--- /dev/null
+++ b/frontend/src/components/network/hooks/usePopoverManagement.js
@@ -0,0 +1,101 @@
+import { useState, useRef, useCallback } from 'react';
+
+const usePopoverManagement = () => {
+ // State for filter menu
+ const [filterAnchorEl, setFilterAnchorEl] = useState(null);
+ const openFilters = Boolean(filterAnchorEl);
+
+ // State for search popover
+ const [searchAnchorEl, setSearchAnchorEl] = useState(null);
+ const openSearch = Boolean(searchAnchorEl);
+ const searchInputRef = useRef(null);
+
+ // State for type popover
+ const [typeAnchorEl, setTypeAnchorEl] = useState(null);
+ const openType = Boolean(typeAnchorEl);
+
+ // State for visibility popover
+ const [visibilityAnchorEl, setVisibilityAnchorEl] = useState(null);
+ const openVisibility = Boolean(visibilityAnchorEl);
+
+ // Add a ref for the filter button
+ const filterButtonRef = useRef(null);
+
+ // Add a ref for the search button
+ const searchButtonRef = useRef(null);
+
+ // Filter popover handlers
+ const handleFilterButtonClick = useCallback((event) => {
+ setFilterAnchorEl(event.currentTarget);
+ }, []);
+
+ const handleCloseFilterPopover = useCallback(() => {
+ setFilterAnchorEl(null);
+ }, []);
+
+ // Search popover handlers
+ const handleSearchButtonClick = useCallback((event) => {
+ setSearchAnchorEl(event.currentTarget);
+ // Focus the search input after a short delay to ensure the popover is open
+ setTimeout(() => {
+ if (searchInputRef.current) {
+ searchInputRef.current.focus();
+ }
+ }, 100);
+ }, []);
+
+ const handleCloseSearchPopover = useCallback(() => {
+ setSearchAnchorEl(null);
+ }, []);
+
+ // System type popover handlers
+ const handleTypeButtonClick = useCallback((event) => {
+ setTypeAnchorEl(event.currentTarget);
+ }, []);
+
+ const handleCloseTypePopover = useCallback(() => {
+ setTypeAnchorEl(null);
+ }, []);
+
+ // Visibility popover handlers
+ const handleVisibilityButtonClick = useCallback((event) => {
+ setVisibilityAnchorEl(event.currentTarget);
+ }, []);
+
+ const handleCloseVisibilityPopover = useCallback(() => {
+ setVisibilityAnchorEl(null);
+ }, []);
+
+ // Function to close all popovers
+ const closeAllPopovers = useCallback(() => {
+ setFilterAnchorEl(null);
+ setSearchAnchorEl(null);
+ setTypeAnchorEl(null);
+ setVisibilityAnchorEl(null);
+ }, []);
+
+ return {
+ filterAnchorEl,
+ openFilters,
+ searchAnchorEl,
+ openSearch,
+ searchInputRef,
+ typeAnchorEl,
+ openType,
+ visibilityAnchorEl,
+ openVisibility,
+ filterButtonRef,
+ searchButtonRef,
+ handleFilterButtonClick,
+ handleCloseFilterPopover,
+ handleSearchButtonClick,
+ handleCloseSearchPopover,
+ handleTypeButtonClick,
+ handleCloseTypePopover,
+ handleVisibilityButtonClick,
+ handleCloseVisibilityPopover,
+ closeAllPopovers
+ };
+};
+
+export default usePopoverManagement;
\ No newline at end of file
diff --git a/frontend/src/components/network/hooks/useSortManagement.js b/frontend/src/components/network/hooks/useSortManagement.js
new file mode 100644
index 000000000..d4424c8a7
--- /dev/null
+++ b/frontend/src/components/network/hooks/useSortManagement.js
@@ -0,0 +1,36 @@
+import { useState, useCallback, useEffect } from 'react';
+import { STORAGE_KEY_SORT } from '../../../constants/networkConstants';
+
+const useSortManagement = () => {
+ // Sort state
+ const [sortConfig, setSortConfig] = useState(() => {
+ try {
+ const saved = localStorage.getItem(STORAGE_KEY_SORT);
+ return saved ? JSON.parse(saved) : { key: 'id', direction: 'asc' };
+ } catch (e) {
+ console.error('Error loading sort preferences:', e);
+ return { key: 'id', direction: 'asc' };
+ }
+ });
+
+ // Request sort by key
+ const requestSort = useCallback((key) => {
+ setSortConfig(prev => ({
+ key,
+ direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc'
+ }));
+ }, []);
+
+ // Save sort preferences whenever they change
+ useEffect(() => {
+ localStorage.setItem(STORAGE_KEY_SORT, JSON.stringify(sortConfig));
+ }, [sortConfig]);
+
+ return {
+ sortConfig,
+ setSortConfig,
+ requestSort
+ };
+};
+
+export default useSortManagement;
\ No newline at end of file