Files
kandev/apps/web/components/system-metrics/status-surface-metrics.tsx
T
José Almeida 9c16d337a2 feat(web): add global app status surface (#1869)
* feat(web): add app status bar

* feat(web): add app status bar visibility toggle

* feat(web): persist status bar item order

* fix(web): refine app status bar behavior

* fix(web): polish app status bar presentation

* test(web): split plugin slot coverage groups

* fix(web): address status surface review feedback

* fix(web): stabilize status bar overlay tests
2026-07-23 12:28:54 +01:00

273 lines
8.3 KiB
TypeScript

"use client";
import {
IconActivity,
IconCpu,
IconDatabase,
IconDisc,
IconFlame,
IconGauge,
IconServer,
} from "@tabler/icons-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { formatDistanceToNow } from "date-fns";
import { useAppStore } from "@/components/state-provider";
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
import { useSystemMetricsSubscription } from "@/hooks/use-system-metrics-subscription";
import type { SystemMetricSample, SystemMetricsSource } from "@/lib/types/system";
type StatusSurfaceMetricsProps = {
presentation: "bar" | "mobile-drawer";
density: "full" | "compact";
drawerOpen: boolean;
};
export function StatusSurfaceMetrics({
presentation,
density,
drawerOpen,
}: StatusSurfaceMetricsProps) {
// Wire/storage name stays stable for existing user settings and API payloads.
const enabled = useAppStore((state) => state.userSettings.systemMetricsDisplay.showInTopbar);
const snapshot = useAppStore((state) => state.system.metrics);
const { isMobile } = useResponsiveBreakpoint();
const shouldSubscribe = enabled && (!isMobile || drawerOpen);
useSystemMetricsSubscription(shouldSubscribe);
if (!enabled || (isMobile && !drawerOpen)) return null;
const host = snapshot?.sources.find((source) => source.kind === "backend");
if (presentation === "mobile-drawer") {
return (
<section data-testid="app-status-metrics" className="space-y-1" aria-label="System metrics">
<h3 className="px-1 text-sm font-medium">System metrics</h3>
{!host ? (
<EmptyMetrics drawer />
) : (
<DrawerSourceMetrics source={host} updatedAt={snapshot?.timestamp} />
)}
</section>
);
}
return (
<div
data-testid="app-status-metrics"
className="flex h-full max-w-[52vw] items-center overflow-hidden leading-none text-current"
aria-label="System metrics"
>
{!host ? (
<EmptyMetrics />
) : (
<BarSourceMetrics
source={host}
updatedAt={snapshot?.timestamp}
showSourceLabel={density === "full"}
metricLimit={density === "compact" ? 2 : 4}
/>
)}
</div>
);
}
function EmptyMetrics({ drawer = false }: { drawer?: boolean }) {
return (
<div
className={
drawer
? "flex min-h-11 items-center gap-2 rounded-md px-3 text-sm text-muted-foreground"
: "flex h-full items-center gap-2 text-[11px] text-current opacity-70"
}
>
<IconActivity className="h-3.5 w-3.5" />
<span>Metrics unavailable</span>
</div>
);
}
function BarSourceMetrics({
source,
updatedAt,
showSourceLabel,
metricLimit,
}: {
source: SystemMetricsSource;
updatedAt?: string;
showSourceLabel: boolean;
metricLimit: number;
}) {
return (
<div className="flex h-full max-w-[360px] items-center gap-3 overflow-hidden text-[11px]">
<SourceBadge source={source} updatedAt={updatedAt} showLabel={showSourceLabel} />
<MetricValues source={source} updatedAt={updatedAt} limit={metricLimit} />
</div>
);
}
function DrawerSourceMetrics({
source,
updatedAt,
}: {
source: SystemMetricsSource;
updatedAt?: string;
}) {
return (
<div className="flex min-h-11 items-center gap-2 rounded-md px-3 text-sm hover:bg-muted/60">
<SourceBadge source={source} updatedAt={updatedAt} showLabel />
<div className="flex min-w-0 flex-1 items-center justify-end gap-3 overflow-hidden">
<MetricValues source={source} updatedAt={updatedAt} limit={4} />
</div>
</div>
);
}
function SourceBadge({
source,
updatedAt,
showLabel,
}: {
source: SystemMetricsSource;
updatedAt?: string;
showLabel: boolean;
}) {
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex shrink-0 items-center gap-1.5 text-current" aria-label="Host metrics">
<IconServer className="size-3.5" stroke={1.6} />
{showLabel ? <span className="max-w-20 truncate">Host</span> : null}
</span>
</TooltipTrigger>
<TooltipContent>
<div className="space-y-1">
<div className="font-medium">Host</div>
<div className="text-xs text-muted-foreground">{source.label}</div>
<div className="text-xs text-muted-foreground">{lastUpdatedText(updatedAt)}</div>
</div>
</TooltipContent>
</Tooltip>
);
}
function MetricValues({
source,
updatedAt,
limit,
}: {
source: SystemMetricsSource;
updatedAt?: string;
limit: number;
}) {
const metrics = source.metrics.slice(0, limit);
if (metrics.length === 0) return <span className="text-muted-foreground">-</span>;
return (
<span className="flex min-w-0 items-center gap-3 overflow-hidden">
{metrics.map((metric) => (
<MetricValue key={metric.id} metric={metric} source={source} updatedAt={updatedAt} />
))}
</span>
);
}
function MetricValue({
metric,
source,
updatedAt,
}: {
metric: SystemMetricSample;
source: SystemMetricsSource;
updatedAt?: string;
}) {
const help =
metric.id === "io_load"
? "Average number of tasks running or waiting for CPU during the last minute. Compare this value with the host's CPU core count."
: null;
return (
<Tooltip>
<TooltipTrigger asChild>
<span
className={`inline-flex shrink-0 items-center gap-1.5 tabular-nums ${metricColor(metric)}`}
aria-label={`${metricLabel(metric.id)} ${formatMetric(metric)}`}
>
{metricIcon(metric.id)}
<MetricMeter metric={metric} />
<span className="font-medium tracking-[-0.015em] [font-family:var(--font-geist-mono)]">
{formatMetric(metric)}
</span>
</span>
</TooltipTrigger>
<TooltipContent>
<div className="space-y-1">
<div className="font-medium">{metricLabel(metric.id)}</div>
{help ? <div className="max-w-72 text-xs text-muted-foreground">{help}</div> : null}
<div className="text-xs text-muted-foreground">Host: {source.label}</div>
<div className="text-xs tabular-nums">{formatMetric(metric)}</div>
{metric.error ? (
<div className="text-xs text-muted-foreground">{metric.error}</div>
) : null}
<div className="text-xs text-muted-foreground">{lastUpdatedText(updatedAt)}</div>
</div>
</TooltipContent>
</Tooltip>
);
}
function metricLabel(id: string) {
return (
{
cpu_percent: "CPU",
memory_percent: "Memory",
disk_percent: "Disk",
cpu_temp: "CPU temperature",
io_load: "System load (1 min)",
}[id] ?? id
);
}
function metricIcon(id: string) {
const Icon =
{
cpu_percent: IconCpu,
memory_percent: IconDatabase,
disk_percent: IconDisc,
cpu_temp: IconFlame,
io_load: IconGauge,
}[id] ?? IconActivity;
return <Icon className="size-3.5 opacity-80" stroke={1.6} />;
}
function MetricMeter({ metric }: { metric: SystemMetricSample }) {
if (metric.unit !== "%" || typeof metric.value !== "number") return null;
const width = `${Math.max(0, Math.min(100, metric.value))}%`;
return (
<span
className="h-1 w-7 overflow-hidden rounded-full bg-muted-foreground/20"
aria-hidden="true"
>
<span className="block h-full rounded-full bg-current opacity-65" style={{ width }} />
</span>
);
}
function formatMetric(metric: SystemMetricSample) {
if (typeof metric.value !== "number") return "-";
const value = metric.unit === "%" ? Math.round(metric.value) : Math.round(metric.value * 10) / 10;
return `${value}${metric.unit ?? ""}`;
}
function metricColor(metric: SystemMetricSample) {
if (!metric.available) return "text-current opacity-50";
const thresholdValue = metric.unit === "%" || metric.id === "cpu_temp" ? metric.value : null;
if (typeof thresholdValue !== "number") return "text-current";
if (thresholdValue > 95) return "text-destructive";
if (thresholdValue >= 80) return "text-yellow-500 dark:text-yellow-400";
return "text-current";
}
function lastUpdatedText(updatedAt?: string) {
if (!updatedAt) return "Last update unknown";
const date = new Date(updatedAt);
if (Number.isNaN(date.getTime())) return "Last update unknown";
return `Updated ${formatDistanceToNow(date, { addSuffix: true })}`;
}