mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Align Proxmox hosts table styling
This commit is contained in:
@@ -124,6 +124,7 @@ import workloadsTableSource from '@/components/Workloads/WorkloadsTable.tsx?raw'
|
||||
import workloadPanelSource from '@/components/Workloads/WorkloadPanel.tsx?raw';
|
||||
import guestRowStateSource from '@/components/Workloads/useGuestRowState.ts?raw';
|
||||
import workloadSelectionStateSource from '@/components/Workloads/useWorkloadSelectionState.ts?raw';
|
||||
import proxmoxNodesTableSource from '@/features/proxmox/ProxmoxNodesTable.tsx?raw';
|
||||
import infrastructureSummarySource from '@/components/Infrastructure/InfrastructureSummary.tsx?raw';
|
||||
import infrastructureSummaryStateSource from '@/components/Infrastructure/useInfrastructureSummaryState.ts?raw';
|
||||
import unifiedResourceHostTableCardSource from '@/components/Infrastructure/UnifiedResourceHostTableCard.tsx?raw';
|
||||
@@ -348,6 +349,7 @@ describe('shared primitive guardrails', () => {
|
||||
expect(summaryTableCardHeaderSource).not.toContain('border-b border-border bg-surface-hover');
|
||||
for (const source of [
|
||||
workloadsTableSource,
|
||||
proxmoxNodesTableSource,
|
||||
unifiedResourceHostTableCardSource,
|
||||
unifiedResourceServiceInfrastructureCardSource,
|
||||
storageContentCardSource,
|
||||
@@ -370,6 +372,7 @@ describe('shared primitive guardrails', () => {
|
||||
|
||||
for (const source of [
|
||||
workloadsTableSource,
|
||||
proxmoxNodesTableSource,
|
||||
unifiedResourceHostTableCardSource,
|
||||
unifiedResourceServiceInfrastructureCardSource,
|
||||
storageContentCardSource,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { For, Show, type Accessor, type Component, type JSX } from 'solid-js';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { For, Show, createMemo, type Accessor, type Component, type JSX } from 'solid-js';
|
||||
import { EmptyState } from '@/components/shared/EmptyState';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { ResponsiveMetricCell } from '@/components/shared/responsive';
|
||||
import { TableCard } from '@/components/shared/TableCard';
|
||||
import { TableCardHeader } from '@/components/shared/TableCardHeader';
|
||||
import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
|
||||
import { StackedDiskBar } from '@/components/Workloads/StackedDiskBar';
|
||||
import { MetricMiniSparkline } from '@/components/Workloads/MetricMiniSparkline';
|
||||
import { TemperatureGauge } from '@/components/shared/TemperatureGauge';
|
||||
import { useBreakpoint } from '@/hooks/useBreakpoint';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -20,6 +22,7 @@ import { asTrimmedString } from '@/utils/stringUtils';
|
||||
import { normalizeDiskArray } from '@/utils/format';
|
||||
import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys';
|
||||
import { useWorkloadTableMetricHistory } from '@/components/Workloads/useWorkloadTableMetricHistory';
|
||||
import { getWorkloadTableLayoutMode } from '@/components/Workloads/guestRowModel';
|
||||
import { type WorkloadsMetricDisplayMode } from '@/components/Workloads/workloadsFilterModel';
|
||||
import { type WorkloadTableMetricHistoryRange } from '@/components/Workloads/workloadMetricHistoryModel';
|
||||
import type { Disk, Node as LegacyNode } from '@/types/api';
|
||||
@@ -29,6 +32,12 @@ import {
|
||||
getResourceNodeName,
|
||||
getResourceVersion,
|
||||
} from './proxmoxPageModel';
|
||||
import {
|
||||
getProxmoxHostColumnWidthStyle,
|
||||
getProxmoxHostTableMinWidthClass,
|
||||
getProxmoxHostVisibleColumnsForLayout,
|
||||
type ProxmoxHostTableColumn,
|
||||
} from './proxmoxHostTableModel';
|
||||
|
||||
// Proxmox Overview mirrors the v5 Dashboard layout: a dedicated nodes table on
|
||||
// top, the canonical Workloads filter + guest table below. The nodes table
|
||||
@@ -103,6 +112,18 @@ const formatPercentLabel = (value: number | null | undefined): string => {
|
||||
return `${Math.round(Math.max(0, normalized))}%`;
|
||||
};
|
||||
|
||||
const getHostHeaderCellClass = (column: ProxmoxHostTableColumn): string => {
|
||||
const align =
|
||||
column.align === 'right' ? 'text-right' : column.align === 'center' ? 'text-center' : '';
|
||||
return `px-1.5 sm:px-2 py-0.5 font-medium ${align}`.trim();
|
||||
};
|
||||
|
||||
const getHostBodyCellClass = (column: ProxmoxHostTableColumn): string => {
|
||||
const align =
|
||||
column.align === 'right' ? 'text-right' : column.align === 'center' ? 'text-center' : '';
|
||||
return `px-1.5 sm:px-2 py-1 ${align}`.trim();
|
||||
};
|
||||
|
||||
export const ProxmoxNodesTable: Component<{
|
||||
nodes: Resource[];
|
||||
guests: Resource[];
|
||||
@@ -112,6 +133,10 @@ export const ProxmoxNodesTable: Component<{
|
||||
emptyTitle: string;
|
||||
emptyDescription: string;
|
||||
}> = (props) => {
|
||||
const breakpoint = useBreakpoint();
|
||||
const layoutMode = createMemo(() => getWorkloadTableLayoutMode(breakpoint.width()));
|
||||
const visibleColumns = createMemo(() => getProxmoxHostVisibleColumnsForLayout(layoutMode()));
|
||||
const visibleColumnIds = createMemo(() => visibleColumns().map((column) => column.id));
|
||||
const displayMode = () => props.metricDisplayMode?.() ?? 'bars';
|
||||
const isSparklineMode = () => displayMode() === 'sparklines';
|
||||
|
||||
@@ -127,38 +152,43 @@ export const ProxmoxNodesTable: Component<{
|
||||
<Show
|
||||
when={props.nodes.length > 0}
|
||||
fallback={
|
||||
<Card padding="lg">
|
||||
<EmptyState
|
||||
icon={props.emptyIcon}
|
||||
title={props.emptyTitle}
|
||||
description={props.emptyDescription}
|
||||
/>
|
||||
</Card>
|
||||
<TableCard>
|
||||
<div class="p-6">
|
||||
<EmptyState
|
||||
icon={props.emptyIcon}
|
||||
title={props.emptyTitle}
|
||||
description={props.emptyDescription}
|
||||
/>
|
||||
</div>
|
||||
</TableCard>
|
||||
}
|
||||
>
|
||||
<Card padding="none" tone="card" class="overflow-hidden">
|
||||
<Table class="w-full min-w-[1080px] border-collapse text-xs">
|
||||
<TableHeader class="bg-surface-alt text-muted border-b border-border">
|
||||
<TableRow class="text-left text-[10px] uppercase tracking-wide">
|
||||
<TableHead class="px-3 py-2 font-medium">Node</TableHead>
|
||||
<TableHead class="px-3 py-2 font-medium">Version</TableHead>
|
||||
<TableHead class="px-3 py-2 font-medium text-right">Uptime</TableHead>
|
||||
<TableHead class="px-3 py-2 font-medium" style={{ width: '180px' }}>
|
||||
CPU
|
||||
</TableHead>
|
||||
<TableHead class="px-3 py-2 font-medium" style={{ width: '180px' }}>
|
||||
Memory
|
||||
</TableHead>
|
||||
<TableHead class="px-3 py-2 font-medium" style={{ width: '180px' }}>
|
||||
Disk
|
||||
</TableHead>
|
||||
<TableHead class="px-3 py-2 font-medium text-right">Temp</TableHead>
|
||||
<TableHead class="px-3 py-2 font-medium text-center">VMs</TableHead>
|
||||
<TableHead class="px-3 py-2 font-medium text-center">CTs</TableHead>
|
||||
<TableHead class="px-3 py-2 font-medium">Cluster</TableHead>
|
||||
<TableCard class="rounded-md">
|
||||
<TableCardHeader title="Hosts" />
|
||||
<Table class={`${getProxmoxHostTableMinWidthClass(layoutMode())} table-fixed text-xs`}>
|
||||
<colgroup>
|
||||
<For each={visibleColumns()}>
|
||||
{(column) => (
|
||||
<col
|
||||
style={getProxmoxHostColumnWidthStyle(
|
||||
column.id,
|
||||
layoutMode(),
|
||||
visibleColumnIds(),
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</colgroup>
|
||||
<TableHeader>
|
||||
<TableRow class="bg-surface-alt text-muted border-b border-border">
|
||||
<For each={visibleColumns()}>
|
||||
{(column) => (
|
||||
<TableHead class={getHostHeaderCellClass(column)}>{column.label}</TableHead>
|
||||
)}
|
||||
</For>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody class="divide-y divide-border-subtle">
|
||||
<TableBody class="divide-y divide-border">
|
||||
<For each={props.nodes}>
|
||||
{(node) => {
|
||||
const name = () => asTrimmedString(node.name) || node.id;
|
||||
@@ -194,151 +224,192 @@ export const ProxmoxNodesTable: Component<{
|
||||
} as Disk)
|
||||
: undefined;
|
||||
const legacyNode = () => projectResourceToLegacyNode(node);
|
||||
const cpuSeries = () =>
|
||||
metricHistory.getNodeMetricSeries(legacyNode(), 'cpu');
|
||||
const cpuSeries = () => metricHistory.getNodeMetricSeries(legacyNode(), 'cpu');
|
||||
const memorySeries = () =>
|
||||
metricHistory.getNodeMetricSeries(legacyNode(), 'memory');
|
||||
const diskSeries = () =>
|
||||
metricHistory.getNodeMetricSeries(legacyNode(), 'disk');
|
||||
const diskSeries = () => metricHistory.getNodeMetricSeries(legacyNode(), 'disk');
|
||||
const renderColumnCell = (column: ProxmoxHostTableColumn): JSX.Element => {
|
||||
switch (column.id) {
|
||||
case 'node':
|
||||
return (
|
||||
<TableCell class={getHostBodyCellClass(column)}>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<StatusDot
|
||||
size="sm"
|
||||
variant={indicator().variant}
|
||||
title={node.status || 'unknown'}
|
||||
ariaHidden
|
||||
/>
|
||||
<span class="truncate font-semibold text-base-content" title={name()}>
|
||||
{name()}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
case 'version':
|
||||
return (
|
||||
<TableCell class={getHostBodyCellClass(column)}>
|
||||
<Show when={version()} fallback={<span class="text-muted">—</span>}>
|
||||
<span class="inline-flex items-center rounded bg-surface-alt px-1.5 py-0.5 font-mono text-[10px] text-base-content">
|
||||
{version()}
|
||||
</span>
|
||||
</Show>
|
||||
</TableCell>
|
||||
);
|
||||
case 'uptime':
|
||||
return (
|
||||
<TableCell
|
||||
class={`${getHostBodyCellClass(column)} tabular-nums ${
|
||||
uptime().warn
|
||||
? 'text-orange-600 dark:text-orange-400'
|
||||
: 'text-base-content'
|
||||
}`}
|
||||
>
|
||||
{uptime().label}
|
||||
</TableCell>
|
||||
);
|
||||
case 'cpu':
|
||||
return (
|
||||
<TableCell class={getHostBodyCellClass(column)}>
|
||||
<Show
|
||||
when={isSparklineMode()}
|
||||
fallback={
|
||||
<ResponsiveMetricCell
|
||||
class="w-full"
|
||||
value={cpuPercent()}
|
||||
type="cpu"
|
||||
resourceId={metricsKey()}
|
||||
isRunning={isOnline()}
|
||||
showMobile={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MetricMiniSparkline
|
||||
series={cpuSeries()}
|
||||
valueLabel={formatPercentLabel(cpuPercent())}
|
||||
title={`${name()} CPU history`}
|
||||
/>
|
||||
</Show>
|
||||
</TableCell>
|
||||
);
|
||||
case 'memory':
|
||||
return (
|
||||
<TableCell class={getHostBodyCellClass(column)}>
|
||||
<Show
|
||||
when={isSparklineMode()}
|
||||
fallback={
|
||||
<Show
|
||||
when={
|
||||
isOnline() && (memoryTotal() > 0 || memoryPercentOnly() != null)
|
||||
}
|
||||
fallback={
|
||||
<div class="flex justify-center">
|
||||
<span class="text-xs text-muted" aria-hidden="true">
|
||||
—
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<StackedMemoryBar
|
||||
used={memoryUsed()}
|
||||
total={memoryTotal()}
|
||||
percentOnly={memoryPercentOnly()}
|
||||
/>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<MetricMiniSparkline
|
||||
series={memorySeries()}
|
||||
valueLabel={formatPercentLabel(memoryPercent())}
|
||||
title={`${name()} memory history`}
|
||||
/>
|
||||
</Show>
|
||||
</TableCell>
|
||||
);
|
||||
case 'disk':
|
||||
return (
|
||||
<TableCell class={getHostBodyCellClass(column)}>
|
||||
<Show
|
||||
when={isSparklineMode()}
|
||||
fallback={
|
||||
<Show
|
||||
when={isOnline() && (aggregateDisk() || node.agent?.disks?.length)}
|
||||
fallback={
|
||||
<div class="flex justify-center">
|
||||
<span class="text-xs text-muted" aria-hidden="true">
|
||||
—
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<StackedDiskBar
|
||||
disks={normalizeDiskArray(node.agent?.disks)}
|
||||
aggregateDisk={aggregateDisk()}
|
||||
/>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<MetricMiniSparkline
|
||||
series={diskSeries()}
|
||||
valueLabel={formatPercentLabel(diskPercent())}
|
||||
title={`${name()} disk history`}
|
||||
/>
|
||||
</Show>
|
||||
</TableCell>
|
||||
);
|
||||
case 'temp':
|
||||
return (
|
||||
<TableCell class={getHostBodyCellClass(column)}>
|
||||
<Show
|
||||
when={
|
||||
typeof temperature() === 'number' && (temperature() as number) > 0
|
||||
}
|
||||
fallback={<span class="text-xs text-muted">—</span>}
|
||||
>
|
||||
<TemperatureGauge value={temperature() as number} />
|
||||
</Show>
|
||||
</TableCell>
|
||||
);
|
||||
case 'vms':
|
||||
return (
|
||||
<TableCell class={getHostBodyCellClass(column)}>
|
||||
<span class={counts().vms > 0 ? VMS_BADGE : ZERO_BADGE}>
|
||||
{counts().vms}
|
||||
</span>
|
||||
</TableCell>
|
||||
);
|
||||
case 'cts':
|
||||
return (
|
||||
<TableCell class={getHostBodyCellClass(column)}>
|
||||
<span class={counts().containers > 0 ? CTS_BADGE : ZERO_BADGE}>
|
||||
{counts().containers}
|
||||
</span>
|
||||
</TableCell>
|
||||
);
|
||||
case 'cluster':
|
||||
return (
|
||||
<TableCell class={getHostBodyCellClass(column)}>
|
||||
<span class="inline-flex items-center rounded-md bg-surface-alt px-2 py-0.5 text-[11px] font-medium text-base-content">
|
||||
{cluster()}
|
||||
</span>
|
||||
</TableCell>
|
||||
);
|
||||
default:
|
||||
column.id satisfies never;
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow class="hover:bg-surface-hover">
|
||||
<TableCell class="px-3 py-2">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<StatusDot
|
||||
size="sm"
|
||||
variant={indicator().variant}
|
||||
title={node.status || 'unknown'}
|
||||
ariaHidden
|
||||
/>
|
||||
<span class="font-semibold text-base-content truncate" title={name()}>
|
||||
{name()}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="px-3 py-2">
|
||||
<Show when={version()} fallback={<span class="text-muted">—</span>}>
|
||||
<span class="inline-flex items-center rounded bg-surface-alt px-1.5 py-0.5 font-mono text-[10px] text-base-content">
|
||||
{version()}
|
||||
</span>
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`px-3 py-2 text-right tabular-nums ${
|
||||
uptime().warn
|
||||
? 'text-orange-600 dark:text-orange-400'
|
||||
: 'text-base-content'
|
||||
}`}
|
||||
>
|
||||
{uptime().label}
|
||||
</TableCell>
|
||||
<TableCell class="px-3 py-2">
|
||||
<Show
|
||||
when={isSparklineMode()}
|
||||
fallback={
|
||||
<ResponsiveMetricCell
|
||||
class="w-full"
|
||||
value={cpuPercent()}
|
||||
type="cpu"
|
||||
resourceId={metricsKey()}
|
||||
isRunning={isOnline()}
|
||||
showMobile={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MetricMiniSparkline
|
||||
series={cpuSeries()}
|
||||
valueLabel={formatPercentLabel(cpuPercent())}
|
||||
title={`${name()} CPU history`}
|
||||
/>
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell class="px-3 py-2">
|
||||
<Show
|
||||
when={isSparklineMode()}
|
||||
fallback={
|
||||
<Show
|
||||
when={isOnline() && (memoryTotal() > 0 || memoryPercentOnly() != null)}
|
||||
fallback={
|
||||
<div class="flex justify-center">
|
||||
<span class="text-xs text-muted" aria-hidden="true">
|
||||
—
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<StackedMemoryBar
|
||||
used={memoryUsed()}
|
||||
total={memoryTotal()}
|
||||
percentOnly={memoryPercentOnly()}
|
||||
/>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<MetricMiniSparkline
|
||||
series={memorySeries()}
|
||||
valueLabel={formatPercentLabel(memoryPercent())}
|
||||
title={`${name()} memory history`}
|
||||
/>
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell class="px-3 py-2">
|
||||
<Show
|
||||
when={isSparklineMode()}
|
||||
fallback={
|
||||
<Show
|
||||
when={isOnline() && (aggregateDisk() || node.agent?.disks?.length)}
|
||||
fallback={
|
||||
<div class="flex justify-center">
|
||||
<span class="text-xs text-muted" aria-hidden="true">
|
||||
—
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<StackedDiskBar
|
||||
disks={normalizeDiskArray(node.agent?.disks)}
|
||||
aggregateDisk={aggregateDisk()}
|
||||
/>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<MetricMiniSparkline
|
||||
series={diskSeries()}
|
||||
valueLabel={formatPercentLabel(diskPercent())}
|
||||
title={`${name()} disk history`}
|
||||
/>
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell class="px-3 py-2 text-right">
|
||||
<Show
|
||||
when={typeof temperature() === 'number' && (temperature() as number) > 0}
|
||||
fallback={<span class="text-xs text-muted">—</span>}
|
||||
>
|
||||
<TemperatureGauge value={temperature() as number} />
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell class="px-3 py-2 text-center">
|
||||
<span class={counts().vms > 0 ? VMS_BADGE : ZERO_BADGE}>
|
||||
{counts().vms}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class="px-3 py-2 text-center">
|
||||
<span class={counts().containers > 0 ? CTS_BADGE : ZERO_BADGE}>
|
||||
{counts().containers}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class="px-3 py-2">
|
||||
<span class="inline-flex items-center rounded-md bg-surface-alt px-2 py-0.5 text-[11px] font-medium text-base-content">
|
||||
{cluster()}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableRow class="text-[11px] sm:text-xs">
|
||||
<For each={visibleColumns()}>{(column) => renderColumnCell(column)}</For>
|
||||
</TableRow>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</TableCard>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
getProxmoxHostColumnWidthStyle,
|
||||
getProxmoxHostTableMinWidthClass,
|
||||
getProxmoxHostVisibleColumnsForLayout,
|
||||
} from '../proxmoxHostTableModel';
|
||||
|
||||
describe('proxmoxHostTableModel', () => {
|
||||
it('prioritizes live utilization columns in the mobile host table', () => {
|
||||
const columns = getProxmoxHostVisibleColumnsForLayout('mobile');
|
||||
const ids = columns.map((column) => column.id);
|
||||
|
||||
expect(ids).toEqual(['node', 'cpu', 'memory', 'disk']);
|
||||
expect(getProxmoxHostTableMinWidthClass('mobile')).toBe('min-w-full');
|
||||
expect(getProxmoxHostColumnWidthStyle('node', 'mobile', ids)).toEqual({ width: '38%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('cpu', 'mobile', ids)).toEqual({ width: '20%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('memory', 'mobile', ids)).toEqual({ width: '21%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('disk', 'mobile', ids)).toEqual({ width: '21%' });
|
||||
});
|
||||
|
||||
it('adds temperature and guest counts before slower-changing metadata on tablet', () => {
|
||||
expect(getProxmoxHostVisibleColumnsForLayout('tablet').map((column) => column.id)).toEqual([
|
||||
'node',
|
||||
'cpu',
|
||||
'memory',
|
||||
'disk',
|
||||
'temp',
|
||||
'vms',
|
||||
'cts',
|
||||
]);
|
||||
expect(getProxmoxHostTableMinWidthClass('tablet')).toBe('min-w-full');
|
||||
});
|
||||
|
||||
it('keeps the full host inventory table on compact and wide layouts', () => {
|
||||
const compactIds = getProxmoxHostVisibleColumnsForLayout('compact').map((column) => column.id);
|
||||
|
||||
expect(compactIds).toEqual([
|
||||
'node',
|
||||
'version',
|
||||
'uptime',
|
||||
'cpu',
|
||||
'memory',
|
||||
'disk',
|
||||
'temp',
|
||||
'vms',
|
||||
'cts',
|
||||
'cluster',
|
||||
]);
|
||||
expect(getProxmoxHostTableMinWidthClass('compact')).toBe('min-w-[1080px]');
|
||||
expect(getProxmoxHostTableMinWidthClass('wide')).toBe('min-w-[1080px]');
|
||||
expect(getProxmoxHostColumnWidthStyle('cluster', 'compact', compactIds)).toEqual({
|
||||
width: '12%',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { JSX } from 'solid-js';
|
||||
|
||||
import type { WorkloadTableLayoutMode } from '@/components/Workloads/guestRowModel';
|
||||
|
||||
export type ProxmoxHostTableColumnId =
|
||||
| 'node'
|
||||
| 'version'
|
||||
| 'uptime'
|
||||
| 'cpu'
|
||||
| 'memory'
|
||||
| 'disk'
|
||||
| 'temp'
|
||||
| 'vms'
|
||||
| 'cts'
|
||||
| 'cluster';
|
||||
|
||||
export type ProxmoxHostTableColumn = {
|
||||
id: ProxmoxHostTableColumnId;
|
||||
label: string;
|
||||
align?: 'left' | 'right' | 'center';
|
||||
};
|
||||
|
||||
const HOST_TABLE_LAYOUT_ORDER: Record<WorkloadTableLayoutMode, number> = {
|
||||
mobile: 0,
|
||||
tablet: 1,
|
||||
compact: 2,
|
||||
wide: 3,
|
||||
};
|
||||
|
||||
const HOST_COLUMN_MIN_LAYOUT: Record<ProxmoxHostTableColumnId, WorkloadTableLayoutMode> = {
|
||||
node: 'mobile',
|
||||
cpu: 'mobile',
|
||||
memory: 'mobile',
|
||||
disk: 'mobile',
|
||||
temp: 'tablet',
|
||||
vms: 'tablet',
|
||||
cts: 'tablet',
|
||||
version: 'compact',
|
||||
uptime: 'compact',
|
||||
cluster: 'compact',
|
||||
};
|
||||
|
||||
const HOST_COLUMN_DESKTOP_WIDTHS: Record<ProxmoxHostTableColumnId, number> = {
|
||||
node: 15,
|
||||
version: 8,
|
||||
uptime: 7,
|
||||
cpu: 13,
|
||||
memory: 13,
|
||||
disk: 15,
|
||||
temp: 7,
|
||||
vms: 5,
|
||||
cts: 5,
|
||||
cluster: 12,
|
||||
};
|
||||
|
||||
const HOST_COLUMN_RESPONSIVE_WEIGHTS: Record<
|
||||
Exclude<WorkloadTableLayoutMode, 'wide'>,
|
||||
Partial<Record<ProxmoxHostTableColumnId, number>>
|
||||
> = {
|
||||
mobile: {
|
||||
node: 38,
|
||||
cpu: 20,
|
||||
memory: 21,
|
||||
disk: 21,
|
||||
},
|
||||
tablet: {
|
||||
node: 26,
|
||||
cpu: 17,
|
||||
memory: 17,
|
||||
disk: 19,
|
||||
temp: 8,
|
||||
vms: 6,
|
||||
cts: 6,
|
||||
},
|
||||
compact: HOST_COLUMN_DESKTOP_WIDTHS,
|
||||
};
|
||||
|
||||
const formatPercentage = (value: number): string => `${Number(value.toFixed(4))}%`;
|
||||
|
||||
export const PROXMOX_HOST_TABLE_COLUMNS: ProxmoxHostTableColumn[] = [
|
||||
{ id: 'node', label: 'Node' },
|
||||
{ id: 'version', label: 'Version' },
|
||||
{ id: 'uptime', label: 'Uptime', align: 'right' },
|
||||
{ id: 'cpu', label: 'CPU' },
|
||||
{ id: 'memory', label: 'Memory' },
|
||||
{ id: 'disk', label: 'Disk' },
|
||||
{ id: 'temp', label: 'Temp', align: 'right' },
|
||||
{ id: 'vms', label: 'VMs', align: 'center' },
|
||||
{ id: 'cts', label: 'CTs', align: 'center' },
|
||||
{ id: 'cluster', label: 'Cluster' },
|
||||
];
|
||||
|
||||
export const getProxmoxHostVisibleColumnsForLayout = (
|
||||
layoutMode: WorkloadTableLayoutMode,
|
||||
): ProxmoxHostTableColumn[] => {
|
||||
const layoutRank = HOST_TABLE_LAYOUT_ORDER[layoutMode];
|
||||
return PROXMOX_HOST_TABLE_COLUMNS.filter(
|
||||
(column) => HOST_TABLE_LAYOUT_ORDER[HOST_COLUMN_MIN_LAYOUT[column.id]] <= layoutRank,
|
||||
);
|
||||
};
|
||||
|
||||
export const getProxmoxHostColumnWidthStyle = (
|
||||
columnId: ProxmoxHostTableColumnId,
|
||||
layoutMode: WorkloadTableLayoutMode,
|
||||
visibleColumnIds: readonly ProxmoxHostTableColumnId[],
|
||||
): JSX.CSSProperties => {
|
||||
const weights =
|
||||
layoutMode === 'wide' ? HOST_COLUMN_DESKTOP_WIDTHS : HOST_COLUMN_RESPONSIVE_WEIGHTS[layoutMode];
|
||||
const columnWeight = weights[columnId] ?? 0;
|
||||
const totalWeight = visibleColumnIds.reduce((total, id) => total + (weights[id] ?? 0), 0);
|
||||
const width = totalWeight > 0 ? (columnWeight / totalWeight) * 100 : 0;
|
||||
|
||||
return { width: formatPercentage(width) };
|
||||
};
|
||||
|
||||
export const getProxmoxHostTableMinWidthClass = (
|
||||
layoutMode: WorkloadTableLayoutMode,
|
||||
): 'min-w-full' | 'min-w-[1080px]' =>
|
||||
layoutMode === 'mobile' || layoutMode === 'tablet' ? 'min-w-full' : 'min-w-[1080px]';
|
||||
Reference in New Issue
Block a user