Added ErrorBoundary with GitHub bug link
Brian Vaughn committed
Apr 11, 2019 at 17:19 UTC
9642d26674671d8aa6a60e63ccfb166712b9f8d6
12 files changed
+211
-35
.eslintrc
+1
@@ -2,6 +2,7 @@
2
"extends": ["react-app","plugin:prettier/recommended"],
3
"plugins": ["react-hooks"],
4
"rules": {
5
+ "jsx-a11y/anchor-has-content": "off",
6
"react-hooks/exhaustive-deps": "error",
7
"react-hooks/rules-of-hooks": "error"
8
},
shells/browser/shared/webpack.backend.js
+10
@@ -1,9 +1,18 @@
1
+const { execSync } = require('child_process');
2
const { readFileSync } = require('fs');
3
const { resolve } = require('path');
4
const { DefinePlugin } = require('webpack');
5
6
const __DEV__ = process.env.NODE_ENV !== 'production';
7
8
+// TODO potentially replac this with an fb.me URL (if it can forward the query params)
9
+const GITHUB_URL = execSync('git remote get-url origin')
10
+ .toString()
11
+ .trim()
12
+ .replace(':', '/')
13
+ .replace('git@', 'https://')
14
+ .replace('.git', '');
15
+
16
const DEVTOOLS_VERSION = JSON.parse(
17
readFileSync(resolve(__dirname, '../../../package.json'))
18
).version;
@@ -27,6 +36,7 @@ module.exports = {
36
new DefinePlugin({
37
__DEV__: true,
38
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
39
+ 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
40
}),
41
],
42
module: {
shells/browser/shared/webpack.config.js
+10
@@ -1,3 +1,4 @@
1
+const { execSync } = require('child_process');
2
const { readFileSync } = require('fs');
3
const { resolve } = require('path');
4
const { DefinePlugin } = require('webpack');
@@ -5,6 +6,14 @@ const { DefinePlugin } = require('webpack');
6
const NODE_ENV = process.env.NODE_ENV;
7
const __DEV__ = NODE_ENV !== 'production';
8
9
+// TODO potentially replac this with an fb.me URL (if it can forward the query params)
10
+const GITHUB_URL = execSync('git remote get-url origin')
11
+ .toString()
12
+ .trim()
13
+ .replace(':', '/')
14
+ .replace('git@', 'https://')
15
+ .replace('.git', '');
16
+
17
const DEVTOOLS_VERSION = JSON.parse(
18
readFileSync(resolve(__dirname, '../../../package.json'))
19
).version;
@@ -33,6 +42,7 @@ module.exports = {
42
new DefinePlugin({
43
__DEV__: false,
44
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
45
+ 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
46
'process.env.NODE_ENV': `"${NODE_ENV}"`,
47
}),
48
],
shells/dev/webpack.config.js
+10
@@ -1,9 +1,18 @@
1
+const { execSync } = require('child_process');
2
const { readFileSync } = require('fs');
3
const { resolve } = require('path');
4
const { DefinePlugin } = require('webpack');
5
6
const __DEV__ = process.env.NODE_ENV !== 'production';
7
8
+// TODO potentially replac this with an fb.me URL (if it can forward the query params)
9
+const GITHUB_URL = execSync('git remote get-url origin')
10
+ .toString()
11
+ .trim()
12
+ .replace(':', '/')
13
+ .replace('git@', 'https://')
14
+ .replace('.git', '');
15
+
16
const DEVTOOLS_VERSION = JSON.parse(
17
readFileSync(resolve(__dirname, '../../package.json'))
18
).version;
@@ -30,6 +39,7 @@ module.exports = {
39
plugins: [
40
new DefinePlugin({
41
__DEV__: __DEV__,
42
+ 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
43
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
44
}),
45
],
src/devtools/views/Components/Components.js
+5
-11
@@ -1,18 +1,14 @@
1
// @flow
2
3
import React from 'react';
4
-import { createPortal } from 'react-dom';
4
import Tree from './Tree';
5
import SelectedElement from './SelectedElement';
6
import styles from './Components.css';
7
+import portaledContent from '../portaledContent';
8
9
-export type Props = {|
10
- portalContainer?: Element,
11
-|};
12
-
13
-export default function Components({ portalContainer }: Props) {
9
+function Components(_: {||}) {
10
// TODO Flex wrappers below should be user resizable.
15
- const children = (
11
+ return (
12
<div className={styles.Components}>
13
<div className={styles.TreeWrapper}>
14
<Tree />
@@ -22,8 +18,6 @@ export default function Components({ portalContainer }: Props) {
18
</div>
19
</div>
20
);
25
-
26
- return portalContainer != null
27
- ? createPortal(children, portalContainer)
28
- : children;
21
}
22
+
23
+export default portaledContent(Components);
src/devtools/views/ErrorBoundary.css
new
+24
@@ -0,0 +1,24 @@
1
+.ErrorBoundary {
2
+ height: 100%;
3
+ width: 100%;
4
+ background-color: white;
5
+ color: red;
6
+ padding: 0.5rem;
7
+ overflow: auto;
8
+}
9
+
10
+.Header {
11
+ font-size: var(--font-size-sans-large);
12
+ font-weight: bold;
13
+}
14
+
15
+.Stack {
16
+ margin-top: 0.5rem;
17
+ white-space: pre;
18
+ font-family: var(--font-family-monospace);
19
+ font-size: var(--font-size-monospace-small);
20
+ background-color: hsl(0, 100%, 97%);
21
+ border: 1px solid hsl(0, 100%, 92%);
22
+ border-radius: 0.25rem;
23
+ padding: 0.5rem;
24
+}
src/devtools/views/ErrorBoundary.js
new
+105
@@ -0,0 +1,105 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+import styles from './ErrorBoundary.css';
5
+
6
+type Props = {|
7
+ children: React$Node,
8
+|};
9
+
10
+type State = {|
11
+ callStack: string | null,
12
+ componentStack: string | null,
13
+ errorMessage: string | null,
14
+ hasError: boolean,
15
+|};
16
+
17
+export default class ErrorBoundary extends Component<Props, State> {
18
+ state: State = {
19
+ callStack: null,
20
+ componentStack: null,
21
+ errorMessage: null,
22
+ hasError: false,
23
+ };
24
+
25
+ componentDidCatch(error: any, { componentStack }: any) {
26
+ const errorMessage =
27
+ typeof error === 'object' && error.hasOwnProperty('message')
28
+ ? error.message
29
+ : error;
30
+
31
+ const callStack =
32
+ typeof error === 'object' && error.hasOwnProperty('stack')
33
+ ? error.stack
34
+ .split('\n')
35
+ .slice(1)
36
+ .join('\n')
37
+ : null;
38
+
39
+ this.setState({
40
+ callStack,
41
+ componentStack,
42
+ errorMessage,
43
+ hasError: true,
44
+ });
45
+ }
46
+
47
+ render() {
48
+ const { children } = this.props;
49
+ const { callStack, componentStack, errorMessage, hasError } = this.state;
50
+
51
+ let bugURL = process.env.GITHUB_URL;
52
+ if (bugURL) {
53
+ const title = `Error: "${errorMessage || ''}"`;
54
+ const label = '😭 bug';
55
+
56
+ let body = '<!-- please provide repro information here -->\n';
57
+ body += '\n---------------------------------------------';
58
+ body += '\nPlease do not remove the text below this line';
59
+ body += '\n---------------------------------------------';
60
+ body += `\n\nDevTools version: ${process.env.DEVTOOLS_VERSION || ''}`;
61
+ if (callStack) {
62
+ body += `\n\nCall stack: ${callStack.trim()}`;
63
+ }
64
+ if (componentStack) {
65
+ body += `\n\nComponent stack: ${componentStack.trim()}`;
66
+ }
67
+
68
+ bugURL += `/issues/new?labels=${encodeURI(label)}&title=${encodeURI(
69
+ title
70
+ )}&body=${encodeURI(body)}`;
71
+ }
72
+
73
+ if (hasError) {
74
+ return (
75
+ <div className={styles.ErrorBoundary}>
76
+ <div className={styles.Header}>
77
+ An error was thrown: "{errorMessage}"
78
+ </div>
79
+ {bugURL && (
80
+ <a
81
+ href={bugURL}
82
+ rel="noopener noreferrer"
83
+ target="_blank"
84
+ title="Report bug"
85
+ >
86
+ Report this issue
87
+ </a>
88
+ )}
89
+ {!!callStack && (
90
+ <div className={styles.Stack}>
91
+ The error was thrown {callStack.trim()}
92
+ </div>
93
+ )}
94
+ {!!componentStack && (
95
+ <div className={styles.Stack}>
96
+ The error occurred {componentStack.trim()}
97
+ </div>
98
+ )}
99
+ </div>
100
+ );
101
+ }
102
+
103
+ return children;
104
+ }
105
+}
src/devtools/views/Profiler/Profiler.js
+6
-13
@@ -1,7 +1,6 @@
1
// @flow
2
3
import React, { Suspense, useCallback, useContext } from 'react';
4
-import { createPortal } from 'react-dom';
4
import {
5
CommitFilterModalContext,
6
CommitFilterModalContextController,
@@ -21,25 +20,21 @@ import SidebarCommitInfo from './SidebarCommitInfo';
20
import SidebarInteractions from './SidebarInteractions';
21
import SidebarSelectedFiberInfo from './SidebarSelectedFiberInfo';
22
import ToggleCommitFilterModalButton from './ToggleCommitFilterModalButton';
23
+import portaledContent from '../portaledContent';
24
25
import styles from './Profiler.css';
26
27
export type Props = {|
28
- portalContainer?: Element,
28
supportsProfiling: boolean,
29
|};
30
32
-export default function Profiler({
33
- portalContainer,
34
- supportsProfiling,
35
-}: Props) {
31
+function Profiler({ supportsProfiling }: Props) {
32
const { hasProfilingData, isProfiling, rootHasProfilingData } = useContext(
33
ProfilerContext
34
);
35
40
- let children = null;
36
if (isProfiling || !rootHasProfilingData) {
42
- children = (
37
+ return (
38
<NonSuspendingProfiler
39
hasProfilingData={hasProfilingData}
40
isProfiling={isProfiling}
@@ -47,16 +42,12 @@ export default function Profiler({
42
/>
43
);
44
} else {
50
- children = (
45
+ return (
46
<CommitFilterModalContextController>
47
<SuspendingProfiler />
48
</CommitFilterModalContextController>
49
);
50
}
56
-
57
- return portalContainer != null
58
- ? createPortal(children, portalContainer)
59
- : children;
51
}
52
53
// This view is rendered when there is no profiler data (either we haven't profiled yet or we're currently profiling).
@@ -271,3 +262,5 @@ const RecortdingInProgress = () => (
262
</div>
263
</div>
264
);
265
+
266
+export default portaledContent(Profiler);
src/devtools/views/Settings/Settings.js
+5
-11
@@ -1,19 +1,15 @@
1
// @flow
2
3
import React, { useCallback, useContext, useMemo } from 'react';
4
-import { createPortal } from 'react-dom';
4
import { useSubscription } from '../hooks';
5
import { StoreContext } from '../context';
6
import { SettingsContext } from './SettingsContext';
7
import Store from 'src/devtools/store';
8
+import portaledContent from '../portaledContent';
9
10
import styles from './Settings.css';
11
12
-export type Props = {|
13
- portalContainer?: Element,
14
-|};
15
-
16
-export default function Settings({ portalContainer }: Props) {
12
+function Settings(_: {||}) {
13
const store = useContext(StoreContext);
14
const { displayDensity, setDisplayDensity, theme, setTheme } = useContext(
15
SettingsContext
@@ -52,7 +48,7 @@ export default function Settings({ portalContainer }: Props) {
48
[store]
49
);
50
55
- const children = (
51
+ return (
52
<div className={styles.Settings}>
53
<div className={styles.Section}>
54
<div className={styles.Header}>Theme</div>
@@ -135,8 +131,6 @@ export default function Settings({ portalContainer }: Props) {
131
)}
132
</div>
133
);
138
-
139
- return portalContainer != null
140
- ? createPortal(children, portalContainer)
141
- : children;
134
}
135
+
136
+export default portaledContent(Settings);
src/devtools/views/Settings/SettingsContext.js
+6
@@ -184,8 +184,14 @@ function updateDisplayDensity(
184
'font-size-monospace-large',
185
documentElements
186
);
187
+ updateStyleHelper(
188
+ displayDensity,
189
+ 'font-size-monospace-small',
190
+ documentElements
191
+ );
192
updateStyleHelper(displayDensity, 'font-size-sans-normal', documentElements);
193
updateStyleHelper(displayDensity, 'font-size-sans-large', documentElements);
194
+ updateStyleHelper(displayDensity, 'font-size-sans-small', documentElements);
195
updateStyleHelper(displayDensity, 'line-height-data', documentElements);
196
}
197
src/devtools/views/portaledContent.js
new
+25
@@ -0,0 +1,25 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { createPortal } from 'react-dom';
5
+import ErrorBoundary from './ErrorBoundary';
6
+
7
+export type Props = {
8
+ portalContainer?: Element,
9
+};
10
+
11
+export default function portaledContent(
12
+ Component: React$StatelessFunctionalComponent<any>
13
+): React$StatelessFunctionalComponent<any> {
14
+ return function PortaledContent({ portalContainer, ...rest }: Props) {
15
+ const children = (
16
+ <ErrorBoundary>
17
+ <Component {...rest} />
18
+ </ErrorBoundary>
19
+ );
20
+
21
+ return portalContainer != null
22
+ ? createPortal(children, portalContainer)
23
+ : children;
24
+ };
25
+}
src/devtools/views/root.css
+4
@@ -96,15 +96,19 @@
96
--dark-color-text-color: #ffffff;
97
98
/* Compact density */
99
+ --compact-font-size-monospace-small: 9px;
100
--compact-font-size-monospace-normal: 11px;
101
--compact-font-size-monospace-large: 15px;
102
+ --compact-font-size-sans-small: 10px;
103
--compact-font-size-sans-normal: 12px;
104
--compact-font-size-sans-large: 14px;
105
--compact-line-height-data: 18px;
106
107
/* Comfortable density */
108
+ --comfortable-font-size-monospace-small: 10px;
109
--comfortable-font-size-monospace-normal: 13px;
110
--comfortable-font-size-monospace-large: 17px;
111
+ --comfortable-font-size-sans-small: 12px;
112
--comfortable-font-size-sans-normal: 14px;
113
--comfortable-font-size-sans-large: 16px;
114
--comfortable-line-height-data: 22px;