Enhance NetworkTableHeader with improved column rendering, intelligent sizing, and better drag-and-drop UX

This commit is contained in:
courtmanr@gmail.com
2025-03-15 19:55:20 +00:00
parent ee4768ce95
commit 3060f1b8fd
@@ -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 = () => (
<Box sx={{
ml: 0.5,
display: 'flex',
alignItems: 'center',
width: 20, // Fixed width for the sort indicator
height: 20, // Fixed height
visibility: sortConfig?.key === column.id ? 'visible' : 'hidden' // Hide but preserve space
}}>
{sortConfig?.key === column.id ? (
sortConfig.direction === 'asc' ? (
<KeyboardArrowUpIcon fontSize="small" />
) : (
<KeyboardArrowDownIcon fontSize="small" />
)
) : (
// Invisible placeholder to reserve space
<KeyboardArrowUpIcon fontSize="small" sx={{ opacity: 0 }} />
)}
</Box>
);
// 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 (
<Box sx={{
position: 'relative',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
flexGrow: 1,
cursor: 'pointer'
}}
onClick={handleTextClick}
>
{/* Removed search filter icon */}
<Typography {...titleTypographyProps}>
{column.label || column.id}
</Typography>
<SortIndicator />
{/* Threshold indicator */}
{hasActiveFilter(column.id) && (
<Typography
variant="caption"
sx={{
ml: 0.5,
color: theme.palette.primary.main,
fontWeight: 500
}}
>
({formatPercentage(getFilterValue(column.id))}+)
</Typography>
)}
</Box>
</Box>
);
};
return (
<TableHead>
<TableRow>
@@ -388,246 +693,101 @@ const NetworkTableHeader = ({
{visibleColumns.map(column => (
<TableCell
key={column.id}
draggable={true}
onDragStart={(e) => 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' ? (
<Box
sx={{
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Typography
variant="body2"
onClick={handleNodeColumnTextClick}
sx={{
fontWeight: sortConfig?.key === column.id ? 600 : 400,
color: sortConfig?.key === column.id
? theme.palette.mode === 'dark'
? '#ffffff'
: theme.palette.primary.main
: 'inherit',
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
}}
>
{column.label || column.id}
</Typography>
<FilterListIcon
fontSize="small"
onClick={(e) => 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)
}
}}
/>
</Box>
) : column.id === 'status' ? (
<Box
sx={{
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Typography
variant="body2"
onClick={handleStatusColumnTextClick}
sx={{
fontWeight: sortConfig?.key === column.id ? 600 : 400,
color: sortConfig?.key === column.id
? theme.palette.mode === 'dark'
? '#ffffff'
: theme.palette.primary.main
: 'inherit',
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
}}
>
{column.label || column.id}
</Typography>
<FilterListIcon
fontSize="small"
onClick={(e) => 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)
}
}}
/>
</Box>
) : column.id === 'type' ? (
<Box
sx={{
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Typography
variant="body2"
onClick={handleTypeColumnTextClick}
sx={{
fontWeight: sortConfig?.key === column.id ? 600 : 400,
color: sortConfig?.key === column.id
? theme.palette.mode === 'dark'
? '#ffffff'
: theme.palette.primary.main
: 'inherit',
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
}}
>
{column.label || column.id}
</Typography>
<FilterListIcon
fontSize="small"
onClick={(e) => 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)
}
}}
/>
</Box>
) : column.id === 'name' || column.id === 'id' ? (
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
height: '100%'
}}>
<Box sx={{
display: 'flex',
alignItems: 'center',
alignItems: 'center',
position: 'relative',
width: '100%',
justifyContent: 'space-between'
height: '100%'
}}>
<Typography
variant="body2"
sx={{
fontWeight: sortConfig?.key === column.id ? 600 : 400,
color: sortConfig?.key === column.id
? theme.palette.mode === 'dark'
? '#ffffff'
: theme.palette.primary.main
: 'inherit',
display: 'flex',
alignItems: 'center'
}}
>
{column.label || column.id}
{sortConfig?.key === column.id && (
<Box component="span" sx={{
ml: 0.5,
display: 'flex',
alignItems: 'center',
fontSize: '0.7rem',
color: theme.palette.mode === 'dark'
? '#ffffff'
: theme.palette.primary.main
}}>
{sortConfig.direction === 'asc' ? '▲' : '▼'}
</Box>
)}
</Typography>
{renderColumnContent(column)}
</Box>
) : (
<Box sx={{
display: 'flex',
alignItems: 'center',
width: '100%'
}}>
<Typography
variant="body2"
sx={{
fontWeight: sortConfig?.key === column.id ? 600 : 400,
color: sortConfig?.key === column.id
? theme.palette.mode === 'dark'
? '#ffffff'
: theme.palette.primary.main
: 'inherit',
display: 'flex',
alignItems: 'center'
}}
>
{column.label || column.id}
{sortConfig?.key === column.id && (
<Box component="span" sx={{
ml: 0.5,
display: 'flex',
alignItems: 'center',
fontSize: '0.7rem',
color: theme.palette.mode === 'dark'
? '#ffffff'
: theme.palette.primary.main
}}>
{sortConfig.direction === 'asc' ? '▲' : '▼'}
</Box>
)}
</Typography>
</Box>
)}
</Box>
</TableCell>
))}
</>
@@ -963,9 +1123,11 @@ const NetworkTableHeader = ({
sx: {
borderRadius: 2,
overflow: 'hidden',
width: 300
width: 300,
maxHeight: '80vh'
}
}}
sx={{ zIndex: 1500 }}
>
<Box sx={{ px: 2, py: 1, borderBottom: '1px solid', borderColor: 'divider' }}>
<Typography variant="subtitle2" gutterBottom>
@@ -1043,29 +1205,6 @@ const NetworkTableHeader = ({
justifyContent: 'space-between'
}}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box
className="drag-handle"
draggable="true"
onDragStart={(e) => {
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()}
>
<DragIndicatorIcon fontSize="small" />
</Box>
<Typography
variant="body2"
sx={{
@@ -1110,22 +1249,30 @@ const NetworkTableHeader = ({
);
};
// Update min width function to account for sorting icon
const getMinWidthForColumn = (columnId) => {
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;