feat(dev): confirm gate before navigating to the dashboard in dev
- 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
This commit is contained in:
parent
4dde790aa8
commit
5883990577
3 changed files with 110 additions and 1 deletions
|
|
@ -11,7 +11,7 @@ import { Metadata } from "next";
|
|||
import { type ResolvedLevel, resolveLevel } from "@/utils/xp/resolveLevel";
|
||||
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
|
||||
import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification";
|
||||
import { canAccessAdminPanel } from "@/utils/access-control/hasPermission";
|
||||
import { canAccessAdminPanel } from "@/utils/access-control/divisionAccess";
|
||||
import { ShipmentToasts } from "@/components/frontend/logistics/ShipmentToasts";
|
||||
import { MissionReminderToasts } from "@/components/frontend/intelligence/MissionReminderToasts";
|
||||
import { GameTickRealtime } from "@/components/frontend/realtime/GameTickRealtime";
|
||||
|
|
@ -29,6 +29,7 @@ import { cookies } from "next/headers";
|
|||
import { IMPERSONATION_ACTIVE_COOKIE } from "@/lib/impersonation";
|
||||
import { ImpersonationBanner } from "@/components/frontend/impersonation/ImpersonationBanner";
|
||||
import { ShimLoader } from "@/components/frontend/shims/ShimLoader";
|
||||
import { DevDashboardGate } from "@/components/frontend/dev/DevDashboardGate";
|
||||
|
||||
// Skip static prerendering of this layout (and all pages under (frontend)/).
|
||||
// The layout calls `getPayload()` at render time to resolve the current user,
|
||||
|
|
@ -184,6 +185,7 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
|
|||
)}
|
||||
{user && <GameTickRealtime />}
|
||||
{user && <SessionWatchdog sessionExp={sessionExp} />}
|
||||
{process.env.NODE_ENV === "development" && user && <DevDashboardGate />}
|
||||
{user && <MissionReminderToasts />}
|
||||
{isLogistics && <ShipmentToasts />}
|
||||
<Toaster />
|
||||
|
|
|
|||
|
|
@ -3576,3 +3576,4 @@
|
|||
[data-radix-popper-content-wrapper] {
|
||||
z-index: 1300 !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
106
src/components/frontend/dev/DevDashboardGate.tsx
Normal file
106
src/components/frontend/dev/DevDashboardGate.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
"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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue