feat: volume stats & charts

This commit is contained in:
Nicolas Meienberger
2025-09-25 21:16:55 +02:00
parent c261590ea3
commit 6b386d28d6
12 changed files with 626 additions and 30 deletions

View File

@@ -15,10 +15,10 @@ export const HealthchecksCard = ({ volume }: Props) => {
});
return (
<Card className="p-6 flex-1 flex flex-col">
<Card className="p-6 flex-1 flex flex-col h-full">
<div className="flex flex-col flex-1 justify-start">
<span className="flex items-center gap-2 mb-4">
<ScanHeartIcon size={24} />
<ScanHeartIcon className="h-4 w-4" />
<h2 className="text-lg font-medium">Health Checks</h2>
</span>
{volume.lastError && <span className="text-md text-red-600 ">{volume.lastError}</span>}

View File

@@ -0,0 +1,93 @@
"use client";
import { HardDrive } from "lucide-react";
import * as React from "react";
import { Label, Pie, PieChart } from "recharts";
import { ByteSize } from "~/components/bytes-size";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "~/components/ui/chart";
import type { StatFs } from "~/lib/types";
type Props = {
statfs: StatFs;
};
export function StorageChart({ statfs }: Props) {
const chartData = React.useMemo(
() => [
{
name: "Used",
value: statfs.used,
fill: "blue",
},
{
name: "Free",
value: statfs.free,
fill: "lightgray",
},
],
[statfs],
);
const chartConfig = {
value: {
label: "Storage",
},
used: {
label: "Used",
color: "hsl(var(--destructive))",
},
free: {
label: "Free",
color: "hsl(var(--primary))",
},
} satisfies ChartConfig;
const usagePercentage = React.useMemo(() => {
return Math.round((statfs.used / statfs.total) * 100);
}, [statfs]);
return (
<Card className="flex flex-col h-full text-sm">
<CardHeader className="items-center pb-0">
<CardTitle className="flex items-center gap-2">
<HardDrive className="h-4 w-4" />
Storage Usage
</CardTitle>
</CardHeader>
<CardContent className="flex-1 pb-0">
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
<PieChart>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
hideLabel
formatter={(value, name) => [<ByteSize key={name} bytes={value as number} />, name]}
/>
}
/>
<Pie data={chartData} dataKey="value" nameKey="name" innerRadius={60} strokeWidth={5}>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text x={viewBox.cx} y={viewBox.cy} textAnchor="middle" dominantBaseline="middle">
<tspan x={viewBox.cx} y={viewBox.cy} className="fill-foreground text-3xl font-bold">
{usagePercentage}%
</tspan>
<tspan x={viewBox.cx} y={(viewBox.cy || 0) + 24} className="fill-muted-foreground">
Used
</tspan>
</text>
);
}
}}
/>
</Pie>
</PieChart>
</ChartContainer>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,54 @@
import { Database, HardDrive } from "lucide-react";
import { ByteSize } from "~/components/bytes-size";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import type { StatFs } from "~/lib/types";
type Props = {
statfs: StatFs;
};
export function StorageInfoCard({ statfs }: Props) {
return (
<Card className="h-full text-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-4 w-4" />
Storage Details
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-col h-full justify-center">
<div className="grid gap-4 w-full">
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
<div className="flex items-center gap-3">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">Total Capacity</span>
</div>
<ByteSize bytes={statfs.total} className="font-mono text-sm" />
</div>
<div className="flex items-center justify-between p-3 rounded-lg bg-blue-500/10">
<div className="flex items-center gap-3">
<div className="h-4 w-4 rounded-full bg-blue-500" />
<span className="font-medium">Used Space</span>
</div>
<div className="text-right">
<ByteSize bytes={statfs.used} className="font-mono text-sm" />
</div>
</div>
<div className="flex items-center justify-between p-3 rounded-lg bg-primary/10">
<div className="flex items-center gap-3">
<div className="h-4 w-4 rounded-full bg-primary" />
<span className="font-medium">Free Space</span>
</div>
<div className="text-right">
<ByteSize bytes={statfs.free} className="font-mono text-sm" />
</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,8 +1,9 @@
import { CreateVolumeForm } from "~/components/create-volume-form";
import { Card } from "~/components/ui/card";
import { HealthchecksCard } from "../components/healthchecks-card";
import type { StatFs, Volume } from "~/lib/types";
import { ByteSize } from "~/components/bytes-size";
import { HealthchecksCard } from "../components/healthchecks-card";
import { StorageChart } from "../components/storage-chart";
import { StorageInfoCard } from "../components/storage-info-card";
type Props = {
volume: Volume;
@@ -11,19 +12,19 @@ type Props = {
export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
return (
<div className="grid gap-4 grid-cols-1 lg:grid-cols-3 lg:grid-rows-[auto_1fr]">
<div className="grid gap-3 grid-cols-1 lg:grid-cols-4 lg:grid-rows-[auto_auto]">
<Card className="p-6 lg:col-span-2 lg:row-span-2">
<CreateVolumeForm initialValues={{ ...volume, ...volume.config }} onSubmit={console.log} />
</Card>
<HealthchecksCard volume={volume} />
<Card className="p-6 h-full">
<h2 className="text-lg font-medium">Volume Information</h2>
Total: <ByteSize bytes={statfs.total} />
<br />
Free: <ByteSize bytes={statfs.free} />
<br />
Used: <ByteSize bytes={statfs.used} />
</Card>
<div className="lg:col-span-2 lg:row-span-1">
<HealthchecksCard volume={volume} />
</div>
<div className="lg:col-span-1">
<StorageChart statfs={statfs} />
</div>
<div className="lg:col-span-1">
<StorageInfoCard statfs={statfs} />
</div>
</div>
);
};