Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

134 linesCodeBlameRaw
1import { useEffect, useMemo, useState } from "react";
2import { Link, isRouteErrorResponse, useLocation, useMatches, useRevalidator, useRouteLoaderData } from "react-router";
3
4import { DebugDetails } from "./debug-details";
5import { PageMain } from "./landmark";
6import { NotFound } from "./not-found";
7import { Button } from "./ui/button";
8import { ButtonLink } from "./ui";
9import { useDeveloperMode } from "../lib/developer-mode";
10import { useReconnect } from "../lib/reconnect";
11import { isNetworkFailure } from "../lib/resilient-fetch";
12import { RELOADED_KEY, RELOAD_GIVE_UP_MS, clientNavigated, reloadFixes, reloadedBefore } from "../lib/stale-build";
13import { isNeedsSignIn } from "../lib/website-token";
14import { usePageNavigation } from "../lib/use-page-navigation";
15
16/**
17 * What a page shows in place of itself when it failed: the root's error
18 * page, and, `contained`, the one a repository's, workspace's or settings'
19 * page shows inside its frame (routes/page-boundary.tsx), so its header,
20 * tabs and sidebar stay and only the page itself is replaced.
21 *
22 * A tab left open across a deploy loads the address again, once; no answer
23 * from g1t at all keeps trying and comes back by itself (lib/reconnect.ts);
24 * a 404 is the not-found page; anything else says what went wrong, with
25 * a way to try again, and in developer mode the error in full.
26 */
27export function RouteError({ error, contained = false }: { error: unknown; contained?: boolean }) {
28 const location = useLocation();
29 const { owns } = usePageNavigation();
30 const matches = useMatches();
31 const revalidator = useRevalidator();
32 const href = location.pathname + location.search + location.hash;
33 // A tab left open across a deploy: load the address again as a whole
34 // page, once, rather than show an error the reload fixes. A document load
35 // never reloads, so the server and the hydrating client render the same.
36 const reloadable = reloadFixes({
37 error,
38 status: isRouteErrorResponse(error) ? error.status : undefined,
39 clientNavigation: clientNavigated(location.key),
40 caughtByCatchAll: matches.at(-1)?.id === "routes/not-found",
41 });
42 // Already loaded again once, or the reload never started: show the page.
43 const tried = useMemo(() => reloadable && reloadedBefore(href), [reloadable, href]);
44 // The error in full, for reporting it (lib/developer-mode.ts).
45 const root = useRouteLoaderData("root") as { developer?: boolean } | undefined;
46 const developer = useDeveloperMode(root?.developer) || import.meta.env.DEV;
47 const [gaveUp, setGaveUp] = useState<string | null>(null);
48 const reload = reloadable && !tried && gaveUp !== href;
49 // No answer from g1t at all: keep trying, and the page comes back by itself.
50 const offline = !reload && isNetworkFailure(error);
51 useReconnect(offline);
52 useEffect(() => {
53 if (!reload || !owns()) return;
54 try {
55 sessionStorage.setItem(RELOADED_KEY, href);
56 } catch {
57 // No session storage: reload anyway; a document load never asks again.
58 }
59 window.location.assign(href);
60 const timer = window.setTimeout(() => setGaveUp(href), RELOAD_GIVE_UP_MS);
61 return () => window.clearTimeout(timer);
62 }, [reload, href, owns]);
63
64 const space = contained ? "py-16" : "py-32";
65 if (reload) {
66 return (
67 <PageMain className={`mx-auto max-w-xl px-4 ${space} text-center text-sm text-muted`} aria-busy="true">
68 <title>Loading · g1t</title>
69 Loading the latest version of g1t…
70 </PageMain>
71 );
72 }
73
74 if (offline) {
75 return (
76 <PageMain className={`mx-auto max-w-xl px-4 ${space} text-center`} aria-busy="true">
77 <title>Reconnecting · g1t</title>
78 <h1 className="text-2xl font-semibold tracking-tight">Can't reach g1t</h1>
79 <p className="mt-3 text-muted">Check your connection. This page loads by itself once g1t answers.</p>
80 </PageMain>
81 );
82 }
83
84 let title = "Something went wrong";
85 let details = contained ? "This page didn't load. The rest of g1t is fine; try again in a moment." : "An unexpected error occurred. Try again in a moment.";
86
87 if (isRouteErrorResponse(error)) {
88 // The same page for something private and something missing.
89 if (error.status === 404) {
90 return (
91 <>
92 <title>Not found · g1t</title>
93 <NotFound data={error.data} />
94 </>
95 );
96 }
97 if (typeof error.data === "string" && error.data) {
98 details = error.data;
99 }
100 // A token asked for what needs a real sign-in (lib/website-token.ts).
101 if (isNeedsSignIn(error.data)) {
102 title = "This needs you to sign in";
103 details = error.data.message;
104 }
105 }
106
107 return (
108 <PageMain className={`mx-auto px-4 text-center ${developer ? `max-w-3xl ${contained ? "py-10" : "py-16"}` : `max-w-xl ${space}`}`}>
109 <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
110 <p className="mt-3 text-muted">{details}</p>
111 <div className="mt-8 flex justify-center gap-2">
112 <Button variant="outline" disabled={revalidator.state !== "idle"} onClick={() => void revalidator.revalidate()}>
113 {revalidator.state !== "idle" ? "Trying again…" : "Try again"}
114 </Button>
115 {!contained && (
116 <ButtonLink to="/" variant="ghost">
117 Back to home
118 </ButtonLink>
119 )}
120 </div>
121 {developer ? (
122 <DebugDetails error={error} />
123 ) : (
124 <p className="mt-6 text-xs text-faint">
125 Turn on developer mode in{" "}
126 <Link to="/settings/account#developer" className="underline underline-offset-4 hover:text-fg">
127 Settings
128 </Link>{" "}
129 to see what went wrong.
130 </p>
131 )}
132 </PageMain>
133 );
134}