refactor(ui): improve table column sizing and progress bar constraints

- Cap progress bars at 140px max-width for better readability
- Constrain shell layout to 1400px max-width, centered
- Fix metric columns (CPU, Memory, Disk) at 156px
- Give name/resource columns extra space via flex
- Remove Docker Disk column (data rarely available from API)
- Remove fixed percentage widths from backups table for auto-sizing
- Improve text contrast in progress bar labels
This commit is contained in:
rcourtman 2025-11-27 12:09:12 +00:00
parent 787f947f2d
commit 244431b6c2
6 changed files with 53 additions and 51 deletions

View file

@ -1869,7 +1869,6 @@ const UnifiedBackups: Component = () => {
<style>{`
.overflow-x-auto::-webkit-scrollbar { display: none; }
.backup-table {
table-layout: fixed;
width: 100%;
}
.backup-table th,
@ -2039,54 +2038,54 @@ const UnifiedBackups: Component = () => {
<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">
<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 w-[8%] min-w-[60px]"
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"
onClick={() => handleSort('vmid')}
>
{hasHostBackups() ? 'VMID/Host' : 'VMID'}{' '}
{hasHostBackups() ? 'ID' : 'VMID'}{' '}
{sortKey() === 'vmid' && (sortDirection() === 'asc' ? '▲' : '▼')}
</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 w-[8%] min-w-[60px]"
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"
onClick={() => handleSort('type')}
>
Type {sortKey() === 'type' && (sortDirection() === 'asc' ? '▲' : '▼')}
</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 w-auto"
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"
onClick={() => handleSort('name')}
>
Name {sortKey() === 'name' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<th
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%]"
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"
onClick={() => handleSort('node')}
>
Node {sortKey() === 'node' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<Show when={backupTypeFilter() === 'all' || backupTypeFilter() === 'remote'}>
<th
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%]"
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"
onClick={() => handleSort('owner')}
>
Owner {sortKey() === 'owner' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
</Show>
<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 w-[14%] min-w-[120px]"
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"
onClick={() => handleSort('backupTime')}
>
Time {sortKey() === 'backupTime' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<Show when={backupTypeFilter() !== 'snapshot'}>
<th
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%]"
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"
onClick={() => handleSort('size')}
>
Size {sortKey() === 'size' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
</Show>
<th
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%]"
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"
onClick={() => handleSort('backupType')}
>
Backup{' '}
@ -2094,7 +2093,7 @@ const UnifiedBackups: Component = () => {
</th>
<Show when={backupTypeFilter() !== 'snapshot'}>
<th
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%]"
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"
onClick={() => handleSort('storage')}
>
Location{' '}
@ -2103,15 +2102,15 @@ const UnifiedBackups: Component = () => {
</Show>
<Show when={backupTypeFilter() === 'all' || backupTypeFilter() === 'remote'}>
<th
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%]"
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"
onClick={() => handleSort('verified')}
>
Verified{' '}
{sortKey() === 'verified' && (sortDirection() === 'asc' ? '▲' : '▼')}
<span title="Verified"></span>
{sortKey() === 'verified' && (sortDirection() === 'asc' ? ' ▲' : ' ▼')}
</th>
</Show>
<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%]"
class="hidden md:table-cell px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider"
>
Details
</th>

View file

@ -111,14 +111,15 @@ interface ColumnDef {
}
export const GUEST_COLUMNS: ColumnDef[] = [
{ id: 'name', label: 'Name', priority: 'essential', minWidth: '100px', flex: 1.5 },
{ id: 'name', label: 'Name', priority: 'essential', minWidth: '100px', maxWidth: '300px' },
{ 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 matching v4.32.5 (grows at breakpoints to fit full header text)
// Metric columns - fixed width to match progress bar max-width (140px + padding)
{ id: 'cpu', label: 'CPU', priority: 'essential', minWidth: '50px', maxWidth: '156px' },
{ id: 'memory', label: 'Memory', priority: 'essential', minWidth: '50px', maxWidth: '156px' },
{ id: 'disk', label: 'Disk', priority: 'essential', minWidth: '50px', maxWidth: '156px' },
// I/O columns - fixed width
{ 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' },

View file

@ -101,15 +101,15 @@ export function MetricBar(props: MetricBarProps) {
<Show
when={viewMode() === 'sparklines' && props.resourceId}
fallback={
// Original progress bar mode
<div ref={containerRef} class="metric-text w-full h-4 flex items-center">
<div class="relative w-full h-full overflow-hidden bg-gray-200 dark:bg-gray-600 rounded">
// Original progress bar mode - capped width for better appearance on wide screens
<div ref={containerRef} class="metric-text w-full h-4 flex items-center justify-center">
<div class="relative w-full max-w-[140px] h-full overflow-hidden bg-gray-200 dark:bg-gray-600 rounded">
<div class={`absolute top-0 left-0 h-full ${progressColorClass()}`} style={{ width: `${width()}%` }} />
<span class="absolute inset-0 flex items-center justify-center text-[10px] font-medium text-gray-800 dark:text-gray-100 leading-none">
<span class="absolute inset-0 flex items-center justify-center text-[10px] font-semibold text-gray-700 dark:text-gray-100 leading-none">
<span class="flex items-center gap-1 whitespace-nowrap px-0.5">
<span>{props.label}</span>
<Show when={showSublabel()}>
<span class="metric-sublabel font-normal">
<span class="metric-sublabel font-normal text-gray-500 dark:text-gray-300">
({props.sublabel})
</span>
</Show>

View file

@ -122,15 +122,16 @@ interface DockerColumnDef extends ColumnConfig {
// - supplementary: Visible on large screens and up (lg: 1024px+)
// - detailed: Visible on extra large screens and up (xl: 1280px+)
export const DOCKER_COLUMNS: DockerColumnDef[] = [
{ id: 'resource', label: 'Resource', priority: 'essential', minWidth: '100px', flex: 1.5, sortKey: 'resource' },
{ id: 'type', label: 'Type', priority: 'essential', minWidth: '24px', maxWidth: '50px', sortKey: 'type' },
{ id: 'image', label: 'Image / Stack', shortLabel: 'Image', priority: 'essential', minWidth: '80px', flex: 1, sortKey: 'image' },
{ id: 'status', label: 'Status', priority: 'essential', minWidth: '60px', maxWidth: '100px', sortKey: 'status' },
{ id: 'cpu', label: 'CPU', priority: 'essential', minWidth: '50px', flex: 1, sortKey: 'cpu' },
{ id: 'memory', label: 'Memory', shortLabel: 'Mem', priority: 'essential', minWidth: '50px', flex: 1, sortKey: 'memory' },
{ id: 'disk', label: 'Disk', priority: 'essential', minWidth: '50px', flex: 1, sortKey: 'disk' },
{ id: 'tasks', label: 'Tasks / Restarts', shortLabel: 'Tasks', priority: 'essential', minWidth: '40px', maxWidth: '70px', sortKey: 'tasks' },
{ id: 'updated', label: 'Updated / Uptime', shortLabel: 'Updated', priority: 'essential', minWidth: '50px', maxWidth: '80px', sortKey: 'updated' },
{ id: 'resource', label: 'Resource', priority: 'essential', minWidth: '100px', flex: 1, sortKey: 'resource' },
{ id: 'type', label: 'Type', priority: 'essential', minWidth: '50px', maxWidth: '80px', sortKey: 'type' },
{ id: 'image', label: 'Image / Stack', shortLabel: 'Image', priority: 'essential', minWidth: '80px', maxWidth: '250px', sortKey: 'image' },
{ id: 'status', label: 'Status', priority: 'essential', minWidth: '80px', maxWidth: '130px', sortKey: 'status' },
// Metric columns - fixed width to match progress bar max-width (140px + padding)
// Note: Disk column removed - Docker API rarely provides this data
{ id: 'cpu', label: 'CPU', priority: 'essential', minWidth: '50px', maxWidth: '156px', sortKey: 'cpu' },
{ id: 'memory', label: 'Memory', shortLabel: 'Mem', priority: 'essential', minWidth: '50px', maxWidth: '156px', sortKey: 'memory' },
{ id: 'tasks', label: 'Tasks', shortLabel: 'Tasks', priority: 'essential', minWidth: '60px', maxWidth: '80px', sortKey: 'tasks' },
{ id: 'updated', label: 'Uptime', shortLabel: 'Uptime', priority: 'essential', minWidth: '70px', maxWidth: '90px', sortKey: 'updated' },
];
// Global state for currently expanded drawer (only one drawer open at a time)
@ -2451,9 +2452,9 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
</div>
</Show>
<Show when={!isResource}>
<div class="flex items-center gap-1">
<span class="hidden xl:inline">{col.label}</span>
<span class="xl:hidden">{col.shortLabel || col.label}</span>
<div class="flex items-center gap-1 overflow-hidden">
<span class="hidden xl:inline truncate">{col.label}</span>
<span class="xl:hidden truncate">{col.shortLabel || col.label}</span>
{colSortKey && renderSortIndicator(colSortKey)}
</div>
</Show>

View file

@ -76,11 +76,12 @@ interface ColumnDef {
}
const BASE_COLUMNS: ColumnDef[] = [
{ id: 'name', label: 'Node', priority: 'essential', minWidth: '70px', flex: 1.5, align: 'left' },
{ id: 'uptime', label: 'Uptime', priority: 'essential', icon: ClockIcon, minWidth: '35px', maxWidth: '70px', align: 'center' },
{ id: 'cpu', label: 'CPU', priority: 'essential', icon: CpuIcon, minWidth: '40px', flex: 1.2, align: 'center' },
{ id: 'memory', label: 'Memory', priority: 'essential', icon: MemoryIcon, minWidth: '40px', flex: 1.2, align: 'center' },
{ id: 'disk', label: 'Disk', priority: 'essential', icon: DiskIcon, minWidth: '40px', flex: 1.2, align: 'center' },
{ id: 'name', label: 'Node', priority: 'essential', minWidth: '100px', maxWidth: '300px', align: 'left' },
{ id: 'uptime', label: 'Uptime', priority: 'essential', icon: ClockIcon, minWidth: '35px', flex: 1, align: 'center' },
// Metric columns - fixed width to match progress bar max-width (140px + padding)
{ id: 'cpu', label: 'CPU', priority: 'essential', icon: CpuIcon, minWidth: '40px', maxWidth: '156px', align: 'center' },
{ id: 'memory', label: 'Memory', priority: 'essential', icon: MemoryIcon, minWidth: '40px', maxWidth: '156px', align: 'center' },
{ id: 'disk', label: 'Disk', priority: 'essential', icon: DiskIcon, minWidth: '40px', maxWidth: '156px', align: 'center' },
];
const TEMP_COLUMN: ColumnDef = {
@ -89,7 +90,7 @@ const TEMP_COLUMN: ColumnDef = {
priority: 'essential',
icon: TempIcon,
minWidth: '35px',
maxWidth: '55px',
flex: 1,
align: 'center'
};
@ -150,16 +151,16 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
switch (props.currentTab) {
case 'dashboard':
return [
{ header: 'VMs', key: 'vmCount', icon: VMIcon, minWidth: '40px', maxWidth: '50px' },
{ header: 'CTs', key: 'containerCount', icon: ContainerIcon, minWidth: '40px', maxWidth: '50px' },
{ header: 'VMs', key: 'vmCount', icon: VMIcon, minWidth: '40px' },
{ header: 'CTs', key: 'containerCount', icon: ContainerIcon, minWidth: '40px' },
];
case 'storage':
return [
{ header: 'Storage', key: 'storageCount', icon: DiskIcon, minWidth: '50px', maxWidth: '70px' },
{ header: 'Disks', key: 'diskCount', icon: DiskIcon, minWidth: '45px', maxWidth: '60px' },
{ header: 'Storage', key: 'storageCount', icon: DiskIcon, minWidth: '50px' },
{ header: 'Disks', key: 'diskCount', icon: DiskIcon, minWidth: '45px' },
];
case 'backups':
return [{ header: 'Backups', key: 'backupCount', icon: BackupIcon, minWidth: '50px', maxWidth: '70px' }];
return [{ header: 'Backups', key: 'backupCount', icon: BackupIcon, minWidth: '50px' }];
default:
return [];
}
@ -181,7 +182,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
cols.push(TEMP_COLUMN);
}
// Add count columns based on tab
// Add count columns based on tab - these share extra space evenly with other flex columns
countColumns().forEach((cc) => {
cols.push({
id: cc.key,
@ -189,7 +190,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
priority: 'essential' as ColumnPriority,
icon: cc.icon,
minWidth: cc.minWidth || '40px',
maxWidth: cc.maxWidth || '50px',
flex: 1,
align: 'center',
});
});

View file

@ -126,7 +126,7 @@
@layer components {
.pulse-shell {
width: 100%;
max-width: min(95vw, 2400px);
max-width: min(95vw, 1400px);
margin-inline: auto;
padding-inline: clamp(1rem, 1.5vw, 2rem);
transition: padding-inline 0.3s ease;