ui: empty state

This commit is contained in:
Nicolas Meienberger
2025-10-04 14:43:29 +02:00
parent 56a4afdc92
commit 3d87814aee
3 changed files with 107 additions and 25 deletions

View File

@@ -450,7 +450,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
</div>
{testMessage && (
<div
className={`text-sm p-2 rounded-md ${
className={`text-xs p-2 rounded-md ${
testMessage.success
? "bg-green-50 text-green-700 border border-green-200"
: "bg-red-50 text-red-700 border border-red-200"
@@ -461,7 +461,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
)}
</div>
{mode === "update" && (
<Button type="submit" className="w-full mt-4" loading={loading}>
<Button type="submit" className="w-full" loading={loading}>
Save Changes
</Button>
)}

View File

@@ -0,0 +1,56 @@
import { Database, HardDrive, HeartPulse, Plus } from "lucide-react";
import { CreateVolumeDialog } from "./create-volume-dialog";
import { useState } from "react";
export function EmptyState() {
const [createVolumeOpen, setCreateVolumeOpen] = useState(false);
return (
<div className="flex flex-col items-center justify-center py-16 px-4 text-center">
<div className="relative mb-8">
<div className="absolute inset-0 animate-pulse">
<div className="w-32 h-32 rounded-full bg-primary/10 blur-2xl" />
</div>
<div className="relative flex items-center justify-center w-32 h-32 rounded-full bg-gradient-to-br from-primary/20 to-primary/5 border-2 border-primary/20">
<Database className="w-16 h-16 text-primary/70" strokeWidth={1.5} />
</div>
</div>
<div className="max-w-md space-y-3 mb-8">
<h3 className="text-2xl font-semibold text-foreground">No volumes yet</h3>
<p className="text-muted-foreground">
Get started by creating your first volume. Manage and monitor all your storage backends in one place with
advanced features like automatic mounting and health checks.
</p>
</div>
<CreateVolumeDialog open={createVolumeOpen} setOpen={setCreateVolumeOpen} />
<div className="mt-12 grid grid-cols-1 md:grid-cols-3 gap-0 max-w-3xl">
<div className="flex flex-col items-center gap-2 p-4 border bg-card-header">
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<Database className="w-5 h-5 text-primary" />
</div>
<h4 className="font-medium text-sm">Multiple Backends</h4>
<p className="text-xs text-muted-foreground">Support for local, NFS, and SMB storage</p>
</div>
<div className="flex flex-col items-center gap-2 p-4 border border-r-0 border-l-0 bg-card-header">
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<HardDrive className="w-5 h-5 text-primary" />
</div>
<h4 className="font-medium text-sm">Auto Mounting</h4>
<p className="text-xs text-muted-foreground">Automatic lifecycle management</p>
</div>
<div className="flex flex-col items-center gap-2 p-4 border bg-card-header">
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<HeartPulse className="w-5 h-5 text-primary" />
</div>
<h4 className="font-medium text-sm">Real-time Monitoring</h4>
<p className="text-xs text-muted-foreground">Live status and health checks</p>
</div>
</div>
</div>
);
}

View File

@@ -5,6 +5,7 @@ import { useNavigate } from "react-router";
import { listVolumes } from "~/api-client";
import { listVolumesOptions } from "~/api-client/@tanstack/react-query.gen";
import { CreateVolumeDialog } from "~/components/create-volume-dialog";
import { EmptyState } from "~/components/empty-state";
import { StatusDot } from "~/components/status-dot";
import { Button } from "~/components/ui/button";
import { Card } from "~/components/ui/card";
@@ -59,6 +60,17 @@ export default function Home({ loaderData }: Route.ComponentProps) {
return matchesSearch && matchesStatus && matchesBackend;
}) || [];
const hasNoVolumes = data?.volumes.length === 0;
const hasNoFilteredVolumes = filteredVolumes.length === 0 && !hasNoVolumes;
if (hasNoVolumes) {
return (
<Card className="p-0 gap-0">
<EmptyState />
</Card>
);
}
return (
<Card className="p-0 gap-0">
<div className="flex flex-col lg:flex-row items-stretch lg:items-center gap-2 md:justify-between p-4 bg-card-header py-4">
@@ -109,35 +121,49 @@ export default function Home({ loaderData }: Route.ComponentProps) {
</TableRow>
</TableHeader>
<TableBody>
{filteredVolumes.map((volume) => (
<TableRow
key={volume.name}
className="hover:bg-accent/50 hover:cursor-pointer"
onClick={() => navigate(`/volumes/${volume.name}`)}
>
<TableCell className="font-medium text-strong-accent">{volume.name}</TableCell>
<TableCell>
<VolumeIcon backend={volume.type} />
</TableCell>
<TableCell className="hidden sm:table-cell">
<span className="flex items-center gap-2">
<span className="text-muted-foreground text-xs truncate bg-primary/10 rounded-md px-2 py-1">
{volume.path}
</span>
<Copy size={10} />
</span>
</TableCell>
<TableCell className="text-center">
<StatusDot status={volume.status} />
{hasNoFilteredVolumes ? (
<TableRow>
<TableCell colSpan={4} className="text-center py-12">
<div className="flex flex-col items-center gap-3">
<p className="text-muted-foreground">No volumes match your filters.</p>
<Button onClick={clearFilters} variant="outline" size="sm">
<RotateCcw className="h-4 w-4 mr-2" />
Clear filters
</Button>
</div>
</TableCell>
</TableRow>
))}
) : (
filteredVolumes.map((volume) => (
<TableRow
key={volume.name}
className="hover:bg-accent/50 hover:cursor-pointer"
onClick={() => navigate(`/volumes/${volume.name}`)}
>
<TableCell className="font-medium text-strong-accent">{volume.name}</TableCell>
<TableCell>
<VolumeIcon backend={volume.type} />
</TableCell>
<TableCell className="hidden sm:table-cell">
<span className="flex items-center gap-2">
<span className="text-muted-foreground text-xs truncate bg-primary/10 rounded-md px-2 py-1">
{volume.path}
</span>
<Copy size={10} />
</span>
</TableCell>
<TableCell className="text-center">
<StatusDot status={volume.status} />
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<div className="px-4 py-2 text-sm text-muted-foreground bg-card-header flex justify-end border-t">
{filteredVolumes.length === 0 ? (
"No volumes found."
{hasNoFilteredVolumes ? (
"No volumes match filters."
) : (
<span>
<span className="text-strong-accent">{filteredVolumes.length}</span> volume