mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-24 12:13:28 +00:00
759 lines
24 KiB
React
759 lines
24 KiB
React
import React, { useEffect, useMemo, useLayoutEffect } from 'react';
|
|
import useSocket from '../../hooks/useSocket';
|
|
import useFormattedMetrics from '../../hooks/useFormattedMetrics';
|
|
import useMockMetrics from '../../hooks/useMockMetrics';
|
|
import { useThemeContext } from '../../context/ThemeContext';
|
|
import { Box, CircularProgress, useTheme, Typography, Button, IconButton, Badge, Tooltip } from '@mui/material';
|
|
import ViewColumnIcon from '@mui/icons-material/ViewColumn';
|
|
import { createPortal } from 'react-dom';
|
|
|
|
// Import hooks
|
|
import {
|
|
useNetworkFilters,
|
|
useColumnManagement,
|
|
usePopoverManagement,
|
|
useNotifications,
|
|
useSortManagement,
|
|
useKeyboardShortcuts,
|
|
useActiveFilteredColumns,
|
|
useDataProcessing
|
|
} from './hooks';
|
|
|
|
// Import components
|
|
import ConnectionErrorDisplay from './ConnectionErrorDisplay';
|
|
import {
|
|
NetworkHeader,
|
|
NetworkPopovers,
|
|
NetworkNotification,
|
|
NetworkTable
|
|
} from './components';
|
|
|
|
import { useSearchContext } from '../../context/SearchContext';
|
|
|
|
const NetworkDisplay = ({ selectedNode = 'all' }) => {
|
|
const {
|
|
isConnected,
|
|
guestData,
|
|
metricsData,
|
|
processedMetricsData,
|
|
forceUpdateCounter: socketUpdateCounter,
|
|
error,
|
|
connectionStatus,
|
|
reconnect,
|
|
nodeData
|
|
} = useSocket();
|
|
|
|
// Debug logging
|
|
console.log('NetworkDisplay - selectedNode:', selectedNode);
|
|
console.log('NetworkDisplay - guestData:', guestData?.length || 0, 'guests');
|
|
console.log('NetworkDisplay - nodeData:', nodeData?.length || 0, 'nodes');
|
|
|
|
// Add useEffect to log detailed data for debugging
|
|
React.useEffect(() => {
|
|
if (guestData && guestData.length > 0) {
|
|
console.log('Guest data sample:', guestData[0]);
|
|
console.log('All guests:', guestData);
|
|
} else {
|
|
console.warn('No guest data available');
|
|
}
|
|
|
|
if (nodeData && nodeData.length > 0) {
|
|
console.log('Node data sample:', nodeData[0]);
|
|
} else {
|
|
console.warn('No node data available');
|
|
}
|
|
|
|
if (metricsData && metricsData.length > 0) {
|
|
console.log('Metrics data sample:', metricsData[0]);
|
|
} else {
|
|
console.warn('No metrics data available');
|
|
}
|
|
}, [guestData, nodeData, metricsData]);
|
|
|
|
// Prepare availableNodes for dropdown
|
|
const availableNodes = useMemo(() => {
|
|
// Start with the "All Nodes" option
|
|
const nodes = [
|
|
{ id: 'all', name: 'All Nodes', count: 0 }
|
|
];
|
|
|
|
// Add nodes from the nodeData
|
|
if (nodeData && nodeData.length > 0) {
|
|
// Add each node
|
|
nodeData.forEach(node => {
|
|
nodes.push({
|
|
id: node.id,
|
|
name: node.name || node.id,
|
|
count: 0
|
|
});
|
|
});
|
|
|
|
// Count guests per node
|
|
if (guestData && guestData.length > 0) {
|
|
// Initialize counts
|
|
const nodeCounts = {};
|
|
nodes.forEach(node => {
|
|
nodeCounts[node.id] = 0;
|
|
});
|
|
|
|
// Count guests for each node
|
|
guestData.forEach(guest => {
|
|
if (guest.node) {
|
|
const normalizedNodeId = guest.node.replace('-', '');
|
|
if (nodeCounts[normalizedNodeId] !== undefined) {
|
|
nodeCounts[normalizedNodeId]++;
|
|
}
|
|
if (nodeCounts[guest.node] !== undefined) {
|
|
nodeCounts[guest.node]++;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update counts in nodes array
|
|
nodes.forEach(node => {
|
|
if (node.id !== 'all') {
|
|
node.count = nodeCounts[node.id] || 0;
|
|
}
|
|
});
|
|
|
|
// Update the "All Nodes" count
|
|
nodes[0].count = guestData.length;
|
|
}
|
|
}
|
|
|
|
return nodes;
|
|
}, [nodeData, guestData]);
|
|
|
|
// Handle node selection change
|
|
const handleNodeChange = (event) => {
|
|
// Check if we can access window.location
|
|
if (window && window.location) {
|
|
try {
|
|
// Create a URL with the selected node as a query parameter
|
|
const url = new URL(window.location);
|
|
const nodeValue = event.target.value;
|
|
|
|
// Update the URL with the new node value
|
|
if (nodeValue === 'all') {
|
|
// Remove the node parameter if 'all' is selected
|
|
url.searchParams.delete('node');
|
|
} else {
|
|
// Set the node parameter to the selected value
|
|
url.searchParams.set('node', nodeValue);
|
|
}
|
|
|
|
// Update the browser history without reloading the page
|
|
window.history.pushState({}, '', url);
|
|
|
|
// Dispatch a custom event to notify App.jsx about the node change
|
|
const nodeChangeEvent = new CustomEvent('nodeChange', {
|
|
detail: { node: nodeValue }
|
|
});
|
|
window.dispatchEvent(nodeChangeEvent);
|
|
|
|
console.log('Node selection changed to:', nodeValue);
|
|
} catch (error) {
|
|
console.error('Error updating URL with node selection:', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Handle status selection change
|
|
const handleStatusChange = (status) => {
|
|
if (window && window.location) {
|
|
try {
|
|
const url = new URL(window.location);
|
|
|
|
// Update the URL with the new status value
|
|
if (status === null) {
|
|
// Remove the status parameter if 'all' is selected
|
|
url.searchParams.delete('status');
|
|
} else if (status === false) {
|
|
// Set the status parameter to 'running'
|
|
url.searchParams.set('status', 'running');
|
|
} else {
|
|
// Set the status parameter to 'stopped'
|
|
url.searchParams.set('status', 'stopped');
|
|
}
|
|
|
|
// Update the browser history without reloading the page
|
|
window.history.pushState({}, '', url);
|
|
|
|
// Update the status filter
|
|
setShowStopped(status);
|
|
|
|
console.log('Status filter changed to:', status === null ? 'all' : status === false ? 'running' : 'stopped');
|
|
} catch (error) {
|
|
console.error('Error updating URL with status selection:', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Handle type selection change
|
|
const handleTypeChange = (type) => {
|
|
if (window && window.location) {
|
|
try {
|
|
const url = new URL(window.location);
|
|
|
|
// Update the URL with the new type value
|
|
if (type === 'all') {
|
|
// Remove the type parameter if 'all' is selected
|
|
url.searchParams.delete('type');
|
|
} else {
|
|
// Set the type parameter to the selected value
|
|
url.searchParams.set('type', type);
|
|
}
|
|
|
|
// Update the browser history without reloading the page
|
|
window.history.pushState({}, '', url);
|
|
|
|
// Update the type filter
|
|
setGuestTypeFilter(type);
|
|
|
|
console.log('Type filter changed to:', type);
|
|
} catch (error) {
|
|
console.error('Error updating URL with type selection:', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Check URL parameters on initial load
|
|
useEffect(() => {
|
|
if (window && window.location) {
|
|
try {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
// Check for status parameter
|
|
const statusParam = urlParams.get('status');
|
|
if (statusParam) {
|
|
if (statusParam === 'running') {
|
|
setShowStopped(false);
|
|
} else if (statusParam === 'stopped') {
|
|
setShowStopped(true);
|
|
}
|
|
}
|
|
|
|
// Check for type parameter
|
|
const typeParam = urlParams.get('type');
|
|
if (typeParam && (typeParam === 'vm' || typeParam === 'ct')) {
|
|
setGuestTypeFilter(typeParam);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error reading URL parameters:', error);
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
// Use the formatted metrics hook
|
|
const formattedMetrics = useFormattedMetrics(metricsData);
|
|
|
|
// Use mock metrics for testing - but only if we're not using real mock data from the server
|
|
const mockMetrics = useMockMetrics(guestData);
|
|
|
|
// Use notification hook - MOVED UP before it's used in the migration event listener
|
|
const {
|
|
snackbarOpen,
|
|
snackbarMessage,
|
|
snackbarSeverity,
|
|
handleSnackbarClose,
|
|
showNotification
|
|
} = useNotifications();
|
|
|
|
// Listen for migration events and show notifications
|
|
React.useEffect(() => {
|
|
const handleMigrationEvent = (event) => {
|
|
try {
|
|
console.log('Migration event received in NetworkDisplay:', event.detail);
|
|
|
|
const { guestId, guestName, fromNode, toNode } = event.detail;
|
|
|
|
// Get node names for better display
|
|
const fromNodeName = nodeData?.find(n => n.id === fromNode)?.name || fromNode;
|
|
const toNodeName = nodeData?.find(n => n.id === toNode)?.name || toNode;
|
|
|
|
// Show a notification about the migration
|
|
showNotification(
|
|
`Migration: ${guestName || 'Guest'} migrated from ${fromNodeName} to ${toNodeName}`,
|
|
'info'
|
|
);
|
|
|
|
// Show browser notification if supported and permission granted
|
|
if ('Notification' in window) {
|
|
// Check if permission is already granted
|
|
if (Notification.permission === 'granted') {
|
|
showBrowserNotification(guestName || 'Guest', fromNodeName, toNodeName);
|
|
}
|
|
// Ask for permission if not denied
|
|
else if (Notification.permission !== 'denied') {
|
|
Notification.requestPermission().then(permission => {
|
|
if (permission === 'granted') {
|
|
showBrowserNotification(guestName || 'Guest', fromNodeName, toNodeName);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
console.log(`Migration event: ${guestName} (${guestId}) migrated from ${fromNodeName} to ${toNodeName}`);
|
|
} catch (error) {
|
|
console.error('Error handling migration event:', error);
|
|
}
|
|
};
|
|
|
|
// Function to show browser notification
|
|
const showBrowserNotification = (guestName, fromNode, toNode) => {
|
|
try {
|
|
const notificationOptions = {
|
|
body: `${guestName} migrated from ${fromNode} to ${toNode}`,
|
|
icon: '/logo192.png',
|
|
tag: 'migration-event',
|
|
requireInteraction: false
|
|
};
|
|
|
|
new Notification('Guest Migration', notificationOptions);
|
|
} catch (error) {
|
|
console.error('Error showing browser notification:', error);
|
|
}
|
|
};
|
|
|
|
// Add event listener
|
|
window.addEventListener('migration', handleMigrationEvent);
|
|
|
|
// Clean up
|
|
return () => {
|
|
window.removeEventListener('migration', handleMigrationEvent);
|
|
};
|
|
}, [nodeData, showNotification]);
|
|
|
|
const combinedMetrics = useMemo(() => {
|
|
// When using the mock data server, always use the processed metrics
|
|
const useMockData = localStorage.getItem('use_mock_data') === 'true' ||
|
|
localStorage.getItem('MOCK_DATA_ENABLED') === 'true';
|
|
|
|
// Create a safe default metrics object structure in case data is missing
|
|
const safeDefaultMetrics = {
|
|
cpu: {},
|
|
memory: {},
|
|
disk: {},
|
|
network: {}
|
|
};
|
|
|
|
// Log the update for debugging
|
|
if (socketUpdateCounter % 20 === 0) {
|
|
console.log(`NetworkDisplay metrics refresh #${socketUpdateCounter}`, {
|
|
useMockData,
|
|
processedMetricsData: {
|
|
cpu: Object.keys(processedMetricsData?.cpu || {}).length,
|
|
memory: Object.keys(processedMetricsData?.memory || {}).length,
|
|
disk: Object.keys(processedMetricsData?.disk || {}).length,
|
|
network: Object.keys(processedMetricsData?.network || {}).length
|
|
}
|
|
});
|
|
}
|
|
|
|
if (useMockData) {
|
|
// Use processedMetricsData which is updated more frequently
|
|
return processedMetricsData || safeDefaultMetrics;
|
|
}
|
|
|
|
// Otherwise, fall back to the previous behavior
|
|
const hasRealCpuMetrics = formattedMetrics?.cpu && Object.keys(formattedMetrics.cpu).length > 0;
|
|
const hasRealMemoryMetrics = formattedMetrics?.memory && Object.keys(formattedMetrics.memory).length > 0;
|
|
const hasRealDiskMetrics = formattedMetrics?.disk && Object.keys(formattedMetrics.disk).length > 0;
|
|
const hasRealNetworkMetrics = formattedMetrics?.network && Object.keys(formattedMetrics.network).length > 0;
|
|
|
|
// If we have real metrics, use them; otherwise, use mock metrics
|
|
return {
|
|
cpu: hasRealCpuMetrics ? formattedMetrics.cpu : (mockMetrics?.cpu || {}),
|
|
memory: hasRealMemoryMetrics ? formattedMetrics.memory : (mockMetrics?.memory || {}),
|
|
disk: hasRealDiskMetrics ? formattedMetrics.disk : (mockMetrics?.disk || {}),
|
|
network: hasRealNetworkMetrics ? formattedMetrics.network : (mockMetrics?.network || {})
|
|
};
|
|
}, [formattedMetrics, mockMetrics, processedMetricsData, socketUpdateCounter]);
|
|
|
|
// Log metrics updates with the counter to verify it's changing
|
|
useEffect(() => {
|
|
console.log(`Metrics update #${socketUpdateCounter}`, {
|
|
combinedMetrics: {
|
|
cpu: Object.keys(combinedMetrics.cpu).length,
|
|
memory: Object.keys(combinedMetrics.memory).length,
|
|
disk: Object.keys(combinedMetrics.disk).length,
|
|
network: Object.keys(combinedMetrics.network).length
|
|
}
|
|
});
|
|
}, [socketUpdateCounter, combinedMetrics]);
|
|
|
|
const theme = useTheme();
|
|
const { darkMode } = useThemeContext();
|
|
|
|
// Use column management hook
|
|
const {
|
|
columnVisibility,
|
|
columnOrder,
|
|
setColumnOrder,
|
|
columnMenuAnchorEl,
|
|
openColumnMenu,
|
|
forceUpdateCounter,
|
|
toggleColumnVisibility,
|
|
resetColumnVisibility,
|
|
handleColumnMenuOpen,
|
|
handleColumnMenuClose
|
|
} = useColumnManagement(showNotification);
|
|
|
|
// Use sort management hook
|
|
const {
|
|
sortConfig,
|
|
requestSort
|
|
} = useSortManagement();
|
|
|
|
// Get search state from context instead of local state
|
|
const {
|
|
searchTerm,
|
|
setSearchTerm,
|
|
activeSearchTerms,
|
|
addSearchTerm,
|
|
removeSearchTerm,
|
|
clearSearchTerms
|
|
} = useSearchContext();
|
|
|
|
// Use the network filters hook but don't use its search state
|
|
const {
|
|
filters,
|
|
setFilters,
|
|
showStopped,
|
|
setShowStopped,
|
|
showFilters,
|
|
setShowFilters,
|
|
guestTypeFilter,
|
|
setGuestTypeFilter,
|
|
sliderDragging,
|
|
updateFilter,
|
|
handleSliderDragStart,
|
|
handleSliderDragEnd,
|
|
clearFilter,
|
|
resetFilters,
|
|
activeFilterCount
|
|
} = useNetworkFilters();
|
|
|
|
// Use popover management hook
|
|
const {
|
|
filterAnchorEl,
|
|
openFilters,
|
|
filterButtonRef,
|
|
handleFilterButtonClick,
|
|
handleCloseFilterPopover,
|
|
closeAllPopovers
|
|
} = usePopoverManagement();
|
|
|
|
// Update resetFilters to also clear search terms from context
|
|
const handleResetFilters = () => {
|
|
resetFilters();
|
|
clearSearchTerms();
|
|
};
|
|
|
|
// Use keyboard shortcuts hook with handleResetFilters instead of resetFilters
|
|
useKeyboardShortcuts({
|
|
openFilters,
|
|
openColumnMenu,
|
|
resetFilters: handleResetFilters,
|
|
closeAllPopovers,
|
|
showNotification
|
|
});
|
|
|
|
// Use data processing hook
|
|
const {
|
|
processedData,
|
|
getNodeName,
|
|
extractNumericId,
|
|
getNodeFilteredGuests,
|
|
formatPercentage,
|
|
formatNetworkRateForFilter
|
|
} = useDataProcessing({
|
|
guestData,
|
|
nodeData,
|
|
sortConfig,
|
|
filters,
|
|
showStopped,
|
|
selectedNode,
|
|
guestTypeFilter,
|
|
metricsData: combinedMetrics
|
|
});
|
|
|
|
// Add debug logging for sorting
|
|
useEffect(() => {
|
|
console.log('Current sort configuration:', sortConfig);
|
|
console.log('Processed data after sorting:', processedData?.length || 0, 'items');
|
|
}, [sortConfig, processedData]);
|
|
|
|
// Use active filtered columns hook
|
|
const activeFilteredColumns = useActiveFilteredColumns({
|
|
filters,
|
|
guestTypeFilter,
|
|
showStopped,
|
|
nodeData
|
|
});
|
|
|
|
// Add an effect to listen for search term events
|
|
useEffect(() => {
|
|
const handleSearchTermAction = (event) => {
|
|
const { term, action } = event.detail;
|
|
|
|
// Handle status: filters
|
|
if (term.startsWith('status:')) {
|
|
const status = term.split(':', 2)[1]?.trim();
|
|
if (action === 'add') {
|
|
if (status === 'running') {
|
|
setShowStopped(false);
|
|
} else if (status === 'stopped') {
|
|
setShowStopped(true);
|
|
}
|
|
} else if (action === 'remove') {
|
|
// Reset status filter
|
|
setShowStopped(null);
|
|
}
|
|
}
|
|
|
|
// Handle type: filters
|
|
if (term.startsWith('type:')) {
|
|
const type = term.split(':', 2)[1]?.trim();
|
|
if (action === 'add') {
|
|
if (type === 'vm' || type === 'qemu') {
|
|
setGuestTypeFilter('vm');
|
|
} else if (type === 'ct' || type === 'lxc' || type === 'container') {
|
|
setGuestTypeFilter('ct');
|
|
}
|
|
} else if (action === 'remove') {
|
|
// Reset type filter to "all"
|
|
setGuestTypeFilter('all');
|
|
}
|
|
}
|
|
|
|
// Handle node: filters
|
|
if (term.startsWith('node:')) {
|
|
const node = term.split(':', 2)[1]?.trim();
|
|
if (action === 'add') {
|
|
// Find the node in availableNodes
|
|
const foundNode = availableNodes.find(n =>
|
|
n.name.toLowerCase() === node ||
|
|
n.id.toLowerCase() === node
|
|
);
|
|
|
|
if (foundNode) {
|
|
// Dispatch a custom event to notify App.jsx about the node change
|
|
const nodeChangeEvent = new CustomEvent('nodeChange', {
|
|
detail: { node: foundNode.id }
|
|
});
|
|
window.dispatchEvent(nodeChangeEvent);
|
|
}
|
|
} else if (action === 'remove') {
|
|
// Reset node filter to "all"
|
|
const nodeChangeEvent = new CustomEvent('nodeChange', {
|
|
detail: { node: 'all' }
|
|
});
|
|
window.dispatchEvent(nodeChangeEvent);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener('searchTermAction', handleSearchTermAction);
|
|
|
|
return () => {
|
|
window.removeEventListener('searchTermAction', handleSearchTermAction);
|
|
};
|
|
}, [setShowStopped, setGuestTypeFilter, availableNodes]);
|
|
|
|
// State for portal root element
|
|
const [portalRoot, setPortalRoot] = React.useState(null);
|
|
|
|
// Use createPortal to place the column visibility button in the app header
|
|
useLayoutEffect(() => {
|
|
// Find the column visibility container in the app header
|
|
const columnVisibilityContainer = document.getElementById('column-visibility-app-header');
|
|
|
|
if (columnVisibilityContainer) {
|
|
// Create a div for the portal if it doesn't exist yet
|
|
let portalContainer = document.getElementById('column-visibility-portal');
|
|
if (!portalContainer) {
|
|
portalContainer = document.createElement('div');
|
|
portalContainer.id = 'column-visibility-portal';
|
|
// Clear previous content and append the portal container
|
|
columnVisibilityContainer.innerHTML = '';
|
|
columnVisibilityContainer.appendChild(portalContainer);
|
|
}
|
|
|
|
// Set the portal root for rendering
|
|
setPortalRoot(portalContainer);
|
|
|
|
// Clean up function
|
|
return () => {
|
|
setPortalRoot(null);
|
|
if (portalContainer && portalContainer.parentNode) {
|
|
portalContainer.parentNode.removeChild(portalContainer);
|
|
}
|
|
};
|
|
}
|
|
}, []);
|
|
|
|
// Render the column visibility button in the portal
|
|
const renderColumnVisibilityPortal = () => {
|
|
if (!portalRoot) return null;
|
|
|
|
// Count hidden columns for the badge
|
|
const hiddenColumnsCount = Object.values(columnVisibility).filter(config => !config.visible).length;
|
|
|
|
return createPortal(
|
|
<Tooltip title={hiddenColumnsCount > 0 ? `Show hidden columns (${hiddenColumnsCount})` : "Column visibility settings"}>
|
|
<IconButton
|
|
onClick={handleColumnMenuOpen}
|
|
color={openColumnMenu ? 'primary' : 'default'}
|
|
size="small"
|
|
aria-controls={openColumnMenu ? 'column-menu' : undefined}
|
|
aria-haspopup="true"
|
|
aria-expanded={openColumnMenu ? 'true' : undefined}
|
|
sx={{
|
|
borderRadius: 1,
|
|
p: 0.5,
|
|
bgcolor: hiddenColumnsCount > 0 ? 'rgba(255, 255, 255, 0.1)' : 'transparent',
|
|
color: 'white', // Make sure the icon is visible in the dark app bar
|
|
'&:hover': {
|
|
bgcolor: 'rgba(255, 255, 255, 0.2)'
|
|
}
|
|
}}
|
|
>
|
|
<Badge
|
|
badgeContent={hiddenColumnsCount}
|
|
color="error"
|
|
invisible={hiddenColumnsCount === 0}
|
|
>
|
|
<ViewColumnIcon />
|
|
</Badge>
|
|
</IconButton>
|
|
</Tooltip>,
|
|
portalRoot
|
|
);
|
|
};
|
|
|
|
// Show loading state if not connected
|
|
if (!isConnected) {
|
|
if (error || connectionStatus === 'disconnected' || connectionStatus === 'error') {
|
|
// Show connection error
|
|
return (
|
|
<ConnectionErrorDisplay
|
|
connectionStatus={connectionStatus}
|
|
error={error}
|
|
onReconnect={reconnect}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
// If we have no data but we're connected, show a message
|
|
if (isConnected && (!guestData || guestData.length === 0)) {
|
|
return (
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
|
|
<Typography variant="h5" gutterBottom>No guest data available</Typography>
|
|
<Typography variant="body1" color="text.secondary">
|
|
The application is connected but no guest data was received.
|
|
</Typography>
|
|
<Button
|
|
variant="contained"
|
|
color="primary"
|
|
sx={{ mt: 2 }}
|
|
onClick={() => {
|
|
if (isConnected) {
|
|
// Request fresh data
|
|
reconnect();
|
|
}
|
|
}}
|
|
>
|
|
Refresh Data
|
|
</Button>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ width: '100%' }}>
|
|
{/* Render the column visibility button portal */}
|
|
{renderColumnVisibilityPortal()}
|
|
|
|
{/* Column Menu - Ensure it's still rendered to handle the actual menu */}
|
|
<Box sx={{ position: 'absolute', top: 0, left: 0, visibility: 'hidden' }}>
|
|
{openColumnMenu && (
|
|
<span
|
|
ref={(node) => {
|
|
if (node && !columnMenuAnchorEl) {
|
|
handleColumnMenuOpen({ currentTarget: node });
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Popovers */}
|
|
<NetworkPopovers
|
|
// Filter popover props
|
|
filterAnchorEl={filterAnchorEl}
|
|
openFilters={openFilters}
|
|
handleCloseFilterPopover={handleCloseFilterPopover}
|
|
filters={filters}
|
|
updateFilter={updateFilter}
|
|
handleSliderDragStart={handleSliderDragStart}
|
|
handleSliderDragEnd={handleSliderDragEnd}
|
|
resetFilters={handleResetFilters}
|
|
|
|
// Formatters
|
|
formatPercentage={formatPercentage}
|
|
formatNetworkRateForFilter={formatNetworkRateForFilter}
|
|
/>
|
|
|
|
{/* Main data table */}
|
|
<NetworkTable
|
|
sortConfig={sortConfig}
|
|
requestSort={requestSort}
|
|
columnVisibility={columnVisibility}
|
|
toggleColumnVisibility={toggleColumnVisibility}
|
|
resetColumnVisibility={resetColumnVisibility}
|
|
columnMenuAnchorEl={columnMenuAnchorEl}
|
|
handleColumnMenuOpen={handleColumnMenuOpen}
|
|
handleColumnMenuClose={handleColumnMenuClose}
|
|
openColumnMenu={openColumnMenu}
|
|
forceUpdateCounter={forceUpdateCounter}
|
|
columnOrder={columnOrder}
|
|
setColumnOrder={setColumnOrder}
|
|
activeFilteredColumns={activeFilteredColumns}
|
|
sortedAndFilteredData={processedData}
|
|
guestData={guestData}
|
|
metricsData={combinedMetrics}
|
|
getNodeName={getNodeName}
|
|
extractNumericId={extractNumericId}
|
|
resetFilters={resetFilters}
|
|
showStopped={showStopped}
|
|
setShowStopped={setShowStopped}
|
|
guestTypeFilter={guestTypeFilter}
|
|
setGuestTypeFilter={setGuestTypeFilter}
|
|
availableNodes={availableNodes}
|
|
selectedNode={selectedNode}
|
|
handleNodeChange={handleNodeChange}
|
|
handleStatusChange={handleStatusChange}
|
|
handleTypeChange={handleTypeChange}
|
|
// Pass filter-related props
|
|
filters={filters}
|
|
updateFilter={updateFilter}
|
|
handleFilterButtonClick={handleFilterButtonClick}
|
|
filterButtonRef={filterButtonRef}
|
|
openFilters={openFilters}
|
|
handleCloseFilterPopover={handleCloseFilterPopover}
|
|
/>
|
|
|
|
{/* Notification Snackbar */}
|
|
<NetworkNotification
|
|
snackbarOpen={snackbarOpen}
|
|
snackbarMessage={snackbarMessage}
|
|
snackbarSeverity={snackbarSeverity}
|
|
handleSnackbarClose={handleSnackbarClose}
|
|
/>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default NetworkDisplay; |