mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-08 01:37:54 +00:00
Simplify metric bar labels
This commit is contained in:
parent
7ee417a629
commit
93faaacbd1
8 changed files with 51 additions and 58 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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%]">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue