diff --git a/frontend/src/components/network/NetworkTableHeader.jsx b/frontend/src/components/network/NetworkTableHeader.jsx index 1153f9022..382774016 100644 --- a/frontend/src/components/network/NetworkTableHeader.jsx +++ b/frontend/src/components/network/NetworkTableHeader.jsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState, useEffect, useCallback } from 'react'; import { TableHead, TableRow, @@ -19,10 +19,15 @@ import { ListItemText, Select, FormControl, - Popover + Popover, + Fade, + Grow, + Slider } from '@mui/material'; import ViewColumnIcon from '@mui/icons-material/ViewColumn'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import TuneIcon from '@mui/icons-material/Tune'; import CheckIcon from '@mui/icons-material/Check'; import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; import CircleIcon from '@mui/icons-material/Circle'; @@ -33,14 +38,22 @@ import ViewListIcon from '@mui/icons-material/ViewList'; import DnsIcon from '@mui/icons-material/Dns'; import ComputerIcon from '@mui/icons-material/Computer'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import FilterListIcon from '@mui/icons-material/FilterList'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import StopIcon from '@mui/icons-material/Stop'; import AllInclusiveIcon from '@mui/icons-material/AllInclusive'; import StorageIcon from '@mui/icons-material/Storage'; import SearchIcon from '@mui/icons-material/Search'; +import CloseIcon from '@mui/icons-material/Close'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; import { DEFAULT_COLUMN_CONFIG } from '../../constants/networkConstants'; import { calculateDynamicColumnWidths } from '../../utils/networkUtils'; +import { useSearchContext } from '../../context/SearchContext'; +import { useSnackbar } from 'notistack'; + +// Modify the Resizer component to be a no-op since we're removing manual resizing +const Resizer = () => null; const NetworkTableHeader = ({ sortConfig, @@ -66,11 +79,104 @@ const NetworkTableHeader = ({ handleNodeChange = () => {}, handleStatusChange = () => {}, handleTypeChange = () => {}, - activeSearchTerms = [], - addSearchTerm = () => {}, - removeSearchTerm = () => {} + filters = {}, + updateFilter = () => {}, + handleFilterButtonClick = () => {}, + filterButtonRef = null, + openFilters = false, + handleCloseFilterPopover = () => {}, }) => { const theme = useTheme(); + const { enqueueSnackbar } = useSnackbar(); + + // Remove resize state and localStorage logic since we're eliminating manual resizing + // Instead, we'll rely on intelligent auto-sizing + + // Define column groups for sizing strategy + const fixedNarrowColumns = ['type', 'id', 'status']; // Very narrow columns + const fixedWidthColumns = ['download', 'upload', 'uptime']; // Fixed width for network stats + const autoSizeColumns = ['name', 'node']; // Auto-size to content + const flexibleEqualColumns = ['cpu', 'memory', 'disk']; // Equal width with progress bars + + // Calculate visible columns and their groups + const getVisibleColumnGroups = useCallback(() => { + if (!columnOrder || !Array.isArray(columnOrder) || columnOrder.length === 0) { + return { fixedNarrow: [], fixedWidth: [], autoSize: [], flexibleEqual: [] }; + } + + const visibleColumnIds = columnOrder.filter(id => columnVisibility[id]?.visible); + + return { + fixedNarrow: visibleColumnIds.filter(id => fixedNarrowColumns.includes(id)), + fixedWidth: visibleColumnIds.filter(id => fixedWidthColumns.includes(id)), + autoSize: visibleColumnIds.filter(id => autoSizeColumns.includes(id)), + flexibleEqual: visibleColumnIds.filter(id => flexibleEqualColumns.includes(id)) + }; + }, [columnOrder, columnVisibility]); + + // Intelligent column width calculation + const getColumnWidth = useCallback((columnId) => { + // Define fixed widths for specific columns + const fixedWidths = { + type: '50px', // Just "VM" or "CT" + id: '60px', // Just numeric IDs + status: '50px', // Just the status circle + download: '90px', // Network rates + upload: '90px', // Network rates + uptime: '90px' // Time display + }; + + // If column has a fixed width, return it + if (fixedWidths[columnId]) { + return fixedWidths[columnId]; + } + + // Get all visible column groups + const columnGroups = getVisibleColumnGroups(); + + // For auto-sized columns, use different strategies for name vs node + if (autoSizeColumns.includes(columnId)) { + // Use max-content for node to fit content more closely + if (columnId === 'node') { + return 'max-content'; + } + // Use auto for name which can be longer + return 'auto'; + } + + // For flexible equal columns (cpu, memory, disk), calculate percentage + if (flexibleEqualColumns.includes(columnId)) { + // Calculate how many flexible columns are visible + const visibleFlexColumns = columnGroups.flexibleEqual.length; + + if (visibleFlexColumns > 0) { + // Calculate total width taken by fixed columns + // (This is an approximation since auto columns are unknown) + const approximateFixedWidth = + (columnGroups.fixedNarrow.length * 60) + + (columnGroups.fixedWidth.length * 90) + + (columnGroups.autoSize.length * 120); // Rough estimate for auto columns + + // Approximate remaining percentage for flexible columns + // Assuming table width is roughly 1000px (this is just a heuristic) + const remainingPercentage = Math.max(10, (1000 - approximateFixedWidth) / 10); // in % + + // Divide equally among visible flexible columns + return `${Math.floor(remainingPercentage / visibleFlexColumns)}%`; + } + + // Fallback if something goes wrong + return '20%'; + } + + // Default fallback + return 'auto'; + }, [getVisibleColumnGroups]); + + // Helper to check column type + const isFlexibleColumn = (columnId) => { + return flexibleEqualColumns.includes(columnId); + }; const [nodeMenuAnchorEl, setNodeMenuAnchorEl] = React.useState(null); const openNodeMenu = Boolean(nodeMenuAnchorEl); @@ -81,10 +187,8 @@ const NetworkTableHeader = ({ const [typeMenuAnchorEl, setTypeMenuAnchorEl] = React.useState(null); const openTypeMenu = Boolean(typeMenuAnchorEl); - const handleNodeColumnClick = (event) => { - event.stopPropagation(); - setNodeMenuAnchorEl(event.currentTarget); - }; + // Get search state from context + const { activeSearchTerms, addSearchTerm, removeSearchTerm } = useSearchContext(); const handleNodeMenuClose = () => { setNodeMenuAnchorEl(null); @@ -110,7 +214,7 @@ const NetworkTableHeader = ({ // Add the new node filter as a search term addSearchTerm(`node:${nodeName}`); - } else { + } else { // Remove any node: filters when "all" is selected const existingNodeTerms = activeSearchTerms.filter(term => term.toLowerCase().startsWith('node:') @@ -122,11 +226,6 @@ const NetworkTableHeader = ({ handleNodeMenuClose(); }; - - const handleStatusColumnClick = (event) => { - event.stopPropagation(); - setStatusMenuAnchorEl(event.currentTarget); - }; const handleStatusMenuClose = () => { setStatusMenuAnchorEl(null); @@ -158,11 +257,6 @@ const NetworkTableHeader = ({ handleStatusMenuClose(); }; - - const handleTypeColumnClick = (event) => { - event.stopPropagation(); - setTypeMenuAnchorEl(event.currentTarget); - }; const handleTypeMenuClose = () => { setTypeMenuAnchorEl(null); @@ -198,27 +292,12 @@ const NetworkTableHeader = ({ const isSortingEnabledForType = guestTypeFilter === 'all'; - const handleColumnClick = (columnId) => { + const handleColumnClick = (columnId, forcedDirection) => { // For normal columns, just requestSort - requestSort(columnId); - }; - - // New handler for node column text click that sorts by node - const handleNodeColumnTextClick = (e) => { - e.stopPropagation(); // Prevent opening the dropdown - requestSort('node'); - }; - - // New handler for status column text click that sorts by status - const handleStatusColumnTextClick = (e) => { - e.stopPropagation(); // Prevent opening the dropdown - requestSort('status'); - }; - - // New handler for type column text click that sorts by type - const handleTypeColumnTextClick = (e) => { - e.stopPropagation(); // Prevent opening the dropdown - requestSort('type'); + console.log(`Requesting sort for column: ${columnId}`, forcedDirection ? `with forced direction: ${forcedDirection}` : ''); + + // Always pass the current columnId and the forcedDirection if provided + requestSort(columnId, forcedDirection); }; const visibleColumnCount = Object.values(columnVisibility).filter(col => col.visible).length; @@ -275,55 +354,81 @@ const NetworkTableHeader = ({ setDraggedColumn(columnId); setPreviewOrder([...columnOrder]); - const dragImage = document.createElement('div'); - dragImage.style.position = 'absolute'; - dragImage.style.width = '1px'; - dragImage.style.height = '1px'; - dragImage.style.top = '-1000px'; - document.body.appendChild(dragImage); + // Create a custom drag image + const draggedElement = e.currentTarget; + const rect = draggedElement.getBoundingClientRect(); - e.dataTransfer.setDragImage(dragImage, 0, 0); + const ghostElement = draggedElement.cloneNode(true); + ghostElement.style.width = `${rect.width}px`; + ghostElement.style.height = `${rect.height}px`; + ghostElement.style.backgroundColor = alpha(theme.palette.primary.main, 0.2); + ghostElement.style.boxShadow = `0 4px 8px ${alpha(theme.palette.primary.dark, 0.2)}`; + ghostElement.style.borderRadius = '4px'; + ghostElement.style.opacity = '0.8'; + ghostElement.style.position = 'absolute'; + ghostElement.style.top = '-1000px'; + ghostElement.style.left = '-1000px'; + ghostElement.style.zIndex = '9999'; + ghostElement.style.pointerEvents = 'none'; + + document.body.appendChild(ghostElement); + e.dataTransfer.setDragImage(ghostElement, rect.width / 2, rect.height / 2); + e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', columnId); + // Add a class to the body to show we're dragging + document.body.classList.add('column-dragging'); + setTimeout(() => { - document.body.removeChild(dragImage); + document.body.removeChild(ghostElement); }, 0); }; const handleDragOver = (e, columnId) => { e.preventDefault(); - if (draggedColumn !== columnId) { - setDragOverColumn(columnId); - - if (previewOrder && draggedColumn && columnId) { - const newPreviewOrder = [...previewOrder]; - const sourceIndex = newPreviewOrder.indexOf(draggedColumn); - const targetIndex = newPreviewOrder.indexOf(columnId); - - if (sourceIndex !== -1 && targetIndex !== -1) { - newPreviewOrder.splice(sourceIndex, 1); - newPreviewOrder.splice(targetIndex, 0, draggedColumn); - console.log('Preview order updated:', newPreviewOrder); - setPreviewOrder(newPreviewOrder); - } - } + if (draggedColumn === columnId) return; + + setDragOverColumn(columnId); + + // Add class to the element for highlight + const targetCell = e.currentTarget; + const dropPosition = getDropPosition(draggedColumn, columnId); + + // Remove any existing highlight classes first + targetCell.classList.remove('drop-highlight-before', 'drop-highlight-after'); + + // Add the appropriate highlight class + if (dropPosition === 'before') { + targetCell.classList.add('drop-highlight-before'); } else { - setDragOverColumn(null); + targetCell.classList.add('drop-highlight-after'); + } + + // Update the preview order if needed + if (columnOrder && draggedColumn && columnId !== draggedColumn) { + const fromIndex = columnOrder.indexOf(draggedColumn); + const toIndex = columnOrder.indexOf(columnId); + + if (fromIndex !== -1 && toIndex !== -1) { + const newOrder = [...columnOrder]; + newOrder.splice(fromIndex, 1); + newOrder.splice(toIndex, 0, draggedColumn); + setPreviewOrder(newOrder); + } } }; - const handleDrop = (e, targetColumnId) => { + const handleDrop = (e, columnId) => { e.preventDefault(); - if (previewOrder && draggedColumn) { - console.log('Setting column order from preview:', previewOrder); - setColumnOrder([...previewOrder]); - } - else if (draggedColumn && targetColumnId && draggedColumn !== targetColumnId) { + // Remove highlight classes + e.currentTarget.classList.remove('drop-highlight-before', 'drop-highlight-after'); + + if (draggedColumn !== columnId && columnOrder) { console.log('Using fallback drop logic'); const sourceIndex = columnOrder.indexOf(draggedColumn); - const targetIndex = columnOrder.indexOf(targetColumnId); + const targetIndex = columnOrder.indexOf(columnId); if (sourceIndex !== -1 && targetIndex !== -1) { const newOrder = [...columnOrder]; @@ -339,16 +444,31 @@ const NetworkTableHeader = ({ }; const handleDragEnd = () => { + // Remove all highlight classes from the table cells + document.querySelectorAll('.drop-highlight-before, .drop-highlight-after') + .forEach(el => { + el.classList.remove('drop-highlight-before', 'drop-highlight-after'); + }); + setDraggedColumn(null); setDragOverColumn(null); setPreviewOrder(null); + + // Remove the dragging class + document.body.classList.remove('column-dragging'); }; - const getDropPosition = (draggedId, targetId) => { - if (!draggedId || !targetId || draggedId === targetId) return null; - const draggedIndex = columnOrder.indexOf(draggedId); + const getDropPosition = (sourceId, targetId) => { + if (!columnOrder || !sourceId || !targetId) return 'before'; + + const sourceIndex = columnOrder.indexOf(sourceId); const targetIndex = columnOrder.indexOf(targetId); - return draggedIndex < targetIndex ? 'after' : 'before'; + + if (sourceIndex === -1 || targetIndex === -1) return 'before'; + + // If source comes before target in the original order, drop after + // If source comes after target in the original order, drop before + return sourceIndex < targetIndex ? 'after' : 'before'; }; const displayOrder = previewOrder || columnOrder; @@ -356,6 +476,191 @@ const NetworkTableHeader = ({ const selectedNodeObject = availableNodes.find(node => node.id === selectedNode); const selectedNodeName = selectedNodeObject ? selectedNodeObject.name : 'All Nodes'; + const showNotification = (message, variant = 'info') => { + enqueueSnackbar(message, { + variant, + autoHideDuration: 3000, + anchorOrigin: { + vertical: 'top', + horizontal: 'center', + } + }); + }; + + // State for column header hover + const [hoveredColumn, setHoveredColumn] = useState(null); + + // Function to handle column header mouse enter + const handleColumnMouseEnter = (columnId) => () => { + setHoveredColumn(columnId); + }; + + // Function to handle column header mouse leave + const handleColumnMouseLeave = () => { + setHoveredColumn(null); + }; + + // Function to hide column + const hideColumn = (columnId, e) => { + if (e) e.stopPropagation(); + toggleColumnVisibility(columnId); + setHoveredColumn(null); + }; + + // Handle opening resource-specific filter + const handleResourceFilterClick = (event, resourceType) => { + event.stopPropagation(); + + // Direct reference to the main filter button's click handler + handleFilterButtonClick(event); + + // Update filter to match the resource type + setTimeout(() => { + // Focus on the specific resource slider + const sliderElement = document.getElementById(`${resourceType}-filter-slider`); + if (sliderElement) { + sliderElement.focus(); + } + }, 100); + }; + + // Format percentage for display + const formatPercentage = (value) => { + return `${value}%`; + }; + + // Function to get filter value by column ID + const getFilterValue = (columnId) => { + switch(columnId) { + case 'cpu': return filters.cpu || 0; + case 'memory': return filters.memory || 0; + case 'disk': return filters.disk || 0; + case 'download': return filters.download || 0; + case 'upload': return filters.upload || 0; + default: return 0; + } + }; + + // Check if a resource has an active filter + const hasActiveFilter = (columnId) => { + return getFilterValue(columnId) > 0; + }; + + // Function to render the column content based on the column type + const renderColumnContent = (column) => { + // Check if this column is currently being hovered over + const isHovered = hoveredColumn === column.id; + + // Check if this column is a resource column that can have thresholds + const isResourceColumn = ['cpu', 'memory', 'disk', 'download', 'upload'].includes(column.id); + + // Check if this column has an active search filter + const hasSearchFilter = activeFilteredColumns[column.id]; + + // Common sort indicator component for all column types + const SortIndicator = () => ( + + {sortConfig?.key === column.id ? ( + sortConfig.direction === 'asc' ? ( + + ) : ( + + ) + ) : ( + // Invisible placeholder to reserve space + + )} + + ); + + // Common column title styling + const titleTypographyProps = { + variant: "body2", + sx: { + fontWeight: sortConfig?.key === column.id || hasSearchFilter ? 600 : 400, + color: hasSearchFilter + ? theme.palette.primary.main + : sortConfig?.key === column.id + ? theme.palette.mode === 'dark' + ? '#ffffff' + : theme.palette.primary.main + : 'inherit' + } + }; + + // Common text click handler for ALL columns + const handleTextClick = (e) => { + e.stopPropagation(); + + // For resource columns, default to descending sort first time + if (isResourceColumn && (!sortConfig || sortConfig.key !== column.id)) { + console.log(`Resource column ${column.id} clicked - defaulting to DESC sort`); + requestSort(column.id, 'desc'); + } else { + // First check if this is the currently sorted column + if (sortConfig && sortConfig.key === column.id) { + // If already sorted, explicitly toggle direction + const newDirection = sortConfig.direction === 'asc' ? 'desc' : 'asc'; + console.log(`Column ${column.id} clicked - toggling from ${sortConfig.direction} to ${newDirection}`); + requestSort(column.id, newDirection); + } else { + // New column sort, use default toggle + console.log(`Column ${column.id} clicked - new sort`); + requestSort(column.id); + } + } + }; + + // Render the appropriate column content + return ( + + + {/* Removed search filter icon */} + + + {column.label || column.id} + + + + {/* Threshold indicator */} + {hasActiveFilter(column.id) && ( + + ({formatPercentage(getFilterValue(column.id))}+) + + )} + + + ); + }; + return ( @@ -388,246 +693,101 @@ const NetworkTableHeader = ({ {visibleColumns.map(column => ( handleDragStart(e, column.id)} + onDragOver={(e) => handleDragOver(e, column.id)} + onDrop={(e) => handleDrop(e, column.id)} + onDragEnd={handleDragEnd} onClick={(e) => { - // For special columns, don't do anything on the cell click - // since we're handling clicks on the components inside - if (column.id !== 'node' && column.id !== 'status' && column.id !== 'type') { - handleColumnClick(column.id); + // Only handle direct cell clicks - all columns now just sort, no dropdown functionality + if (e.target === e.currentTarget) { + const handleTextClick = (columnId) => { + // For resource columns, default to descending sort first time + const isResourceColumn = ['cpu', 'memory', 'disk', 'download', 'upload'].includes(columnId); + + if (isResourceColumn && (!sortConfig || sortConfig.key !== columnId)) { + console.log(`Resource column ${columnId} clicked - defaulting to DESC sort`); + requestSort(columnId, 'desc'); + } else { + // First check if this is the currently sorted column + if (sortConfig && sortConfig.key === columnId) { + // If already sorted, explicitly toggle direction + const newDirection = sortConfig.direction === 'asc' ? 'desc' : 'asc'; + console.log(`Column ${columnId} clicked - toggling from ${sortConfig.direction} to ${newDirection}`); + requestSort(columnId, newDirection); + } else { + // New column sort, use default toggle + console.log(`Column ${columnId} clicked - new sort`); + requestSort(columnId); + } + } + }; + + // All columns handle sorting the same way + handleTextClick(column.id); } }} + onMouseEnter={handleColumnMouseEnter(column.id)} + onMouseLeave={handleColumnMouseLeave} sx={{ - width: columnWidths[column.id] || 'auto', - minWidth: getMinWidthForColumn(column.id), - backgroundColor: theme.palette.background.paper, + width: getColumnWidth(column.id), + minWidth: `${getMinWidthForColumn(column.id)}px`, + maxWidth: autoSizeColumns.includes(column.id) ? '300px' : 'none', + padding: fixedNarrowColumns.includes(column.id) ? '0px 8px' : '16px 8px', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + backgroundColor: draggedColumn === column.id + ? alpha(theme.palette.primary.main, 0.1) + : dragOverColumn === column.id + ? alpha(theme.palette.primary.main, 0.05) + : theme.palette.background.paper, borderBottom: '1px solid', borderBottomColor: 'divider', borderTop: 'none', - boxShadow: 'none', - ...((column.id === 'status' || column.id === 'type') && { + boxShadow: draggedColumn === column.id + ? `0 0 8px ${alpha(theme.palette.primary.main, 0.5)}` + : 'none', + ...(fixedNarrowColumns.includes(column.id) && { textAlign: 'center', padding: '0px 8px' }), - cursor: 'pointer', - '&:hover': { - backgroundColor: theme.palette.mode === 'dark' - ? alpha(theme.palette.primary.light, 0.15) - : alpha(theme.palette.primary.light, 0.1) + cursor: 'grab', + userSelect: 'auto', + position: 'relative', + transition: 'background-color 0.2s, transform 0.1s, box-shadow 0.2s', + + backgroundColor: hoveredColumn === column.id + ? alpha(theme.palette.primary.light, 0.1) + : draggedColumn === column.id + ? alpha(theme.palette.primary.main, 0.15) + : alpha(theme.palette.primary.light, 0.05), + '& .drag-handle': { + visibility: 'visible', + opacity: 0.7 + }, + '&:active': { + cursor: 'grabbing', + backgroundColor: alpha(theme.palette.primary.main, 0.15) } }} > - {column.id === 'node' ? ( - - - {column.label || column.id} - - handleNodeColumnClick(e)} - sx={{ - fontSize: '1rem', - opacity: activeFilteredColumns['node'] ? 1 : 0.7, - ml: 0.5, - color: activeFilteredColumns['node'] - ? theme.palette.primary.main - : 'inherit', - cursor: 'pointer', - p: 0.3, - borderRadius: '50%', - backgroundColor: activeFilteredColumns['node'] ? - alpha(theme.palette.primary.main, 0.1) : 'transparent', - '&:hover': { - backgroundColor: alpha(theme.palette.primary.main, 0.1) - } - }} - /> - - ) : column.id === 'status' ? ( - - - {column.label || column.id} - - handleStatusColumnClick(e)} - sx={{ - fontSize: '1rem', - opacity: activeFilteredColumns['status'] ? 1 : 0.7, - ml: 0.5, - color: activeFilteredColumns['status'] - ? theme.palette.primary.main - : 'inherit', - cursor: 'pointer', - p: 0.3, - borderRadius: '50%', - backgroundColor: activeFilteredColumns['status'] ? - alpha(theme.palette.primary.main, 0.1) : 'transparent', - '&:hover': { - backgroundColor: alpha(theme.palette.primary.main, 0.1) - } - }} - /> - - ) : column.id === 'type' ? ( - - - {column.label || column.id} - - handleTypeColumnClick(e)} - sx={{ - fontSize: '1rem', - opacity: activeFilteredColumns['type'] ? 1 : 0.7, - ml: 0.5, - color: activeFilteredColumns['type'] - ? theme.palette.primary.main - : 'inherit', - cursor: 'pointer', - p: 0.3, - borderRadius: '50%', - backgroundColor: activeFilteredColumns['type'] ? - alpha(theme.palette.primary.main, 0.1) : 'transparent', - '&:hover': { - backgroundColor: alpha(theme.palette.primary.main, 0.1) - } - }} - /> - - ) : column.id === 'name' || column.id === 'id' ? ( + - - {column.label || column.id} - {sortConfig?.key === column.id && ( - - {sortConfig.direction === 'asc' ? '▲' : '▼'} - - )} - + {renderColumnContent(column)} - ) : ( - - - {column.label || column.id} - {sortConfig?.key === column.id && ( - - {sortConfig.direction === 'asc' ? '▲' : '▼'} - - )} - - - )} + ))} @@ -963,9 +1123,11 @@ const NetworkTableHeader = ({ sx: { borderRadius: 2, overflow: 'hidden', - width: 300 + width: 300, + maxHeight: '80vh' } }} + sx={{ zIndex: 1500 }} > @@ -1043,29 +1205,6 @@ const NetworkTableHeader = ({ justifyContent: 'space-between' }}> - { - e.stopPropagation(); - handleDragStart(e, columnId); - }} - sx={{ - opacity: draggedColumn === columnId ? 1 : 0.3, - mr: 1, - cursor: draggedColumn === columnId ? 'grabbing' : 'grab', - display: 'flex', - alignItems: 'center', - color: draggedColumn === columnId ? 'primary.main' : 'text.secondary', - '&:hover': { - opacity: 1, - color: 'primary.main' - } - }} - onClick={(e) => e.stopPropagation()} - > - - { const minWidths = { - node: 70, - type: 40, - id: 60, - status: 40, - name: 130, - cpu: 100, - memory: 100, - disk: 100, - download: 90, - upload: 90, - uptime: 85 + // Fixed narrow columns - add a few pixels to accommodate sort icon + type: 50, // Very small - just "VM" or "CT" + id: 60, // Very small - just numeric IDs + status: 50, // Minimal - just an icon + + // Auto-sized columns + name: 120, // Names - minimum width + node: 80, // Node names - minimum width (reduced from 100) + + // Flexible equal columns + cpu: 100, // Medium - progress bar with percentage + memory: 140, // Progress bar with byte values + disk: 140, // Progress bar with byte values + + // Fixed width columns + download: 90, // Network rates + upload: 90, // Network rates + uptime: 90 // Time display }; - return minWidths[columnId] || 90; + return minWidths[columnId] || 80; }; export default NetworkTableHeader; \ No newline at end of file