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.

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