From ffca433a43efa77407bca723f3a27d27b395dc1b Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 9 Nov 2025 13:04:14 +0200 Subject: [PATCH] fix: accessibility and responsiveness fixes --- .dockerignore | 7 +- apps/client/app/app.css | 285 ++++++++-------- apps/client/app/components/auth-layout.tsx | 48 +-- apps/client/app/components/layout.tsx | 135 ++++---- apps/client/app/components/onoff.tsx | 49 +-- apps/client/app/components/status-dot.tsx | 90 ++--- .../components/create-schedule-form.tsx | 2 +- .../routes/repository-details.tsx | 246 +++++++------- .../repositories/routes/snapshot-details.tsx | 162 ++++----- .../modules/volumes/routes/volume-details.tsx | 312 +++++++++--------- apps/client/app/root.tsx | 168 ++++++---- apps/client/vite.config.ts | 41 +-- 12 files changed, 820 insertions(+), 725 deletions(-) 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 ( -
-
-
-
- - Ironmount -
+ return ( +
+
+
+
+ + Ironmount +
-
-

{title}

-

{description}

-
+
+

{title}

+

{description}

+
- {children} -
-
-
-
- ); + {children} +
+
+
+
+ ); } diff --git a/apps/client/app/components/layout.tsx b/apps/client/app/components/layout.tsx index fae5a9f..ca5cc29 100644 --- a/apps/client/app/components/layout.tsx +++ b/apps/client/app/components/layout.tsx @@ -15,73 +15,84 @@ import { AppSidebar } from "./app-sidebar"; export const clientMiddleware = [authMiddleware]; export async function clientLoader({ context }: Route.LoaderArgs) { - const ctx = context.get(appContext); + const ctx = context.get(appContext); - if (ctx.user && !ctx.user.hasDownloadedResticPassword) { - throw redirect("/download-recovery-key"); - } + if (ctx.user && !ctx.user.hasDownloadedResticPassword) { + throw redirect("/download-recovery-key"); + } - return ctx; + return ctx; } export default function Layout({ loaderData }: Route.ComponentProps) { - const navigate = useNavigate(); + const navigate = useNavigate(); - const logout = useMutation({ - ...logoutMutation(), - onSuccess: async () => { - navigate("/login", { replace: true }); - }, - onError: (error) => { - console.error(error); - toast.error("Logout failed", { description: error.message }); - }, - }); + const logout = useMutation({ + ...logoutMutation(), + onSuccess: async () => { + navigate("/login", { replace: true }); + }, + onError: (error) => { + console.error(error); + toast.error("Logout failed", { description: error.message }); + }, + }); - return ( - - -
-
-
-
- - -
- {loaderData.user && ( -
- - Welcome,  - {loaderData.user?.username} - - - -
- )} -
-
-
- -
- -
-
-
-
-
- ); + return ( + + +
+
+
+
+ + +
+ {loaderData.user && ( +
+ + Welcome,  + + {loaderData.user?.username} + + + + +
+ )} +
+
+
+ +
+ +
+
+
+
+
+ ); } diff --git a/apps/client/app/components/onoff.tsx b/apps/client/app/components/onoff.tsx index 1ee4a6f..d4100e2 100644 --- a/apps/client/app/components/onoff.tsx +++ b/apps/client/app/components/onoff.tsx @@ -2,25 +2,36 @@ import { cn } from "~/lib/utils"; import { Switch } from "./ui/switch"; type Props = { - isOn: boolean; - toggle: (v: boolean) => void; - enabledLabel: string; - disabledLabel: string; - disabled?: boolean; + isOn: boolean; + toggle: (v: boolean) => void; + enabledLabel: string; + disabledLabel: string; + disabled?: boolean; }; -export const OnOff = ({ isOn, toggle, enabledLabel, disabledLabel, disabled }: Props) => { - return ( -
- {isOn ? enabledLabel : disabledLabel} - -
- ); +export const OnOff = ({ + isOn, + toggle, + enabledLabel, + disabledLabel, + disabled, +}: Props) => { + return ( +
+ {isOn ? enabledLabel : disabledLabel} + +
+ ); }; diff --git a/apps/client/app/components/status-dot.tsx b/apps/client/app/components/status-dot.tsx index 721c29d..37c0439 100644 --- a/apps/client/app/components/status-dot.tsx +++ b/apps/client/app/components/status-dot.tsx @@ -3,47 +3,53 @@ import { cn } from "~/lib/utils"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; export const StatusDot = ({ status }: { status: VolumeStatus }) => { - const statusMapping = { - mounted: { - color: "bg-green-500", - colorLight: "bg-emerald-400", - animated: true, - }, - unmounted: { - color: "bg-gray-500", - colorLight: "bg-gray-400", - animated: false, - }, - error: { - color: "bg-red-500", - colorLight: "bg-amber-700", - animated: true, - }, - unknown: { - color: "bg-yellow-500", - colorLight: "bg-yellow-400", - animated: true, - }, - }[status]; + const statusMapping = { + mounted: { + color: "bg-green-500", + colorLight: "bg-emerald-400", + animated: true, + }, + unmounted: { + color: "bg-gray-500", + colorLight: "bg-gray-400", + animated: false, + }, + error: { + color: "bg-red-500", + colorLight: "bg-amber-700", + animated: true, + }, + unknown: { + color: "bg-yellow-500", + colorLight: "bg-yellow-400", + animated: true, + }, + }[status]; - return ( - - - - {statusMapping.animated && ( - - )} - - - - -

{status}

-
-
- ); + return ( + + + + {statusMapping.animated && ( + + )} + + + + +

{status}

+
+
+ ); }; diff --git a/apps/client/app/modules/backups/components/create-schedule-form.tsx b/apps/client/app/modules/backups/components/create-schedule-form.tsx index 498e10d..e72262f 100644 --- a/apps/client/app/modules/backups/components/create-schedule-form.tsx +++ b/apps/client/app/modules/backups/components/create-schedule-form.tsx @@ -232,7 +232,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: onSelectionChange={handleSelectionChange} withCheckboxes={true} foldersOnly={true} - className="overflow-auto flex-1 border rounded-md bg-card p-2 min-h-[300px] max-h-[400px]" + className="max-w-2xs xs:max-w-screen flex-1 border rounded-md bg-card p-2 min-h-[300px] max-h-[400px]" /> {selectedPaths.size > 0 && (
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} -
-
-
- -
-
+ return ( + <> +
+
+ + {data.status || "unknown"} + + + {data.type} + +
+
+ +
+
- 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)} - - -
-
-
- - - -
-
- setSearchParams({ tab: value })} className="mt-4"> - - Configuration - Files - {dockerAvailable && Docker} - - - - - - - - {dockerAvailable && ( - - - - )} - + return ( + <> +
+
+ + {" "} + {volume.status[0].toUpperCase() + volume.status.slice(1)} + + +
+
+ + + +
+
+ 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, + }, });