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:
parent
787f947f2d
commit
244431b6c2
6 changed files with 53 additions and 51 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue