mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: volume stats & charts
This commit is contained in:
@@ -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>}
|
||||
|
||||
93
apps/client/app/modules/details/components/storage-chart.tsx
Normal file
93
apps/client/app/modules/details/components/storage-chart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user