refactor: improve buttons and space with dropdowns (#738)
This commit is contained in:
parent
4bf1463406
commit
e77723164b
5 changed files with 190 additions and 158 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue