diff --git a/.dockerignore b/.dockerignore
index 47deeba..f1df89d 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,4 +1,4 @@
-*
+**
!turbo.json
!bun.lock
@@ -20,6 +20,11 @@
!packages/**/src/**
# License files and attributions
+
!LICENSE
!NOTICES.md
!LICENSES/**
+
+# Node modules
+
+**/node_modules/**
diff --git a/apps/client/app/app.css b/apps/client/app/app.css
index b7b1465..3901f0d 100644
--- a/apps/client/app/app.css
+++ b/apps/client/app/app.css
@@ -5,170 +5,171 @@
@custom-variant dark (&:is(.dark *));
@theme {
- --font-sans:
- "Google Sans Code", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
- "Noto Color Emoji";
+ --breakpoint-xs: 32rem;
+ --font-sans:
+ "Google Sans Code", ui-sans-serif, system-ui, sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
html,
body {
- @apply bg-white dark:bg-[#131313];
- overflow-x: hidden;
- width: 100%;
- position: relative;
- overscroll-behavior: none;
- scrollbar-width: thin;
+ overflow-x: hidden;
+ width: 100%;
+ position: relative;
+ overscroll-behavior: none;
+ scrollbar-width: thin;
+}
- @media (prefers-color-scheme: dark) {
- color-scheme: dark;
- }
+body {
+ @apply bg-[#131313];
+ min-height: 100dvh;
}
.main-content {
- scrollbar-width: thin;
- scrollbar-gutter: stable;
+ scrollbar-width: thin;
+ scrollbar-gutter: stable;
}
@theme inline {
- --radius-sm: calc(var(--radius) - 4px);
- --radius-md: calc(var(--radius) - 2px);
- --radius-lg: var(--radius);
- --radius-xl: calc(var(--radius) + 4px);
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --color-card: var(--card);
- --color-card-foreground: var(--card-foreground);
- --color-card-header: var(--card-header);
- --color-popover: var(--popover);
- --color-popover-foreground: var(--popover-foreground);
- --color-primary: var(--primary);
- --color-primary-foreground: var(--primary-foreground);
- --color-secondary: var(--secondary);
- --color-secondary-foreground: var(--secondary-foreground);
- --color-muted: var(--muted);
- --color-muted-foreground: var(--muted-foreground);
- --color-accent: var(--accent);
- --color-accent-foreground: var(--accent-foreground);
- --color-destructive: var(--destructive);
- --color-border: var(--border);
- --color-input: var(--input);
- --color-ring: var(--ring);
- --color-chart-1: var(--chart-1);
- --color-chart-2: var(--chart-2);
- --color-chart-3: var(--chart-3);
- --color-chart-4: var(--chart-4);
- --color-chart-5: var(--chart-5);
- --color-sidebar: var(--sidebar);
- --color-sidebar-foreground: var(--sidebar-foreground);
- --color-sidebar-primary: var(--sidebar-primary);
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
- --color-sidebar-accent: var(--sidebar-accent);
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
- --color-sidebar-border: var(--sidebar-border);
- --color-sidebar-ring: var(--sidebar-ring);
- --color-strong-accent: var(--strong-accent);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-card-header: var(--card-header);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-strong-accent: var(--strong-accent);
}
:root {
- color-scheme: dark;
-
- --radius: 0.625rem;
- --background: oklch(1 0 0);
- --foreground: oklch(0.145 0 0);
- --card: oklch(1 0 0);
- --card-foreground: oklch(0.145 0 0);
- --card-header: oklch(0.922 0 0);
- --popover: oklch(1 0 0);
- --popover-foreground: oklch(0.145 0 0);
- --primary: oklch(0.205 0 0);
- --primary-foreground: oklch(0.985 0 0);
- --secondary: oklch(0.97 0 0);
- --secondary-foreground: oklch(0.205 0 0);
- --muted: oklch(0.97 0 0);
- --muted-foreground: oklch(0.556 0 0);
- --accent: oklch(0.97 0 0);
- --accent-foreground: oklch(0.205 0 0);
- --destructive: oklch(0.577 0.245 27.325);
- --border: oklch(0.922 0 0);
- --input: oklch(0.922 0 0);
- --ring: oklch(0.708 0 0);
- --chart-1: oklch(0.646 0.222 41.116);
- --chart-2: oklch(0.6 0.118 184.704);
- --chart-3: oklch(0.398 0.07 227.392);
- --chart-4: oklch(0.828 0.189 84.429);
- --chart-5: oklch(0.769 0.188 70.08);
- --sidebar: oklch(0.985 0 0);
- --sidebar-foreground: oklch(0.145 0 0);
- --sidebar-primary: oklch(0.205 0 0);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.97 0 0);
- --sidebar-accent-foreground: oklch(0.205 0 0);
- --sidebar-border: oklch(0.922 0 0);
- --sidebar-ring: oklch(0.708 0 0);
- --strong-accent: #ff543a;
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --card-header: oklch(0.922 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+ --strong-accent: #ff543a;
}
.dark {
- --background: #131313;
- --foreground: oklch(0.985 0 0);
- --card: #131313;
- --card-header: #1b1b1b;
- /* --card: oklch(0.205 0 0); ORIGINAL */
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.205 0 0);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.922 0 0);
- --primary-foreground: oklch(0.205 0 0);
- --secondary: oklch(0.1448 0 0);
- /* --secondary: oklch(0.269 0 0); ORIGINAL */
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
- --accent: oklch(0.269 0 0);
- --accent-foreground: oklch(0.985 0 0);
- --destructive: #ff543a;
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.556 0 0);
- --chart-1: oklch(0.488 0.243 264.376);
- --chart-2: oklch(0.696 0.17 162.48);
- --chart-3: oklch(0.769 0.188 70.08);
- --chart-4: oklch(0.627 0.265 303.9);
- --chart-5: oklch(0.645 0.246 16.439);
- --sidebar: oklch(0.205 0 0);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.269 0 0);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.556 0 0);
- --strong-accent: #ff543a;
+ color-scheme: dark;
+
+ --background: #131313;
+ --foreground: oklch(0.985 0 0);
+ --card: #131313;
+ --card-header: #1b1b1b;
+ /* --card: oklch(0.205 0 0); ORIGINAL */
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.1448 0 0);
+ /* --secondary: oklch(0.269 0 0); ORIGINAL */
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: #ff543a;
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+ --strong-accent: #ff543a;
}
@layer base {
- * {
- @apply border-border outline-ring/50;
- }
- body {
- @apply bg-background text-foreground;
- }
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
}
@layer base {
- :root {
- --chart-1: oklch(0.646 0.222 41.116);
- --chart-2: oklch(0.6 0.118 184.704);
- --chart-3: oklch(0.398 0.07 227.392);
- --chart-4: oklch(0.828 0.189 84.429);
- --chart-5: oklch(0.769 0.188 70.08);
- }
+ :root {
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ }
- .dark {
- --chart-1: oklch(0.488 0.243 264.376);
- --chart-2: oklch(0.696 0.17 162.48);
- --chart-3: oklch(0.769 0.188 70.08);
- --chart-4: oklch(0.627 0.265 303.9);
- --chart-5: oklch(0.645 0.246 16.439);
- }
+ .dark {
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ }
}
diff --git a/apps/client/app/components/auth-layout.tsx b/apps/client/app/components/auth-layout.tsx
index 63653bb..eb8b668 100644
--- a/apps/client/app/components/auth-layout.tsx
+++ b/apps/client/app/components/auth-layout.tsx
@@ -2,33 +2,33 @@ import { Mountain } from "lucide-react";
import type { ReactNode } from "react";
type AuthLayoutProps = {
- title: string;
- description: string;
- children: ReactNode;
+ title: string;
+ description: string;
+ children: ReactNode;
};
export function AuthLayout({ title, description, children }: AuthLayoutProps) {
- return (
-
diff --git a/apps/client/app/modules/repositories/routes/repository-details.tsx b/apps/client/app/modules/repositories/routes/repository-details.tsx
index 61f9ec3..e00ab92 100644
--- a/apps/client/app/modules/repositories/routes/repository-details.tsx
+++ b/apps/client/app/modules/repositories/routes/repository-details.tsx
@@ -3,19 +3,19 @@ import { useNavigate, useParams, useSearchParams } from "react-router";
import { toast } from "sonner";
import { useState, useEffect } from "react";
import {
- deleteRepositoryMutation,
- getRepositoryOptions,
- listSnapshotsOptions,
+ deleteRepositoryMutation,
+ getRepositoryOptions,
+ listSnapshotsOptions,
} from "~/api-client/@tanstack/react-query.gen";
import { Button } from "~/components/ui/button";
import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogHeader,
- AlertDialogTitle,
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogHeader,
+ AlertDialogTitle,
} from "~/components/ui/alert-dialog";
import { parseError } from "~/lib/errors";
import { getRepository } from "~/api-client/sdk.gen";
@@ -26,127 +26,137 @@ import { RepositoryInfoTabContent } from "../tabs/info";
import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
export function meta({ params }: Route.MetaArgs) {
- return [
- { title: params.name },
- {
- name: "description",
- content: "View repository configuration, status, and snapshots.",
- },
- ];
+ return [
+ { title: params.name },
+ {
+ name: "description",
+ content: "View repository configuration, status, and snapshots.",
+ },
+ ];
}
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
- const repository = await getRepository({ path: { name: params.name ?? "" } });
- if (repository.data) return repository.data;
+ const repository = await getRepository({ path: { name: params.name ?? "" } });
+ if (repository.data) return repository.data;
};
-export default function RepositoryDetailsPage({ loaderData }: Route.ComponentProps) {
- const { name } = useParams<{ name: string }>();
- const navigate = useNavigate();
- const queryClient = useQueryClient();
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+export default function RepositoryDetailsPage({
+ loaderData,
+}: Route.ComponentProps) {
+ const { name } = useParams<{ name: string }>();
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
- const [searchParams, setSearchParams] = useSearchParams();
- const activeTab = searchParams.get("tab") || "info";
+ const [searchParams, setSearchParams] = useSearchParams();
+ const activeTab = searchParams.get("tab") || "info";
- const { data } = useQuery({
- ...getRepositoryOptions({ path: { name: name ?? "" } }),
- initialData: loaderData,
- refetchInterval: 10000,
- refetchOnWindowFocus: true,
- });
+ const { data } = useQuery({
+ ...getRepositoryOptions({ path: { name: name ?? "" } }),
+ initialData: loaderData,
+ refetchInterval: 10000,
+ refetchOnWindowFocus: true,
+ });
- useEffect(() => {
- if (name) {
- queryClient.prefetchQuery(listSnapshotsOptions({ path: { name } }));
- }
- }, [name, queryClient]);
+ useEffect(() => {
+ if (name) {
+ queryClient.prefetchQuery(listSnapshotsOptions({ path: { name } }));
+ }
+ }, [name, queryClient]);
- const deleteRepo = useMutation({
- ...deleteRepositoryMutation(),
- onSuccess: () => {
- toast.success("Repository deleted successfully");
- navigate("/repositories");
- },
- onError: (error) => {
- toast.error("Failed to delete repository", {
- description: parseError(error)?.message,
- });
- },
- });
+ const deleteRepo = useMutation({
+ ...deleteRepositoryMutation(),
+ onSuccess: () => {
+ toast.success("Repository deleted successfully");
+ navigate("/repositories");
+ },
+ onError: (error) => {
+ toast.error("Failed to delete repository", {
+ description: parseError(error)?.message,
+ });
+ },
+ });
- const handleConfirmDelete = () => {
- setShowDeleteConfirm(false);
- deleteRepo.mutate({ path: { name: name ?? "" } });
- };
+ const handleConfirmDelete = () => {
+ setShowDeleteConfirm(false);
+ deleteRepo.mutate({ path: { name: name ?? "" } });
+ };
- if (!name) {
- return
Repository not found
;
- }
+ if (!name) {
+ return
Repository not found
;
+ }
- if (!data) {
- return
Loading...
;
- }
+ if (!data) {
+ return
Loading...
;
+ }
- return (
- <>
-
-
-
-
- {data.status || "unknown"}
-
- {data.type}
-
-
-
- setShowDeleteConfirm(true)} disabled={deleteRepo.isPending}>
- Delete
-
-
-
+ return (
+ <>
+
+
+
+ {data.status || "unknown"}
+
+
+ {data.type}
+
+
+
+ setShowDeleteConfirm(true)}
+ disabled={deleteRepo.isPending}
+ >
+ Delete
+
+
+
-
setSearchParams({ tab: value })}>
-
- Configuration
- Snapshots
-
-
-
-
-
-
-
-
+
setSearchParams({ tab: value })}
+ >
+
+ Configuration
+ Snapshots
+
+
+
+
+
+
+
+
-
-
-
- Delete repository?
-
- Are you sure you want to delete the repository {name} ? This action cannot be undone and
- will remove all backup data.
-
-
-
-
Cancel
-
- Delete repository
-
-
-
-
- >
- );
+
+
+
+ Delete repository?
+
+ Are you sure you want to delete the repository{" "}
+ {name} ? This action cannot be undone and will
+ remove all backup data.
+
+
+
+
Cancel
+
+ Delete repository
+
+
+
+
+ >
+ );
}
diff --git a/apps/client/app/modules/repositories/routes/snapshot-details.tsx b/apps/client/app/modules/repositories/routes/snapshot-details.tsx
index 3624b9f..3776768 100644
--- a/apps/client/app/modules/repositories/routes/snapshot-details.tsx
+++ b/apps/client/app/modules/repositories/routes/snapshot-details.tsx
@@ -8,90 +8,102 @@ import { getSnapshotDetails } from "~/api-client";
import type { Route } from "./+types/snapshot-details";
export function meta({ params }: Route.MetaArgs) {
- return [
- { title: `Snapshot ${params.snapshotId}` },
- {
- name: "description",
- content: "Browse and restore files from a backup snapshot.",
- },
- ];
+ return [
+ { title: `Snapshot ${params.snapshotId}` },
+ {
+ name: "description",
+ content: "Browse and restore files from a backup snapshot.",
+ },
+ ];
}
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
- const snapshot = await getSnapshotDetails({ path: { name: params.name, snapshotId: params.snapshotId } });
- if (snapshot.data) return snapshot.data;
+ const snapshot = await getSnapshotDetails({
+ path: { name: params.name, snapshotId: params.snapshotId },
+ });
+ if (snapshot.data) return snapshot.data;
- return redirect("/repositories");
+ return redirect("/repositories");
};
-export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps) {
- const { name, snapshotId } = useParams<{ name: string; snapshotId: string }>();
+export default function SnapshotDetailsPage({
+ loaderData,
+}: Route.ComponentProps) {
+ const { name, snapshotId } = useParams<{
+ name: string;
+ snapshotId: string;
+ }>();
- const { data } = useQuery({
- ...listSnapshotFilesOptions({
- path: { name: name ?? "", snapshotId: snapshotId ?? "" },
- query: { path: "/" },
- }),
- enabled: !!name && !!snapshotId,
- });
+ const { data } = useQuery({
+ ...listSnapshotFilesOptions({
+ path: { name: name ?? "", snapshotId: snapshotId ?? "" },
+ query: { path: "/" },
+ }),
+ enabled: !!name && !!snapshotId,
+ });
- if (!name || !snapshotId) {
- return (
-
-
Invalid snapshot reference
-
- );
- }
+ if (!name || !snapshotId) {
+ return (
+
+
Invalid snapshot reference
+
+ );
+ }
- return (
-
-
-
-
{name}
-
Snapshot: {snapshotId}
-
-
-
+ return (
+
+
+
+
{name}
+
+ Snapshot: {snapshotId}
+
+
+
+
-
+
- {data?.snapshot && (
-
-
- Snapshot Information
-
-
-
-
-
Snapshot ID:
-
{data.snapshot.id}
-
-
-
Short ID:
-
{data.snapshot.short_id}
-
-
-
Hostname:
-
{data.snapshot.hostname}
-
-
-
Time:
-
{new Date(data.snapshot.time).toLocaleString()}
-
-
-
Paths:
-
- {data.snapshot.paths.map((path) => (
-
- {path}
-
- ))}
-
-
-
-
-
- )}
-
- );
+ {data?.snapshot && (
+
+
+ Snapshot Information
+
+
+
+
+
Snapshot ID:
+
{data.snapshot.id}
+
+
+
Short ID:
+
{data.snapshot.short_id}
+
+
+
Hostname:
+
{data.snapshot.hostname}
+
+
+
Time:
+
{new Date(data.snapshot.time).toLocaleString()}
+
+
+
Paths:
+
+ {data.snapshot.paths.map((path) => (
+
+ {path}
+
+ ))}
+
+
+
+
+
+ )}
+
+ );
}
diff --git a/apps/client/app/modules/volumes/routes/volume-details.tsx b/apps/client/app/modules/volumes/routes/volume-details.tsx
index edf3169..cc19a3e 100644
--- a/apps/client/app/modules/volumes/routes/volume-details.tsx
+++ b/apps/client/app/modules/volumes/routes/volume-details.tsx
@@ -3,23 +3,23 @@ import { useNavigate, useParams, useSearchParams } from "react-router";
import { toast } from "sonner";
import { useState } from "react";
import {
- deleteVolumeMutation,
- getVolumeOptions,
- getSystemInfoOptions,
- mountVolumeMutation,
- unmountVolumeMutation,
+ deleteVolumeMutation,
+ getVolumeOptions,
+ getSystemInfoOptions,
+ mountVolumeMutation,
+ unmountVolumeMutation,
} from "~/api-client/@tanstack/react-query.gen";
import { StatusDot } from "~/components/status-dot";
import { Button } from "~/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogHeader,
- AlertDialogTitle,
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogHeader,
+ AlertDialogTitle,
} from "~/components/ui/alert-dialog";
import { VolumeIcon } from "~/components/volume-icon";
import { parseError } from "~/lib/errors";
@@ -31,161 +31,169 @@ import { FilesTabContent } from "../tabs/files";
import { DockerTabContent } from "../tabs/docker";
export function meta({ params }: Route.MetaArgs) {
- return [
- { title: params.name },
- {
- name: "description",
- content: "View and manage volume details, configuration, and files.",
- },
- ];
+ return [
+ { title: params.name },
+ {
+ name: "description",
+ content: "View and manage volume details, configuration, and files.",
+ },
+ ];
}
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
- const volume = await getVolume({ path: { name: params.name ?? "" } });
- if (volume.data) return volume.data;
+ const volume = await getVolume({ path: { name: params.name ?? "" } });
+ if (volume.data) return volume.data;
};
export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
- const { name } = useParams<{ name: string }>();
- const navigate = useNavigate();
- const [searchParams, setSearchParams] = useSearchParams();
- const activeTab = searchParams.get("tab") || "info";
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+ const { name } = useParams<{ name: string }>();
+ const navigate = useNavigate();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const activeTab = searchParams.get("tab") || "info";
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
- const { data } = useQuery({
- ...getVolumeOptions({ path: { name: name ?? "" } }),
- initialData: loaderData,
- refetchInterval: 10000,
- refetchOnWindowFocus: true,
- });
+ const { data } = useQuery({
+ ...getVolumeOptions({ path: { name: name ?? "" } }),
+ initialData: loaderData,
+ refetchInterval: 10000,
+ refetchOnWindowFocus: true,
+ });
- const { data: systemInfo } = useQuery({
- ...getSystemInfoOptions(),
- });
+ const { data: systemInfo } = useQuery({
+ ...getSystemInfoOptions(),
+ });
- const deleteVol = useMutation({
- ...deleteVolumeMutation(),
- onSuccess: () => {
- toast.success("Volume deleted successfully");
- navigate("/volumes");
- },
- onError: (error) => {
- toast.error("Failed to delete volume", {
- description: parseError(error)?.message,
- });
- },
- });
+ const deleteVol = useMutation({
+ ...deleteVolumeMutation(),
+ onSuccess: () => {
+ toast.success("Volume deleted successfully");
+ navigate("/volumes");
+ },
+ onError: (error) => {
+ toast.error("Failed to delete volume", {
+ description: parseError(error)?.message,
+ });
+ },
+ });
- const mountVol = useMutation({
- ...mountVolumeMutation(),
- onSuccess: () => {
- toast.success("Volume mounted successfully");
- },
- onError: (error) => {
- toast.error("Failed to mount volume", {
- description: parseError(error)?.message,
- });
- },
- });
+ const mountVol = useMutation({
+ ...mountVolumeMutation(),
+ onSuccess: () => {
+ toast.success("Volume mounted successfully");
+ },
+ onError: (error) => {
+ toast.error("Failed to mount volume", {
+ description: parseError(error)?.message,
+ });
+ },
+ });
- const unmountVol = useMutation({
- ...unmountVolumeMutation(),
- onSuccess: () => {
- toast.success("Volume unmounted successfully");
- },
- onError: (error) => {
- toast.error("Failed to unmount volume", {
- description: parseError(error)?.message,
- });
- },
- });
+ const unmountVol = useMutation({
+ ...unmountVolumeMutation(),
+ onSuccess: () => {
+ toast.success("Volume unmounted successfully");
+ },
+ onError: (error) => {
+ toast.error("Failed to unmount volume", {
+ description: parseError(error)?.message,
+ });
+ },
+ });
- const handleConfirmDelete = () => {
- setShowDeleteConfirm(false);
- deleteVol.mutate({ path: { name: name ?? "" } });
- };
+ const handleConfirmDelete = () => {
+ setShowDeleteConfirm(false);
+ deleteVol.mutate({ path: { name: name ?? "" } });
+ };
- if (!name) {
- return
Volume not found
;
- }
+ if (!name) {
+ return
Volume not found
;
+ }
- if (!data) {
- return
Loading...
;
- }
+ if (!data) {
+ return
Loading...
;
+ }
- const { volume, statfs } = data;
- const dockerAvailable = systemInfo?.capabilities?.docker ?? false;
+ const { volume, statfs } = data;
+ const dockerAvailable = systemInfo?.capabilities?.docker ?? false;
- return (
- <>
-
-
-
-
- {volume.status[0].toUpperCase() + volume.status.slice(1)}
-
-
-
-
-
- mountVol.mutate({ path: { name } })}
- loading={mountVol.isPending}
- className={cn({ hidden: volume.status === "mounted" })}
- >
- Mount
-
- unmountVol.mutate({ path: { name } })}
- loading={unmountVol.isPending}
- className={cn({ hidden: volume.status !== "mounted" })}
- >
- Unmount
-
- setShowDeleteConfirm(true)} disabled={deleteVol.isPending}>
- Delete
-
-
-
-
setSearchParams({ tab: value })} className="mt-4">
-
- Configuration
- Files
- {dockerAvailable && Docker }
-
-
-
-
-
-
-
- {dockerAvailable && (
-
-
-
- )}
-
+ return (
+ <>
+
+
+
+ {" "}
+ {volume.status[0].toUpperCase() + volume.status.slice(1)}
+
+
+
+
+ mountVol.mutate({ path: { name } })}
+ loading={mountVol.isPending}
+ className={cn({ hidden: volume.status === "mounted" })}
+ >
+ Mount
+
+ unmountVol.mutate({ path: { name } })}
+ loading={unmountVol.isPending}
+ className={cn({ hidden: volume.status !== "mounted" })}
+ >
+ Unmount
+
+ setShowDeleteConfirm(true)}
+ disabled={deleteVol.isPending}
+ >
+ Delete
+
+
+
+
setSearchParams({ tab: value })}
+ className="mt-4"
+ >
+
+ Configuration
+ Files
+ {dockerAvailable && Docker }
+
+
+
+
+
+
+
+ {dockerAvailable && (
+
+
+
+ )}
+
-
-
-
- Delete volume?
-
- Are you sure you want to delete the volume {name} ? This action cannot be undone.
-
-
-
-
Cancel
-
- Delete volume
-
-
-
-
- >
- );
+
+
+
+ Delete volume?
+
+ Are you sure you want to delete the volume {name}
+ ? This action cannot be undone.
+
+
+
+
Cancel
+
+ Delete volume
+
+
+
+
+ >
+ );
}
diff --git a/apps/client/app/root.tsx b/apps/client/app/root.tsx
index 7b238c4..2058fc0 100644
--- a/apps/client/app/root.tsx
+++ b/apps/client/app/root.tsx
@@ -1,5 +1,16 @@
-import { MutationCache, QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
+import {
+ MutationCache,
+ QueryClient,
+ QueryClientProvider,
+} from "@tanstack/react-query";
+import {
+ isRouteErrorResponse,
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+} from "react-router";
import { Toaster } from "~/components/ui/sonner";
import type { Route } from "./+types/root";
@@ -8,89 +19,108 @@ import { client } from "./api-client/client.gen";
import { useServerEvents } from "./hooks/use-server-events";
client.setConfig({
- baseUrl: "/",
+ baseUrl: "/",
});
export const links: Route.LinksFunction = () => [
- { rel: "preconnect", href: "https://fonts.googleapis.com" },
- {
- rel: "preconnect",
- href: "https://fonts.gstatic.com",
- crossOrigin: "anonymous",
- },
- {
- rel: "stylesheet",
- href: "https://fonts.googleapis.com/css2?family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&display=swap",
- },
+ { rel: "preconnect", href: "https://fonts.googleapis.com" },
+ {
+ rel: "preconnect",
+ href: "https://fonts.gstatic.com",
+ crossOrigin: "anonymous",
+ },
+ {
+ rel: "stylesheet",
+ href: "https://fonts.googleapis.com/css2?family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&display=swap",
+ },
];
const queryClient = new QueryClient({
- mutationCache: new MutationCache({
- onSuccess: () => {
- queryClient.invalidateQueries();
- },
- onError: (error) => {
- console.error("Mutation error:", error);
- queryClient.invalidateQueries();
- },
- }),
+ mutationCache: new MutationCache({
+ onSuccess: () => {
+ queryClient.invalidateQueries();
+ },
+ onError: (error) => {
+ console.error("Mutation error:", error);
+ queryClient.invalidateQueries();
+ },
+ }),
});
export function Layout({ children }: { children: React.ReactNode }) {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
- );
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+ );
}
export default function App() {
- useServerEvents();
+ useServerEvents();
- return
;
+ return
;
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
- let message = "Oops!";
- let details = "An unexpected error occurred.";
- let stack: string | undefined;
+ let message = "Oops!";
+ let details = "An unexpected error occurred.";
+ let stack: string | undefined;
- if (isRouteErrorResponse(error)) {
- message = error.status === 404 ? "404" : "Error";
- details = error.status === 404 ? "The requested page could not be found." : error.statusText || details;
- } else if (import.meta.env.DEV && error && error instanceof Error) {
- details = error.message;
- stack = error.stack;
- }
+ if (isRouteErrorResponse(error)) {
+ message = error.status === 404 ? "404" : "Error";
+ details =
+ error.status === 404
+ ? "The requested page could not be found."
+ : error.statusText || details;
+ } else if (import.meta.env.DEV && error && error instanceof Error) {
+ details = error.message;
+ stack = error.stack;
+ }
- return (
-
- {message}
- {details}
- {stack && (
-
- {stack}
-
- )}
-
- );
+ return (
+
+ {message}
+ {details}
+ {stack && (
+
+ {stack}
+
+ )}
+
+ );
}
diff --git a/apps/client/vite.config.ts b/apps/client/vite.config.ts
index 988d817..c4096c6 100644
--- a/apps/client/vite.config.ts
+++ b/apps/client/vite.config.ts
@@ -7,27 +7,28 @@ const alias = {};
const { NODE_ENV } = process.env;
if (NODE_ENV === "production") {
- // @ts-expect-error
- alias["react-dom/server"] = "react-dom/server.node";
+ // @ts-expect-error
+ alias["react-dom/server"] = "react-dom/server.node";
}
export default defineConfig({
- plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
- resolve: {
- alias,
- },
- build: {
- outDir: "dist",
- // sourcemap: true,
- },
- server: {
- host: true,
- port: 4097,
- proxy: {
- "/api": {
- target: "http://localhost:4096",
- changeOrigin: true,
- },
- },
- },
+ plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
+ resolve: {
+ alias,
+ },
+ build: {
+ outDir: "dist",
+ // sourcemap: true,
+ },
+ server: {
+ host: true,
+ port: 4097,
+ proxy: {
+ "/api": {
+ target: "http://localhost:4096",
+ changeOrigin: true,
+ },
+ },
+ allowedHosts: true,
+ },
});