refactor: status dot one component

This commit is contained in:
Nicolas Meienberger 2025-11-22 14:50:36 +01:00
parent c9e6bd04c9
commit 6d65cd9700
7 changed files with 86 additions and 118 deletions

View file

@ -1,30 +1,42 @@
import type { VolumeStatus } from "~/client/lib/types";
import { cn } from "~/client/lib/utils"; import { cn } from "~/client/lib/utils";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
export const StatusDot = ({ status }: { status: VolumeStatus }) => { type StatusVariant = "success" | "neutral" | "error" | "warning" | "info";
interface StatusDotProps {
variant: StatusVariant;
label: string;
animated?: boolean;
}
export const StatusDot = ({ variant, label, animated }: StatusDotProps) => {
const statusMapping = { const statusMapping = {
mounted: { success: {
color: "bg-green-500", color: "bg-green-500",
colorLight: "bg-emerald-400", colorLight: "bg-emerald-400",
animated: true, animated: animated ?? true,
}, },
unmounted: { neutral: {
color: "bg-gray-500", color: "bg-gray-500",
colorLight: "bg-gray-400", colorLight: "bg-gray-400",
animated: false, animated: animated ?? false,
}, },
error: { error: {
color: "bg-red-500", color: "bg-red-500",
colorLight: "bg-amber-700", colorLight: "bg-red-400",
animated: true, animated: animated ?? true,
}, },
unknown: { warning: {
color: "bg-yellow-500", color: "bg-yellow-500",
colorLight: "bg-yellow-400", colorLight: "bg-yellow-400",
animated: true, animated: animated ?? true,
}, },
}[status]; info: {
color: "bg-blue-500",
colorLight: "bg-blue-400",
animated: animated ?? true,
},
}[variant];
return ( return (
<Tooltip> <Tooltip>
@ -42,7 +54,7 @@ export const StatusDot = ({ status }: { status: VolumeStatus }) => {
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p className="capitalize">{status}</p> <p>{label}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
); );

View file

@ -1,7 +1,4 @@
import { cn } from "~/client/lib/utils"; import { StatusDot } from "~/client/components/status-dot";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
type BackupStatus = "active" | "paused" | "error" | "in_progress";
export const BackupStatusDot = ({ export const BackupStatusDot = ({
enabled, enabled,
@ -12,60 +9,22 @@ export const BackupStatusDot = ({
hasError?: boolean; hasError?: boolean;
isInProgress?: boolean; isInProgress?: boolean;
}) => { }) => {
let status: BackupStatus = "paused"; let variant: "success" | "neutral" | "error" | "info";
let label: string;
if (isInProgress) { if (isInProgress) {
status = "in_progress"; variant = "info";
label = "Backup in progress";
} else if (hasError) { } else if (hasError) {
status = "error"; variant = "error";
label = "Error";
} else if (enabled) { } else if (enabled) {
status = "active"; variant = "success";
label = "Active";
} else {
variant = "neutral";
label = "Paused";
} }
const statusMapping = { return <StatusDot variant={variant} label={label} />;
active: {
color: "bg-green-500",
colorLight: "bg-emerald-400",
animated: true,
label: "Active",
},
paused: {
color: "bg-gray-500",
colorLight: "bg-gray-400",
animated: false,
label: "Paused",
},
error: {
color: "bg-red-500",
colorLight: "bg-red-400",
animated: true,
label: "Error",
},
in_progress: {
color: "bg-blue-500",
colorLight: "bg-blue-400",
animated: true,
label: "Backup in progress",
},
}[status];
return (
<Tooltip>
<TooltipTrigger>
<span className="relative flex size-3 mx-auto">
{statusMapping.animated && (
<span
className={cn(
"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75",
`${statusMapping.colorLight}`,
)}
/>
)}
<span className={cn("relative inline-flex size-3 rounded-full", `${statusMapping.color}`)} />
</span>
</TooltipTrigger>
<TooltipContent>
<p>{statusMapping.label}</p>
</TooltipContent>
</Tooltip>
);
}; };

View file

@ -31,9 +31,9 @@ export default function CreateNotification() {
const createNotification = useMutation({ const createNotification = useMutation({
...createNotificationDestinationMutation(), ...createNotificationDestinationMutation(),
onSuccess: (data) => { onSuccess: () => {
toast.success("Notification destination created successfully"); toast.success("Notification destination created successfully");
navigate(`/notifications/${data.id}`); navigate(`/notifications`);
}, },
}); });

View file

@ -56,7 +56,6 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
const navigate = useNavigate(); const navigate = useNavigate();
const formId = useId(); const formId = useId();
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const { data } = useQuery({ const { data } = useQuery({
...getNotificationDestinationOptions({ path: { id: String(loaderData.id) } }), ...getNotificationDestinationOptions({ path: { id: String(loaderData.id) } }),
@ -82,7 +81,6 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
...updateNotificationDestinationMutation(), ...updateNotificationDestinationMutation(),
onSuccess: () => { onSuccess: () => {
toast.success("Notification destination updated successfully"); toast.success("Notification destination updated successfully");
setIsEditing(false);
}, },
onError: (error) => { onError: (error) => {
toast.error("Failed to update notification destination", { toast.error("Failed to update notification destination", {
@ -146,11 +144,6 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
<TestTube2 className="h-4 w-4 mr-2" /> <TestTube2 className="h-4 w-4 mr-2" />
Test Test
</Button> </Button>
{!isEditing && (
<Button onClick={() => setIsEditing(true)} variant="outline">
Edit
</Button>
)}
<Button <Button
onClick={() => setShowDeleteConfirm(true)} onClick={() => setShowDeleteConfirm(true)}
variant="destructive" variant="destructive"
@ -180,46 +173,20 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
<>
{isEditing ? ( <CreateNotificationForm
<> mode="update"
<CreateNotificationForm formId={formId}
mode="update" onSubmit={handleSubmit}
formId={formId} initialValues={data.config}
onSubmit={handleSubmit} loading={updateDestination.isPending}
initialValues={data.config} />
loading={updateDestination.isPending} <div className="flex justify-end gap-2 pt-4 border-t">
/> <Button type="submit" form={formId} loading={updateDestination.isPending}>
<div className="flex justify-end gap-2 pt-4 border-t"> Save Changes
<Button </Button>
type="button"
variant="secondary"
onClick={() => setIsEditing(false)}
disabled={updateDestination.isPending}
>
Cancel
</Button>
<Button type="submit" form={formId} loading={updateDestination.isPending}>
Save Changes
</Button>
</div>
</>
) : (
<div className="space-y-4">
<div>
<div className="text-sm font-medium text-muted-foreground">Type</div>
<div className="mt-1 capitalize">{data.type}</div>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground">Created</div>
<div className="mt-1">{new Date(data.createdAt * 1000).toLocaleString()}</div>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground">Last Updated</div>
<div className="mt-1">{new Date(data.updatedAt * 1000).toLocaleString()}</div>
</div>
</div> </div>
)} </>
</CardContent> </CardContent>
</Card> </Card>

View file

@ -157,7 +157,7 @@ export default function Notifications({ loaderData }: Route.ComponentProps) {
<TableCell className="font-medium text-strong-accent">{notification.name}</TableCell> <TableCell className="font-medium text-strong-accent">{notification.name}</TableCell>
<TableCell className="capitalize">{notification.type}</TableCell> <TableCell className="capitalize">{notification.type}</TableCell>
<TableCell className="text-center"> <TableCell className="text-center">
<StatusDot status={notification.enabled ? "mounted" : "unmounted"} /> <StatusDot variant={notification.enabled ? "success" : "neutral"} label={notification.enabled ? "Enabled" : "Disabled"} />
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))

View file

@ -24,6 +24,7 @@ import { DockerTabContent } from "../tabs/docker";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
import { useSystemInfo } from "~/client/hooks/use-system-info"; import { useSystemInfo } from "~/client/hooks/use-system-info";
import { getVolume } from "~/client/api-client"; import { getVolume } from "~/client/api-client";
import type { VolumeStatus } from "~/client/lib/types";
import { import {
deleteVolumeMutation, deleteVolumeMutation,
getVolumeOptions, getVolumeOptions,
@ -31,6 +32,16 @@ import {
unmountVolumeMutation, unmountVolumeMutation,
} from "~/client/api-client/@tanstack/react-query.gen"; } from "~/client/api-client/@tanstack/react-query.gen";
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 const handle = { export const handle = {
breadcrumb: (match: Route.MetaArgs) => [{ label: "Volumes", href: "/volumes" }, { label: match.params.name }], breadcrumb: (match: Route.MetaArgs) => [{ label: "Volumes", href: "/volumes" }, { label: match.params.name }],
}; };
@ -124,7 +135,12 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
<div className="flex flex-col items-start xs:items-center xs:flex-row xs:justify-between"> <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"> <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"> <span className="flex items-center gap-2">
<StatusDot status={volume.status} /> {volume.status[0].toUpperCase() + volume.status.slice(1)} <StatusDot
variant={getVolumeStatusVariant(volume.status)}
label={volume.status[0].toUpperCase() + volume.status.slice(1)}
/>
&nbsp;
{volume.status[0].toUpperCase() + volume.status.slice(1)}
</span> </span>
<VolumeIcon size={14} backend={volume?.config.backend} /> <VolumeIcon size={14} backend={volume?.config.backend} />
</div> </div>

View file

@ -13,6 +13,17 @@ import { VolumeIcon } from "~/client/components/volume-icon";
import type { Route } from "./+types/volumes"; import type { Route } from "./+types/volumes";
import { listVolumes } from "~/client/api-client"; import { listVolumes } from "~/client/api-client";
import { listVolumesOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { listVolumesOptions } from "~/client/api-client/@tanstack/react-query.gen";
import type { VolumeStatus } from "~/client/lib/types";
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 const handle = { export const handle = {
breadcrumb: () => [{ label: "Volumes" }], breadcrumb: () => [{ label: "Volumes" }],
@ -157,7 +168,10 @@ export default function Volumes({ loaderData }: Route.ComponentProps) {
<VolumeIcon backend={volume.type} /> <VolumeIcon backend={volume.type} />
</TableCell> </TableCell>
<TableCell className="text-center"> <TableCell className="text-center">
<StatusDot status={volume.status} /> <StatusDot
variant={getVolumeStatusVariant(volume.status)}
label={volume.status[0].toUpperCase() + volume.status.slice(1)}
/>
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))