From e77723164b6522136a824b001d644de8b795b3ca Mon Sep 17 00:00:00 2001
From: Nico <47644445+nicotsx@users.noreply.github.com>
Date: Thu, 2 Apr 2026 21:44:32 +0200
Subject: [PATCH] refactor: improve buttons and space with dropdowns (#738)
---
.../backups/components/schedule-summary.tsx | 85 ++++++------
.../routes/notification-details.tsx | 36 ++++--
.../repositories/routes/repositories.tsx | 20 ++-
app/client/modules/repositories/tabs/info.tsx | 122 ++++++++++--------
app/client/modules/volumes/tabs/info.tsx | 85 ++++++------
5 files changed, 190 insertions(+), 158 deletions(-)
diff --git a/app/client/modules/backups/components/schedule-summary.tsx b/app/client/modules/backups/components/schedule-summary.tsx
index 80b7d000..fb1adfbd 100644
--- a/app/client/modules/backups/components/schedule-summary.tsx
+++ b/app/client/modules/backups/components/schedule-summary.tsx
@@ -1,4 +1,4 @@
-import { Check, Database, Eraser, HardDrive, Pencil, Play, Square, Trash2, X } from "lucide-react";
+import { Check, ChevronDown, Database, Eraser, HardDrive, Pencil, Play, Square, Trash2, X } from "lucide-react";
import { useMemo, useState } from "react";
import { OnOff } from "~/client/components/onoff";
import { Button } from "~/client/components/ui/button";
@@ -12,6 +12,13 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "~/client/components/ui/alert-dialog";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "~/client/components/ui/dropdown-menu";
import type { BackupSchedule } from "~/client/lib/types";
import { BackupProgressCard } from "./backup-progress-card";
import { getBackupProgressOptions, runForgetMutation } from "~/client/api-client/@tanstack/react-query.gen";
@@ -46,9 +53,6 @@ export const ScheduleSummary = (props: Props) => {
const runForget = useMutation({
...runForgetMutation(),
- onSuccess: () => {
- toast.success("Retention policy applied successfully");
- },
onError: (error) => {
handleRepositoryError("Failed to apply retention policy", error, schedule.repository.shortId);
},
@@ -83,7 +87,10 @@ export const ScheduleSummary = (props: Props) => {
const handleConfirmForget = () => {
setShowForgetConfirm(false);
- runForget.mutate({ path: { shortId: schedule.shortId } });
+ toast.promise(runForget.mutateAsync({ path: { shortId: schedule.shortId } }), {
+ loading: "Running cleanup...",
+ success: "Retention policy applied successfully",
+ });
};
const handleConfirmStop = () => {
@@ -132,53 +139,45 @@ export const ScheduleSummary = (props: Props) => {
/>
-
+
{schedule.lastBackupStatus === "in_progress" ? (
-
diff --git a/app/client/modules/notifications/routes/notification-details.tsx b/app/client/modules/notifications/routes/notification-details.tsx
index 303cea4c..ca9d0222 100644
--- a/app/client/modules/notifications/routes/notification-details.tsx
+++ b/app/client/modules/notifications/routes/notification-details.tsx
@@ -18,10 +18,16 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "~/client/components/ui/alert-dialog";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "~/client/components/ui/dropdown-menu";
import { parseError } from "~/client/lib/errors";
import { cn } from "~/client/lib/utils";
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
-import { Bell, Save, TestTube2, Trash2 } from "lucide-react";
+import { Bell, ChevronDown, Save, TestTube2, Trash2 } from "lucide-react";
import { Alert, AlertDescription } from "~/client/components/ui/alert";
import { CreateNotificationForm, type NotificationFormValues } from "../components/create-notification-form";
import { useNavigate } from "@tanstack/react-router";
@@ -105,7 +111,7 @@ export function NotificationDetailsPage({ notificationId }: { notificationId: st
{data.type}
-
+
Test
- setShowDeleteConfirm(true)}
- variant="destructive"
- loading={deleteDestination.isPending}
- >
-
- Delete
-
+
+
+
+ Actions
+
+
+
+
+ setShowDeleteConfirm(true)}
+ disabled={deleteDestination.isPending}
+ >
+
+ Delete
+
+
+
diff --git a/app/client/modules/repositories/routes/repositories.tsx b/app/client/modules/repositories/routes/repositories.tsx
index eeed85c8..c7f65534 100644
--- a/app/client/modules/repositories/routes/repositories.tsx
+++ b/app/client/modules/repositories/routes/repositories.tsx
@@ -9,6 +9,7 @@ import { Input } from "~/client/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
import { cn } from "~/client/lib/utils";
+import { StatusDot } from "~/client/components/status-dot";
import { EmptyState } from "~/client/components/empty-state";
import { useNavigate } from "@tanstack/react-router";
@@ -143,17 +144,14 @@ export function RepositoriesPage() {
-
- {repository.status || "unknown"}
-
+
))}
diff --git a/app/client/modules/repositories/tabs/info.tsx b/app/client/modules/repositories/tabs/info.tsx
index fdaae655..12c23bec 100644
--- a/app/client/modules/repositories/tabs/info.tsx
+++ b/app/client/modules/repositories/tabs/info.tsx
@@ -1,7 +1,7 @@
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { toast } from "sonner";
-import { Pencil, Square, Stethoscope, Trash2, Unlock } from "lucide-react";
+import { ChevronDown, Pencil, Square, Stethoscope, Trash2, Unlock } from "lucide-react";
import { Card, CardContent, CardTitle } from "~/client/components/ui/card";
import { Button } from "~/client/components/ui/button";
import {
@@ -13,6 +13,13 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "~/client/components/ui/alert-dialog";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "~/client/components/ui/dropdown-menu";
import type { Repository } from "~/client/lib/types";
import type { GetRepositoryStatsResponse } from "~/client/api-client/types.gen";
import {
@@ -84,14 +91,6 @@ export const RepositoryInfoTabContent = ({ repository, initialStats }: Props) =>
const unlockRepo = useMutation({
...unlockRepositoryMutation(),
- onSuccess: () => {
- toast.success("Repository unlocked successfully");
- },
- onError: (error) => {
- toast.error("Failed to unlock repository", {
- description: parseError(error)?.message,
- });
- },
});
const handleConfirmDelete = () => {
@@ -117,53 +116,64 @@ export const RepositoryInfoTabContent = ({ repository, initialStats }: Props) =>
{repository.provisioningId && }
-
-
navigate({ to: `/repositories/${repository.shortId}/edit` })}
- >
-
- Edit
-
-
cancelDoctor.mutate({ path: { shortId: repository.shortId } })}
- >
-
- Cancel doctor
-
-
startDoctor.mutate({ path: { shortId: repository.shortId } })}
- disabled={startDoctor.isPending}
- >
-
- Run doctor
-
-
unlockRepo.mutate({ path: { shortId: repository.shortId } })}
- loading={unlockRepo.isPending}
- >
-
- Unlock
-
-
setShowDeleteConfirm(true)}
- disabled={deleteRepo.isPending}
- >
-
- Delete
-
+
+ {isDoctorRunning ? (
+
cancelDoctor.mutate({ path: { shortId: repository.shortId } })}
+ >
+
+ Cancel doctor
+
+ ) : (
+
startDoctor.mutate({ path: { shortId: repository.shortId } })}
+ disabled={startDoctor.isPending}
+ >
+
+ Run doctor
+
+ )}
+
+
+
+ Actions
+
+
+
+
+ navigate({ to: `/repositories/${repository.shortId}/edit` })}>
+
+ Edit
+
+
+ toast.promise(unlockRepo.mutateAsync({ path: { shortId: repository.shortId } }), {
+ loading: "Unlocking repo",
+ success: "Repository unlocked successfully",
+ error: (e) => parseError(e)?.message || "Failed to unlock repository",
+ })
+ }
+ disabled={unlockRepo.isPending}
+ >
+
+ Unlock
+
+
+ setShowDeleteConfirm(true)}
+ disabled={deleteRepo.isPending}
+ >
+
+ Delete
+
+
+
diff --git a/app/client/modules/volumes/tabs/info.tsx b/app/client/modules/volumes/tabs/info.tsx
index e9656d75..3b48d123 100644
--- a/app/client/modules/volumes/tabs/info.tsx
+++ b/app/client/modules/volumes/tabs/info.tsx
@@ -1,7 +1,7 @@
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { toast } from "sonner";
-import { Pencil, Plug, Trash2, Unplug } from "lucide-react";
+import { ChevronDown, Pencil, Plug, Trash2, Unplug } from "lucide-react";
import { CreateVolumeForm } from "~/client/modules/volumes/components/create-volume-form";
import {
AlertDialog,
@@ -15,6 +15,13 @@ import {
} from "~/client/components/ui/alert-dialog";
import { Button } from "~/client/components/ui/button";
import { Card } from "~/client/components/ui/card";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "~/client/components/ui/dropdown-menu";
import type { StatFs, Volume } from "~/client/lib/types";
import { HealthchecksCard } from "../components/healthchecks-card";
import { StorageChart } from "../components/storage-chart";
@@ -37,26 +44,10 @@ 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({
@@ -89,23 +80,21 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
- Volume Configuration
+
Volume Configuration
{volume.provisioningId && }
-
-
navigate({ to: `/volumes/${volume.shortId}/edit` })}
- >
-
- Edit
-
+
{volume.status !== "mounted" ? (
mountVol.mutate({ path: { shortId: volume.shortId } })}
+ onClick={() =>
+ toast.promise(mountVol.mutateAsync({ path: { shortId: volume.shortId } }), {
+ loading: "Mounting volume...",
+ success: "Volume mounted successfully",
+ error: (error) => parseError(error)?.message || "Failed to mount volume",
+ })
+ }
loading={mountVol.isPending}
>
@@ -115,22 +104,42 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
unmountVol.mutate({ path: { shortId: volume.shortId } })}
+ onClick={() =>
+ toast.promise(unmountVol.mutateAsync({ path: { shortId: volume.shortId } }), {
+ loading: "Unmounting volume...",
+ success: "Volume unmounted successfully",
+ error: (error) => parseError(error)?.message || "Failed to unmount volume",
+ })
+ }
loading={unmountVol.isPending}
>
Unmount
)}
- setShowDeleteConfirm(true)}
- disabled={deleteVol.isPending}
- >
-
- Delete
-
+
+
+
+ Actions
+
+
+
+
+ navigate({ to: `/volumes/${volume.shortId}/edit` })}>
+
+ Edit
+
+
+ setShowDeleteConfirm(true)}
+ disabled={deleteVol.isPending}
+ >
+
+ Delete
+
+
+