refactor: unify layout across tabs

This commit is contained in:
Nicolas Meienberger
2025-09-26 22:07:06 +02:00
parent 4fe742e5c8
commit 604e92ab1e
4 changed files with 92 additions and 51 deletions

View File

@@ -1,7 +1,7 @@
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { ScanHeartIcon } from "lucide-react"; import { HeartIcon } from "lucide-react";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Card } from "~/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import { Switch } from "~/components/ui/switch"; import { Switch } from "~/components/ui/switch";
import type { Volume } from "~/lib/types"; import type { Volume } from "~/lib/types";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
@@ -16,20 +16,24 @@ export const HealthchecksCard = ({ volume }: Props) => {
}); });
return ( return (
<Card className="p-6 flex-1 flex flex-col h-full"> <Card className="flex-1 flex flex-col h-full">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<HeartIcon className="h-4 w-4" />
Health Checks
</CardTitle>
<CardDescription>Monitor and automatically remount volumes on errors to ensure availability.</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col flex-1 justify-start"> <div className="flex flex-col flex-1 justify-start">
<span className="flex items-center gap-2 mb-4"> {volume.lastError && <span className="text-sm text-red-500 ">{volume.lastError}</span>}
<ScanHeartIcon className="h-4 w-4" />
<h2 className="text-lg font-medium">Health Checks</h2>
</span>
{volume.lastError && <span className="text-md text-amber-600 ">{volume.lastError}</span>}
{volume.status === "mounted" && <span className="text-md text-emerald-500">Healthy</span>} {volume.status === "mounted" && <span className="text-md text-emerald-500">Healthy</span>}
{volume.status !== "unmounted" && ( {volume.status !== "unmounted" && (
<span className="text-xs text-muted-foreground mb-4">Checked {timeAgo || "never"}</span> <span className="text-xs text-muted-foreground mb-4">Checked {timeAgo || "never"}</span>
)} )}
<span className="flex justify-between items-center gap-2"> <span className="flex justify-between items-center gap-2">
Remount on error <span className="text-sm">Remount on error</span>
<div <div
className={cn( className={cn(
"flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-semibold uppercase tracking-wide transition-colors", "flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-semibold uppercase tracking-wide transition-colors",
@@ -43,7 +47,14 @@ export const HealthchecksCard = ({ volume }: Props) => {
</div> </div>
</span> </span>
</div> </div>
<Button variant="outline">Run Health Check</Button> {volume.status !== "unmounted" && (
<div className="flex justify-center">
<Button variant="outline" className="mt-4 self-end">
Run Health Check
</Button>
</div>
)}
</CardContent>
</Card> </Card>
); );
}; };

View File

@@ -134,14 +134,16 @@ export const VolumeBackupsTabContent = ({ volume }: Props) => {
}; };
return ( return (
<div className="grid gap-4 grid-cols-1 xl:grid-cols-[minmax(0,_2fr)_minmax(260px,_1fr)]"> <div className="grid gap-4 xl:grid-cols-[minmax(0,_2.3fr)_minmax(320px,_1fr)]">
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="grid gap-4"> <form onSubmit={form.handleSubmit(handleSubmit)} className="grid gap-4">
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between gap-4"> <CardHeader className="flex flex-row items-center justify-between gap-4">
<div> <div>
<CardTitle>Backup automation</CardTitle> <CardTitle>Backup automation</CardTitle>
<CardDescription>Enable scheduled snapshots and off-site replication for this volume.</CardDescription> <CardDescription className="mt-1">
Enable scheduled snapshots and off-site replication for this volume.
</CardDescription>
</div> </div>
<FormField <FormField
control={form.control} control={form.control}

View File

@@ -1,7 +1,7 @@
import { Card } from "~/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import { CodeBlock } from "~/components/ui/code-block";
import type { Volume } from "~/lib/types"; import type { Volume } from "~/lib/types";
import * as YML from "yaml"; import * as YML from "yaml";
import { CodeBlock } from "~/components/ui/code-block";
type Props = { type Props = {
volume: Volume; volume: Volume;
@@ -22,20 +22,46 @@ export const DockerTabContent = ({ volume }: Props) => {
}, },
}); });
const dockerRunCommand = `docker run -v ${volume.name}:/path/in/container nginx:latest`;
return ( return (
<div className="grid gap-4 grid-cols-1 lg:grid-cols-3 lg:grid-rows-[auto_1fr]"> <div className="grid gap-4 xl:grid-cols-[minmax(0,_2.3fr)_minmax(320px,_1fr)]">
<Card className="p-6 lg:col-span-2 lg:row-span-2"> <Card>
<div className="text-sm text-muted-foreground"> <CardHeader>
<CardTitle>Plug-and-play Docker integration</CardTitle>
<CardDescription className="mt-2">
This volume can be used in your Docker Compose files by referencing it as an external volume. The example This volume can be used in your Docker Compose files by referencing it as an external volume. The example
demonstrates how to mount the volume to a service (nginx in this case). Make sure to adjust the path inside demonstrates how to mount the volume to a service (nginx in this case). Make sure to adjust the path inside
the container to fit your application's needs. the container to fit your application's needs
</div> </CardDescription>
</CardHeader>
<CardContent>
<div className="relative space-y-6">
<div className="space-y-4">
<div className="flex flex-col gap-4">
<CodeBlock code={yamlString} language="yaml" filename="docker-compose.yml" /> <CodeBlock code={yamlString} language="yaml" filename="docker-compose.yml" />
<div className="text-sm text-muted-foreground">
Alternatively, you can use the following command to run a Docker container with the volume mounted:
</div> </div>
<CodeBlock code={`docker run -v ${volume.name}:/path/in/container nginx:latest`} /> <div className="text-sm text-muted-foreground">
Alternatively, you can use the following command to run a Docker container with the volume mounted
</div>
<div className="flex flex-col gap-4">
<CodeBlock code={dockerRunCommand} filename="CLI one-liner" />
</div>
</div>
</div>
</CardContent>
</Card> </Card>
<div className="grid">
<Card>
<CardHeader>
<CardTitle>Best practices</CardTitle>
<CardDescription>Validate the automation before enabling it in production.</CardDescription>
</CardHeader>
<CardContent className="space-y-4 text-sm"></CardContent>
</Card>
</div>
</div> </div>
); );
}; };

View File

@@ -11,16 +11,18 @@ type Props = {
export const VolumeInfoTabContent = ({ volume, statfs }: Props) => { export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
return ( return (
<div className="grid gap-3 grid-cols-1 lg:grid-cols-4 lg:grid-rows-[auto_auto]"> <div className="grid gap-4 xl:grid-cols-[minmax(0,_2.3fr)_minmax(320px,_1fr)]">
<Card className="p-6 lg:col-span-2 lg:row-span-2"> <Card className="p-6">
<CreateVolumeForm initialValues={{ ...volume, ...volume.config }} onSubmit={console.log} /> <CreateVolumeForm initialValues={{ ...volume, ...volume.config }} onSubmit={console.log} />
</Card> </Card>
<div className="lg:col-span-2 lg:row-span-1"> <div className="grid gap-4">
<div className="lg:row-span-1">
<HealthchecksCard volume={volume} /> <HealthchecksCard volume={volume} />
</div> </div>
<div className="lg:col-span-2"> <div className="">
<StorageChart statfs={statfs} /> <StorageChart statfs={statfs} />
</div> </div>
</div> </div>
</div>
); );
}; };