Enhanced Server Stats Dashboard with Performance Optimizations (#746)

* Improve the layout of server mini stats in the dashboard.

- Server stats and tags made siblings for clearer responsibilities
- Changed margin to padding
- Unreachable indicator made into an overlay of the stats

* feat: optimize dashboard server stats with lazy loading and smart server availability checks

- Add enabled prop to ServerStatsMini for conditional data fetching
- Implement server availability check (only fetch stats for Ok servers, not NotOk/Disabled)
- Prevent 500 errors by avoiding API calls to offline servers
- Increase polling interval from 10s to 15s and add 5s stale time
- Add useMemo for expensive calculations to reduce re-renders
- Add conditional overlay rendering for unreachable servers
- Only render stats when showServerStats preference is enabled

* fix: show disabled servers with overlay instead of hiding component

- Maintain consistent layout by showing disabled state overlay
- Prevent UX inconsistency where disabled servers disappeared entirely

* fix: show button height

* feat: add enhance card animations

* cleanup
This commit is contained in:
Marcel Pfennig
2025-08-20 03:33:07 +02:00
committed by GitHub
parent 5bbd5510a1
commit 38857cbc0b
2 changed files with 65 additions and 31 deletions

View File

@@ -3,10 +3,12 @@ import { cn } from "@lib/utils";
import { Progress } from "@ui/progress";
import { ServerState } from "komodo_client/dist/types";
import { Cpu, Database, MemoryStick, LucideIcon } from "lucide-react";
import { useMemo } from "react";
interface ServerStatsMiniProps {
id: string;
className?: string;
enabled?: boolean;
}
interface StatItemProps {
@@ -22,7 +24,7 @@ const StatItem = ({ icon: Icon, label, percentage, type, isUnreachable, getTextC
<div className="flex items-center gap-2">
<Icon className="w-3 h-3 text-muted-foreground" aria-hidden="true" />
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center justify-between pb-1">
<span className="text-xs text-muted-foreground">{label}</span>
<span
className={cn(
@@ -41,12 +43,20 @@ const StatItem = ({ icon: Icon, label, percentage, type, isUnreachable, getTextC
</div>
);
export const ServerStatsMini = ({ id, className }: ServerStatsMiniProps) => {
export const ServerStatsMini = ({ id, className, enabled = true }: ServerStatsMiniProps) => {
const calculatePercentage = (value: number) =>
Number((value ?? 0).toFixed(2));
const server = useRead("ListServers", {}).data?.find((s) => s.id === id);
const serverDetails = useRead("GetServer", { server: id }).data;
const servers = useRead("ListServers", {}).data;
const server = servers?.find((s) => s.id === id);
const isServerAvailable = server &&
server.info.state !== ServerState.Disabled &&
server.info.state !== ServerState.NotOk;
const serverDetails = useRead("GetServer", { server: id }, {
enabled: enabled && isServerAvailable
}).data;
const cpuWarning = serverDetails?.config?.cpu_warning ?? 75;
const cpuCritical = serverDetails?.config?.cpu_critical ?? 90;
@@ -63,42 +73,52 @@ export const ServerStatsMini = ({ id, className }: ServerStatsMiniProps) => {
if (percentage >= warning) return "text-yellow-600";
return "text-green-600";
};
const stats = useRead(
"GetSystemStats",
{ server: id },
{
enabled: server ? server.info.state !== "Disabled" : false,
refetchInterval: 10_000,
enabled: enabled && isServerAvailable,
refetchInterval: 15_000,
staleTime: 5_000,
},
).data;
if (!server || server.info.state === "Disabled") {
if (!server) {
return null;
}
const cpuPercentage = stats ? calculatePercentage(stats.cpu_perc) : 0;
const memoryPercentage = stats && stats.mem_total_gb > 0 ? calculatePercentage((stats.mem_used_gb / stats.mem_total_gb) * 100) : 0;
const calculations = useMemo(() => {
const cpuPercentage = stats ? calculatePercentage(stats.cpu_perc) : 0;
const memoryPercentage = stats && stats.mem_total_gb > 0 ? calculatePercentage((stats.mem_used_gb / stats.mem_total_gb) * 100) : 0;
const diskUsed = stats ? stats.disks.reduce((acc, disk) => acc + disk.used_gb, 0) : 0;
const diskTotal = stats ? stats.disks.reduce((acc, disk) => acc + disk.total_gb, 0) : 0;
const diskPercentage = diskTotal > 0? calculatePercentage((diskUsed / diskTotal) * 100) : 0;
const diskUsed = stats ? stats.disks.reduce((acc, disk) => acc + disk.used_gb, 0) : 0;
const diskTotal = stats ? stats.disks.reduce((acc, disk) => acc + disk.total_gb, 0) : 0;
const diskPercentage = diskTotal > 0 ? calculatePercentage((diskUsed / diskTotal) * 100) : 0;
const isUnreachable = !stats || server.info.state === ServerState.NotOk;
const isDisabled = server.info.state === ServerState.Disabled;
const isUnreachable = !stats || server.info.state === ServerState.NotOk;
const unreachableClass = isUnreachable ? "opacity-50" : "";
return {
cpuPercentage,
memoryPercentage,
diskPercentage,
isUnreachable,
isDisabled
};
}, [stats, server.info.state]);
const statItems = [
const { cpuPercentage, memoryPercentage, diskPercentage, isUnreachable, isDisabled } = calculations;
const overlayClass = (isUnreachable || isDisabled) ? "opacity-50" : "";
const statItems = useMemo(() => [
{ icon: Cpu, label: "CPU", percentage: cpuPercentage, type: "cpu" as const },
{ icon: MemoryStick, label: "Memory", percentage: memoryPercentage, type: "memory" as const },
{ icon: Database, label: "Disk", percentage: diskPercentage, type: "disk" as const },
];
], [cpuPercentage, memoryPercentage, diskPercentage]);
return (
<div className={cn("flex flex-col gap-2", unreachableClass, className)}>
{isUnreachable && (
<div className="text-xs text-muted-foreground italic text-center">
Unreachable
</div>
)}
<div className={cn("relative flex flex-col gap-2", overlayClass, className)}>
{statItems.map((item) => (
<StatItem
key={item.label}
@@ -106,10 +126,20 @@ export const ServerStatsMini = ({ id, className }: ServerStatsMiniProps) => {
label={item.label}
percentage={item.percentage}
type={item.type}
isUnreachable={isUnreachable}
isUnreachable={isUnreachable || isDisabled}
getTextColor={getTextColor}
/>
))}
{isDisabled && (
<div className="absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-black/60 z-10">
<span className="text-xs text-foreground font-bold italic text-center">Disabled</span>
</div>
)}
{isUnreachable && !isDisabled && (
<div className="absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-black/60 z-10">
<span className="text-xs text-foreground font-bold italic text-center">Unreachable</span>
</div>
)}
</div>
);
};

View File

@@ -43,7 +43,6 @@ export default function Dashboard() {
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() =>
updatePreference(
"showServerStats",
@@ -170,7 +169,7 @@ const RecentCard = ({
<Link
to={`${usableResourcePath(type)}/${id}`}
className={cn(
"w-full px-3 py-2 border rounded-md hover:bg-accent/25 hover:-translate-y-1 transition-all flex flex-col justify-between",
"w-full px-3 py-2 border rounded-md hover:bg-accent/25 hover:-translate-y-1 transition-all duration-1000 linear flex flex-col justify-between",
showServerStats ? "min-h-32" : "h-20",
className,
)}
@@ -185,17 +184,22 @@ const RecentCard = ({
{type === "Stack" && <StackUpdateAvailable id={id} small />}
</div>
<div
<div
className={cn(
"flex flex-col gap-2 w-full",
showServerStats ? "mt-2 flex-1" : "mt-auto",
"overflow-hidden w-full transition-opacity transition-all duration-1000 linear",
showServerStats
? "max-h-40 opacity-100 py-2"
: "max-h-0 opacity-0 py-0"
)}
>
{showServerStats && <ServerStatsMini id={id} />}
<div className="flex gap-2 w-full py-1">
<TagsWithBadge className="flex-row" tag_ids={tags} />
<div className="flex flex-col gap-2">
<ServerStatsMini id={id} enabled={showServerStats} />
</div>
</div>
<div className="flex flex-row gap-2 w-full py-1">
<TagsWithBadge tag_ids={tags} />
</div>
</Link>
);
};