Files
zerobyte/app/client/modules/volumes/tabs/info.tsx
Nico 7b5c53bb7d fix(sftp): allow legacy ssh rsa to add support for older servers (#921)
* refactor(e2e): use more stable assertion

* feat(sftp): add legacy ssh-rsa option
2026-05-27 21:19:23 +02:00

222 lines
6.9 KiB
TypeScript

import { useMemo } from "react";
import { FolderOpen, HardDrive, Settings, Unplug } from "lucide-react";
import { Label, Pie, PieChart } from "recharts";
import { ByteSize } from "~/client/components/bytes-size";
import { Card, CardTitle } from "~/client/components/ui/card";
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "~/client/components/ui/chart";
import type { StatFs, Volume } from "~/client/lib/types";
import { cn } from "~/client/lib/utils";
type Props = {
volume: Volume;
statfs: StatFs;
};
const backendLabels: Record<Volume["type"], string> = {
directory: "Directory",
nfs: "NFS",
smb: "SMB",
webdav: "WebDAV",
rclone: "rclone",
sftp: "SFTP",
};
type ConfigRowProps = {
icon: React.ReactNode;
label: string;
value: string;
mono?: boolean;
};
function ConfigRow({ icon, label, value, mono }: ConfigRowProps) {
return (
<div className="flex items-center gap-3 py-3 first:pt-0 last:pb-0">
<span className="text-muted-foreground shrink-0">{icon}</span>
<span className="text-sm text-muted-foreground w-40 shrink-0">{label}</span>
<span className={cn("text-sm break-all", { "font-mono bg-muted/50 px-2 py-0.5 rounded": mono })}>
{value}
</span>
</div>
);
}
function BackendConfigRows({ volume }: { volume: Volume }) {
const config = volume.config;
switch (config.backend) {
case "directory":
return (
<ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Directory Path" value={config.path} mono />
);
case "nfs":
return (
<>
<ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Server" value={config.server} mono />
<ConfigRow
icon={<FolderOpen className="h-4 w-4" />}
label="Export Path"
value={config.exportPath}
mono
/>
</>
);
case "smb":
return (
<>
<ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Server" value={config.server} mono />
<ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Share" value={config.share} mono />
</>
);
case "webdav":
return (
<>
<ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Server" value={config.server} mono />
<ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Path" value={config.path} mono />
</>
);
case "rclone":
return (
<>
<ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Remote" value={config.remote} mono />
<ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Path" value={config.path} mono />
</>
);
case "sftp":
return (
<>
<ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Host" value={config.host} mono />
<ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Username" value={config.username} />
<ConfigRow icon={<FolderOpen className="h-4 w-4" />} label="Path" value={config.path} mono />
</>
);
}
}
function DonutChart({ statfs }: { statfs: StatFs }) {
const { used = 0, total = 0 } = statfs;
const chartData = useMemo(
() => [
{ name: "Used", value: statfs.used, fill: "var(--strong-accent)" },
{ name: "Free", value: statfs.free, fill: "lightgray" },
],
[statfs],
);
const usagePercentage = useMemo(() => {
return Math.round((used / total) * 100);
}, [used, total]);
return (
<ChartContainer config={{}} className="mx-auto aspect-square max-h-[200px]">
<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={50} 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-2xl font-bold"
>
{usagePercentage}%
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 20}
className="fill-muted-foreground text-xs"
>
Used
</tspan>
</text>
);
}
}}
/>
</Pie>
</PieChart>
</ChartContainer>
);
}
export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
const { total = 0, used = 0, free = 0 } = statfs;
const hasStorage = total > 0;
return (
<Card className="px-6 py-6 @container/inner">
<div className="grid grid-cols-1 @3xl/inner:grid-cols-[1fr_280px] gap-8">
<div>
<CardTitle className="flex items-center gap-2 mb-5">
<Settings className="h-4 w-4 text-muted-foreground" />
Configuration
</CardTitle>
<div className="space-y-0 divide-y divide-border/50">
<ConfigRow icon={<HardDrive className="h-4 w-4" />} label="Name" value={volume.name} />
<ConfigRow
icon={<HardDrive className="h-4 w-4" />}
label="Backend"
value={backendLabels[volume.type]}
/>
<BackendConfigRows volume={volume} />
</div>
</div>
{hasStorage ? (
<div className="@3xl/inner:border-l @3xl/inner:border-border/50 @3xl/inner:pl-8">
<CardTitle className="flex items-center gap-2 mb-2 text-center @3xl/inner:text-left">
<HardDrive className="h-4 w-4" />
Storage
</CardTitle>
<DonutChart statfs={statfs} />
<div className="grid gap-2 mt-2">
<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="text-sm font-medium">Total</span>
</div>
<ByteSize bytes={total} className="font-mono text-sm" />
</div>
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
<div className="flex items-center gap-3">
<div className="h-3 w-3 rounded-full bg-strong-accent" />
<span className="text-sm font-medium">Used</span>
</div>
<ByteSize bytes={used} className="font-mono text-sm" />
</div>
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
<div className="flex items-center gap-3">
<div className="h-3 w-3 rounded-full bg-primary" />
<span className="text-sm font-medium">Free</span>
</div>
<ByteSize bytes={free} className="font-mono text-sm" />
</div>
</div>
</div>
) : (
<div className="@3xl/inner:border-l @3xl/inner:border-border/50 @3xl/inner:pl-8 flex flex-col items-center justify-center text-center py-8">
<Unplug className="mb-4 h-5 w-5 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
No storage data available.
<br />
Mount the volume to see usage.
</p>
</div>
)}
</div>
</Card>
);
};