Dashboard and sparkline improvements
This commit is contained in:
parent
90c45968e7
commit
fc8d88a7c7
6 changed files with 47 additions and 46 deletions
|
|
@ -1082,7 +1082,7 @@ export function Dashboard(props: DashboardProps) {
|
|||
<ComponentErrorBoundary name="Guest Table">
|
||||
<Card padding="none" tone="glass" class="mb-4 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full border-collapse whitespace-nowrap table-fixed">
|
||||
<table class="w-full border-collapse whitespace-nowrap" style={{ "min-width": "900px" }}>
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 border-b border-gray-200 dark:border-gray-700">
|
||||
<For each={visibleColumns()}>
|
||||
|
|
@ -1097,10 +1097,18 @@ export function Dashboard(props: DashboardProps) {
|
|||
class={`py-1 text-[11px] sm:text-xs font-medium uppercase tracking-wider whitespace-nowrap
|
||||
${isFirst() ? 'pl-4 pr-2 text-left' : 'px-2 text-center'}
|
||||
${isSortable ? 'cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600' : ''}`}
|
||||
style={col.width ? { width: col.width } : undefined}
|
||||
style={col.width ? { "min-width": col.width } : undefined}
|
||||
onClick={() => isSortable && handleSort(sortKeyForCol!)}
|
||||
title={col.icon ? col.label : undefined}
|
||||
>
|
||||
{col.label} {isSorted() && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
<span class="inline-flex items-center justify-center gap-0.5">
|
||||
{col.icon ? (
|
||||
<span innerHTML={col.icon} />
|
||||
) : (
|
||||
col.label
|
||||
)}
|
||||
{isSorted() && (sortDirection() === 'asc' ? ' ▲' : ' ▼')}
|
||||
</span>
|
||||
</th>
|
||||
);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -410,6 +410,7 @@ function BackupStatusCell(props: { lastBackup: string | number | null | undefine
|
|||
export interface GuestColumnDef {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string; // SVG icon markup for compact column headers
|
||||
priority: ColumnPriority;
|
||||
toggleable?: boolean;
|
||||
width?: string; // Fixed width for consistent column sizing
|
||||
|
|
@ -420,31 +421,31 @@ export interface GuestColumnDef {
|
|||
}
|
||||
|
||||
export const GUEST_COLUMNS: GuestColumnDef[] = [
|
||||
// Essential - always visible
|
||||
{ id: 'name', label: 'Name', priority: 'essential', sortKey: 'name' },
|
||||
// Essential - always visible (fixed widths ensure no overlap)
|
||||
{ id: 'name', label: 'Name', priority: 'essential', width: '120px', sortKey: 'name' },
|
||||
{ id: 'type', label: 'Type', priority: 'essential', width: '40px', sortKey: 'type' },
|
||||
{ id: 'vmid', label: 'VMID', priority: 'essential', width: '45px', sortKey: 'vmid' },
|
||||
{ id: 'vmid', label: 'ID', priority: 'essential', width: '45px', sortKey: 'vmid' },
|
||||
|
||||
// Core metrics - percentage width for proportional sizing
|
||||
{ id: 'cpu', label: 'CPU', priority: 'essential', width: '15%', sortKey: 'cpu' },
|
||||
{ id: 'memory', label: 'Memory', priority: 'essential', width: '15%', sortKey: 'memory' },
|
||||
{ id: 'disk', label: 'Disk', priority: 'essential', width: '15%', sortKey: 'disk' },
|
||||
// Core metrics - fixed minimum widths to prevent content overlap
|
||||
{ id: 'cpu', label: 'CPU', priority: 'essential', width: '140px', sortKey: 'cpu' },
|
||||
{ id: 'memory', label: 'Mem', priority: 'essential', width: '140px', sortKey: 'memory' },
|
||||
{ id: 'disk', label: 'Disk', priority: 'essential', width: '140px', sortKey: 'disk' },
|
||||
|
||||
// Secondary - visible on md+ (768px), user toggleable
|
||||
{ id: 'ip', label: 'IP', priority: 'secondary', width: '90px', toggleable: true },
|
||||
{ id: 'uptime', label: 'Uptime', priority: 'secondary', width: '65px', toggleable: true, sortKey: 'uptime' },
|
||||
{ id: 'node', label: 'Node', priority: 'secondary', width: '60px', toggleable: true, sortKey: 'node' },
|
||||
// Secondary - visible on md+ (768px), user toggleable - use icons
|
||||
{ id: 'ip', label: 'IP', icon: '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/></svg>', priority: 'secondary', width: '45px', toggleable: true },
|
||||
{ id: 'uptime', label: 'Uptime', icon: '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>', priority: 'secondary', width: '60px', toggleable: true, sortKey: 'uptime' },
|
||||
{ id: 'node', label: 'Node', icon: '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/></svg>', priority: 'secondary', width: '55px', toggleable: true, sortKey: 'node' },
|
||||
|
||||
// Supplementary - visible on lg+ (1024px), user toggleable
|
||||
{ id: 'backup', label: 'Backup', priority: 'supplementary', width: '55px', toggleable: true },
|
||||
{ id: 'tags', label: 'Tags', priority: 'supplementary', width: '80px', toggleable: true },
|
||||
{ id: 'backup', label: 'Backup', icon: '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>', priority: 'supplementary', width: '50px', toggleable: true },
|
||||
{ id: 'tags', label: 'Tags', icon: '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/></svg>', priority: 'supplementary', width: '60px', toggleable: true },
|
||||
|
||||
// Detailed - visible on xl+ (1280px), user toggleable
|
||||
{ id: 'os', label: 'OS', priority: 'detailed', width: '45px', toggleable: true },
|
||||
{ id: 'diskRead', label: 'D Read', priority: 'detailed', width: '55px', toggleable: true, sortKey: 'diskRead' },
|
||||
{ id: 'diskWrite', label: 'D Write', priority: 'detailed', width: '55px', toggleable: true, sortKey: 'diskWrite' },
|
||||
{ id: 'netIn', label: 'Net In', priority: 'detailed', width: '55px', toggleable: true, sortKey: 'networkIn' },
|
||||
{ id: 'netOut', label: 'Net Out', priority: 'detailed', width: '55px', toggleable: true, sortKey: 'networkOut' },
|
||||
{ id: 'diskRead', label: 'D Read', icon: '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>', priority: 'detailed', width: '55px', toggleable: true, sortKey: 'diskRead' },
|
||||
{ id: 'diskWrite', label: 'D Write', icon: '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>', priority: 'detailed', width: '55px', toggleable: true, sortKey: 'diskWrite' },
|
||||
{ id: 'netIn', label: 'Net In', icon: '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16l-4-4m0 0l4-4m-4 4h18"/></svg>', priority: 'detailed', width: '55px', toggleable: true, sortKey: 'networkIn' },
|
||||
{ id: 'netOut', label: 'Net Out', icon: '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/></svg>', priority: 'detailed', width: '55px', toggleable: true, sortKey: 'networkOut' },
|
||||
];
|
||||
|
||||
interface GuestRowProps {
|
||||
|
|
@ -937,17 +938,7 @@ export function GuestRow(props: GuestRowProps) {
|
|||
</Show>
|
||||
</div>
|
||||
|
||||
{/* Show tags inline only if tags column is hidden */}
|
||||
<Show when={!isEditingUrl() && !isColVisible('tags')}>
|
||||
<div class="hidden md:flex" data-prevent-toggle onClick={(event) => event.stopPropagation()}>
|
||||
<TagBadges
|
||||
tags={Array.isArray(props.guest.tags) ? props.guest.tags : []}
|
||||
maxVisible={3}
|
||||
onTagClick={props.onTagClick}
|
||||
activeSearch={props.activeSearch}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
||||
<Show when={lockLabel()}>
|
||||
<span
|
||||
|
|
@ -1166,7 +1157,7 @@ export function GuestRow(props: GuestRowProps) {
|
|||
<div class="flex justify-center" onClick={(event) => event.stopPropagation()}>
|
||||
<TagBadges
|
||||
tags={Array.isArray(props.guest.tags) ? props.guest.tags : []}
|
||||
maxVisible={2}
|
||||
maxVisible={0}
|
||||
onTagClick={props.onTagClick}
|
||||
activeSearch={props.activeSearch}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ interface TagBadgesProps {
|
|||
}
|
||||
|
||||
export const TagBadges: Component<TagBadgesProps> = (props) => {
|
||||
const maxVisible = () => props.maxVisible ?? 3;
|
||||
// maxVisible: 0 means show all, undefined defaults to 3
|
||||
const maxVisible = () => props.maxVisible === 0 ? Infinity : (props.maxVisible ?? 3);
|
||||
const darkModeSignal = useDarkMode();
|
||||
const isDark = () => props.isDarkMode ?? darkModeSignal();
|
||||
|
||||
|
|
|
|||
|
|
@ -342,7 +342,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
|||
return (
|
||||
<Card padding="none" tone="glass" class="mb-4 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full border-collapse whitespace-nowrap table-fixed">
|
||||
<table class="w-full border-collapse whitespace-nowrap" style={{ "min-width": "800px" }}>
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 border-b border-gray-200 dark:border-gray-700">
|
||||
<th
|
||||
|
|
@ -351,41 +351,41 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
|||
>
|
||||
{props.currentTab === 'backups' ? 'Node / PBS' : 'Node'} {renderSortIndicator('name')}
|
||||
</th>
|
||||
<th class={thClass} style={{ width: '80px' }} onClick={() => handleSort('uptime')}>
|
||||
<th class={thClass} style={{ "min-width": '80px' }} onClick={() => handleSort('uptime')}>
|
||||
Uptime {renderSortIndicator('uptime')}
|
||||
</th>
|
||||
<th class={thClass} style={{ width: '18%' }} onClick={() => handleSort('cpu')}>
|
||||
<th class={thClass} style={{ "min-width": '140px' }} onClick={() => handleSort('cpu')}>
|
||||
CPU {renderSortIndicator('cpu')}
|
||||
</th>
|
||||
<th class={thClass} style={{ width: '18%' }} onClick={() => handleSort('memory')}>
|
||||
<th class={thClass} style={{ "min-width": '140px' }} onClick={() => handleSort('memory')}>
|
||||
Memory {renderSortIndicator('memory')}
|
||||
</th>
|
||||
<th class={thClass} style={{ width: '18%' }} onClick={() => handleSort('disk')}>
|
||||
<th class={thClass} style={{ "min-width": '140px' }} onClick={() => handleSort('disk')}>
|
||||
Disk {renderSortIndicator('disk')}
|
||||
</th>
|
||||
<Show when={hasAnyTemperatureData()}>
|
||||
<th class={thClass} style={{ width: '60px' }} onClick={() => handleSort('temperature')}>
|
||||
<th class={thClass} style={{ "min-width": '60px' }} onClick={() => handleSort('temperature')}>
|
||||
Temp {renderSortIndicator('temperature')}
|
||||
</th>
|
||||
</Show>
|
||||
<Show when={props.currentTab === 'dashboard'}>
|
||||
<th class={thClass} style={{ width: '50px' }} onClick={() => handleSort('vmCount')}>
|
||||
<th class={thClass} style={{ "min-width": '50px' }} onClick={() => handleSort('vmCount')}>
|
||||
VMs {renderSortIndicator('vmCount')}
|
||||
</th>
|
||||
<th class={thClass} style={{ width: '50px' }} onClick={() => handleSort('containerCount')}>
|
||||
<th class={thClass} style={{ "min-width": '50px' }} onClick={() => handleSort('containerCount')}>
|
||||
CTs {renderSortIndicator('containerCount')}
|
||||
</th>
|
||||
</Show>
|
||||
<Show when={props.currentTab === 'storage'}>
|
||||
<th class={thClass} style={{ width: '70px' }} onClick={() => handleSort('storageCount')}>
|
||||
<th class={thClass} style={{ "min-width": '70px' }} onClick={() => handleSort('storageCount')}>
|
||||
Storage {renderSortIndicator('storageCount')}
|
||||
</th>
|
||||
<th class={thClass} style={{ width: '60px' }} onClick={() => handleSort('diskCount')}>
|
||||
<th class={thClass} style={{ "min-width": '60px' }} onClick={() => handleSort('diskCount')}>
|
||||
Disks {renderSortIndicator('diskCount')}
|
||||
</th>
|
||||
</Show>
|
||||
<Show when={props.currentTab === 'backups'}>
|
||||
<th class={thClass} style={{ width: '70px' }} onClick={() => handleSort('backupCount')}>
|
||||
<th class={thClass} style={{ "min-width": '70px' }} onClick={() => handleSort('backupCount')}>
|
||||
Backups {renderSortIndicator('backupCount')}
|
||||
</th>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -253,10 +253,10 @@ export const Sparkline: Component<SparklineProps> = (props) => {
|
|||
setHoveredPoint(null);
|
||||
};
|
||||
|
||||
// Format timestamp for tooltip
|
||||
// Format timestamp for tooltip (24-hour clock)
|
||||
const formatTime = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useBreakpoint, type ColumnPriority, PRIORITY_BREAKPOINTS, type Breakpoi
|
|||
export interface ColumnDef {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string; // SVG icon markup for compact column headers
|
||||
priority: ColumnPriority;
|
||||
toggleable?: boolean;
|
||||
width?: string; // Fixed width for consistent column sizing
|
||||
|
|
|
|||
Loading…
Reference in a new issue