fix: restore I/O column styling to match v4.32.5 release
- Restore green/yellow/red color thresholds for disk and network I/O - Use consistent text-xs font size across all columns - Expand column widths to fit full header text at larger breakpoints - Show full header names (Disk Read, VMID, Uptime, Memory) at xl+ screens - Use shared GUEST_COLUMNS config for header/row grid alignment - Add whitespace-nowrap to prevent header text wrapping
This commit is contained in:
parent
34137aeca5
commit
17c99377f6
3 changed files with 82 additions and 52 deletions
|
|
@ -1,7 +1,8 @@
|
|||
import { createSignal, createMemo, createEffect, For, Show, onMount } from 'solid-js';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import type { VM, Container, Node } from '@/types/api';
|
||||
import { GuestRow } from './GuestRow';
|
||||
import { GuestRow, GUEST_COLUMNS } from './GuestRow';
|
||||
import { useGridTemplate } from '@/components/shared/responsive';
|
||||
import { useWebSocket } from '@/App';
|
||||
import { getAlertStyles } from '@/utils/alerts';
|
||||
import { useAlertsActivation } from '@/stores/alertsActivation';
|
||||
|
|
@ -258,6 +259,9 @@ export function Dashboard(props: DashboardProps) {
|
|||
const [sortKey, setSortKey] = createSignal<keyof (VM | Container) | null>('vmid');
|
||||
const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc');
|
||||
|
||||
// Use the same grid template as GuestRow for header alignment
|
||||
const { gridTemplate: headerGridTemplate } = useGridTemplate({ columns: GUEST_COLUMNS });
|
||||
|
||||
|
||||
// Load all guest metadata on mount (single API call for all guests)
|
||||
onMount(async () => {
|
||||
|
|
@ -928,85 +932,111 @@ export function Dashboard(props: DashboardProps) {
|
|||
<Card padding="none" class="mb-4 bg-white dark:bg-gray-800">
|
||||
<div class="overflow-x-auto">
|
||||
{/* Desktop Header */}
|
||||
<div class="grid grid-cols-[minmax(100px,1.5fr)_minmax(24px,32px)_minmax(28px,36px)_minmax(28px,50px)_minmax(50px,1fr)_minmax(50px,1fr)_minmax(50px,1fr)_minmax(44px,54px)_minmax(44px,54px)_minmax(44px,54px)_minmax(44px,54px)] border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 text-[11px] sm:text-xs font-medium uppercase tracking-wider sticky top-0 z-20 min-w-[520px] md:min-w-0">
|
||||
<div
|
||||
class="grid border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 text-[11px] sm:text-xs font-medium uppercase tracking-wider sticky top-0 z-20 min-w-[520px] md:min-w-0"
|
||||
style={{ 'grid-template-columns': headerGridTemplate() }}
|
||||
>
|
||||
{/* Name Header */}
|
||||
<div
|
||||
class="pl-4 pr-2 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center"
|
||||
class="pl-4 pr-2 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center whitespace-nowrap"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
Name
|
||||
{sortKey() === 'name' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
Name {sortKey() === 'name' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
</div>
|
||||
|
||||
{/* Type */}
|
||||
<div
|
||||
class="px-0.5 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center text-[10px]"
|
||||
class="px-1 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center whitespace-nowrap"
|
||||
onClick={() => handleSort('type')}
|
||||
title="Type"
|
||||
>
|
||||
Type {sortKey() === 'type' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
<span class="hidden xl:inline">Type</span>
|
||||
<span class="xl:hidden">T</span>
|
||||
{sortKey() === 'type' && (sortDirection() === 'asc' ? ' ▲' : ' ▼')}
|
||||
</div>
|
||||
{/* VMID */}
|
||||
<div
|
||||
class="px-0.5 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center text-[10px]"
|
||||
class="px-1 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center whitespace-nowrap"
|
||||
onClick={() => handleSort('vmid')}
|
||||
title="VMID"
|
||||
>
|
||||
ID {sortKey() === 'vmid' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
<span class="hidden xl:inline">VMID</span>
|
||||
<span class="xl:hidden">ID</span>
|
||||
{sortKey() === 'vmid' && (sortDirection() === 'asc' ? ' ▲' : ' ▼')}
|
||||
</div>
|
||||
{/* Uptime */}
|
||||
<div
|
||||
class="px-0.5 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center text-[10px]"
|
||||
class="px-1 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center whitespace-nowrap"
|
||||
onClick={() => handleSort('uptime')}
|
||||
title="Uptime"
|
||||
>
|
||||
Up {sortKey() === 'uptime' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
<span class="hidden xl:inline">Uptime</span>
|
||||
<span class="xl:hidden">Up</span>
|
||||
{sortKey() === 'uptime' && (sortDirection() === 'asc' ? ' ▲' : ' ▼')}
|
||||
</div>
|
||||
{/* CPU */}
|
||||
<div
|
||||
class="px-0.5 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center text-[10px]"
|
||||
class="px-1 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center whitespace-nowrap"
|
||||
onClick={() => handleSort('cpu')}
|
||||
>
|
||||
CPU {sortKey() === 'cpu' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
</div>
|
||||
{/* Memory */}
|
||||
<div
|
||||
class="px-0.5 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center text-[10px]"
|
||||
class="px-1 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center whitespace-nowrap"
|
||||
onClick={() => handleSort('memory')}
|
||||
title="Memory"
|
||||
>
|
||||
Mem {sortKey() === 'memory' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
<span class="hidden xl:inline">Memory</span>
|
||||
<span class="xl:hidden">Mem</span>
|
||||
{sortKey() === 'memory' && (sortDirection() === 'asc' ? ' ▲' : ' ▼')}
|
||||
</div>
|
||||
{/* Disk */}
|
||||
<div
|
||||
class="px-0.5 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center text-[10px]"
|
||||
class="px-1 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center whitespace-nowrap"
|
||||
onClick={() => handleSort('disk')}
|
||||
>
|
||||
Disk {sortKey() === 'disk' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
</div>
|
||||
{/* Disk Read */}
|
||||
<div
|
||||
class="px-0.5 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center text-[10px]"
|
||||
class="px-1 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center whitespace-nowrap"
|
||||
onClick={() => handleSort('diskRead')}
|
||||
title="Disk Read"
|
||||
>
|
||||
D Rd {sortKey() === 'diskRead' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
<span class="hidden xl:inline">Disk Read</span>
|
||||
<span class="xl:hidden">D Rd</span>
|
||||
{sortKey() === 'diskRead' && (sortDirection() === 'asc' ? ' ▲' : ' ▼')}
|
||||
</div>
|
||||
{/* Disk Write */}
|
||||
<div
|
||||
class="px-0.5 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center text-[10px]"
|
||||
class="px-1 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center whitespace-nowrap"
|
||||
onClick={() => handleSort('diskWrite')}
|
||||
title="Disk Write"
|
||||
>
|
||||
D Wr {sortKey() === 'diskWrite' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
<span class="hidden xl:inline">Disk Write</span>
|
||||
<span class="xl:hidden">D Wr</span>
|
||||
{sortKey() === 'diskWrite' && (sortDirection() === 'asc' ? ' ▲' : ' ▼')}
|
||||
</div>
|
||||
{/* Net In */}
|
||||
<div
|
||||
class="px-0.5 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center text-[10px]"
|
||||
class="px-1 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center whitespace-nowrap"
|
||||
onClick={() => handleSort('networkIn')}
|
||||
title="Net In"
|
||||
>
|
||||
N In {sortKey() === 'networkIn' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
<span class="hidden xl:inline">Net In</span>
|
||||
<span class="xl:hidden">N In</span>
|
||||
{sortKey() === 'networkIn' && (sortDirection() === 'asc' ? ' ▲' : ' ▼')}
|
||||
</div>
|
||||
{/* Net Out */}
|
||||
<div
|
||||
class="px-0.5 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center text-[10px]"
|
||||
class="px-1 py-1 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 flex items-center justify-center whitespace-nowrap"
|
||||
onClick={() => handleSort('networkOut')}
|
||||
title="Net Out"
|
||||
>
|
||||
N Out {sortKey() === 'networkOut' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
<span class="hidden xl:inline">Net Out</span>
|
||||
<span class="xl:hidden">N Out</span>
|
||||
{sortKey() === 'networkOut' && (sortDirection() === 'asc' ? ' ▲' : ' ▼')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@ type Guest = VM | Container;
|
|||
|
||||
/**
|
||||
* Get color class for I/O values based on throughput (bytes/sec)
|
||||
* Higher throughput = more prominent color to draw attention
|
||||
* Uses color intensity to indicate activity level (green/yellow/red)
|
||||
*/
|
||||
function getIOColorClass(bytesPerSec: number): string {
|
||||
const mbps = bytesPerSec / (1024 * 1024);
|
||||
if (mbps < 1) return 'text-gray-400 dark:text-gray-500';
|
||||
if (mbps < 10) return 'text-gray-600 dark:text-gray-300';
|
||||
if (mbps < 50) return 'text-gray-700 dark:text-gray-200 font-medium';
|
||||
return 'text-gray-900 dark:text-white font-semibold';
|
||||
if (mbps < 1) return 'text-gray-300 dark:text-gray-400';
|
||||
if (mbps < 10) return 'text-green-600 dark:text-green-400';
|
||||
if (mbps < 50) return 'text-yellow-600 dark:text-yellow-400';
|
||||
return 'text-red-600 dark:text-red-400';
|
||||
}
|
||||
|
||||
// Global state for currently expanded drawer (only one drawer open at a time)
|
||||
|
|
@ -59,19 +59,19 @@ interface ColumnDef {
|
|||
flex?: number;
|
||||
}
|
||||
|
||||
const GUEST_COLUMNS: ColumnDef[] = [
|
||||
export const GUEST_COLUMNS: ColumnDef[] = [
|
||||
{ id: 'name', label: 'Name', priority: 'essential', minWidth: '100px', flex: 1.5 },
|
||||
{ id: 'type', label: 'Type', priority: 'essential', minWidth: '24px', maxWidth: '32px' },
|
||||
{ id: 'vmid', label: 'VMID', priority: 'essential', minWidth: '28px', maxWidth: '36px' },
|
||||
{ id: 'uptime', label: 'Uptime', priority: 'essential', minWidth: '28px', maxWidth: '50px' },
|
||||
{ id: 'type', label: 'Type', priority: 'essential', minWidth: '24px', maxWidth: '50px' },
|
||||
{ id: 'vmid', label: 'VMID', priority: 'essential', minWidth: '28px', maxWidth: '55px' },
|
||||
{ id: 'uptime', label: 'Uptime', priority: 'essential', minWidth: '28px', maxWidth: '65px' },
|
||||
{ id: 'cpu', label: 'CPU', priority: 'essential', minWidth: '50px', flex: 1 },
|
||||
{ id: 'memory', label: 'Memory', priority: 'essential', minWidth: '50px', flex: 1 },
|
||||
{ id: 'disk', label: 'Disk', priority: 'essential', minWidth: '50px', flex: 1 },
|
||||
// I/O columns - fixed width, no flex
|
||||
{ id: 'diskRead', label: 'D Read', priority: 'essential', minWidth: '44px', maxWidth: '54px' },
|
||||
{ id: 'diskWrite', label: 'D Write', priority: 'essential', minWidth: '44px', maxWidth: '54px' },
|
||||
{ id: 'netIn', label: 'Net In', priority: 'essential', minWidth: '44px', maxWidth: '54px' },
|
||||
{ id: 'netOut', label: 'Net Out', priority: 'essential', minWidth: '44px', maxWidth: '54px' },
|
||||
// I/O columns - fixed width matching v4.32.5 (grows at breakpoints to fit full header text)
|
||||
{ id: 'diskRead', label: 'Disk Read', priority: 'essential', minWidth: '56px', maxWidth: '90px' },
|
||||
{ id: 'diskWrite', label: 'Disk Write', priority: 'essential', minWidth: '56px', maxWidth: '90px' },
|
||||
{ id: 'netIn', label: 'Net In', priority: 'essential', minWidth: '56px', maxWidth: '70px' },
|
||||
{ id: 'netOut', label: 'Net Out', priority: 'essential', minWidth: '56px', maxWidth: '70px' },
|
||||
];
|
||||
|
||||
interface GuestRowProps {
|
||||
|
|
@ -662,36 +662,36 @@ export function GuestRow(props: GuestRowProps) {
|
|||
|
||||
case 'diskRead':
|
||||
return (
|
||||
<div class="py-1 flex justify-center items-center text-[9px] font-mono whitespace-nowrap">
|
||||
<Show when={isRunning()} fallback={<span class="text-gray-400">-</span>}>
|
||||
<span class={getIOColorClass(diskRead())}>{formatSpeed(diskRead())}</span>
|
||||
<div class="py-1 flex justify-center items-center min-h-[24px]">
|
||||
<Show when={isRunning()} fallback={<span class="text-xs text-gray-400">-</span>}>
|
||||
<span class={`text-xs ${getIOColorClass(diskRead())}`}>{formatSpeed(diskRead())}</span>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'diskWrite':
|
||||
return (
|
||||
<div class="py-1 flex justify-center items-center text-[9px] font-mono whitespace-nowrap">
|
||||
<Show when={isRunning()} fallback={<span class="text-gray-400">-</span>}>
|
||||
<span class={getIOColorClass(diskWrite())}>{formatSpeed(diskWrite())}</span>
|
||||
<div class="py-1 flex justify-center items-center min-h-[24px]">
|
||||
<Show when={isRunning()} fallback={<span class="text-xs text-gray-400">-</span>}>
|
||||
<span class={`text-xs ${getIOColorClass(diskWrite())}`}>{formatSpeed(diskWrite())}</span>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'netIn':
|
||||
return (
|
||||
<div class="py-1 flex justify-center items-center text-[9px] font-mono whitespace-nowrap">
|
||||
<Show when={isRunning()} fallback={<span class="text-gray-400">-</span>}>
|
||||
<span class={getIOColorClass(networkIn())}>{formatSpeed(networkIn())}</span>
|
||||
<div class="py-1 flex justify-center items-center min-h-[24px]">
|
||||
<Show when={isRunning()} fallback={<span class="text-xs text-gray-400">-</span>}>
|
||||
<span class={`text-xs ${getIOColorClass(networkIn())}`}>{formatSpeed(networkIn())}</span>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'netOut':
|
||||
return (
|
||||
<div class="py-1 flex justify-center items-center text-[9px] font-mono whitespace-nowrap">
|
||||
<Show when={isRunning()} fallback={<span class="text-gray-400">-</span>}>
|
||||
<span class={getIOColorClass(networkOut())}>{formatSpeed(networkOut())}</span>
|
||||
<div class="py-1 flex justify-center items-center min-h-[24px]">
|
||||
<Show when={isRunning()} fallback={<span class="text-xs text-gray-400">-</span>}>
|
||||
<span class={`text-xs ${getIOColorClass(networkOut())}`}>{formatSpeed(networkOut())}</span>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function IOMetric(props: IOMetricProps) {
|
|||
}
|
||||
});
|
||||
|
||||
// Color based on speed (MB/s) - matching current dashboard
|
||||
// Color based on speed (MB/s) - uses color intensity to indicate activity level
|
||||
const colorClass = createMemo(() => {
|
||||
if (props.disabled) return 'text-gray-400 dark:text-gray-500';
|
||||
|
||||
|
|
@ -38,9 +38,9 @@ export function IOMetric(props: IOMetricProps) {
|
|||
return (
|
||||
<Show
|
||||
when={!props.disabled}
|
||||
fallback={<div class="min-h-[14px] flex items-center text-xs text-gray-400">-</div>}
|
||||
fallback={<div class="min-h-[24px] flex items-center text-xs text-gray-400">-</div>}
|
||||
>
|
||||
<div class={`min-h-[14px] text-xs font-mono ${colorClass()} flex items-center`}>
|
||||
<div class={`min-h-[24px] text-xs ${colorClass()} flex items-center`}>
|
||||
{formatSpeed(currentValue(), 0)}
|
||||
</div>
|
||||
</Show>
|
||||
|
|
|
|||
Loading…
Reference in a new issue