Skip to content

Commit 2d6531b

Browse files
committed
added error boundary and error ui components
1 parent ba818b3 commit 2d6531b

File tree

12 files changed

+327
-23
lines changed

12 files changed

+327
-23
lines changed

.vscode/settings.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"rpty",
5252
"sdkproto",
5353
"Signup",
54+
"sourcemapped",
5455
"stretchr",
5556
"TCGETS",
5657
"tcpip",
@@ -76,7 +77,7 @@
7677
},
7778
{
7879
"match": "provisionerd/proto/provisionerd.proto",
79-
"cmd": "make provisionerd/proto/provisionerd.pb.go",
80+
"cmd": "make provisionerd/proto/provisionerd.pb.go"
8081
}
8182
]
8283
},
@@ -104,5 +105,5 @@
104105
},
105106
// We often use a version of TypeScript that's ahead of the version shipped
106107
// with VS Code.
107-
"typescript.tsdk": "./site/node_modules/typescript/lib",
108+
"typescript.tsdk": "./site/node_modules/typescript/lib"
108109
}

site/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"react": "17.0.2",
4242
"react-dom": "17.0.2",
4343
"react-router-dom": "6.3.0",
44+
"sourcemapped-stacktrace": "1.1.11",
4445
"swr": "1.2.2",
4546
"uuid": "8.3.2",
4647
"xstate": "4.32.1",

site/src/app.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React from "react"
44
import { BrowserRouter as Router } from "react-router-dom"
55
import { SWRConfig } from "swr"
66
import { AppRouter } from "./AppRouter"
7+
import { ErrorBoundary } from "./components/ErrorBoundary/ErrorBoundary"
78
import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar"
89
import { dark } from "./theme"
910
import "./theme/globalFonts"
@@ -30,13 +31,15 @@ export const App: React.FC = () => {
3031
},
3132
}}
3233
>
33-
<XServiceProvider>
34-
<ThemeProvider theme={dark}>
35-
<CssBaseline />
36-
<AppRouter />
37-
<GlobalSnackbar />
38-
</ThemeProvider>
39-
</XServiceProvider>
34+
<ThemeProvider theme={dark}>
35+
<CssBaseline />
36+
<ErrorBoundary>
37+
<XServiceProvider>
38+
<AppRouter />
39+
<GlobalSnackbar />
40+
</XServiceProvider>
41+
</ErrorBoundary>
42+
</ThemeProvider>
4043
</SWRConfig>
4144
</Router>
4245
)

site/src/components/CodeBlock/CodeBlock.tsx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,30 @@ import { combineClasses } from "../../util/combineClasses"
55

66
export interface CodeBlockProps {
77
lines: string[]
8+
ctas?: React.ReactElement[]
89
className?: string
910
}
1011

11-
export const CodeBlock: React.FC<CodeBlockProps> = ({ lines, className = "" }) => {
12+
export const CodeBlock: React.FC<CodeBlockProps> = ({ lines, ctas, className = "" }) => {
1213
const styles = useStyles()
1314

1415
return (
15-
<div className={combineClasses([styles.root, className])}>
16-
{lines.map((line, idx) => (
17-
<div className={styles.line} key={idx}>
18-
{line}
16+
<>
17+
<div className={combineClasses([styles.root, className])}>
18+
{lines.map((line, idx) => (
19+
<div className={styles.line} key={idx}>
20+
{line}
21+
</div>
22+
))}
23+
</div>
24+
{ctas && ctas.length && (
25+
<div className={styles.ctaBar}>
26+
{ctas.map((cta, i) => {
27+
return <React.Fragment key={i}>{cta}</React.Fragment>
28+
})}
1929
</div>
20-
))}
21-
</div>
30+
)}
31+
</>
2232
)
2333
}
2434

@@ -36,4 +46,8 @@ const useStyles = makeStyles((theme) => ({
3646
line: {
3747
whiteSpace: "pre-wrap",
3848
},
49+
ctaBar: {
50+
display: "flex",
51+
justifyContent: "space-between",
52+
},
3953
}))

site/src/components/CopyButton/CopyButton.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,25 @@ import { makeStyles } from "@material-ui/core/styles"
33
import Tooltip from "@material-ui/core/Tooltip"
44
import Check from "@material-ui/icons/Check"
55
import React, { useState } from "react"
6+
import { combineClasses } from "../../util/combineClasses"
67
import { FileCopyIcon } from "../Icons/FileCopyIcon"
78

89
interface CopyButtonProps {
910
text: string
10-
className?: string
11+
ctaCopy?: string
12+
wrapperClassName?: string
13+
buttonClassName?: string
1114
}
1215

1316
/**
1417
* Copy button used inside the CodeBlock component internally
1518
*/
16-
export const CopyButton: React.FC<CopyButtonProps> = ({ className = "", text }) => {
19+
export const CopyButton: React.FC<CopyButtonProps> = ({
20+
text,
21+
ctaCopy,
22+
wrapperClassName = "",
23+
buttonClassName = "",
24+
}) => {
1725
const styles = useStyles()
1826
const [isCopied, setIsCopied] = useState<boolean>(false)
1927

@@ -36,9 +44,16 @@ export const CopyButton: React.FC<CopyButtonProps> = ({ className = "", text })
3644

3745
return (
3846
<Tooltip title="Copy to Clipboard" placement="top">
39-
<div className={`${styles.copyButtonWrapper} ${className}`}>
40-
<Button className={styles.copyButton} onClick={copyToClipboard} size="small">
41-
{isCopied ? <Check className={styles.fileCopyIcon} /> : <FileCopyIcon className={styles.fileCopyIcon} />}
47+
<div className={combineClasses([styles.copyButtonWrapper, wrapperClassName])}>
48+
<Button
49+
className={combineClasses([styles.copyButton, buttonClassName])}
50+
onClick={copyToClipboard}
51+
size="small"
52+
startIcon={
53+
isCopied ? <Check className={styles.fileCopyIcon} /> : <FileCopyIcon className={styles.fileCopyIcon} />
54+
}
55+
>
56+
{ctaCopy && ctaCopy}
4257
</Button>
4358
</div>
4459
</Tooltip>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from "react"
2+
import { RuntimeErrorState } from "../RuntimeErrorState/RuntimeErrorState"
3+
4+
type ErrorBoundaryProps = Record<string, unknown>
5+
6+
interface ErrorBoundaryState {
7+
error: Error | null
8+
}
9+
10+
/**
11+
* Our app's Error Boundary
12+
* Read more about React Error Boundaries: https://reactjs.org/docs/error-boundaries.html
13+
*/
14+
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
15+
constructor(props: ErrorBoundaryProps) {
16+
super(props)
17+
this.state = { error: null }
18+
}
19+
20+
static getDerivedStateFromError(error: Error): { error: Error } {
21+
return { error }
22+
}
23+
24+
render(): React.ReactNode {
25+
if (this.state.error) {
26+
return <RuntimeErrorState error={this.state.error} />
27+
}
28+
29+
return this.props.children
30+
}
31+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import Button from "@material-ui/core/Button"
2+
import { makeStyles } from "@material-ui/core/styles"
3+
import RefreshIcon from "@material-ui/icons/Refresh"
4+
import React from "react"
5+
import { CopyButton } from "../CopyButton/CopyButton"
6+
7+
const Language = {
8+
reloadApp: "Reload Application",
9+
copyReport: "Copy Report",
10+
}
11+
12+
/**
13+
* A wrapper component for a full-width copy button
14+
*/
15+
const CopyStackButton = ({ text }: { text: string }): React.ReactElement => {
16+
const styles = useStyles()
17+
18+
return (
19+
<CopyButton
20+
text={text}
21+
ctaCopy={Language.copyReport}
22+
wrapperClassName={styles.buttonWrapper}
23+
buttonClassName={styles.copyButton}
24+
/>
25+
)
26+
}
27+
28+
/**
29+
* A button that reloads our application
30+
*/
31+
const ReloadAppButton = (): React.ReactElement => {
32+
const styles = useStyles()
33+
34+
return (
35+
<Button
36+
className={styles.buttonWrapper}
37+
variant="outlined"
38+
color="primary"
39+
startIcon={<RefreshIcon />}
40+
onClick={() => location.replace("/")}
41+
>
42+
{Language.reloadApp}
43+
</Button>
44+
)
45+
}
46+
47+
/**
48+
* createCtas generates an array of buttons to be used with our error boundary UI
49+
*/
50+
export const createCtas = (codeBlock: string[]): React.ReactElement[] => {
51+
// REMARK: we don't have to worry about key order changing
52+
// eslint-disable-next-line react/jsx-key
53+
return [<CopyStackButton text={codeBlock.join("\r\n")} />, <ReloadAppButton />]
54+
}
55+
56+
const useStyles = makeStyles((theme) => ({
57+
buttonWrapper: {
58+
marginTop: theme.spacing(1),
59+
marginLeft: 0,
60+
flex: theme.spacing(1),
61+
textTransform: "uppercase",
62+
},
63+
64+
copyButton: {
65+
width: "100%",
66+
marginRight: theme.spacing(1),
67+
backgroundColor: theme.palette.primary.main,
68+
textTransform: "uppercase",
69+
},
70+
}))
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { makeStyles } from "@material-ui/core/styles"
2+
import React from "react"
3+
import { CodeBlock } from "../CodeBlock/CodeBlock"
4+
import { createCtas } from "./ReportButtons"
5+
6+
const Language = {
7+
reportLoading: "Generating crash report...",
8+
}
9+
10+
interface ReportState {
11+
error: Error
12+
mappedStack: string[] | null
13+
}
14+
15+
interface StackTraceAvailableMsg {
16+
type: "stackTraceAvailable"
17+
stackTrace: string[]
18+
}
19+
20+
/**
21+
* stackTraceUnavailable is a Msg describing a stack trace not being available
22+
*/
23+
export const stackTraceUnavailable = {
24+
type: "stackTraceUnavailable",
25+
} as const
26+
27+
type ReportMessage = StackTraceAvailableMsg | typeof stackTraceUnavailable
28+
29+
export const stackTraceAvailable = (stackTrace: string[]): StackTraceAvailableMsg => {
30+
return {
31+
type: "stackTraceAvailable",
32+
stackTrace,
33+
}
34+
}
35+
36+
const setStackTrace = (model: ReportState, mappedStack: string[]): ReportState => {
37+
return {
38+
...model,
39+
mappedStack,
40+
}
41+
}
42+
43+
export const reducer = (model: ReportState, msg: ReportMessage): ReportState => {
44+
switch (msg.type) {
45+
case "stackTraceAvailable":
46+
return setStackTrace(model, msg.stackTrace)
47+
case "stackTraceUnavailable":
48+
return setStackTrace(model, ["Unable to get stack trace"])
49+
}
50+
}
51+
52+
/**
53+
* A code block component that contains the error stack resulting from an error boundary trigger
54+
*/
55+
export const RuntimeErrorReport = ({ error, mappedStack }: ReportState): React.ReactElement => {
56+
const styles = useStyles()
57+
58+
if (!mappedStack) {
59+
return <CodeBlock lines={[Language.reportLoading]} className={styles.codeBlock} />
60+
}
61+
62+
const codeBlock = [
63+
"======================= STACK TRACE ========================",
64+
"",
65+
error.message,
66+
...mappedStack,
67+
"",
68+
"============================================================",
69+
]
70+
71+
return <CodeBlock lines={codeBlock} className={styles.codeBlock} ctas={createCtas(codeBlock)} />
72+
}
73+
74+
const useStyles = makeStyles(() => ({
75+
codeBlock: {
76+
minHeight: "auto",
77+
userSelect: "all",
78+
width: "100%",
79+
},
80+
}))

0 commit comments

Comments
 (0)