refactor: unify backend and frontend servers (#3)

* refactor: unify backend and frontend servers

* refactor: correct paths for openapi & drizzle

* refactor: move api-client to client

* fix: drizzle paths

* chore: fix linting issues

* fix: form reset issue
This commit is contained in:
Nico
2025-11-13 20:11:46 +01:00
committed by GitHub
parent 8d7e50508d
commit 95a0d44b45
240 changed files with 5171 additions and 5875 deletions

View File

@@ -0,0 +1,133 @@
import { useQuery } from "@tanstack/react-query";
import { Unplug } from "lucide-react";
import * as YML from "yaml";
import { getContainersUsingVolumeOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import { CodeBlock } from "~/client/components/ui/code-block";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
import type { Volume } from "~/client/lib/types";
type Props = {
volume: Volume;
};
export const DockerTabContent = ({ volume }: Props) => {
const yamlString = YML.stringify({
services: {
nginx: {
image: "nginx:latest",
volumes: [`im-${volume.name}:/path/in/container`],
},
},
volumes: {
[`im-${volume.name}`]: {
external: true,
},
},
});
const dockerRunCommand = `docker run -v im-${volume.name}:/path/in/container nginx:latest`;
const {
data: containersData,
isLoading,
error,
} = useQuery({
...getContainersUsingVolumeOptions({ path: { name: volume.name } }),
refetchInterval: 10000,
refetchOnWindowFocus: true,
});
const containers = containersData || [];
const getStateClass = (state: string) => {
switch (state) {
case "running":
return "bg-green-100 text-green-800";
case "exited":
return "bg-orange-100 text-orange-800";
default:
return "bg-gray-100 text-gray-800";
}
};
return (
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<Card>
<CardHeader>
<CardTitle>Plug-and-play Docker integration</CardTitle>
<CardDescription>
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
the container to fit your application's needs
</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" />
</div>
<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>
<div className="grid">
<Card>
<CardHeader>
<CardTitle>Containers Using This Volume</CardTitle>
<CardDescription>List of Docker containers mounting this volume.</CardDescription>
</CardHeader>
<CardContent className="space-y-4 text-sm h-full">
{isLoading && <div>Loading containers...</div>}
{error && <div className="text-destructive">Failed to load containers: {String(error)}</div>}
{!isLoading && !error && containers.length === 0 && (
<div className="flex flex-col items-center justify-center text-center h-full">
<Unplug className="mb-4 h-5 w-5 text-muted-foreground" />
<p className="text-muted-foreground">No Docker containers are currently using this volume.</p>
</div>
)}
{!isLoading && !error && containers.length > 0 && (
<div className="max-h-130 overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>ID</TableHead>
<TableHead>State</TableHead>
<TableHead>Image</TableHead>
</TableRow>
</TableHeader>
<TableBody className="text-sm">
{containers.map((container) => (
<TableRow key={container.id}>
<TableCell>{container.name}</TableCell>
<TableCell>{container.id.slice(0, 12)}</TableCell>
<TableCell>
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${getStateClass(container.state)}`}
>
{container.state}
</span>
</TableCell>
<TableCell>{container.image}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
};

View File

@@ -0,0 +1,40 @@
import { FolderOpen } from "lucide-react";
import { VolumeFileBrowser } from "~/client/components/volume-file-browser";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import type { Volume } from "~/client/lib/types";
type Props = {
volume: Volume;
};
export const FilesTabContent = ({ volume }: Props) => {
if (volume.status !== "mounted") {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center text-center py-12">
<FolderOpen className="mb-4 h-12 w-12 text-muted-foreground" />
<p className="text-muted-foreground">Volume must be mounted to browse files.</p>
<p className="text-sm text-muted-foreground mt-2">Mount the volume to explore its contents.</p>
</CardContent>
</Card>
);
}
return (
<Card className="h-[600px] flex flex-col">
<CardHeader>
<CardTitle>File Explorer</CardTitle>
<CardDescription>Browse the files and folders in this volume.</CardDescription>
</CardHeader>
<CardContent className="flex-1 overflow-hidden flex flex-col">
<VolumeFileBrowser
volumeName={volume.name}
enabled={volume.status === "mounted"}
className="overflow-auto flex-1 border rounded-md bg-card p-2"
emptyMessage="This volume is empty."
emptyDescription="Files and folders will appear here once you add them."
/>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,95 @@
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { toast } from "sonner";
import { CreateVolumeForm, type FormValues } from "~/client/components/create-volume-form";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "~/client/components/ui/alert-dialog";
import { Card } from "~/client/components/ui/card";
import type { StatFs, Volume } from "~/client/lib/types";
import { HealthchecksCard } from "../components/healthchecks-card";
import { StorageChart } from "../components/storage-chart";
import { updateVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen";
type Props = {
volume: Volume;
statfs: StatFs;
};
export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
const updateMutation = useMutation({
...updateVolumeMutation(),
onSuccess: (_) => {
toast.success("Volume updated successfully");
setOpen(false);
setPendingValues(null);
},
onError: (error) => {
toast.error("Failed to update volume", { description: error.message });
setOpen(false);
setPendingValues(null);
},
});
const [open, setOpen] = useState(false);
const [pendingValues, setPendingValues] = useState<FormValues | null>(null);
const handleSubmit = (values: FormValues) => {
setPendingValues(values);
setOpen(true);
};
const confirmUpdate = () => {
if (pendingValues) {
updateMutation.mutate({
path: { name: volume.name },
body: { config: pendingValues },
});
}
};
return (
<>
<div className="grid gap-4 xl:grid-cols-[minmax(0,2.3fr)_minmax(320px,1fr)]">
<Card className="p-6">
<CreateVolumeForm
initialValues={{ ...volume, ...volume.config }}
onSubmit={handleSubmit}
mode="update"
loading={updateMutation.isPending}
/>
</Card>
<div className="flex flex-col gap-4">
<div className="self-start w-full">
<HealthchecksCard volume={volume} />
</div>
<div className="flex-1 w-full">
<StorageChart statfs={statfs} />
</div>
</div>
</div>
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Update Volume Configuration</AlertDialogTitle>
<AlertDialogDescription>
Editing the volume will remount it with the new config immediately. This may temporarily disrupt access to
the volume. Continue?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={confirmUpdate}>Update</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};