Simplify metric bar labels

This commit is contained in:
rcourtman 2025-10-29 10:37:18 +00:00
parent 7ee417a629
commit 93faaacbd1
8 changed files with 51 additions and 58 deletions

View file

@ -1,7 +1,7 @@
import { Component, createSignal, Show, For, createMemo, createEffect } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import { useWebSocket } from '@/App';
import { formatBytes, formatAbsoluteTime, formatRelativeTime, formatUptime } from '@/utils/format';
import { formatBytes, formatAbsoluteTime, formatRelativeTime, formatUptime, formatPercent } from '@/utils/format';
import { createLocalStorageBooleanSignal, STORAGE_KEYS } from '@/utils/localStorage';
import { parseFilterStack, evaluateFilterStack } from '@/utils/searchQuery';
import { UnifiedNodeSelector } from '@/components/shared/UnifiedNodeSelector';
@ -1302,15 +1302,15 @@ const UnifiedBackups: Component = () => {
</div>
</td>
<td class="p-0.5 px-1.5 min-w-[180px]">
<MetricBar value={cpuPercent()} label={`${cpuPercent()}%`} type="cpu" />
<MetricBar value={cpuPercent()} label={formatPercent(cpuPercent())} type="cpu" />
</td>
<td class="p-0.5 px-1.5 min-w-[180px]">
<MetricBar
value={memPercent()}
label={`${memPercent()}%`}
label={formatPercent(memPercent())}
sublabel={
pbs.memoryTotal
? `${formatBytes(pbs.memoryUsed)}/${formatBytes(pbs.memoryTotal)}`
? `${formatBytes(pbs.memoryUsed, 0)}/${formatBytes(pbs.memoryTotal, 0)}`
: undefined
}
type="memory"
@ -1319,8 +1319,8 @@ const UnifiedBackups: Component = () => {
<td class="p-0.5 px-1.5 min-w-[180px]">
<MetricBar
value={storage.percent}
label={`${storage.percent}%`}
sublabel={`${formatBytes(storage.used)}/${formatBytes(storage.total)}`}
label={formatPercent(storage.percent)}
sublabel={`${formatBytes(storage.used, 0)}/${formatBytes(storage.total, 0)}`}
type="disk"
/>
</td>

View file

@ -1,6 +1,6 @@
import { createMemo, createSignal, createEffect, on, Show, For } from 'solid-js';
import type { VM, Container } from '@/types/api';
import { formatBytes, formatUptime } from '@/utils/format';
import { formatBytes, formatPercent, formatUptime } from '@/utils/format';
import { MetricBar } from './MetricBar';
import { IOMetric } from './IOMetric';
import { TagBadges } from './TagBadges';
@ -113,7 +113,7 @@ export function GuestRow(props: GuestRowProps) {
if (!props.guest.memory) return undefined;
const used = props.guest.memory.used ?? 0;
const total = props.guest.memory.total ?? 0;
return `${formatBytes(used)}/${formatBytes(total)}`;
return `${formatBytes(used, 0)}/${formatBytes(total, 0)}`;
});
const memoryExtraLines = createMemo(() => {
if (!props.guest.memory) return undefined;
@ -124,11 +124,11 @@ export function GuestRow(props: GuestRowProps) {
props.guest.memory.balloon > 0 &&
props.guest.memory.balloon !== total
) {
lines.push(`Balloon: ${formatBytes(props.guest.memory.balloon)}`);
lines.push(`Balloon: ${formatBytes(props.guest.memory.balloon, 0)}`);
}
if (props.guest.memory.swapTotal && props.guest.memory.swapTotal > 0) {
const swapUsed = props.guest.memory.swapUsed ?? 0;
lines.push(`Swap: ${formatBytes(swapUsed)} / ${formatBytes(props.guest.memory.swapTotal)}`);
lines.push(`Swap: ${formatBytes(swapUsed, 0)} / ${formatBytes(props.guest.memory.swapTotal, 0)}`);
}
return lines.length > 0 ? lines : undefined;
});
@ -584,10 +584,10 @@ export function GuestRow(props: GuestRowProps) {
<Show when={showGuestMetrics()} fallback={<span class="text-sm text-gray-400">-</span>}>
<MetricBar
value={cpuPercent()}
label={`${cpuPercent().toFixed(0)}%`}
label={formatPercent(cpuPercent())}
sublabel={
props.guest.cpus
? `${((props.guest.cpu || 0) * props.guest.cpus).toFixed(1)}/${props.guest.cpus} cores`
? `${props.guest.cpus} ${props.guest.cpus === 1 ? 'core' : 'cores'}`
: undefined
}
type="cpu"
@ -601,7 +601,7 @@ export function GuestRow(props: GuestRowProps) {
<Show when={showGuestMetrics()} fallback={<span class="text-sm text-gray-400">-</span>}>
<MetricBar
value={memPercent()}
label={`${memPercent().toFixed(0)}%`}
label={formatPercent(memPercent())}
sublabel={memoryUsageLabel()}
type="memory"
/>
@ -621,10 +621,10 @@ export function GuestRow(props: GuestRowProps) {
>
<MetricBar
value={diskPercent()}
label={`${diskPercent().toFixed(0)}%`}
label={formatPercent(diskPercent())}
sublabel={
props.guest.disk
? `${formatBytes(props.guest.disk.used)}/${formatBytes(props.guest.disk.total)}`
? `${formatBytes(props.guest.disk.used, 0)}/${formatBytes(props.guest.disk.total, 0)}`
: undefined
}
type="disk"

View file

@ -95,7 +95,7 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
? clampPercent((memoryUsed / memoryTotal) * 100)
: 0;
const memoryLabel =
memoryTotal > 0 ? `${formatBytes(memoryUsed)} / ${formatBytes(memoryTotal)}` : undefined;
memoryTotal > 0 ? `${formatBytes(memoryUsed, 0)} / ${formatBytes(memoryTotal, 0)}` : undefined;
let diskPercent = 0;
let diskLabel: string | undefined;
@ -110,7 +110,7 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
);
if (totals.total > 0) {
diskPercent = clampPercent((totals.used / totals.total) * 100);
diskLabel = `${formatBytes(totals.used)} / ${formatBytes(totals.total)}`;
diskLabel = `${formatBytes(totals.used, 0)} / ${formatBytes(totals.total, 0)}`;
}
}

View file

@ -533,9 +533,9 @@ const DockerContainerRow: Component<{
const memPercent = () => Math.max(0, Math.min(100, container.memoryPercent ?? 0));
const memUsageLabel = () => {
if (!container.memoryUsageBytes) return undefined;
const used = formatBytes(container.memoryUsageBytes);
const used = formatBytes(container.memoryUsageBytes, 0);
const limit = container.memoryLimitBytes
? formatBytes(container.memoryLimitBytes)
? formatBytes(container.memoryLimitBytes, 0)
: undefined;
return limit ? `${used} / ${limit}` : used;
};
@ -695,7 +695,7 @@ const DockerContainerRow: Component<{
{statusLabel()}
</span>
</td>
<td class="px-2 py-0.5">
<td class="px-2 py-0.5 min-w-[160px]">
<Show
when={isRunning() && container.cpuPercent && container.cpuPercent > 0}
fallback={<span class="text-xs text-gray-400"></span>}
@ -703,7 +703,7 @@ const DockerContainerRow: Component<{
<MetricBar value={cpuPercent()} label={formatPercent(cpuPercent())} type="cpu" />
</Show>
</td>
<td class="px-2 py-0.5">
<td class="px-2 py-0.5 min-w-[220px]">
<Show
when={isRunning() && container.memoryUsageBytes && container.memoryUsageBytes > 0}
fallback={<span class="text-xs text-gray-400"></span>}
@ -1132,8 +1132,8 @@ const DockerServiceRow: Component<{
{badge.label}
</span>
</td>
<td class="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"></td>
<td class="px-2 py-0.5 text-xs text-gray-400 dark:text-gray-500 min-w-[150px]"></td>
<td class="px-2 py-0.5 text-xs text-gray-400 dark:text-gray-500 min-w-[210px]"></td>
<td class="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">
{(service.runningTasks ?? 0)}/{service.desiredTasks ?? 0}
@ -1451,8 +1451,8 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
}
>
<Card padding="none" class="overflow-hidden">
<ScrollableTable minWidth="900px">
<table class="w-full min-w-[900px] table-fixed border-collapse whitespace-nowrap">
<ScrollableTable minWidth="1024px">
<table class="w-full min-w-[1024px] table-fixed border-collapse whitespace-nowrap">
<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="pl-4 pr-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[24%]">
@ -1467,10 +1467,10 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
<th class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[15%]">
Status
</th>
<th class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%]">
<th class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[14%] min-w-[150px]">
CPU
</th>
<th class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[11%]">
<th class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[17%] min-w-[210px]">
Memory
</th>
<th class="px-2 py-1 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%]">

View file

@ -2,7 +2,7 @@ import type { Component } from 'solid-js';
import { For, Show, createMemo, createSignal, createEffect, on, onMount, onCleanup } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import type { Host } from '@/types/api';
import { formatBytes, formatRelativeTime, formatUptime } from '@/utils/format';
import { formatBytes, formatPercent, formatRelativeTime, formatUptime } from '@/utils/format';
import { Card } from '@/components/shared/Card';
import { ScrollableTable } from '@/components/shared/ScrollableTable';
import { EmptyState } from '@/components/shared/EmptyState';
@ -211,8 +211,8 @@ export const HostsOverview: Component<HostsOverviewProps> = (props) => {
{(host) => {
const cpuPercent = () => host.cpuUsage ?? 0;
const memPercent = () => host.memory?.usage ?? 0;
const memUsed = () => formatBytes(host.memory?.used ?? 0);
const memTotal = () => formatBytes(host.memory?.total ?? 0);
const memUsed = () => formatBytes(host.memory?.used ?? 0, 0);
const memTotal = () => formatBytes(host.memory?.total ?? 0, 0);
// Drawer state
const [drawerOpen, setDrawerOpen] = createSignal(drawerState.get(host.id) ?? false);
@ -301,7 +301,7 @@ export const HostsOverview: Component<HostsOverviewProps> = (props) => {
fallback={<span class="text-xs text-gray-500 dark:text-gray-400"></span>}
>
<MetricBar
label={`${cpuPercent().toFixed(1)}%`}
label={formatPercent(cpuPercent())}
value={cpuPercent()}
type="cpu"
/>
@ -413,14 +413,14 @@ export const HostsOverview: Component<HostsOverviewProps> = (props) => {
<div class="flex items-center justify-between">
<span class="font-medium truncate">{disk.mountpoint || disk.device}</span>
<span class="text-[10px] text-gray-500 dark:text-gray-400">
{formatBytes(disk.used ?? 0)} / {formatBytes(disk.total ?? 0)}
{formatBytes(disk.used ?? 0, 0)} / {formatBytes(disk.total ?? 0, 0)}
</span>
</div>
<Show when={diskPercent() > 0}>
<div class="mt-0.5">
<MetricBar
value={diskPercent()}
label={`${diskPercent().toFixed(1)}%`}
label={formatPercent(diskPercent())}
type="disk"
/>
</div>

View file

@ -2,7 +2,7 @@ import { Component, For, Show, createSignal, createMemo, createEffect } from 'so
import { useNavigate } from '@solidjs/router';
import { useWebSocket } from '@/App';
import { getAlertStyles } from '@/utils/alerts';
import { formatBytes } from '@/utils/format';
import { formatBytes, formatPercent } from '@/utils/format';
import type { Storage as StorageType, CephCluster } from '@/types/api';
import { ComponentErrorBoundary } from '@/components/ErrorBoundary';
import { UnifiedNodeSelector } from '@/components/shared/UnifiedNodeSelector';
@ -858,7 +858,7 @@ const Storage: Component = () => {
const used = Math.max(0, cluster.usedBytes || 0);
const percent = total > 0 ? (used / total) * 100 : 0;
parts.push(
`${formatBytes(used)} / ${formatBytes(total)} (${percent.toFixed(1)}%)`,
`${formatBytes(used, 0)} / ${formatBytes(total, 0)} (${formatPercent(percent)})`,
);
if (
Number.isFinite(cluster.numOsds) &&
@ -883,7 +883,7 @@ const Storage: Component = () => {
if (totals.total > 0) {
const percent = (totals.used / totals.total) * 100;
parts.push(
`${formatBytes(totals.used)} / ${formatBytes(totals.total)} (${percent.toFixed(1)}%)`,
`${formatBytes(totals.used, 0)} / ${formatBytes(totals.total, 0)} (${formatPercent(percent)})`,
);
}
}
@ -900,7 +900,7 @@ const Storage: Component = () => {
if (!pool) return '';
const total = Math.max(1, pool.storedBytes + pool.availableBytes);
const percent = total > 0 ? (pool.storedBytes / total) * 100 : 0;
return `${pool.name}: ${percent.toFixed(1)}%`;
return `${pool.name}: ${formatPercent(percent)}`;
})
.filter(Boolean)
.join(', ');
@ -917,7 +917,7 @@ const Storage: Component = () => {
const total = Math.max(1, item.total || 0);
const used = Math.max(0, item.used || 0);
const percent = total > 0 ? (used / total) * 100 : 0;
return `${item.name}: ${percent.toFixed(1)}%`;
return `${item.name}: ${formatPercent(percent)}`;
})
.filter(Boolean)
.join(', ');
@ -1124,18 +1124,18 @@ const Storage: Component = () => {
/>
<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="whitespace-nowrap px-0.5">
{usagePercent.toFixed(0)}% (
{formatBytes(storage.used || 0)}/
{formatBytes(storage.total || 0)})
{formatPercent(usagePercent)} (
{formatBytes(storage.used || 0, 0)}/
{formatBytes(storage.total || 0, 0)})
</span>
</span>
</div>
</td>
<td class="p-0.5 px-1.5 text-xs hidden sm:table-cell whitespace-nowrap">
{formatBytes(storage.free || 0)}
{formatBytes(storage.free || 0, 0)}
</td>
<td class="p-0.5 px-1.5 text-xs whitespace-nowrap">
{formatBytes(storage.total || 0)}
{formatBytes(storage.total || 0, 0)}
</td>
<td class="p-0.5 px-1.5"></td>
</tr>

View file

@ -195,12 +195,12 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
if (item.type === 'pve') {
const node = item.data as Node;
if (!node.disk) return undefined;
return `${formatBytes(node.disk.used)}/${formatBytes(node.disk.total)}`;
return `${formatBytes(node.disk.used, 0)}/${formatBytes(node.disk.total, 0)}`;
}
const pbs = item.data as PBSInstance;
if (!pbs.datastores || pbs.datastores.length === 0) return undefined;
const totals = getPbsTotals(pbs);
return `${formatBytes(totals.used)}/${formatBytes(totals.total)}`;
return `${formatBytes(totals.used, 0)}/${formatBytes(totals.total, 0)}`;
};
const getTemperatureValue = (item: SortableItem) => {
@ -583,9 +583,9 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
label={formatPercent(memoryPercentValue ?? 0)}
sublabel={
isPVE && node!.memory
? `${formatBytes(node!.memory.used)}/${formatBytes(node!.memory.total)}`
? `${formatBytes(node!.memory.used, 0)}/${formatBytes(node!.memory.total, 0)}`
: isPBS && pbs!.memoryTotal
? `${formatBytes(pbs!.memoryUsed)}/${formatBytes(pbs!.memoryTotal)}`
? `${formatBytes(pbs!.memoryUsed, 0)}/${formatBytes(pbs!.memoryTotal, 0)}`
: undefined
}
type="memory"

View file

@ -17,19 +17,12 @@ export function formatSpeed(bytesPerSecond: number, decimals = 0): string {
export function formatPercent(value: number): string {
if (!Number.isFinite(value)) return '0%';
const abs = Math.abs(value);
if (abs >= 10) {
return `${Math.round(value)}%`;
if (abs === 0) return '0%';
if (abs < 0.5) {
return '0%';
}
if (abs >= 1) {
return `${value.toFixed(1)}%`;
}
// Preserve tiny signals without overly long labels
return `${value.toFixed(2)}%`;
return `${Math.round(value)}%`;
}
export function formatUptime(seconds: number): string {