- DevDashboardGate intercepts left-clicks on links resolving to / and asks before navigating; the dev-mode dashboard render takes ~100s while the browser aborts at ~60s, so links otherwise look like a hang - mounted only when NODE_ENV is development and a user is logged in; modifier-clicks, new-tab and download anchors bypass the gate - layout now imports canAccessAdminPanel from divisionAccess
106 lines
3.7 KiB
TypeScript
106 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
|
|
/**
|
|
* Dev-only guard for routes whose server render is pathologically slow under
|
|
* the Next.js 16 dev runtime (see payloadcms/payload#14487): the dashboard
|
|
* takes roughly 100s per render in dev while the browser gives up around 60s,
|
|
* so clicking any link to it looks like a hang. This component intercepts
|
|
* plain left-clicks on anchors pointing at those routes, shows a confirm
|
|
* dialog, and only navigates when the user insists.
|
|
*
|
|
* Gaps by design: direct URL entry, page refresh, modifier-clicks (Ctrl/Cmd +
|
|
* click), and programmatic router.push calls (e.g. the command palette) are
|
|
* not gated. Anchors inside a [data-dev-gate-bypass] ancestor are also
|
|
* exempt, so a specific link can opt out.
|
|
*/
|
|
|
|
const GATED_PATHS = new Set<string>(["/"]);
|
|
|
|
function resolveTarget(href: string): string | null {
|
|
try {
|
|
const url = new URL(href, window.location.origin);
|
|
if (url.origin !== window.location.origin) return null;
|
|
if (!GATED_PATHS.has(url.pathname)) return null;
|
|
return url.pathname + url.search + url.hash;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function DevDashboardGate() {
|
|
const router = useRouter();
|
|
const [open, setOpen] = useState(false);
|
|
const [target, setTarget] = useState("/");
|
|
const proceedRef = useRef<() => void>(() => undefined);
|
|
|
|
useEffect(() => {
|
|
if (process.env.NODE_ENV !== "development") return undefined;
|
|
|
|
function onClick(event: MouseEvent) {
|
|
if (event.defaultPrevented) return;
|
|
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
return;
|
|
}
|
|
const anchor = (event.target as Element | null)?.closest?.("a");
|
|
if (!anchor) return;
|
|
if (anchor.hasAttribute("download")) return;
|
|
const targetAttr = anchor.getAttribute("target");
|
|
if (targetAttr && targetAttr !== "_self") return;
|
|
if (anchor.closest("[data-dev-gate-bypass]")) return;
|
|
const next = resolveTarget(anchor.getAttribute("href") ?? "");
|
|
if (!next) return;
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
proceedRef.current = () => {
|
|
setOpen(false);
|
|
router.push(next);
|
|
};
|
|
setTarget(next);
|
|
setOpen(true);
|
|
}
|
|
|
|
document.addEventListener("click", onClick, { capture: true });
|
|
return () => document.removeEventListener("click", onClick, { capture: true });
|
|
}, [router]);
|
|
|
|
if (process.env.NODE_ENV !== "development") return null;
|
|
|
|
return (
|
|
<AlertDialog
|
|
open={open}
|
|
onOpenChange={(next) => {
|
|
if (!next) setOpen(false);
|
|
}}
|
|
>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Open the dashboard in dev mode?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
In dev, <span className="font-mono text-xs">{target}</span> renders the dashboard
|
|
server-side in roughly 100 seconds (a known Next.js 16 dev-runtime regression) and the
|
|
browser usually gives up around 60s with an empty response. Production serves the same
|
|
page in a few seconds. Stay here, or open anyway and wait.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Stay here</AlertDialogCancel>
|
|
<AlertDialogAction onClick={() => proceedRef.current()}>Open anyway</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
);
|
|
}
|