refactor: improve buttons and space with dropdowns (#738)

This commit is contained in:
Nico 2026-04-02 21:44:32 +02:00 committed by GitHub
parent 4bf1463406
commit e77723164b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 190 additions and 158 deletions

View file

@ -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) => {
/>
</div>
</div>
<div className="flex flex-col @wide:flex-row gap-2">
<div className="flex items-center gap-2">
{schedule.lastBackupStatus === "in_progress" ? (
<Button
variant="destructive"
size="sm"
onClick={() => setShowStopConfirm(true)}
className="w-full @medium:w-auto"
>
<Button variant="destructive" size="sm" onClick={() => setShowStopConfirm(true)}>
<Square className="h-4 w-4 mr-2" />
<span>Stop backup</span>
</Button>
) : (
<Button variant="default" size="sm" onClick={handleRunBackupNow} className="w-full @medium:w-auto">
<Button variant="default" size="sm" onClick={handleRunBackupNow}>
<Play className="h-4 w-4 mr-2" />
<span>Backup now</span>
</Button>
)}
{schedule.retentionPolicy && (
<Button
variant="outline"
size="sm"
loading={runForget.isPending}
onClick={() => setShowForgetConfirm(true)}
className="w-full @medium:w-auto"
>
<Eraser className="h-4 w-4 mr-2" />
<span>Run cleanup</span>
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={() => navigate({ to: "/backups/$backupId/edit", params: { backupId: schedule.shortId } })}
className="w-full @medium:w-auto"
>
<Pencil className="h-4 w-4 mr-2" />
<span>Edit schedule</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setShowDeleteConfirm(true)}
className="text-destructive hover:text-destructive w-full @medium:w-auto"
>
<Trash2 className="h-4 w-4 mr-2" />
<span>Delete</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
Actions
<ChevronDown className="h-4 w-4 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{schedule.retentionPolicy && (
<DropdownMenuItem onClick={() => setShowForgetConfirm(true)} disabled={runForget.isPending}>
<Eraser />
Run cleanup
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => navigate({ to: "/backups/$backupId/edit", params: { backupId: schedule.shortId } })}
>
<Pencil />
Edit schedule
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onClick={() => setShowDeleteConfirm(true)}>
<Trash2 />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent className="grid gap-4 grid-cols-1 @medium:grid-cols-2 @wide:grid-cols-4">

View file

@ -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
</span>
<span className="text-xs bg-primary/10 rounded-md px-2 py-1 capitalize">{data.type}</span>
</div>
<div className="flex gap-2">
<div className="flex items-center gap-2">
<Button
onClick={handleTest}
disabled={testDestination.isPending || !data.enabled}
@ -115,14 +121,24 @@ export function NotificationDetailsPage({ notificationId }: { notificationId: st
<TestTube2 className="h-4 w-4 mr-2" />
Test
</Button>
<Button
onClick={() => setShowDeleteConfirm(true)}
variant="destructive"
loading={deleteDestination.isPending}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
Actions
<ChevronDown className="h-4 w-4 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
variant="destructive"
onClick={() => setShowDeleteConfirm(true)}
disabled={deleteDestination.isPending}
>
<Trash2 />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>

View file

@ -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() {
</span>
</TableCell>
<TableCell className="text-center">
<span
className={cn(
"inline-flex items-center gap-2 px-2 py-1 rounded-md text-xs bg-gray-500/10 text-gray-500",
{
"bg-success/10 text-success": repository.status === "healthy",
"bg-red-500/10 text-red-500": repository.status === "error",
},
)}
>
{repository.status || "unknown"}
</span>
<StatusDot
variant={
repository.status === "healthy" ? "success" : repository.status === "error" ? "error" : "warning"
}
label={
repository.status ? repository.status[0].toUpperCase() + repository.status.slice(1) : "Unknown"
}
/>
</TableCell>
</TableRow>
))}

View file

@ -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 && <ManagedBadge />}
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button
type="button"
variant="outline"
onClick={() => navigate({ to: `/repositories/${repository.shortId}/edit` })}
>
<Pencil className="h-4 w-4 mr-2" />
Edit
</Button>
<Button
type="button"
variant="destructive"
className={cn({ hidden: !isDoctorRunning })}
loading={cancelDoctor.isPending}
onClick={() => cancelDoctor.mutate({ path: { shortId: repository.shortId } })}
>
<Square className="h-4 w-4 mr-2" />
<span>Cancel doctor</span>
</Button>
<Button
type="button"
variant="outline"
className={cn({ hidden: isDoctorRunning })}
onClick={() => startDoctor.mutate({ path: { shortId: repository.shortId } })}
disabled={startDoctor.isPending}
>
<Stethoscope className="h-4 w-4 mr-2" />
Run doctor
</Button>
<Button
type="button"
variant="outline"
onClick={() => unlockRepo.mutate({ path: { shortId: repository.shortId } })}
loading={unlockRepo.isPending}
>
<Unlock className="h-4 w-4 mr-2" />
Unlock
</Button>
<Button
type="button"
variant="destructive"
onClick={() => setShowDeleteConfirm(true)}
disabled={deleteRepo.isPending}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
<div className="flex items-center gap-2">
{isDoctorRunning ? (
<Button
type="button"
variant="destructive"
loading={cancelDoctor.isPending}
onClick={() => cancelDoctor.mutate({ path: { shortId: repository.shortId } })}
>
<Square className="h-4 w-4 mr-2" />
Cancel doctor
</Button>
) : (
<Button
type="button"
variant="outline"
onClick={() => startDoctor.mutate({ path: { shortId: repository.shortId } })}
disabled={startDoctor.isPending}
>
<Stethoscope className="h-4 w-4 mr-2" />
Run doctor
</Button>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
Actions
<ChevronDown className="h-4 w-4 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => navigate({ to: `/repositories/${repository.shortId}/edit` })}>
<Pencil />
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
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 />
Unlock
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={() => setShowDeleteConfirm(true)}
disabled={deleteRepo.isPending}
>
<Trash2 />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>

View file

@ -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) => {
<div className="flex flex-col @xl:flex-row items-start @xl:items-center justify-between gap-4 mb-6">
<div>
<div className="flex items-center gap-2">
<span className="text-lg font-semibold">Volume Configuration</span>
<h2 className="text-lg font-semibold tracking-tight">Volume Configuration</h2>
{volume.provisioningId && <ManagedBadge />}
</div>
</div>
<div className="flex flex-col @xl:flex-row w-full @xl:w-auto gap-2">
<Button
type="button"
variant="outline"
onClick={() => navigate({ to: `/volumes/${volume.shortId}/edit` })}
>
<Pencil className="h-4 w-4 mr-2" />
Edit
</Button>
<div className="flex items-center gap-2">
{volume.status !== "mounted" ? (
<Button
type="button"
onClick={() => 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}
>
<Plug className="h-4 w-4 mr-2" />
@ -115,22 +104,42 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
<Button
type="button"
variant="secondary"
onClick={() => 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}
>
<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>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
Actions
<ChevronDown className="h-4 w-4 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => navigate({ to: `/volumes/${volume.shortId}/edit` })}>
<Pencil />
Edit
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={() => setShowDeleteConfirm(true)}
disabled={deleteVol.isPending}
>
<Trash2 />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<CreateVolumeForm