Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commita0e4212

Browse files
Kira-Pilotkylecarbs
authored andcommitted
feat: added error boundary (#1602)
* added error boundary and error ui components* add body txt and standardize btn size* added story* feat: added error boundarycloses#1013* committing lockfile* added email body to help link
1 parentef1b6be commita0e4212

File tree

13 files changed

+454
-22
lines changed

13 files changed

+454
-22
lines changed

‎.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"rpty",
5353
"sdkproto",
5454
"Signup",
55+
"sourcemapped",
5556
"stretchr",
5657
"TCGETS",
5758
"tcpip",

‎site/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"react":"17.0.2",
4343
"react-dom":"17.0.2",
4444
"react-router-dom":"6.3.0",
45+
"sourcemapped-stacktrace":"1.1.11",
4546
"swr":"1.2.2",
4647
"uuid":"8.3.2",
4748
"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{BrowserRouterasRouter}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-
<ThemeProvidertheme={dark}>
35-
<CssBaseline/>
36-
<AppRouter/>
37-
<GlobalSnackbar/>
38-
</ThemeProvider>
39-
</XServiceProvider>
34+
<ThemeProvidertheme={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: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,38 @@ import { combineClasses } from "../../util/combineClasses"
55

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

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

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

2535
constuseStyles=makeStyles((theme)=>({
2636
root:{
2737
minHeight:156,
38+
maxHeight:240,
39+
overflowY:"scroll",
2840
background:theme.palette.background.default,
2941
color:theme.palette.text.primary,
3042
fontFamily:MONOSPACE_FONT_FAMILY,
@@ -36,4 +48,8 @@ const useStyles = makeStyles((theme) => ({
3648
line:{
3749
whiteSpace:"pre-wrap",
3850
},
51+
ctaBar:{
52+
display:"flex",
53+
justifyContent:"space-between",
54+
},
3955
}))

‎site/src/components/CopyButton/CopyButton.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
1-
importButtonfrom"@material-ui/core/Button"
1+
importIconButtonfrom"@material-ui/core/Button"
22
import{makeStyles}from"@material-ui/core/styles"
33
importTooltipfrom"@material-ui/core/Tooltip"
44
importCheckfrom"@material-ui/icons/Check"
55
importReact,{useState}from"react"
6+
import{combineClasses}from"../../util/combineClasses"
67
import{FileCopyIcon}from"../Icons/FileCopyIcon"
78

89
interfaceCopyButtonProps{
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-
exportconstCopyButton:React.FC<CopyButtonProps>=({ className="", text})=>{
19+
exportconstCopyButton:React.FC<CopyButtonProps>=({
20+
text,
21+
ctaCopy,
22+
wrapperClassName="",
23+
buttonClassName="",
24+
})=>{
1725
conststyles=useStyles()
1826
const[isCopied,setIsCopied]=useState<boolean>(false)
1927

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

3745
return(
3846
<Tooltiptitle="Copy to Clipboard"placement="top">
39-
<divclassName={`${styles.copyButtonWrapper}${className}`}>
40-
<ButtonclassName={styles.copyButton}onClick={copyToClipboard}size="small">
47+
<divclassName={combineClasses([styles.copyButtonWrapper,wrapperClassName])}>
48+
<IconButton
49+
className={combineClasses([styles.copyButton,buttonClassName])}
50+
onClick={copyToClipboard}
51+
size="small"
52+
>
4153
{isCopied ?<CheckclassName={styles.fileCopyIcon}/> :<FileCopyIconclassName={styles.fileCopyIcon}/>}
42-
</Button>
54+
{ctaCopy&&<divclassName={styles.buttonCopy}>{ctaCopy}</div>}
55+
</IconButton>
4356
</div>
4457
</Tooltip>
4558
)
@@ -65,4 +78,7 @@ const useStyles = makeStyles((theme) => ({
6578
width:20,
6679
height:20,
6780
},
81+
buttonCopy:{
82+
marginLeft:theme.spacing(1),
83+
},
6884
}))
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
importReactfrom"react"
2+
import{RuntimeErrorState}from"../RuntimeErrorState/RuntimeErrorState"
3+
4+
typeErrorBoundaryProps=Record<string,unknown>
5+
6+
interfaceErrorBoundaryState{
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+
exportclassErrorBoundaryextendsReact.Component<ErrorBoundaryProps,ErrorBoundaryState>{
15+
constructor(props:ErrorBoundaryProps){
16+
super(props)
17+
this.state={error:null}
18+
}
19+
20+
staticgetDerivedStateFromError(error:Error):{error:Error}{
21+
return{ error}
22+
}
23+
24+
render():React.ReactNode{
25+
if(this.state.error){
26+
return<RuntimeErrorStateerror={this.state.error}/>
27+
}
28+
29+
returnthis.props.children
30+
}
31+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import{makeStyles}from"@material-ui/core/styles"
2+
importReactfrom"react"
3+
import{CodeBlock}from"../CodeBlock/CodeBlock"
4+
import{createCtas}from"./createCtas"
5+
6+
constLanguage={
7+
reportLoading:"Generating crash report...",
8+
}
9+
10+
interfaceReportState{
11+
error:Error
12+
mappedStack:string[]|null
13+
}
14+
15+
interfaceStackTraceAvailableMsg{
16+
type:"stackTraceAvailable"
17+
stackTrace:string[]
18+
}
19+
20+
/**
21+
* stackTraceUnavailable is a Msg describing a stack trace not being available
22+
*/
23+
exportconststackTraceUnavailable={
24+
type:"stackTraceUnavailable",
25+
}asconst
26+
27+
typeReportMessage=StackTraceAvailableMsg|typeofstackTraceUnavailable
28+
29+
exportconststackTraceAvailable=(stackTrace:string[]):StackTraceAvailableMsg=>{
30+
return{
31+
type:"stackTraceAvailable",
32+
stackTrace,
33+
}
34+
}
35+
36+
constsetStackTrace=(model:ReportState,mappedStack:string[]):ReportState=>{
37+
return{
38+
...model,
39+
mappedStack,
40+
}
41+
}
42+
43+
exportconstreducer=(model:ReportState,msg:ReportMessage):ReportState=>{
44+
switch(msg.type){
45+
case"stackTraceAvailable":
46+
returnsetStackTrace(model,msg.stackTrace)
47+
case"stackTraceUnavailable":
48+
returnsetStackTrace(model,["Unable to get stack trace"])
49+
}
50+
}
51+
52+
exportconstcreateFormattedStackTrace=(error:Error,mappedStack:string[]|null):string[]=>{
53+
return[
54+
"======================= STACK TRACE ========================",
55+
"",
56+
error.message,
57+
...(mappedStack ?mappedStack :[]),
58+
"",
59+
"============================================================",
60+
]
61+
}
62+
63+
/**
64+
* A code block component that contains the error stack resulting from an error boundary trigger
65+
*/
66+
exportconstRuntimeErrorReport=({ error, mappedStack}:ReportState):React.ReactElement=>{
67+
conststyles=useStyles()
68+
69+
if(!mappedStack){
70+
return<CodeBlocklines={[Language.reportLoading]}className={styles.codeBlock}/>
71+
}
72+
73+
constformattedStackTrace=createFormattedStackTrace(error,mappedStack)
74+
return<CodeBlocklines={formattedStackTrace}className={styles.codeBlock}ctas={createCtas(formattedStackTrace)}/>
75+
}
76+
77+
constuseStyles=makeStyles(()=>({
78+
codeBlock:{
79+
minHeight:"auto",
80+
userSelect:"all",
81+
width:"100%",
82+
},
83+
}))
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import{ComponentMeta,Story}from"@storybook/react"
2+
importReactfrom"react"
3+
import{RuntimeErrorState,RuntimeErrorStateProps}from"./RuntimeErrorState"
4+
5+
consterror=newError("An error occurred")
6+
7+
exportdefault{
8+
title:"components/RuntimeErrorState",
9+
component:RuntimeErrorState,
10+
argTypes:{
11+
error:{
12+
defaultValue:error,
13+
},
14+
},
15+
}asComponentMeta<typeofRuntimeErrorState>
16+
17+
constTemplate:Story<RuntimeErrorStateProps>=(args)=><RuntimeErrorState{...args}/>
18+
19+
exportconstErrored=Template.bind({})
20+
Errored.parameters={
21+
// The RuntimeErrorState is noisy for chromatic, because it renders an actual error
22+
// along with the stacktrace - and the stacktrace includes the full URL of
23+
// scripts in the stack. This is problematic, because every deployment uses
24+
// a different URL, causing the validation to fail.
25+
chromatic:{disableSnapshot:true},
26+
}
27+
28+
Errored.args={
29+
error,
30+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import{screen}from"@testing-library/react"
2+
importReactfrom"react"
3+
import{render}from"../../testHelpers/renderHelpers"
4+
import{LanguageasButtonLanguage}from"./createCtas"
5+
import{LanguageasRuntimeErrorStateLanguage,RuntimeErrorState}from"./RuntimeErrorState"
6+
7+
describe("RuntimeErrorState",()=>{
8+
beforeEach(()=>{
9+
// Given
10+
consterrorText="broken!"
11+
consterrorStateProps={
12+
error:newError(errorText),
13+
}
14+
15+
// When
16+
render(<RuntimeErrorState{...errorStateProps}/>)
17+
})
18+
19+
it("should show stack when encountering runtime error",()=>{
20+
// Then
21+
constreportError=screen.getByText("broken!")
22+
expect(reportError).toBeDefined()
23+
24+
// Despite appearances, this is the stack trace
25+
conststackTrace=screen.getByText("Unable to get stack trace")
26+
expect(stackTrace).toBeDefined()
27+
})
28+
29+
it("should have a button bar",()=>{
30+
// Then
31+
constcopyCta=screen.getByText(ButtonLanguage.copyReport)
32+
expect(copyCta).toBeDefined()
33+
34+
constreloadCta=screen.getByText(ButtonLanguage.reloadApp)
35+
expect(reloadCta).toBeDefined()
36+
})
37+
38+
it("should have an email link",()=>{
39+
// Then
40+
constemailLink=screen.getByText(RuntimeErrorStateLanguage.link)
41+
expect(emailLink.closest("a")).toHaveAttribute("href",expect.stringContaining("mailto:support@coder.com"))
42+
})
43+
})

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp