mirror of
https://github.com/tale/headplane.git
synced 2026-08-11 06:16:52 +00:00
feat: correctly use error boundaries per-page
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { RefreshCw, ServerOff } from "lucide-react";
|
||||
import { isRouteErrorResponse, useRevalidator } from "react-router";
|
||||
|
||||
import { isConnectionError } from "~/server/headscale/api/error-client";
|
||||
import cn from "~/utils/cn";
|
||||
|
||||
import Button from "./Button";
|
||||
import { ErrorBanner } from "./error-banner";
|
||||
|
||||
interface PageErrorProps {
|
||||
error: unknown;
|
||||
page: string;
|
||||
}
|
||||
|
||||
export default function PageError({ error, page }: PageErrorProps) {
|
||||
const { revalidate, state } = useRevalidator();
|
||||
|
||||
if (isRouteErrorResponse(error) && isConnectionError(error.data)) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-center">
|
||||
<ServerOff className={cn("h-12 w-12", "text-mist-400 dark:text-mist-500")} />
|
||||
<h2 className="mt-4 text-lg font-semibold">{page} Unavailable</h2>
|
||||
<p className="mt-1 max-w-sm text-sm text-mist-500 dark:text-mist-400">
|
||||
This page could not be loaded because the Headscale server is unreachable. It will be
|
||||
available once the connection is restored.
|
||||
</p>
|
||||
<Button
|
||||
className="mt-6"
|
||||
variant="light"
|
||||
onPress={() => revalidate()}
|
||||
isDisabled={state === "loading"}
|
||||
>
|
||||
<RefreshCw
|
||||
className={cn("mr-2 inline-block h-4 w-4", state === "loading" && "animate-spin")}
|
||||
/>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <ErrorBanner className="max-w-2xl" error={error} />;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { AlertCircle, CloudOff, Info, TriangleAlert, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import cn from "~/utils/cn";
|
||||
|
||||
type Variant = "info" | "warning" | "error" | "critical";
|
||||
|
||||
interface StatusBannerProps {
|
||||
variant: Variant;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
dismissable?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const icons: Record<Variant, React.ReactNode> = {
|
||||
info: <Info className="h-5 w-5 shrink-0" />,
|
||||
warning: <TriangleAlert className="h-5 w-5 shrink-0" />,
|
||||
error: <AlertCircle className="h-5 w-5 shrink-0" />,
|
||||
critical: <CloudOff className="h-5 w-5 shrink-0" />,
|
||||
};
|
||||
|
||||
export default function StatusBanner({
|
||||
variant,
|
||||
title,
|
||||
children,
|
||||
dismissable = true,
|
||||
className,
|
||||
}: StatusBannerProps) {
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
if (dismissed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className={cn(
|
||||
"flex items-start gap-3 rounded-lg border px-4 py-3 text-sm",
|
||||
variant === "info" && [
|
||||
"border-indigo-200 bg-indigo-50 text-indigo-900",
|
||||
"dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-200",
|
||||
],
|
||||
variant === "warning" && [
|
||||
"border-yellow-300 bg-yellow-50 text-yellow-900",
|
||||
"dark:border-yellow-500/30 dark:bg-yellow-500/10 dark:text-yellow-200",
|
||||
],
|
||||
variant === "error" && [
|
||||
"border-red-300 bg-red-50 text-red-900",
|
||||
"dark:border-red-500/30 dark:bg-red-500/10 dark:text-red-200",
|
||||
],
|
||||
variant === "critical" && [
|
||||
"border-red-400 bg-red-100 text-red-900",
|
||||
"dark:border-red-500/40 dark:bg-red-500/20 dark:text-red-100",
|
||||
],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{icons[variant]}
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">{title}</p>
|
||||
<div className="mt-0.5 text-current/80">{children}</div>
|
||||
</div>
|
||||
{dismissable && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Dismiss"
|
||||
onClick={() => setDismissed(true)}
|
||||
className={cn(
|
||||
"shrink-0 rounded p-0.5 transition-colors",
|
||||
"hover:bg-black/10 dark:hover:bg-white/10",
|
||||
)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user