fix: multiple mobile and responsiveness issues (#537)
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled
* fix: multiple mobile and responsiveness issues fix(mobile): scroll reset on snapshot selection fix(mobile): layout improvements refactor(volume-details): improve layout refactor: better card breakpoints layouts in backps list fix(ui): keep sidebar in the state it was before reloading refactor(ui): keep the same grid size in all breakpoints refactor: manual hotkey devpanel to tanstack hotkeys * chore: pr feedback
This commit is contained in:
parent
16df0dc875
commit
f059a23ecc
19 changed files with 175 additions and 209 deletions
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
- Never create migration files manually. Always use the provided command to generate migrations
|
||||
- If you realize an automated migration is incorrect, make sure to remove all the associated entries from the `_journal.json` and the newly created files located in `app/drizzle/` before re-generating the migration
|
||||
- The dev server is running at http://localhost:3000. Username is `admin` and password is `password`
|
||||
|
||||
## Project Overview
|
||||
|
||||
|
|
|
|||
17
app/app.css
17
app/app.css
|
|
@ -5,7 +5,16 @@
|
|||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@custom-variant @narrow (@container (min-width: 28rem));
|
||||
@custom-variant @medium (@container (min-width: 40rem));
|
||||
@custom-variant @wide (@container (min-width: 60rem));
|
||||
@custom-variant @xwide (@container (min-width: 80rem));
|
||||
|
||||
@theme {
|
||||
--container-narrow: 28rem;
|
||||
--container-medium: 40rem;
|
||||
--container-wide: 60rem;
|
||||
--container-xwide: 80rem;
|
||||
--breakpoint-xs: 32rem;
|
||||
--font-sans:
|
||||
"Google Sans Code", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||
|
|
@ -17,10 +26,16 @@ body {
|
|||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overscroll-behavior: none;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
@media (min-width: 48rem) {
|
||||
html,
|
||||
body {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-[#131313];
|
||||
min-height: 100dvh;
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const items = [
|
|||
];
|
||||
|
||||
export function AppSidebar() {
|
||||
const { state } = useSidebar();
|
||||
const { state, isMobile, setOpenMobile } = useSidebar();
|
||||
const { updates, hasUpdate } = useUpdates();
|
||||
const [showReleaseNotes, setShowReleaseNotes] = useState(false);
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ export function AppSidebar() {
|
|||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link to={item.url}>
|
||||
<Link to={item.url} onClick={() => isMobile && setOpenMobile(false)}>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<item.icon className={cn({ "text-strong-accent": isActive })} />
|
||||
|
|
|
|||
|
|
@ -1,51 +1,18 @@
|
|||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useHotkey } from "@tanstack/react-hotkeys";
|
||||
import { getDevPanelOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { DevPanel } from "./dev-panel";
|
||||
|
||||
export function DevPanelListener() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const pressedKeysRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const { data: devPanelStatus } = useQuery({
|
||||
...getDevPanelOptions(),
|
||||
});
|
||||
|
||||
const isEnabled = devPanelStatus?.enabled ?? false;
|
||||
useHotkey("Mod+Shift+D", () => setIsOpen(true), { enabled: !!devPanelStatus?.enabled, preventDefault: true });
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!isEnabled) return;
|
||||
pressedKeysRef.current.add(e.key.toLowerCase());
|
||||
|
||||
const keys = pressedKeysRef.current;
|
||||
if (keys.has("d") && keys.has("e") && keys.has("v")) {
|
||||
setIsOpen(true);
|
||||
pressedKeysRef.current.clear();
|
||||
}
|
||||
},
|
||||
[isEnabled],
|
||||
);
|
||||
|
||||
const handleKeyUp = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!isEnabled) return;
|
||||
pressedKeysRef.current.delete(e.key.toLowerCase());
|
||||
},
|
||||
[isEnabled],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
};
|
||||
}, [handleKeyDown, handleKeyUp]);
|
||||
|
||||
if (!isEnabled) {
|
||||
if (!devPanelStatus?.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export function GridBackground({ children, className, containerClassName }: Grid
|
|||
<div
|
||||
className={cn(
|
||||
"relative min-h-full w-full",
|
||||
"bg-size-[20px_20px] sm:bg-size-[40px_40px]",
|
||||
"bg-size-[40px_40px]",
|
||||
"bg-[linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)]",
|
||||
"dark:bg-[linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]",
|
||||
containerClassName,
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ export function Layout({ loaderData }: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<SidebarProvider defaultOpen={true}>
|
||||
<SidebarProvider defaultOpen={loaderData.sidebarOpen}>
|
||||
<AppSidebar />
|
||||
<div className="w-full relative flex flex-col h-screen overflow-hidden">
|
||||
<div className="w-full relative flex flex-col min-h-screen md:h-screen md:overflow-hidden">
|
||||
<header className="z-50 bg-card-header border-b border-border/50 shrink-0">
|
||||
<div className="flex items-center justify-between py-3 sm:py-4 px-2 sm:px-8 mx-auto container gap-4">
|
||||
<div className="flex items-center gap-4 min-w-0">
|
||||
|
|
@ -66,9 +66,9 @@ export function Layout({ loaderData }: Props) {
|
|||
)}
|
||||
</div>
|
||||
</header>
|
||||
<div className="main-content flex-1 overflow-y-auto">
|
||||
<div className="main-content flex-1 md:overflow-y-auto">
|
||||
<GridBackground>
|
||||
<main className="flex flex-col p-2 pb-6 pt-2 sm:p-8 sm:pt-6 mx-auto @container">
|
||||
<main className="flex flex-col p-2 pb-6 pt-2 sm:p-8 sm:pt-6 mx-auto">
|
||||
<Outlet />
|
||||
</main>
|
||||
</GridBackground>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "
|
|||
import { Skeleton } from "~/client/components/ui/skeleton";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/client/components/ui/tooltip";
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
export const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "14rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
|
|
|
|||
|
|
@ -86,16 +86,20 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||
<div className="space-y-4">
|
||||
<Card className="h-150 flex flex-col">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle>File Browser</CardTitle>
|
||||
<CardDescription
|
||||
className={cn({ hidden: !snapshot.time })}
|
||||
>{`Viewing snapshot from ${formatDateTime(snapshot?.time)}`}</CardDescription>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 flex-wrap sm:flex-nowrap">
|
||||
<Link
|
||||
to={backupId ? "/backups/$backupId/$snapshotId/restore" : "/repositories/$repositoryId/$snapshotId/restore"}
|
||||
to={
|
||||
backupId
|
||||
? "/backups/$backupId/$snapshotId/restore"
|
||||
: "/repositories/$repositoryId/$snapshotId/restore"
|
||||
}
|
||||
params={
|
||||
backupId
|
||||
? { backupId, snapshotId: snapshot.short_id }
|
||||
|
|
|
|||
|
|
@ -223,6 +223,7 @@ export function ScheduleDetailsPage(props: Props) {
|
|||
void navigate({
|
||||
to: ".",
|
||||
search: () => ({ ...searchParams, snapshot: snapshotId }),
|
||||
resetScroll: false,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -97,10 +97,10 @@ export function BackupsPage() {
|
|||
const scheduleMap = new Map(schedules.map((s) => [s.id, s]));
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-6">
|
||||
<div className="container @container mx-auto space-y-6">
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={items} strategy={rectSortingStrategy}>
|
||||
<div className="grid gap-4 @md:grid-cols-1 @lg:grid-cols-2 @2xl:grid-cols-3 auto-rows-fr">
|
||||
<div className="grid gap-4 @narrow:grid-cols-1 @medium:grid-cols-2 @wide:grid-cols-3 auto-rows-fr">
|
||||
{items.map((id) => {
|
||||
const schedule = scheduleMap.get(id);
|
||||
if (!schedule) return null;
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ export function CreateBackupPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-6">
|
||||
<div className="container mx-auto space-y-4">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Select value={selectedVolumeId?.toString()} onValueChange={(v) => setSelectedVolumeId(Number(v))}>
|
||||
|
|
|
|||
|
|
@ -96,12 +96,12 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Card className="p-6 space-y-6">
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-2">
|
||||
<Card className="p-6 @container">
|
||||
<div className="flex flex-col @xl:flex-row items-start @xl:items-center justify-between gap-4">
|
||||
<div>
|
||||
<span className="text-lg font-semibold mb-4">Repository Settings</span>
|
||||
<span className="text-lg font-semibold">Repository Settings</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-end gap-2 sm:gap-4">
|
||||
<div className="flex flex-col @xl:flex-row w-full @xl:w-auto gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
|
@ -153,7 +153,7 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
|||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Current Configuration</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 @xl:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-muted-foreground">Name</div>
|
||||
<p className="mt-1 text-sm">{repository.name}</p>
|
||||
|
|
@ -167,7 +167,7 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
|||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Repository Information</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 @xl:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-muted-foreground">Backend</div>
|
||||
<p className="mt-1 text-sm">{repository.type}</p>
|
||||
|
|
|
|||
|
|
@ -1,143 +1,27 @@
|
|||
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { toast } from "sonner";
|
||||
import { useState } from "react";
|
||||
import { Plug, Unplug } from "lucide-react";
|
||||
import { StatusDot } from "~/client/components/status-dot";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "~/client/components/ui/alert-dialog";
|
||||
import { VolumeIcon } from "~/client/components/volume-icon";
|
||||
import { parseError } from "~/client/lib/errors";
|
||||
import { cn } from "~/client/lib/utils";
|
||||
import { VolumeInfoTabContent } from "../tabs/info";
|
||||
import { FilesTabContent } from "../tabs/files";
|
||||
import type { VolumeStatus } from "~/client/lib/types";
|
||||
import {
|
||||
deleteVolumeMutation,
|
||||
getVolumeOptions,
|
||||
mountVolumeMutation,
|
||||
unmountVolumeMutation,
|
||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { getVolumeOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
const getVolumeStatusVariant = (status: VolumeStatus): "success" | "neutral" | "error" | "warning" => {
|
||||
const statusMap = {
|
||||
mounted: "success" as const,
|
||||
unmounted: "neutral" as const,
|
||||
error: "error" as const,
|
||||
unknown: "warning" as const,
|
||||
};
|
||||
return statusMap[status];
|
||||
};
|
||||
|
||||
export function VolumeDetails({ volumeId }: { volumeId: string }) {
|
||||
const navigate = useNavigate();
|
||||
const searchParams = useSearch({ from: "/(dashboard)/volumes/$volumeId" });
|
||||
|
||||
const activeTab = searchParams.tab || "info";
|
||||
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
|
||||
const { data } = useSuspenseQuery({
|
||||
...getVolumeOptions({ path: { id: volumeId } }),
|
||||
});
|
||||
|
||||
const deleteVol = useMutation({
|
||||
...deleteVolumeMutation(),
|
||||
onSuccess: () => {
|
||||
toast.success("Volume deleted successfully");
|
||||
void navigate({ to: "/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 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: { id: volumeId } });
|
||||
};
|
||||
|
||||
const { volume, statfs } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-start xs:items-center xs:flex-row xs:justify-between">
|
||||
<div className="text-sm font-semibold mb-2 xs:mb-0 text-muted-foreground flex items-center gap-2">
|
||||
<span className="flex items-center gap-2">
|
||||
<StatusDot
|
||||
variant={getVolumeStatusVariant(volume.status)}
|
||||
label={volume.status[0].toUpperCase() + volume.status.slice(1)}
|
||||
/>
|
||||
|
||||
{volume.status[0].toUpperCase() + volume.status.slice(1)}
|
||||
</span>
|
||||
<VolumeIcon backend={volume?.config.backend} />
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
onClick={() => mountVol.mutate({ path: { id: volumeId } })}
|
||||
loading={mountVol.isPending}
|
||||
className={cn({ hidden: volume.status === "mounted" })}
|
||||
>
|
||||
<Plug className="h-4 w-4 mr-2" />
|
||||
Mount
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => unmountVol.mutate({ path: { id: volumeId } })}
|
||||
loading={unmountVol.isPending}
|
||||
className={cn({ hidden: volume.status !== "mounted" })}
|
||||
>
|
||||
<Unplug className="h-4 w-4 mr-2" />
|
||||
Unmount
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={() => setShowDeleteConfirm(true)} disabled={deleteVol.isPending}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(value) => navigate({ to: ".", search: () => ({ tab: value }) })}
|
||||
className="mt-4"
|
||||
>
|
||||
<Tabs value={activeTab} onValueChange={(value) => navigate({ to: ".", search: () => ({ tab: value }) })}>
|
||||
<TabsList className="mb-2">
|
||||
<TabsTrigger value="info">Configuration</TabsTrigger>
|
||||
<TabsTrigger value="files">Files</TabsTrigger>
|
||||
|
|
@ -149,29 +33,6 @@ export function VolumeDetails({ volumeId }: { volumeId: string }) {
|
|||
<FilesTabContent volume={volume} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete volume?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete the volume <strong>{volume.name}</strong>? This action cannot be undone.
|
||||
<br />
|
||||
<br />
|
||||
All backup schedules associated with this volume will also be removed.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Delete volume
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Check } from "lucide-react";
|
||||
import { Check, Plug, Trash2, Unplug } from "lucide-react";
|
||||
import { CreateVolumeForm, type FormValues } from "~/client/modules/volumes/components/create-volume-form";
|
||||
import {
|
||||
AlertDialog,
|
||||
|
|
@ -13,13 +13,20 @@ import {
|
|||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "~/client/components/ui/alert-dialog";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
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";
|
||||
import {
|
||||
deleteVolumeMutation,
|
||||
mountVolumeMutation,
|
||||
unmountVolumeMutation,
|
||||
updateVolumeMutation,
|
||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import type { UpdateVolumeResponse } from "~/client/api-client/types.gen";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { parseError } from "~/client/lib/errors";
|
||||
|
||||
type Props = {
|
||||
volume: Volume;
|
||||
|
|
@ -47,8 +54,46 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
|
|||
},
|
||||
});
|
||||
|
||||
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 deleteVol = useMutation({
|
||||
...deleteVolumeMutation(),
|
||||
onSuccess: () => {
|
||||
toast.success("Volume deleted successfully");
|
||||
void navigate({ to: "/volumes" });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to delete volume", {
|
||||
description: parseError(error)?.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pendingValues, setPendingValues] = useState<FormValues | null>(null);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
|
||||
const handleSubmit = (values: FormValues) => {
|
||||
setPendingValues(values);
|
||||
|
|
@ -64,10 +109,51 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
setShowDeleteConfirm(false);
|
||||
deleteVol.mutate({ path: { id: volume.shortId } });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,2.3fr)_minmax(320px,1fr)]">
|
||||
<Card className="p-6">
|
||||
<Card className="p-6 @container">
|
||||
<div className="flex flex-col @xl:flex-row items-start @xl:items-center justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<span className="text-lg font-semibold">Volume Configuration</span>
|
||||
</div>
|
||||
<div className="flex flex-col @xl:flex-row w-full @xl:w-auto gap-2">
|
||||
{volume.status !== "mounted" ? (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => mountVol.mutate({ path: { id: volume.shortId } })}
|
||||
loading={mountVol.isPending}
|
||||
>
|
||||
<Plug className="h-4 w-4 mr-2" />
|
||||
Mount
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => unmountVol.mutate({ path: { id: volume.shortId } })}
|
||||
loading={unmountVol.isPending}
|
||||
>
|
||||
<Unplug className="h-4 w-4 mr-2" />
|
||||
Unmount
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
disabled={deleteVol.isPending}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<CreateVolumeForm
|
||||
initialValues={{ ...volume, ...volume.config }}
|
||||
onSubmit={handleSubmit}
|
||||
|
|
@ -102,6 +188,30 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
|
|||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete volume?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete the volume <strong>{volume.name}</strong>? This action cannot be undone.
|
||||
<br />
|
||||
<br />
|
||||
All backup schedules associated with this volume will also be removed.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
disabled={deleteVol.isPending}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete volume
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ type User = {
|
|||
export type AppContext = {
|
||||
user: User | null;
|
||||
hasUsers: boolean;
|
||||
sidebarOpen: boolean;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { getRequestHeaders } from "@tanstack/react-start/server";
|
||||
import { getCookie, getRequestHeaders } from "@tanstack/react-start/server";
|
||||
import { Layout } from "~/client/components/layout";
|
||||
import { SIDEBAR_COOKIE_NAME } from "~/client/components/ui/sidebar";
|
||||
import { authMiddleware } from "~/middleware/auth";
|
||||
import { auth } from "~/server/lib/auth";
|
||||
import { authService } from "~/server/modules/auth/auth.service";
|
||||
|
|
@ -11,7 +12,10 @@ export const fetchUser = createServerFn({ method: "GET" }).handler(async () => {
|
|||
const session = await auth.api.getSession({ headers });
|
||||
const hasUsers = await authService.hasUsers();
|
||||
|
||||
return { user: session?.user ?? null, hasUsers };
|
||||
const sidebarCookie = getCookie(SIDEBAR_COOKIE_NAME);
|
||||
const sidebarOpen = sidebarCookie === null ? true : sidebarCookie === "true";
|
||||
|
||||
return { user: session?.user ?? null, hasUsers, sidebarOpen };
|
||||
});
|
||||
|
||||
export const Route = createFileRoute("/(dashboard)")({
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import { Outlet, HeadContent, Scripts, createRootRouteWithContext } from "@tanst
|
|||
import appCss from "../app.css?url";
|
||||
import { apiClientMiddleware } from "~/middleware/api-client";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||
import { Toaster } from "~/client/components/ui/sonner";
|
||||
import { useServerEvents } from "~/client/hooks/use-server-events";
|
||||
|
||||
|
|
@ -52,8 +50,6 @@ function RootLayout() {
|
|||
</head>
|
||||
<body className="dark">
|
||||
<Outlet />
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
<ReactQueryDevtools buttonPosition="bottom-left" />
|
||||
<Toaster />
|
||||
<Scripts />
|
||||
</body>
|
||||
|
|
|
|||
5
bun.lock
5
bun.lock
|
|
@ -26,6 +26,7 @@
|
|||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@scalar/hono-api-reference": "^0.9.41",
|
||||
"@tanstack/react-hotkeys": "^0.1.0",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-query-devtools": "^5.91.3",
|
||||
"@tanstack/react-router": "^1.160.2",
|
||||
|
|
@ -756,10 +757,14 @@
|
|||
|
||||
"@tanstack/history": ["@tanstack/history@1.154.14", "", {}, "sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA=="],
|
||||
|
||||
"@tanstack/hotkeys": ["@tanstack/hotkeys@0.1.0", "", { "dependencies": { "@tanstack/store": "^0.8.0" } }, "sha512-2ypLO3C2vKxMbCjiOfA3RzDaUIUYsQtyLc+Qwl6oKzQvWxReGlQN7DOQZSeKyX9D395d3E65e2v7Fm14BzUGew=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
|
||||
|
||||
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.93.0", "", {}, "sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg=="],
|
||||
|
||||
"@tanstack/react-hotkeys": ["@tanstack/react-hotkeys@0.1.0", "", { "dependencies": { "@tanstack/hotkeys": "0.1.0", "@tanstack/react-store": "^0.8.0" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-Hteo+hdPKdR2G7uAwTmScu1JUHM/f03lVihxVfR/A3XMyVJmbzi4SJieRk/kLgYKeXFRGIUx/xSI3mHeVc6DbQ=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="],
|
||||
|
||||
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.3", "", { "dependencies": { "@tanstack/query-devtools": "5.93.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.20", "react": "^18 || ^19" } }, "sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA=="],
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@scalar/hono-api-reference": "^0.9.41",
|
||||
"@tanstack/react-hotkeys": "^0.1.0",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-query-devtools": "^5.91.3",
|
||||
"@tanstack/react-router": "^1.160.2",
|
||||
|
|
|
|||
Loading…
Reference in a new issue