mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-24 12:13:28 +00:00
Enhance NetworkTableHeader with improved column rendering, intelligent sizing, and better drag-and-drop UX
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user