Enhance UI responsiveness for tables: apply percentage-based widths and selective column hiding

This commit is contained in:
courtmanr@gmail.com 2025-11-25 10:57:33 +00:00
parent 809676cb4f
commit ac747ddf11
7 changed files with 738 additions and 756 deletions

View file

@ -1871,7 +1871,6 @@ const UnifiedBackups: Component = () => {
.backup-table { .backup-table {
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
min-width: 1200px;
} }
.backup-table th, .backup-table th,
.backup-table td { .backup-table td {
@ -2036,76 +2035,67 @@ const UnifiedBackups: Component = () => {
</div> </div>
{/* Desktop Table View */} {/* Desktop Table View */}
<table class="backup-table hidden lg:table"> <table class="backup-table hidden md:table">
<thead> <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-600"> <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-600">
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 w-[8%] min-w-[60px]"
onClick={() => handleSort('vmid')} onClick={() => handleSort('vmid')}
style="width: 60px;"
> >
{hasHostBackups() ? 'VMID/Host' : 'VMID'}{' '} {hasHostBackups() ? 'VMID/Host' : 'VMID'}{' '}
{sortKey() === 'vmid' && (sortDirection() === 'asc' ? '▲' : '▼')} {sortKey() === 'vmid' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 w-[8%] min-w-[60px]"
onClick={() => handleSort('type')} onClick={() => handleSort('type')}
style="width: 60px;"
> >
Type {sortKey() === 'type' && (sortDirection() === 'asc' ? '▲' : '▼')} Type {sortKey() === 'type' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 w-auto"
onClick={() => handleSort('name')} onClick={() => handleSort('name')}
style="width: 150px;"
> >
Name {sortKey() === 'name' && (sortDirection() === 'asc' ? '▲' : '▼')} Name {sortKey() === 'name' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden xl:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 w-[8%]"
onClick={() => handleSort('node')} onClick={() => handleSort('node')}
style="width: 100px;"
> >
Node {sortKey() === 'node' && (sortDirection() === 'asc' ? '▲' : '▼')} Node {sortKey() === 'node' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<Show when={backupTypeFilter() === 'all' || backupTypeFilter() === 'remote'}> <Show when={backupTypeFilter() === 'all' || backupTypeFilter() === 'remote'}>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden 2xl:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 w-[8%]"
onClick={() => handleSort('owner')} onClick={() => handleSort('owner')}
style="width: 80px;"
> >
Owner {sortKey() === 'owner' && (sortDirection() === 'asc' ? '▲' : '▼')} Owner {sortKey() === 'owner' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
</Show> </Show>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 w-[14%] min-w-[120px]"
onClick={() => handleSort('backupTime')} onClick={() => handleSort('backupTime')}
style="width: 140px;"
> >
Time {sortKey() === 'backupTime' && (sortDirection() === 'asc' ? '▲' : '▼')} Time {sortKey() === 'backupTime' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<Show when={backupTypeFilter() !== 'snapshot'}> <Show when={backupTypeFilter() !== 'snapshot'}>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden lg:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 w-[8%]"
onClick={() => handleSort('size')} onClick={() => handleSort('size')}
style="width: 80px;"
> >
Size {sortKey() === 'size' && (sortDirection() === 'asc' ? '▲' : '▼')} Size {sortKey() === 'size' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
</Show> </Show>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden lg:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 w-[8%]"
onClick={() => handleSort('backupType')} onClick={() => handleSort('backupType')}
style="width: 80px;"
> >
Backup{' '} Backup{' '}
{sortKey() === 'backupType' && (sortDirection() === 'asc' ? '▲' : '▼')} {sortKey() === 'backupType' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<Show when={backupTypeFilter() !== 'snapshot'}> <Show when={backupTypeFilter() !== 'snapshot'}>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden xl:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 w-[12%]"
onClick={() => handleSort('storage')} onClick={() => handleSort('storage')}
style="width: 150px;"
> >
Location{' '} Location{' '}
{sortKey() === 'storage' && (sortDirection() === 'asc' ? '▲' : '▼')} {sortKey() === 'storage' && (sortDirection() === 'asc' ? '▲' : '▼')}
@ -2113,17 +2103,15 @@ const UnifiedBackups: Component = () => {
</Show> </Show>
<Show when={backupTypeFilter() === 'all' || backupTypeFilter() === 'remote'}> <Show when={backupTypeFilter() === 'all' || backupTypeFilter() === 'remote'}>
<th <th
class="px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden lg:table-cell px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 w-[6%]"
onClick={() => handleSort('verified')} onClick={() => handleSort('verified')}
style="width: 60px;"
> >
Verified{' '} Verified{' '}
{sortKey() === 'verified' && (sortDirection() === 'asc' ? '▲' : '▼')} {sortKey() === 'verified' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
</Show> </Show>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider" class="hidden md:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[15%]"
style="width: 200px;"
> >
Details Details
</th> </th>
@ -2136,14 +2124,17 @@ const UnifiedBackups: Component = () => {
<tr class="bg-gray-50 dark:bg-gray-900/40"> <tr class="bg-gray-50 dark:bg-gray-900/40">
<td <td
colspan={(() => { colspan={(() => {
let cols = 7; // Base columns: VMID, Type, Name, Node, Time, Backup, Details let cols = 4; // Base: VMID, Type, Name, Time
if (backupTypeFilter() === 'all' || backupTypeFilter() === 'remote') if (backupTypeFilter() === 'all' || backupTypeFilter() === 'remote') {
cols++; // Add Owner column // Owner - hidden on 2xl
if (backupTypeFilter() !== 'snapshot') cols++; // Add Size column // We can't easily adjust colspan based on media query in JS without a hook.
if (backupTypeFilter() === 'all' || backupTypeFilter() === 'remote') // For now, let's just set a high colspan or use a simpler approach.
cols++; // Add Verified column // A safer bet for responsive tables with colspans is to just set it to a large number like 100
if (backupTypeFilter() !== 'snapshot') cols++; // Add Location column // provided the table layout handles it, or try to be accurate.
return cols; // Given the complexity of responsive hiding, colspan=100 is often the easiest hack for full-width rows.
return 20;
}
return 20;
})()} })()}
class="py-1 pr-2 pl-3 text-[12px] sm:text-sm font-semibold text-slate-700 dark:text-slate-100" class="py-1 pr-2 pl-3 text-[12px] sm:text-sm font-semibold text-slate-700 dark:text-slate-100"
> >
@ -2176,13 +2167,13 @@ const UnifiedBackups: Component = () => {
<td class="p-0.5 px-1.5 text-sm align-middle"> <td class="p-0.5 px-1.5 text-sm align-middle">
{item.name || '-'} {item.name || '-'}
</td> </td>
<td class="p-0.5 px-1.5 text-sm align-middle">{item.node}</td> <td class="hidden xl:table-cell p-0.5 px-1.5 text-sm align-middle">{item.node}</td>
<Show <Show
when={ when={
backupTypeFilter() === 'all' || backupTypeFilter() === 'remote' backupTypeFilter() === 'all' || backupTypeFilter() === 'remote'
} }
> >
<td class="p-0.5 px-1.5 text-xs align-middle text-gray-500 dark:text-gray-400"> <td class="hidden 2xl:table-cell p-0.5 px-1.5 text-xs align-middle text-gray-500 dark:text-gray-400">
{item.owner ? item.owner.split('@')[0] : '-'} {item.owner ? item.owner.split('@')[0] : '-'}
</td> </td>
</Show> </Show>
@ -2193,12 +2184,12 @@ const UnifiedBackups: Component = () => {
</td> </td>
<Show when={backupTypeFilter() !== 'snapshot'}> <Show when={backupTypeFilter() !== 'snapshot'}>
<td <td
class={`p-0.5 px-1.5 align-middle ${getSizeColor(item.size)}`} class={`hidden lg:table-cell p-0.5 px-1.5 align-middle ${getSizeColor(item.size)}`}
> >
{item.size ? formatBytes(item.size) : '-'} {item.size ? formatBytes(item.size) : '-'}
</td> </td>
</Show> </Show>
<td class="p-0.5 px-1.5 align-middle"> <td class="hidden lg:table-cell p-0.5 px-1.5 align-middle">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<span <span
class={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${item.backupType === 'snapshot' class={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${item.backupType === 'snapshot'
@ -2255,7 +2246,7 @@ const UnifiedBackups: Component = () => {
</div> </div>
</td> </td>
<Show when={backupTypeFilter() !== 'snapshot'}> <Show when={backupTypeFilter() !== 'snapshot'}>
<td class="p-0.5 px-1.5 text-sm align-middle"> <td class="hidden xl:table-cell p-0.5 px-1.5 text-sm align-middle">
{item.storage || {item.storage ||
(item.datastore && (item.datastore &&
(item.namespace && item.namespace !== 'root' (item.namespace && item.namespace !== 'root'
@ -2269,7 +2260,7 @@ const UnifiedBackups: Component = () => {
backupTypeFilter() === 'all' || backupTypeFilter() === 'remote' backupTypeFilter() === 'all' || backupTypeFilter() === 'remote'
} }
> >
<td class="p-0.5 px-1.5 text-center align-middle"> <td class="hidden lg:table-cell p-0.5 px-1.5 text-center align-middle">
{item.backupType === 'remote' ? ( {item.backupType === 'remote' ? (
item.verified ? ( item.verified ? (
<span title="PBS backup verified"> <span title="PBS backup verified">
@ -2315,7 +2306,7 @@ const UnifiedBackups: Component = () => {
</td> </td>
</Show> </Show>
<td <td
class="p-0.5 px-1.5 cursor-help align-middle" class="hidden md:table-cell p-0.5 px-1.5 cursor-help align-middle"
onMouseEnter={(e) => { onMouseEnter={(e) => {
const details = []; const details = [];

View file

@ -927,12 +927,12 @@ export function Dashboard(props: DashboardProps) {
<Show when={connected() && initialDataReceived() && filteredGuests().length > 0}> <Show when={connected() && initialDataReceived() && filteredGuests().length > 0}>
<ComponentErrorBoundary name="Guest Table"> <ComponentErrorBoundary name="Guest Table">
<Card padding="none" class="mb-4 overflow-hidden"> <Card padding="none" class="mb-4 overflow-hidden">
<ScrollableTable minWidth="750px"> <ScrollableTable minWidth="600px">
<table class="w-full min-w-[750px] md:min-w-[840px] lg:min-w-[970px] table-fixed border-collapse"> <table class="w-full min-w-[600px] table-fixed border-collapse">
<thead> <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-600"> <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-600">
<th <th
class="pl-4 pr-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500" class="pl-4 pr-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 w-auto"
onClick={() => handleSort('name')} onClick={() => handleSort('name')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('name')} onKeyDown={(e) => e.key === 'Enter' && handleSort('name')}
tabindex="0" tabindex="0"
@ -942,7 +942,7 @@ export function Dashboard(props: DashboardProps) {
Name {sortKey() === 'name' && (sortDirection() === 'asc' ? '▲' : '▼')} Name {sortKey() === 'name' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[52px] sm:w-[58px] lg:w-[64px] xl:w-[70px] 2xl:w-[87px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden lg:table-cell px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[5%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('type')} onClick={() => handleSort('type')}
title="Type" title="Type"
> >
@ -951,7 +951,7 @@ export function Dashboard(props: DashboardProps) {
{sortKey() === 'type' && (sortDirection() === 'asc' ? '▲' : '▼')} {sortKey() === 'type' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-1.5 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wide w-[50px] sm:w-[56px] lg:w-[62px] xl:w-[70px] 2xl:w-[92px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden lg:table-cell px-1.5 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wide w-[5%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('vmid')} onClick={() => handleSort('vmid')}
title="VM/Container ID" title="VM/Container ID"
> >
@ -960,7 +960,7 @@ export function Dashboard(props: DashboardProps) {
{sortKey() === 'vmid' && (sortDirection() === 'asc' ? '▲' : '▼')} {sortKey() === 'vmid' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-1.5 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wide w-[56px] sm:w-[64px] lg:w-[72px] xl:w-[84px] 2xl:w-[125px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden sm:table-cell px-1.5 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wide w-[8%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('uptime')} onClick={() => handleSort('uptime')}
title="Uptime" title="Uptime"
> >
@ -969,13 +969,13 @@ export function Dashboard(props: DashboardProps) {
{sortKey() === 'uptime' && (sortDirection() === 'asc' ? '▲' : '▼')} {sortKey() === 'uptime' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[80px] sm:w-[90px] lg:w-[110px] xl:w-[160px] 2xl:w-[204px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('cpu')} onClick={() => handleSort('cpu')}
> >
CPU {sortKey() === 'cpu' && (sortDirection() === 'asc' ? '▲' : '▼')} CPU {sortKey() === 'cpu' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[80px] sm:w-[90px] lg:w-[110px] xl:w-[160px] 2xl:w-[204px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('memory')} onClick={() => handleSort('memory')}
title="Memory" title="Memory"
> >
@ -984,13 +984,13 @@ export function Dashboard(props: DashboardProps) {
{sortKey() === 'memory' && (sortDirection() === 'asc' ? '▲' : '▼')} {sortKey() === 'memory' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[80px] sm:w-[90px] lg:w-[110px] xl:w-[160px] 2xl:w-[204px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden sm:table-cell px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('disk')} onClick={() => handleSort('disk')}
> >
Disk {sortKey() === 'disk' && (sortDirection() === 'asc' ? '▲' : '▼')} Disk {sortKey() === 'disk' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[56px] sm:w-[62px] lg:w-[70px] xl:w-[78px] 2xl:w-[106px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden lg:table-cell px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[6%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('diskRead')} onClick={() => handleSort('diskRead')}
title="Disk Read" title="Disk Read"
> >
@ -999,7 +999,7 @@ export function Dashboard(props: DashboardProps) {
{sortKey() === 'diskRead' && (sortDirection() === 'asc' ? '▲' : '▼')} {sortKey() === 'diskRead' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[56px] sm:w-[62px] lg:w-[70px] xl:w-[78px] 2xl:w-[106px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden lg:table-cell px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[6%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('diskWrite')} onClick={() => handleSort('diskWrite')}
title="Disk Write" title="Disk Write"
> >
@ -1008,7 +1008,7 @@ export function Dashboard(props: DashboardProps) {
{sortKey() === 'diskWrite' && (sortDirection() === 'asc' ? '▲' : '▼')} {sortKey() === 'diskWrite' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[56px] sm:w-[62px] lg:w-[70px] xl:w-[78px] 2xl:w-[106px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden lg:table-cell px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[6%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('networkIn')} onClick={() => handleSort('networkIn')}
title="Network In" title="Network In"
> >
@ -1017,7 +1017,7 @@ export function Dashboard(props: DashboardProps) {
{sortKey() === 'networkIn' && (sortDirection() === 'asc' ? '▲' : '▼')} {sortKey() === 'networkIn' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[56px] sm:w-[62px] lg:w-[70px] xl:w-[78px] 2xl:w-[106px] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden lg:table-cell px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[6%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('networkOut')} onClick={() => handleSort('networkOut')}
title="Network Out" title="Network Out"
> >

View file

@ -406,7 +406,7 @@ export function GuestRow(props: GuestRowProps) {
// Get first cell styling // Get first cell styling
const firstCellClass = createMemo(() => { const firstCellClass = createMemo(() => {
const base = const base =
'py-0.5 pr-2 whitespace-nowrap relative w-[140px] sm:w-[160px] lg:w-[180px] xl:w-[240px] 2xl:w-[380px]'; 'py-0.5 pr-2 whitespace-nowrap relative w-auto';
const indent = props.isGroupedView ? GROUPED_FIRST_CELL_INDENT : DEFAULT_FIRST_CELL_INDENT; const indent = props.isGroupedView ? GROUPED_FIRST_CELL_INDENT : DEFAULT_FIRST_CELL_INDENT;
return `${base} ${indent}`; return `${base} ${indent}`;
}); });
@ -552,11 +552,10 @@ export function GuestRow(props: GuestRowProps) {
</td> </td>
{/* Type */} {/* Type */}
<td class="py-0.5 px-2 whitespace-nowrap w-[52px] sm:w-[58px] lg:w-[64px] xl:w-[70px] 2xl:w-[87px]"> <td class="hidden lg:table-cell py-0.5 px-2 whitespace-nowrap w-[5%]">
<div class="flex h-[24px] items-center"> <div class="flex h-[24px] items-center">
<span <span
class={`inline-block px-1.5 py-0.5 text-xs font-medium rounded ${ class={`inline-block px-1.5 py-0.5 text-xs font-medium rounded ${props.guest.type === 'qemu'
props.guest.type === 'qemu'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300' ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300'
: 'bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300' : 'bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300'
}`} }`}
@ -567,14 +566,13 @@ export function GuestRow(props: GuestRowProps) {
</td> </td>
{/* VMID */} {/* VMID */}
<td class="py-0.5 px-1.5 whitespace-nowrap w-[50px] sm:w-[56px] lg:w-[62px] xl:w-[70px] 2xl:w-[92px] text-sm text-gray-600 dark:text-gray-400 align-middle"> <td class="hidden lg:table-cell py-0.5 px-1.5 whitespace-nowrap w-[5%] text-sm text-gray-600 dark:text-gray-400 align-middle">
{props.guest.vmid} {props.guest.vmid}
</td> </td>
{/* Uptime */} {/* Uptime */}
<td <td
class={`py-0.5 px-1.5 w-[56px] sm:w-[64px] lg:w-[72px] xl:w-[84px] 2xl:w-[125px] text-sm whitespace-nowrap align-middle ${ class={`hidden sm:table-cell py-0.5 px-1.5 w-[8%] text-sm whitespace-nowrap align-middle ${props.guest.uptime < 3600 ? 'text-orange-500' : 'text-gray-600 dark:text-gray-400'
props.guest.uptime < 3600 ? 'text-orange-500' : 'text-gray-600 dark:text-gray-400'
}`} }`}
> >
<Show when={isRunning()} fallback="-"> <Show when={isRunning()} fallback="-">
@ -583,7 +581,7 @@ export function GuestRow(props: GuestRowProps) {
</td> </td>
{/* CPU */} {/* CPU */}
<td class="py-0.5 px-2 w-[80px] sm:w-[90px] lg:w-[110px] xl:w-[160px] 2xl:w-[204px]"> <td class="py-0.5 px-2 w-[10%]">
<Show when={isRunning()} fallback={<span class="text-sm text-gray-400">-</span>}> <Show when={isRunning()} fallback={<span class="text-sm text-gray-400">-</span>}>
<MetricBar <MetricBar
value={cpuPercent()} value={cpuPercent()}
@ -600,7 +598,7 @@ export function GuestRow(props: GuestRowProps) {
</td> </td>
{/* Memory */} {/* Memory */}
<td class="py-0.5 px-2 w-[80px] sm:w-[90px] lg:w-[110px] xl:w-[160px] 2xl:w-[204px]"> <td class="py-0.5 px-2 w-[10%]">
<div title={memoryTooltip() ?? undefined}> <div title={memoryTooltip() ?? undefined}>
<Show when={isRunning()} fallback={<span class="text-sm text-gray-400">-</span>}> <Show when={isRunning()} fallback={<span class="text-sm text-gray-400">-</span>}>
<MetricBar <MetricBar
@ -615,7 +613,7 @@ export function GuestRow(props: GuestRowProps) {
</td> </td>
{/* Disk surface usage even if guest is currently stopped so users can see last reported values */} {/* Disk surface usage even if guest is currently stopped so users can see last reported values */}
<td class="py-0.5 px-2 w-[80px] sm:w-[90px] lg:w-[110px] xl:w-[160px] 2xl:w-[204px]"> <td class="hidden sm:table-cell py-0.5 px-2 w-[10%]">
<Show <Show
when={hasDiskUsage()} when={hasDiskUsage()}
fallback={ fallback={
@ -639,24 +637,24 @@ export function GuestRow(props: GuestRowProps) {
</td> </td>
{/* Disk I/O */} {/* Disk I/O */}
<td class="py-0.5 px-2 w-[56px] sm:w-[62px] lg:w-[70px] xl:w-[78px] 2xl:w-[106px]"> <td class="hidden lg:table-cell py-0.5 px-2 w-[6%]">
<div class="flex h-[24px] items-center"> <div class="flex h-[24px] items-center">
<IOMetric value={props.guest.diskRead} disabled={!isRunning()} /> <IOMetric value={props.guest.diskRead} disabled={!isRunning()} />
</div> </div>
</td> </td>
<td class="py-0.5 px-2 w-[56px] sm:w-[62px] lg:w-[70px] xl:w-[78px] 2xl:w-[106px]"> <td class="hidden lg:table-cell py-0.5 px-2 w-[6%]">
<div class="flex h-[24px] items-center"> <div class="flex h-[24px] items-center">
<IOMetric value={props.guest.diskWrite} disabled={!isRunning()} /> <IOMetric value={props.guest.diskWrite} disabled={!isRunning()} />
</div> </div>
</td> </td>
{/* Network I/O */} {/* Network I/O */}
<td class="py-0.5 px-2 w-[56px] sm:w-[62px] lg:w-[70px] xl:w-[78px] 2xl:w-[106px]"> <td class="hidden lg:table-cell py-0.5 px-2 w-[6%]">
<div class="flex h-[24px] items-center"> <div class="flex h-[24px] items-center">
<IOMetric value={props.guest.networkIn} disabled={!isRunning()} /> <IOMetric value={props.guest.networkIn} disabled={!isRunning()} />
</div> </div>
</td> </td>
<td class="py-0.5 px-2 w-[56px] sm:w-[62px] lg:w-[70px] xl:w-[78px] 2xl:w-[106px]"> <td class="hidden lg:table-cell py-0.5 px-2 w-[6%]">
<div class="flex h-[24px] items-center"> <div class="flex h-[24px] items-center">
<IOMetric value={props.guest.networkOut} disabled={!isRunning()} /> <IOMetric value={props.guest.networkOut} disabled={!isRunning()} />
</div> </div>
@ -664,8 +662,7 @@ export function GuestRow(props: GuestRowProps) {
</tr> </tr>
<Show when={drawerOpen() && canShowDrawer()}> <Show when={drawerOpen() && canShowDrawer()}>
<tr <tr
class={`text-[11px] ${ class={`text-[11px] ${isRunning() && props.parentNodeOnline !== false
isRunning() && props.parentNodeOnline !== false
? 'bg-gray-50/60 text-gray-600 dark:bg-gray-800/40 dark:text-gray-300' ? 'bg-gray-50/60 text-gray-600 dark:bg-gray-800/40 dark:text-gray-300'
: 'bg-gray-100/70 text-gray-400 dark:bg-gray-900/30 dark:text-gray-500' : 'bg-gray-100/70 text-gray-400 dark:bg-gray-900/30 dark:text-gray-500'
}`} }`}
@ -673,8 +670,7 @@ export function GuestRow(props: GuestRowProps) {
> >
<td class="px-4 py-2" colSpan={11}> <td class="px-4 py-2" colSpan={11}>
<div <div
class={`flex flex-wrap gap-3 justify-start ${ class={`flex flex-wrap gap-3 justify-start ${drawerDisabled() ? 'opacity-50 saturate-75 pointer-events-none' : ''
drawerDisabled() ? 'opacity-50 saturate-75 pointer-events-none' : ''
}`} }`}
> >
<Show <Show

View file

@ -1182,7 +1182,7 @@ const DockerContainerRow: Component<{
</div> </div>
</div> </div>
</td> </td>
<td class="px-2 py-0.5"> <td class="hidden sm:table-cell px-2 py-0.5">
<span <span
class={`inline-flex items-center rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${runtimeInfo.badgeClass}`} class={`inline-flex items-center rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${runtimeInfo.badgeClass}`}
title={ title={
@ -1194,7 +1194,7 @@ const DockerContainerRow: Component<{
{runtimeInfo.label} {runtimeInfo.label}
</span> </span>
</td> </td>
<td class="px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300"> <td class="hidden lg:table-cell px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300">
<span title={container.image}> <span title={container.image}>
{container.image || '—'} {container.image || '—'}
</span> </span>
@ -1204,7 +1204,7 @@ const DockerContainerRow: Component<{
{statusLabel()} {statusLabel()}
</span> </span>
</td> </td>
<td class="px-2 py-0.5 min-w-[180px]"> <td class="hidden md:table-cell px-2 py-0.5">
<Show <Show
when={isRunning() && container.cpuPercent && container.cpuPercent > 0} when={isRunning() && container.cpuPercent && container.cpuPercent > 0}
fallback={<span class="text-xs text-gray-400"></span>} fallback={<span class="text-xs text-gray-400"></span>}
@ -1217,7 +1217,7 @@ const DockerContainerRow: Component<{
/> />
</Show> </Show>
</td> </td>
<td class="px-2 py-0.5 min-w-[240px]"> <td class="hidden md:table-cell px-2 py-0.5">
<Show <Show
when={isRunning() && container.memoryUsageBytes && container.memoryUsageBytes > 0} when={isRunning() && container.memoryUsageBytes && container.memoryUsageBytes > 0}
fallback={<span class="text-xs text-gray-400"></span>} fallback={<span class="text-xs text-gray-400"></span>}
@ -1231,7 +1231,7 @@ const DockerContainerRow: Component<{
/> />
</Show> </Show>
</td> </td>
<td class="px-2 py-0.5 min-w-[220px]"> <td class="hidden xl:table-cell px-2 py-0.5">
<Show when={hasDiskStats()} fallback={<span class="text-xs text-gray-400"></span>}> <Show when={hasDiskStats()} fallback={<span class="text-xs text-gray-400"></span>}>
<Show <Show
when={diskPercent() !== null} when={diskPercent() !== null}
@ -1247,13 +1247,13 @@ const DockerContainerRow: Component<{
</Show> </Show>
</Show> </Show>
</td> </td>
<td class="px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300"> <td class="hidden lg:table-cell px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300">
<Show when={isRunning()} fallback={<span class="text-gray-400"></span>}> <Show when={isRunning()} fallback={<span class="text-gray-400"></span>}>
{restarts()} {restarts()}
<span class="text-[10px] text-gray-500 dark:text-gray-400 ml-1">restarts</span> <span class="text-[10px] text-gray-500 dark:text-gray-400 ml-1">restarts</span>
</Show> </Show>
</td> </td>
<td class="px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300"> <td class="hidden sm:table-cell px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300">
<Show when={isRunning()} fallback={<span class="text-gray-400"></span>}> <Show when={isRunning()} fallback={<span class="text-gray-400"></span>}>
{uptime()} {uptime()}
</Show> </Show>
@ -1946,12 +1946,12 @@ const DockerServiceRow: Component<{
</div> </div>
</div> </div>
</td> </td>
<td class="px-2 py-0.5"> <td class="hidden sm:table-cell px-2 py-0.5">
<span class={`inline-flex items-center rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${typeBadgeClass('service')}`}> <span class={`inline-flex items-center rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${typeBadgeClass('service')}`}>
Service Service
</span> </span>
</td> </td>
<td class="px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300"> <td class="hidden lg:table-cell px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300">
<span title={service.image}> <span title={service.image}>
{service.image || '—'} {service.image || '—'}
</span> </span>
@ -1961,16 +1961,16 @@ const DockerServiceRow: Component<{
{badge.label} {badge.label}
</span> </span>
</td> </td>
<td class="px-2 py-0.5 text-xs text-gray-400 dark:text-gray-500 min-w-[150px]"></td> <td class="hidden md:table-cell px-2 py-0.5 text-xs text-gray-400 dark:text-gray-500"></td>
<td class="px-2 py-0.5 text-xs text-gray-400 dark:text-gray-500 min-w-[210px]"></td> <td class="hidden md:table-cell px-2 py-0.5 text-xs text-gray-400 dark:text-gray-500"></td>
<td class="px-2 py-0.5 text-xs text-gray-400 dark:text-gray-500 min-w-[200px]"></td> <td class="hidden xl:table-cell px-2 py-0.5 text-xs text-gray-400 dark:text-gray-500"></td>
<td class="px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300 whitespace-nowrap"> <td class="hidden lg:table-cell px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300 whitespace-nowrap">
<span class="font-semibold text-gray-900 dark:text-gray-100"> <span class="font-semibold text-gray-900 dark:text-gray-100">
{(service.runningTasks ?? 0)}/{service.desiredTasks ?? 0} {(service.runningTasks ?? 0)}/{service.desiredTasks ?? 0}
</span> </span>
<span class="ml-1 text-gray-500 dark:text-gray-400">tasks</span> <span class="ml-1 text-gray-500 dark:text-gray-400">tasks</span>
</td> </td>
<td class="px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300 whitespace-nowrap"> <td class="hidden sm:table-cell px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300 whitespace-nowrap">
<Show when={updatedAt} fallback="—"> <Show when={updatedAt} fallback="—">
{(timestamp) => ( {(timestamp) => (
<span title={new Date(timestamp()).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}> <span title={new Date(timestamp()).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}>
@ -2425,12 +2425,12 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
} }
> >
<Card padding="none" class="overflow-hidden"> <Card padding="none" class="overflow-hidden">
<ScrollableTable minWidth="1080px"> <ScrollableTable>
<table class="w-full min-w-[1080px] table-fixed border-collapse whitespace-nowrap"> <table class="w-full table-fixed border-collapse whitespace-nowrap">
<thead> <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-600"> <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-600">
<th <th
class="pl-4 pr-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[22%] cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500" class="pl-4 pr-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-auto cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
onClick={() => handleSort('resource')} onClick={() => handleSort('resource')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('resource')} onKeyDown={(e) => e.key === 'Enter' && handleSort('resource')}
tabIndex={0} tabIndex={0}
@ -2459,7 +2459,7 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
</div> </div>
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%] cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500" class="hidden sm:table-cell px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%] cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
onClick={() => handleSort('type')} onClick={() => handleSort('type')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('type')} onKeyDown={(e) => e.key === 'Enter' && handleSort('type')}
tabIndex={0} tabIndex={0}
@ -2473,7 +2473,7 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
</div> </div>
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[16%] cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500" class="hidden lg:table-cell px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[16%] cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
onClick={() => handleSort('image')} onClick={() => handleSort('image')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('image')} onKeyDown={(e) => e.key === 'Enter' && handleSort('image')}
tabIndex={0} tabIndex={0}
@ -2501,7 +2501,7 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
</div> </div>
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[13%] min-w-[150px] cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500" class="hidden md:table-cell px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[13%] cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
onClick={() => handleSort('cpu')} onClick={() => handleSort('cpu')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('cpu')} onKeyDown={(e) => e.key === 'Enter' && handleSort('cpu')}
tabIndex={0} tabIndex={0}
@ -2515,7 +2515,7 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
</div> </div>
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[15%] min-w-[210px] cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500" class="hidden md:table-cell px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[15%] cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
onClick={() => handleSort('memory')} onClick={() => handleSort('memory')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('memory')} onKeyDown={(e) => e.key === 'Enter' && handleSort('memory')}
tabIndex={0} tabIndex={0}
@ -2529,7 +2529,7 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
</div> </div>
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[16%] min-w-[200px] cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500" class="hidden xl:table-cell px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[16%] cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
onClick={() => handleSort('disk')} onClick={() => handleSort('disk')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('disk')} onKeyDown={(e) => e.key === 'Enter' && handleSort('disk')}
tabIndex={0} tabIndex={0}
@ -2543,7 +2543,7 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
</div> </div>
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[9%] cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500" class="hidden lg:table-cell px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[9%] cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
onClick={() => handleSort('tasks')} onClick={() => handleSort('tasks')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('tasks')} onKeyDown={(e) => e.key === 'Enter' && handleSort('tasks')}
tabIndex={0} tabIndex={0}
@ -2557,7 +2557,7 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
</div> </div>
</th> </th>
<th <th
class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[8%] cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500" class="hidden sm:table-cell px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[8%] cursor-pointer select-none hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
onClick={() => handleSort('updated')} onClick={() => handleSort('updated')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('updated')} onKeyDown={(e) => e.key === 'Enter' && handleSort('updated')}
tabIndex={0} tabIndex={0}

View file

@ -154,31 +154,31 @@ export const DiskList: Component<DiskListProps> = (props) => {
<table class="w-full"> <table class="w-full">
<thead> <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-600"> <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-600">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider"> <th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%]">
Node Node
</th> </th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider"> <th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%]">
Device Device
</th> </th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider"> <th class="hidden md:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[20%]">
Model Model
</th> </th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider"> <th class="hidden sm:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[8%]">
Type Type
</th> </th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider"> <th class="hidden lg:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[8%]">
FS FS
</th> </th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider"> <th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%]">
Health Health
</th> </th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider"> <th class="hidden md:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[15%]">
SSD Life SSD Life
</th> </th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider hidden sm:table-cell"> <th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider hidden sm:table-cell w-[8%]">
Temp Temp
</th> </th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider"> <th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%]">
Size Size
</th> </th>
<th class="px-2 py-1.5 w-8"></th> <th class="px-2 py-1.5 w-8"></th>
@ -202,19 +202,19 @@ export const DiskList: Component<DiskListProps> = (props) => {
{disk.devPath} {disk.devPath}
</span> </span>
</td> </td>
<td class="px-2 py-1.5 text-xs"> <td class="hidden md:table-cell px-2 py-1.5 text-xs">
<span class="text-gray-700 dark:text-gray-300"> <span class="text-gray-700 dark:text-gray-300">
{disk.model || 'Unknown'} {disk.model || 'Unknown'}
</span> </span>
</td> </td>
<td class="px-2 py-1.5 text-xs"> <td class="hidden sm:table-cell px-2 py-1.5 text-xs">
<span <span
class={`inline-block px-1.5 py-0.5 text-[10px] font-medium rounded ${getDiskTypeBadge(disk.type)}`} class={`inline-block px-1.5 py-0.5 text-[10px] font-medium rounded ${getDiskTypeBadge(disk.type)}`}
> >
{disk.type.toUpperCase()} {disk.type.toUpperCase()}
</span> </span>
</td> </td>
<td class="px-2 py-1.5 text-xs"> <td class="hidden lg:table-cell px-2 py-1.5 text-xs">
<Show <Show
when={disk.used && disk.used !== 'unknown'} when={disk.used && disk.used !== 'unknown'}
fallback={<span class="text-gray-400">-</span>} fallback={<span class="text-gray-400">-</span>}
@ -231,15 +231,14 @@ export const DiskList: Component<DiskListProps> = (props) => {
{health.text} {health.text}
</span> </span>
</td> </td>
<td class="px-2 py-1.5 text-xs"> <td class="hidden md:table-cell px-2 py-1.5 text-xs">
<Show <Show
when={disk.wearout > 0} when={disk.wearout > 0}
fallback={<span class="text-gray-400">-</span>} fallback={<span class="text-gray-400">-</span>}
> >
<div class="relative w-24 h-3.5 rounded overflow-hidden bg-gray-200 dark:bg-gray-600"> <div class="relative w-24 h-3.5 rounded overflow-hidden bg-gray-200 dark:bg-gray-600">
<div <div
class={`absolute top-0 left-0 h-full ${ class={`absolute top-0 left-0 h-full ${disk.wearout >= 50
disk.wearout >= 50
? 'bg-green-500/60 dark:bg-green-500/50' ? 'bg-green-500/60 dark:bg-green-500/50'
: disk.wearout >= 20 : disk.wearout >= 20
? 'bg-yellow-500/60 dark:bg-yellow-500/50' ? 'bg-yellow-500/60 dark:bg-yellow-500/50'
@ -261,8 +260,7 @@ export const DiskList: Component<DiskListProps> = (props) => {
fallback={<span class="font-medium text-gray-400">-</span>} fallback={<span class="font-medium text-gray-400">-</span>}
> >
<span <span
class={`font-medium ${ class={`font-medium ${disk.temperature > 70
disk.temperature > 70
? 'text-red-600 dark:text-red-400' ? 'text-red-600 dark:text-red-400'
: disk.temperature > 60 : disk.temperature > 60
? 'text-yellow-600 dark:text-yellow-400' ? 'text-yellow-600 dark:text-yellow-400'

View file

@ -750,30 +750,30 @@ const Storage: Component = () => {
<table class="w-full"> <table class="w-full">
<thead> <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-600"> <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-600">
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider"> <th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-auto">
Storage Storage
</th> </th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider hidden md:table-cell"> <th class="hidden md:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%]">
Type Type
</th> </th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider hidden lg:table-cell"> <th class="hidden lg:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[15%]">
Content Content
</th> </th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider hidden sm:table-cell"> <th class="hidden sm:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%]">
Status Status
</th> </th>
<Show when={viewMode() === 'node'}> <Show when={viewMode() === 'node'}>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider hidden lg:table-cell"> <th class="hidden lg:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[8%]">
Shared Shared
</th> </th>
</Show> </Show>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-[200px]"> <th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[25%] min-w-[150px]">
Usage Usage
</th> </th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider hidden sm:table-cell"> <th class="hidden sm:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%]">
Free Free
</th> </th>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider"> <th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%]">
Total Total
</th> </th>
<th class="px-2 py-1.5 w-8"></th> <th class="px-2 py-1.5 w-8"></th>
@ -1109,12 +1109,12 @@ const Storage: Component = () => {
</Show> </Show>
</div> </div>
</td> </td>
<td class="p-0.5 px-1.5 hidden md:table-cell"> <td class="hidden md:table-cell p-0.5 px-1.5">
<span class="inline-block px-1.5 py-0.5 text-[10px] font-medium rounded bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300"> <span class="inline-block px-1.5 py-0.5 text-[10px] font-medium rounded bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300">
{storage.type} {storage.type}
</span> </span>
</td> </td>
<td class="p-0.5 px-1.5 hidden lg:table-cell"> <td class="hidden lg:table-cell p-0.5 px-1.5">
<span <span
class="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap truncate max-w-[220px]" class="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap truncate max-w-[220px]"
title={storage.content || '-'} title={storage.content || '-'}
@ -1122,7 +1122,7 @@ const Storage: Component = () => {
{storage.content || '-'} {storage.content || '-'}
</span> </span>
</td> </td>
<td class="p-0.5 px-1.5 text-xs hidden sm:table-cell whitespace-nowrap"> <td class="hidden sm:table-cell p-0.5 px-1.5 text-xs whitespace-nowrap">
<span <span
class={`${storage.status === 'available' class={`${storage.status === 'available'
? 'text-green-600 dark:text-green-400' ? 'text-green-600 dark:text-green-400'
@ -1133,7 +1133,7 @@ const Storage: Component = () => {
</span> </span>
</td> </td>
<Show when={viewMode() === 'node'}> <Show when={viewMode() === 'node'}>
<td class="p-0.5 px-1.5 hidden lg:table-cell"> <td class="hidden lg:table-cell p-0.5 px-1.5">
<span class="text-xs text-gray-600 dark:text-gray-400"> <span class="text-xs text-gray-600 dark:text-gray-400">
{storage.shared ? '✓' : '-'} {storage.shared ? '✓' : '-'}
</span> </span>
@ -1141,7 +1141,7 @@ const Storage: Component = () => {
</Show> </Show>
<td class="p-0.5 px-1.5"> <td class="p-0.5 px-1.5">
<div class="relative min-w-[200px] h-3.5 rounded overflow-hidden bg-gray-200 dark:bg-gray-600"> <div class="relative min-w-[150px] h-3.5 rounded overflow-hidden bg-gray-200 dark:bg-gray-600">
<div <div
class={`absolute top-0 left-0 h-full ${getProgressBarColor(usagePercent)}`} class={`absolute top-0 left-0 h-full ${getProgressBarColor(usagePercent)}`}
style={{ width: `${usagePercent}%` }} style={{ width: `${usagePercent}%` }}
@ -1155,7 +1155,7 @@ const Storage: Component = () => {
</span> </span>
</div> </div>
</td> </td>
<td class="p-0.5 px-1.5 text-xs hidden sm:table-cell whitespace-nowrap"> <td class="hidden sm:table-cell p-0.5 px-1.5 text-xs whitespace-nowrap">
{formatBytes(storage.free || 0, 0)} {formatBytes(storage.free || 0, 0)}
</td> </td>
<td class="p-0.5 px-1.5 text-xs whitespace-nowrap"> <td class="p-0.5 px-1.5 text-xs whitespace-nowrap">

View file

@ -356,42 +356,41 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
<> <>
<Card padding="none" class="mb-4 overflow-hidden"> <Card padding="none" class="mb-4 overflow-hidden">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full min-w-[600px] table-fixed border-collapse"> <table class="w-full table-fixed border-collapse">
<thead> <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-600"> <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-600">
<th <th
class="pl-3 pr-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[18%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 whitespace-nowrap" class="pl-3 pr-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-auto cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 whitespace-nowrap"
onClick={() => handleSort('name')} onClick={() => handleSort('name')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('name')} onKeyDown={(e) => e.key === 'Enter' && handleSort('name')}
tabindex="0" tabindex="0"
role="button" role="button"
aria-label={`Sort by name ${ aria-label={`Sort by name ${sortKey() === 'name' ? (sortDirection() === 'asc' ? 'ascending' : 'descending') : ''
sortKey() === 'name' ? (sortDirection() === 'asc' ? 'ascending' : 'descending') : ''
}`} }`}
> >
{props.currentTab === 'backups' ? 'Node / PBS' : 'Node'}{' '} {props.currentTab === 'backups' ? 'Node / PBS' : 'Node'}{' '}
{sortKey() === 'name' && (sortDirection() === 'asc' ? '▲' : '▼')} {sortKey() === 'name' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap" class="hidden sm:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[12%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap"
onClick={() => handleSort('uptime')} onClick={() => handleSort('uptime')}
> >
Uptime {sortKey() === 'uptime' && (sortDirection() === 'asc' ? '▲' : '▼')} Uptime {sortKey() === 'uptime' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[16%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap" class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[25%] sm:w-[15%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap"
onClick={() => handleSort('cpu')} onClick={() => handleSort('cpu')}
> >
CPU {sortKey() === 'cpu' && (sortDirection() === 'asc' ? '▲' : '▼')} CPU {sortKey() === 'cpu' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[16%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap" class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[25%] sm:w-[15%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap"
onClick={() => handleSort('memory')} onClick={() => handleSort('memory')}
> >
Memory {sortKey() === 'memory' && (sortDirection() === 'asc' ? '▲' : '▼')} Memory {sortKey() === 'memory' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th> </th>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[16%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap" class="hidden md:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[15%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap"
onClick={() => handleSort('disk')} onClick={() => handleSort('disk')}
> >
{props.currentTab === 'backups' && props.pbsInstances ? 'Storage / Disk' : 'Disk'}{' '} {props.currentTab === 'backups' && props.pbsInstances ? 'Storage / Disk' : 'Disk'}{' '}
@ -399,7 +398,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
</th> </th>
<Show when={hasAnyTemperatureData()}> <Show when={hasAnyTemperatureData()}>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[8%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap" class="hidden lg:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[8%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap"
onClick={() => handleSort('temperature')} onClick={() => handleSort('temperature')}
> >
Temp{' '} Temp{' '}
@ -409,7 +408,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
<For each={countColumns()}> <For each={countColumns()}>
{(column) => ( {(column) => (
<th <th
class="px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[8%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap" class="hidden lg:table-cell px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[8%] cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 whitespace-nowrap"
onClick={() => handleSort(column.key)} onClick={() => handleSort(column.key)}
> >
{column.header}{' '} {column.header}{' '}
@ -549,8 +548,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
</Show> </Show>
<Show when={isPVE && node!.isClusterMember !== undefined}> <Show when={isPVE && node!.isClusterMember !== undefined}>
<span <span
class={`text-[9px] px-1 py-0 rounded text-[8px] font-medium whitespace-nowrap ${ class={`text-[9px] px-1 py-0 rounded text-[8px] font-medium whitespace-nowrap ${node!.isClusterMember
node!.isClusterMember
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
: 'bg-gray-100 text-gray-600 dark:bg-gray-700/50 dark:text-gray-400' : 'bg-gray-100 text-gray-600 dark:bg-gray-700/50 dark:text-gray-400'
}`} }`}
@ -570,10 +568,9 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
</Show> </Show>
</div> </div>
</td> </td>
<td class="px-2 py-0.5 whitespace-nowrap"> <td class="hidden sm:table-cell px-2 py-0.5 whitespace-nowrap">
<span <span
class={`text-xs ${ class={`text-xs ${isPVE && (node?.uptime ?? 0) < 3600
isPVE && (node?.uptime ?? 0) < 3600
? 'text-orange-500' ? 'text-orange-500'
: 'text-gray-600 dark:text-gray-400' : 'text-gray-600 dark:text-gray-400'
}`} }`}
@ -624,7 +621,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
/> />
</Show> </Show>
</td> </td>
<td class="px-2 py-0.5"> <td class="hidden md:table-cell px-2 py-0.5">
<Show <Show
when={online && diskPercentValue !== null} when={online && diskPercentValue !== null}
fallback={<span class="text-xs text-gray-500 dark:text-gray-400">-</span>} fallback={<span class="text-xs text-gray-500 dark:text-gray-400">-</span>}
@ -639,7 +636,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
</Show> </Show>
</td> </td>
<Show when={hasAnyTemperatureData()}> <Show when={hasAnyTemperatureData()}>
<td class="px-2 py-0.5 whitespace-nowrap text-center"> <td class="hidden lg:table-cell px-2 py-0.5 whitespace-nowrap text-center">
<Show <Show
when={ when={
online && online &&
@ -723,7 +720,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
? 'text-xs text-gray-700 dark:text-gray-300' ? 'text-xs text-gray-700 dark:text-gray-300'
: 'text-xs text-gray-400 dark:text-gray-500'; : 'text-xs text-gray-400 dark:text-gray-500';
return ( return (
<td class="px-2 py-0.5 whitespace-nowrap text-center"> <td class="hidden lg:table-cell px-2 py-0.5 whitespace-nowrap text-center">
<span class={textClass}>{display}</span> <span class={textClass}>{display}</span>
</td> </td>
); );