|
1 |
| -import Box from "@material-ui/core/Box" |
| 1 | +import Button from "@material-ui/core/Button" |
2 | 2 | import Link from "@material-ui/core/Link"
|
3 | 3 | import { makeStyles } from "@material-ui/core/styles"
|
4 |
| -import ErrorOutlineIcon from "@material-ui/icons/ErrorOutline" |
5 |
| -import { useEffect, useReducer, FC } from "react" |
6 |
| -import { mapStackTrace } from "sourcemapped-stacktrace" |
| 4 | +import RefreshOutlined from "@material-ui/icons/RefreshOutlined" |
| 5 | +import { CoderIcon } from "components/Icons/CoderIcon" |
| 6 | +import { FullScreenLoader } from "components/Loader/FullScreenLoader" |
| 7 | +import { Stack } from "components/Stack/Stack" |
| 8 | +import { FC, useEffect, useState } from "react" |
| 9 | +import { Helmet } from "react-helmet-async" |
7 | 10 | import { Margins } from "../Margins/Margins"
|
8 |
| -import { Section } from "../Section/Section" |
9 |
| -import { Typography } from "../Typography/Typography" |
10 |
| -import { |
11 |
| - createFormattedStackTrace, |
12 |
| - reducer, |
13 |
| - RuntimeErrorReport, |
14 |
| - stackTraceAvailable, |
15 |
| - stackTraceUnavailable, |
16 |
| -} from "./RuntimeErrorReport" |
17 | 11 |
|
18 |
| -export const Language = { |
19 |
| - title: "Coder encountered an error", |
20 |
| - body: "Please copy the crash log using the button below and", |
21 |
| - link: "send it to us.", |
22 |
| -} |
23 |
| - |
24 |
| -export interface RuntimeErrorStateProps { |
25 |
| - error: Error |
26 |
| -} |
27 |
| - |
28 |
| -/** |
29 |
| - * A title for our error boundary UI |
30 |
| - */ |
31 |
| -const ErrorStateTitle = () => { |
32 |
| - const styles = useStyles() |
33 |
| - return ( |
34 |
| - <Box className={styles.title} display="flex" alignItems="center"> |
35 |
| - <ErrorOutlineIcon /> |
36 |
| - <span>{Language.title}</span> |
37 |
| - </Box> |
38 |
| - ) |
39 |
| -} |
40 |
| - |
41 |
| -/** |
42 |
| - * A description for our error boundary UI |
43 |
| - */ |
44 |
| -const ErrorStateDescription = ({ emailBody }: { emailBody?: string }) => { |
45 |
| - const styles = useStyles() |
46 |
| - return ( |
47 |
| - <Typography variant="body2" color="textSecondary"> |
48 |
| - {Language.body} |
49 |
| - <Link |
50 |
| - href={`mailto:support@coder.com?subject=Error Report from Coder&body=${ |
51 |
| - emailBody && emailBody.replace(/\r\n|\r|\n/g, "%0D%0A") // preserving line breaks |
52 |
| - }`} |
53 |
| - className={styles.link} |
54 |
| - > |
55 |
| - {Language.link} |
56 |
| - </Link> |
57 |
| - </Typography> |
58 |
| - ) |
59 |
| -} |
| 12 | +const fetchDynamicallyImportedModuleError = |
| 13 | + "Failed to fetch dynamically imported module" as const |
60 | 14 |
|
61 |
| -/** |
62 |
| - * An error UI that is displayed when our error boundary (ErrorBoundary.tsx) is triggered |
63 |
| - */ |
64 |
| -export const RuntimeErrorState: FC<RuntimeErrorStateProps> = ({ error }) => { |
| 15 | +export const RuntimeErrorState: FC<{ error: Error }> = ({ error }) => { |
65 | 16 | const styles = useStyles()
|
66 |
| - const [reportState, dispatch] = useReducer(reducer, { |
67 |
| - error, |
68 |
| - mappedStack: null, |
69 |
| - }) |
| 17 | + const [shouldDisplayMessage, setShouldDisplayMessage] = useState(false) |
70 | 18 |
|
| 19 | + // We use an effect to show a loading state if the page is trying to reload |
71 | 20 | useEffect(() => {
|
72 |
| - try { |
73 |
| - mapStackTrace(error.stack, (mappedStack) => |
74 |
| - dispatch(stackTraceAvailable(mappedStack)), |
75 |
| - ) |
76 |
| - } catch { |
77 |
| - dispatch(stackTraceUnavailable) |
| 21 | + const isImportError = error.message.includes( |
| 22 | + fetchDynamicallyImportedModuleError, |
| 23 | + ) |
| 24 | + const isRetried = window.location.search.includes("retries=1") |
| 25 | + |
| 26 | + if (isImportError && !isRetried) { |
| 27 | + const url = new URL(location.href) |
| 28 | + // Add a retry to avoid loops |
| 29 | + url.searchParams.set("retries", "1") |
| 30 | + location.assign(url.search) |
| 31 | + return |
78 | 32 | }
|
79 |
| - }, [error]) |
| 33 | + |
| 34 | + setShouldDisplayMessage(true) |
| 35 | + }, [error.message]) |
80 | 36 |
|
81 | 37 | return (
|
82 |
| - <Box display="flex" flexDirection="column"> |
83 |
| - <Margins> |
84 |
| - <Section |
85 |
| - className={styles.reportContainer} |
86 |
| - title={<ErrorStateTitle />} |
87 |
| - description={ |
88 |
| - <ErrorStateDescription |
89 |
| - emailBody={createFormattedStackTrace( |
90 |
| - reportState.error, |
91 |
| - reportState.mappedStack, |
92 |
| - ).join("\r\n")} |
93 |
| - /> |
94 |
| - } |
95 |
| - > |
96 |
| - <RuntimeErrorReport |
97 |
| - error={reportState.error} |
98 |
| - mappedStack={reportState.mappedStack} |
99 |
| - /> |
100 |
| - </Section> |
101 |
| - </Margins> |
102 |
| - </Box> |
| 38 | + <> |
| 39 | + <Helmet> |
| 40 | + <title>Something went wrong...</title> |
| 41 | + </Helmet> |
| 42 | + {shouldDisplayMessage ? ( |
| 43 | + <Margins size="small" className={styles.root}> |
| 44 | + <div> |
| 45 | + <CoderIcon className={styles.logo} /> |
| 46 | + <h1 className={styles.title}>Something went wrong...</h1> |
| 47 | + <p className={styles.text}> |
| 48 | + Please try reloading the page, if that doesn‘t work, you can |
| 49 | + ask for help in the{" "} |
| 50 | + <Link href="https://discord.gg/coder"> |
| 51 | + Coder Discord community |
| 52 | + </Link>{" "} |
| 53 | + or{" "} |
| 54 | + <Link href="https://github.com/coder/coder/issues/new"> |
| 55 | + open an issue |
| 56 | + </Link> |
| 57 | + . |
| 58 | + </p> |
| 59 | + <Stack direction="row" justifyContent="center"> |
| 60 | + <Button |
| 61 | + startIcon={<RefreshOutlined />} |
| 62 | + onClick={() => { |
| 63 | + window.location.reload() |
| 64 | + }} |
| 65 | + > |
| 66 | + Reload page |
| 67 | + </Button> |
| 68 | + <Button component="a" href="/" variant="outlined"> |
| 69 | + Go to dashboard |
| 70 | + </Button> |
| 71 | + </Stack> |
| 72 | + </div> |
| 73 | + </Margins> |
| 74 | + ) : ( |
| 75 | + <FullScreenLoader /> |
| 76 | + )} |
| 77 | + </> |
103 | 78 | )
|
104 | 79 | }
|
105 | 80 |
|
106 | 81 | const useStyles = makeStyles((theme) => ({
|
107 |
| - title: { |
108 |
| - "& span": { |
109 |
| - paddingLeft: theme.spacing(1), |
110 |
| - }, |
| 82 | + root: { |
| 83 | + paddingTop: theme.spacing(4), |
| 84 | + paddingBottom: theme.spacing(4), |
| 85 | + textAlign: "center", |
| 86 | + display: "flex", |
| 87 | + alignItems: "center", |
| 88 | + justifyContent: "center", |
| 89 | + minHeight: "100vh", |
| 90 | + }, |
111 | 91 |
|
112 |
| - "& .MuiSvgIcon-root": { |
113 |
| - color: theme.palette.error.main, |
114 |
| - }, |
| 92 | + logo: { |
| 93 | + fontSize: theme.spacing(8), |
115 | 94 | },
|
116 |
| - link: { |
117 |
| - textDecoration: "none", |
118 |
| - color: theme.palette.primary.main, |
| 95 | + |
| 96 | + title: { |
| 97 | + fontSize: theme.spacing(4), |
| 98 | + fontWeight: 400, |
119 | 99 | },
|
120 |
| - reportContainer: { |
121 |
| - display: "flex", |
122 |
| - justifyContent: "center", |
123 |
| - marginTop: theme.spacing(5), |
| 100 | + |
| 101 | + text: { |
| 102 | + fontSize: 16, |
| 103 | + color: theme.palette.text.secondary, |
| 104 | + lineHeight: "160%", |
| 105 | + marginBottom: theme.spacing(4), |
124 | 106 | },
|
125 | 107 | }))
|
0 commit comments