diff --git a/frontend/src/components/network/NetworkTableRow.jsx b/frontend/src/components/network/NetworkTableRow.jsx
index db3b7e7bd..98b67116f 100644
--- a/frontend/src/components/network/NetworkTableRow.jsx
+++ b/frontend/src/components/network/NetworkTableRow.jsx
@@ -12,6 +12,9 @@ import {
import { StatusIndicator, ProgressWithLabel } from './UIComponents';
import { formatBytes, formatNetworkRate, formatUptime, formatPercentage, formatBytesWithUnit } from '../../utils/formatters';
import { calculateDynamicColumnWidths } from '../../utils/networkUtils';
+import { useSearchContext } from '../../context/SearchContext';
+import { useUserSettings } from '../../context/UserSettingsContext';
+import { getNodeColor, getNodeTextColor } from '../../utils/colorUtils';
const NetworkTableRow = ({
guest,
@@ -20,10 +23,131 @@ const NetworkTableRow = ({
getNodeName,
extractNumericId,
columnOrder,
- activeFilteredColumns = {}
+ activeFilteredColumns = {},
+ thresholdColumn,
+ thresholdValue
}) => {
const theme = useTheme();
+ // Get search terms for highlighting
+ const { searchTerm, activeSearchTerms } = useSearchContext();
+
+ // Get user settings for compact mode
+ const { getTableCellPadding } = useUserSettings();
+
+ // Get the node name for this guest
+ const nodeName = getNodeName(guest?.node);
+
+ // Generate a text color for the node name
+ const nodeTextColor = useMemo(() => {
+ return getNodeTextColor(nodeName, theme.palette.mode);
+ }, [nodeName, theme.palette.mode]);
+
+ // Combine all active search terms for highlighting
+ const allSearchTerms = useMemo(() => {
+ const terms = [...activeSearchTerms];
+ if (searchTerm && !terms.includes(searchTerm)) {
+ terms.push(searchTerm);
+ }
+
+ // Process compound terms (e.g., with spaces or |)
+ const processedTerms = [];
+ // Keep track of column-specific search terms and their associated values
+ const columnSearchTerms = {};
+
+ terms.forEach(term => {
+ const termLower = term.toLowerCase().trim();
+
+ // Skip empty terms
+ if (!termLower) return;
+
+ // Handle prefixed terms with colon (e.g., "node:pve-01")
+ if (termLower.includes(':')) {
+ const [prefix, value] = termLower.split(':', 2);
+ const prefixTrim = prefix.trim();
+
+ // Add the original term
+ processedTerms.push({
+ term: termLower,
+ isColumnSpecific: true,
+ columnId: prefixTrim
+ });
+
+ // Store the value for this column for highlighting only in that column
+ if (value && value.trim() !== '') {
+ if (!columnSearchTerms[prefixTrim]) {
+ columnSearchTerms[prefixTrim] = [];
+ }
+ columnSearchTerms[prefixTrim].push(value.trim());
+ }
+ } else if (termLower.split(' ').length > 1) {
+ // Handle OR search with spaces
+ const spaceTerms = termLower.split(' ').map(t => t.trim()).filter(t => t);
+ spaceTerms.forEach(t => {
+ processedTerms.push({
+ term: t,
+ isColumnSpecific: false
+ });
+ });
+ } else if (termLower.includes('|')) {
+ // Handle OR search with pipe character
+ const orTerms = termLower.split('|').map(t => t.trim()).filter(t => t);
+ orTerms.forEach(t => {
+ processedTerms.push({
+ term: t,
+ isColumnSpecific: false
+ });
+ });
+ } else {
+ processedTerms.push({
+ term: termLower,
+ isColumnSpecific: false
+ });
+ }
+ });
+
+ // For numerical terms, remove leading zeros
+ const finalTerms = processedTerms.map(item => {
+ if (/^\d+$/.test(item.term)) {
+ return {
+ ...item,
+ term: String(parseInt(item.term, 10))
+ };
+ }
+ return item;
+ });
+
+ return {
+ terms: finalTerms,
+ columnSearchTerms
+ };
+ }, [searchTerm, activeSearchTerms]);
+
+ // Function to check if text contains any of the search terms
+ const hasSearchMatch = (text, columnId) => {
+ if (!text || !allSearchTerms || !allSearchTerms.terms || allSearchTerms.terms.length === 0) {
+ return false;
+ }
+
+ // Convert to string to handle numeric values
+ const textStr = String(text || '').toLowerCase();
+
+ // Check for column-specific terms first
+ if (columnId && allSearchTerms.columnSearchTerms && allSearchTerms.columnSearchTerms[columnId]) {
+ // Look for matches from column-specific values
+ if (allSearchTerms.columnSearchTerms[columnId].some(term =>
+ textStr.includes(String(term).toLowerCase())
+ )) {
+ return true;
+ }
+ }
+
+ // Check non-column-specific terms
+ return allSearchTerms.terms
+ .filter(item => !item.isColumnSpecific) // Only use generic terms
+ .some(item => textStr.includes(String(item.term).toLowerCase()));
+ };
+
// Determine if the guest is running
const isRunning = guest.status?.toLowerCase() === 'running';
@@ -66,10 +190,27 @@ const NetworkTableRow = ({
// Get uptime
const uptime = guest.uptime ?? 0;
- // Calculate dynamic column widths based on visible columns
- const columnWidths = useMemo(() => {
- return calculateDynamicColumnWidths(columnVisibility);
- }, [columnVisibility]);
+ // Define column groups for sizing strategy (same as in header)
+ 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 = useMemo(() => {
+ 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]);
// Get visible columns in the correct order
const visibleColumns = useMemo(() => {
@@ -84,6 +225,107 @@ const NetworkTableRow = ({
.filter(Boolean);
}, [columnVisibility, columnOrder]);
+ // Helper function to get width for a column
+ const getColumnWidth = (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];
+ }
+
+ // 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), use percentage
+ if (flexibleEqualColumns.includes(columnId)) {
+ const visibleFlexColumns = getVisibleColumnGroups.flexibleEqual.length;
+
+ if (visibleFlexColumns > 0) {
+ // Use the same calculation as in the header
+ return `${Math.floor(100 / visibleFlexColumns)}%`;
+ }
+
+ // Fallback if something goes wrong
+ return '20%';
+ }
+
+ // Default fallback
+ return 'auto';
+ };
+
+ // Check if a column is a flexible column
+ const isFlexibleColumn = (columnId) => {
+ return flexibleEqualColumns.includes(columnId);
+ };
+
+ // Function to check if a cell contains content that matches search terms
+ const cellHasMatch = (columnId) => {
+ if (!columnId || !allSearchTerms || !allSearchTerms.terms || allSearchTerms.terms.length === 0) {
+ return false;
+ }
+
+ // Check for column-specific search terms first
+ const columnPrefixedTerms = allSearchTerms.terms.filter(item => {
+ if (!item.isColumnSpecific) return false;
+ return item.columnId === columnId;
+ });
+
+ // If we have column-specific terms targeting this column, it's a match
+ if (columnPrefixedTerms.length > 0) {
+ return true;
+ }
+
+ // Get the text content for this cell based on column ID
+ let cellText;
+ switch (columnId) {
+ case 'node':
+ cellText = getNodeName(guest?.node);
+ break;
+ case 'type':
+ cellText = isVM ? 'VM virtual machine' : 'CT container';
+ break;
+ case 'id':
+ cellText = extractNumericId(guest?.id);
+ break;
+ case 'status':
+ cellText = isRunning ? 'running' : 'stopped';
+ break;
+ case 'name':
+ cellText = guest?.name;
+ break;
+ case 'download':
+ cellText = formatNetworkRate(downloadRate);
+ break;
+ case 'upload':
+ cellText = formatNetworkRate(uploadRate);
+ break;
+ case 'uptime':
+ cellText = isRunning ? formatUptime(uptime) : '—';
+ break;
+ default:
+ cellText = '';
+ }
+
+ // Now check for general search terms that match this cell's content
+ return hasSearchMatch(cellText, columnId);
+ };
+
// Render cell content based on column ID
const renderCellContent = (columnId) => {
if (!columnId) return null;
@@ -94,21 +336,21 @@ const NetworkTableRow = ({
{getNodeName(guest?.node)}
- {isSharedGuest && isPrimaryNode && (
-
-
-
- )}
+ {isSharedGuest && isPrimaryNode && (
+
+
+
+ )}
);
case 'type':
@@ -211,6 +453,7 @@ const NetworkTableRow = ({
}
};
+ // Render the table row
return (
{/* Render cells based on column order */}
{Array.isArray(visibleColumns) && visibleColumns.length > 0 ? (
- visibleColumns.map(column => (
- column && (
-
- {renderCellContent(column.id)}
-
- )
- ))
+ visibleColumns.map(column => {
+ // Check if this cell has a matching search term
+ const hasMatch = cellHasMatch(column.id);
+
+ // Special handling for node column to use custom text color
+ const isNodeColumn = column.id === 'node';
+
+ return (
+ column && (
+
+ {renderCellContent(column.id)}
+
+ )
+ );
+ })
) : (
No visible columns
)}
@@ -253,20 +520,27 @@ const NetworkTableRow = ({
// Helper function to get minimum width for each column
const getMinWidthForColumn = (columnId) => {
const minWidths = {
- node: 70,
- type: 35, // Reduced from 45px to make it more compact
- id: 60,
- status: 40, // Reduced from 90px since we're only showing the icon now
- name: 130,
- cpu: 100,
- memory: 100,
- disk: 100,
- download: 90,
- upload: 90,
- uptime: 85 // Increased from 70px to accommodate longer uptime strings
+ // 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 NetworkTableRow;
\ No newline at end of file