mirror of https://github.com/coder/coder.git
213 lines
5.8 KiB
TypeScript
213 lines
5.8 KiB
TypeScript
import { css, type Interpolation, type Theme } from "@emotion/react";
|
|
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
|
import Button from "@mui/material/Button";
|
|
import Link from "@mui/material/Link";
|
|
import { type FC, useEffect, useState } from "react";
|
|
import { Helmet } from "react-helmet-async";
|
|
import type { BuildInfoResponse } from "api/typesGenerated";
|
|
import { CopyButton } from "components/CopyButton/CopyButton";
|
|
import { CoderIcon } from "components/Icons/CoderIcon";
|
|
import { FullScreenLoader } from "components/Loader/FullScreenLoader";
|
|
import { Margins } from "components/Margins/Margins";
|
|
import { Stack } from "components/Stack/Stack";
|
|
|
|
const fetchDynamicallyImportedModuleError =
|
|
"Failed to fetch dynamically imported module";
|
|
|
|
export type RuntimeErrorStateProps = { error: Error };
|
|
|
|
export const RuntimeErrorState: FC<RuntimeErrorStateProps> = ({ error }) => {
|
|
const [checkingError, setCheckingError] = useState(true);
|
|
const [staticBuildInfo, setStaticBuildInfo] = useState<BuildInfoResponse>();
|
|
const coderVersion = staticBuildInfo?.version;
|
|
|
|
// We use an effect to show a loading state if the page is trying to reload
|
|
useEffect(() => {
|
|
const isImportError = error.message.includes(
|
|
fetchDynamicallyImportedModuleError,
|
|
);
|
|
const isRetried = window.location.search.includes("retries=1");
|
|
|
|
if (isImportError && !isRetried) {
|
|
const url = new URL(location.href);
|
|
// Add a retry to avoid loops
|
|
url.searchParams.set("retries", "1");
|
|
location.assign(url.search);
|
|
return;
|
|
}
|
|
|
|
setCheckingError(false);
|
|
}, [error.message]);
|
|
|
|
useEffect(() => {
|
|
if (!checkingError) {
|
|
setStaticBuildInfo(getStaticBuildInfo());
|
|
}
|
|
}, [checkingError]);
|
|
|
|
return (
|
|
<>
|
|
<Helmet>
|
|
<title>Something went wrong...</title>
|
|
</Helmet>
|
|
{!checkingError ? (
|
|
<Margins css={styles.root}>
|
|
<div css={{ width: "100%" }}>
|
|
<CoderIcon css={styles.logo} />
|
|
<h1 css={styles.title}>Something went wrong...</h1>
|
|
<p css={styles.text}>
|
|
Please try reloading the page, if that doesn‘t work, you can
|
|
ask for help in the{" "}
|
|
<Link href="https://discord.gg/coder">
|
|
Coder Discord community
|
|
</Link>{" "}
|
|
or{" "}
|
|
<Link
|
|
href={`https://github.com/coder/coder/issues/new?body=${encodeURIComponent(
|
|
[
|
|
["**Version**", coderVersion ?? "-- Set version --"].join(
|
|
"\n",
|
|
),
|
|
["**Path**", "`" + location.pathname + "`"].join("\n"),
|
|
["**Error**", "```\n" + error.stack + "\n```"].join("\n"),
|
|
].join("\n\n"),
|
|
)}`}
|
|
target="_blank"
|
|
>
|
|
open an issue
|
|
</Link>
|
|
.
|
|
</p>
|
|
<Stack direction="row" justifyContent="center">
|
|
<Button
|
|
startIcon={<RefreshOutlined />}
|
|
onClick={() => {
|
|
window.location.reload();
|
|
}}
|
|
>
|
|
Reload page
|
|
</Button>
|
|
<Button component="a" href="/">
|
|
Go to dashboard
|
|
</Button>
|
|
</Stack>
|
|
{error.stack && (
|
|
<div css={styles.stack}>
|
|
<div css={styles.stackHeader}>
|
|
Stacktrace
|
|
<CopyButton
|
|
buttonStyles={styles.copyButton}
|
|
text={error.stack}
|
|
tooltipTitle="Copy stacktrace"
|
|
/>
|
|
</div>
|
|
<pre css={styles.stackCode}>{error.stack}</pre>
|
|
</div>
|
|
)}
|
|
{coderVersion && (
|
|
<div css={styles.version}>Version: {coderVersion}</div>
|
|
)}
|
|
</div>
|
|
</Margins>
|
|
) : (
|
|
<FullScreenLoader />
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
// During the build process, we inject the build info into the HTML
|
|
const getStaticBuildInfo = () => {
|
|
const buildInfoJson = document
|
|
.querySelector("meta[property=build-info]")
|
|
?.getAttribute("content");
|
|
|
|
if (buildInfoJson) {
|
|
try {
|
|
return JSON.parse(buildInfoJson) as BuildInfoResponse;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
};
|
|
|
|
const styles = {
|
|
root: {
|
|
paddingTop: 32,
|
|
paddingBottom: 32,
|
|
textAlign: "center",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
minHeight: "100%",
|
|
maxWidth: 600,
|
|
},
|
|
|
|
logo: {
|
|
fontSize: 64,
|
|
},
|
|
|
|
title: {
|
|
fontSize: 32,
|
|
fontWeight: 400,
|
|
},
|
|
|
|
text: (theme) => ({
|
|
fontSize: 16,
|
|
color: theme.palette.text.secondary,
|
|
lineHeight: "160%",
|
|
marginBottom: 32,
|
|
}),
|
|
|
|
stack: (theme) => ({
|
|
border: `1px solid ${theme.palette.divider}`,
|
|
borderRadius: 4,
|
|
marginTop: 64,
|
|
display: "block",
|
|
textAlign: "left",
|
|
}),
|
|
|
|
stackHeader: (theme) => ({
|
|
fontSize: 10,
|
|
textTransform: "uppercase",
|
|
fontWeight: 600,
|
|
letterSpacing: 1,
|
|
padding: "8px 8px 8px 16px",
|
|
backgroundColor: theme.palette.background.paper,
|
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
color: theme.palette.text.secondary,
|
|
display: "flex",
|
|
flexAlign: "center",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
}),
|
|
|
|
stackCode: {
|
|
padding: 16,
|
|
margin: 0,
|
|
wordWrap: "break-word",
|
|
whiteSpace: "break-spaces",
|
|
},
|
|
|
|
version: (theme) => ({
|
|
marginTop: 32,
|
|
fontSize: 12,
|
|
color: theme.palette.text.secondary,
|
|
}),
|
|
|
|
copyButton: css`
|
|
background-color: transparent;
|
|
border: 0;
|
|
border-radius: 999px;
|
|
min-height: 32px;
|
|
min-width: 32px;
|
|
height: 32px;
|
|
width: 32px;
|
|
|
|
& svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
`,
|
|
} satisfies Record<string, Interpolation<Theme>>;
|