DevTools: Improve error boundary (#18956)
1. Add support for dark mode 2. Add retry option for case where Profiler data is corrupted
Brian Vaughn committed
May 21, 2020 at 11:21 UTC
099f73710e5aec28b9d86bc3a8fdb1cad5a9f490
7 files changed
+119
-23
packages/react-devtools-shared/src/devtools/views/ErrorBoundary.css
+35
-5
@@ -1,8 +1,7 @@
1
.ErrorBoundary {
2
height: 100%;
3
width: 100%;
4
- background-color: white;
5
- color: red;
4
+ background-color: var(--color-background);
5
padding: 0.5rem;
6
overflow: auto;
7
}
@@ -10,15 +9,46 @@
9
.Header {
10
font-size: var(--font-size-sans-large);
11
font-weight: bold;
12
+ color: var(--color-error-text);
13
}
14
15
.Stack {
16
margin-top: 0.5rem;
17
white-space: pre-wrap;
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%);
19
+ font-size: var(--font-size-monospace-normal);
20
+ -webkit-font-smoothing: initial;
21
+ background-color: var(--color-error-background);
22
+ border: 1px solid var(--color-error-border);
23
+ color: var(--color-error-text);
24
border-radius: 0.25rem;
25
padding: 0.5rem;
26
}
27
+
28
+.IconAndLinkRow {
29
+ display: flex;
30
+ align-items: center;
31
+ margin-top: 0.5rem;
32
+ color: var(--color-text);
33
+}
34
+
35
+.RetryIcon {
36
+ margin-right: 0.25rem;
37
+ color: var(--color-button-active);
38
+}
39
+
40
+.RetryButton {
41
+ margin-right: 0.25rem;
42
+ color: var(--color-text);
43
+}
44
+.RetryButton:hover {
45
+ color: var(--color-button-hover);
46
+}
47
+
48
+.ReportIcon {
49
+ margin-right: 0.25rem;
50
+}
51
+
52
+.ReportLink {
53
+ color: var(--color-link);
54
+}
\ No newline at end of file
packages/react-devtools-shared/src/devtools/views/ErrorBoundary.js
+46
-16
@@ -9,10 +9,16 @@
9
10
import * as React from 'react';
11
import {Component} from 'react';
12
+import Button from './Button';
13
+import ButtonIcon from './ButtonIcon';
14
+import Icon from './Icon';
15
import styles from './ErrorBoundary.css';
16
+import Store from 'react-devtools-shared/src/devtools/store';
17
18
type Props = {|
19
children: React$Node,
20
+ onRetry?: (store: Store) => void,
21
+ store: Store,
22
|};
23
24
type State = {|
@@ -22,13 +28,15 @@ type State = {|
28
hasError: boolean,
29
|};
30
31
+const InitialState: State = {
32
+ callStack: null,
33
+ componentStack: null,
34
+ errorMessage: null,
35
+ hasError: false,
36
+};
37
+
38
export default class ErrorBoundary extends Component<Props, State> {
26
- state: State = {
27
- callStack: null,
28
- componentStack: null,
29
- errorMessage: null,
30
- hasError: false,
31
- };
39
+ state: State = InitialState;
40
41
componentDidCatch(error: any, {componentStack}: any) {
42
const errorMessage =
@@ -85,17 +93,30 @@ export default class ErrorBoundary extends Component<Props, State> {
93
return (
94
<div className={styles.ErrorBoundary}>
95
<div className={styles.Header}>
88
- An error was thrown: "{errorMessage}"
96
+ Uncaught Error: {errorMessage || ''}
97
+ </div>
98
+ <div className={styles.IconAndLinkRow}>
99
+ <Button
100
+ className={styles.RetryButton}
101
+ title="Retry"
102
+ onClick={this.handleRetry}>
103
+ <ButtonIcon className={styles.RetryIcon} type="reload" />
104
+ Retry
105
+ </Button>
106
+ {bugURL && (
107
+ <>
108
+ <Icon className={styles.ReportIcon} type="bug" />
109
+ <a
110
+ className={styles.ReportLink}
111
+ href={bugURL}
112
+ rel="noopener noreferrer"
113
+ target="_blank"
114
+ title="Report bug">
115
+ Report this issue
116
+ </a>
117
+ </>
118
+ )}
119
</div>
90
- {bugURL && (
91
- <a
92
- href={bugURL}
93
- rel="noopener noreferrer"
94
- target="_blank"
95
- title="Report bug">
96
- Report this issue
97
- </a>
98
- )}
120
{!!callStack && (
121
<div className={styles.Stack}>
122
The error was thrown {callStack.trim()}
@@ -112,4 +133,13 @@ export default class ErrorBoundary extends Component<Props, State> {
133
134
return children;
135
}
136
+
137
+ handleRetry = () => {
138
+ const {onRetry, store} = this.props;
139
+ if (typeof onRetry === 'function') {
140
+ onRetry(store);
141
+ }
142
+
143
+ this.setState(InitialState);
144
+ };
145
}
packages/react-devtools-shared/src/devtools/views/Icon.js
+11
@@ -12,6 +12,7 @@ import styles from './Icon.css';
12
13
export type IconType =
14
| 'arrow'
15
+ | 'bug'
16
| 'code'
17
| 'components'
18
| 'copy'
@@ -34,6 +35,9 @@ export default function Icon({className = '', type}: Props) {
35
case 'arrow':
36
pathData = PATH_ARROW;
37
break;
38
+ case 'bug':
39
+ pathData = PATH_BUG;
40
+ break;
41
case 'code':
42
pathData = PATH_CODE;
43
break;
@@ -84,6 +88,13 @@ export default function Icon({className = '', type}: Props) {
88
89
const PATH_ARROW = 'M8 5v14l11-7z';
90
91
+const PATH_BUG = `
92
+ M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4.41 15.59 3l-2.17 2.17C12.96 5.06 12.49 5 12 5c-.49
93
+ 0-.96.06-1.41.17L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09
94
+ 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21
95
+ 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z
96
+`;
97
+
98
const PATH_CODE = `
99
M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z
100
`;
packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js
+8
-1
@@ -28,6 +28,7 @@ import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/Set
28
import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
29
import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
30
import portaledContent from '../portaledContent';
31
+import Store from '../../store';
32
33
import styles from './Profiler.css';
34
@@ -201,4 +202,10 @@ const RecordingInProgress = () => (
202
</div>
203
);
204
204
-export default portaledContent(Profiler);
205
+function onErrorRetry(store: Store) {
206
+ // If an error happened in the Profiler,
207
+ // we should clear data on retry (or it will just happen again).
208
+ store.profilerStore.profilingData = null;
209
+}
210
+
211
+export default portaledContent(Profiler, onErrorRetry);
packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
+4
@@ -319,7 +319,11 @@ function updateThemeVariables(
319
updateStyleHelper(theme, 'color-dim', documentElements);
320
updateStyleHelper(theme, 'color-dimmer', documentElements);
321
updateStyleHelper(theme, 'color-dimmest', documentElements);
322
+ updateStyleHelper(theme, 'color-error-background', documentElements);
323
+ updateStyleHelper(theme, 'color-error-border', documentElements);
324
+ updateStyleHelper(theme, 'color-error-text', documentElements);
325
updateStyleHelper(theme, 'color-expand-collapse-toggle', documentElements);
326
+ updateStyleHelper(theme, 'color-link', documentElements);
327
updateStyleHelper(theme, 'color-modal-background', documentElements);
328
updateStyleHelper(theme, 'color-record-active', documentElements);
329
updateStyleHelper(theme, 'color-record-hover', documentElements);
packages/react-devtools-shared/src/devtools/views/portaledContent.js
+7
-1
@@ -8,17 +8,23 @@
8
*/
9
10
import * as React from 'react';
11
+import {useContext} from 'react';
12
import {createPortal} from 'react-dom';
13
import ErrorBoundary from './ErrorBoundary';
14
+import {StoreContext} from './context';
15
+import Store from '../store';
16
17
export type Props = {portalContainer?: Element, ...};
18
19
export default function portaledContent(
20
Component: React$StatelessFunctionalComponent<any>,
21
+ onErrorRetry?: (store: Store) => void,
22
): React$StatelessFunctionalComponent<any> {
23
return function PortaledContent({portalContainer, ...rest}: Props) {
24
+ const store = useContext(StoreContext);
25
+
26
const children = (
21
- <ErrorBoundary>
27
+ <ErrorBoundary store={store} onRetry={onErrorRetry}>
28
<Component {...rest} />
29
</ErrorBoundary>
30
);
packages/react-devtools-shared/src/devtools/views/root.css
+8
@@ -53,7 +53,11 @@
53
--light-color-dim: #777d88;
54
--light-color-dimmer: #cfd1d5;
55
--light-color-dimmest: #eff0f1;
56
+ --light-color-error-background: hsl(0, 100%, 97%);
57
+ --light-color-error-border: hsl(0, 100%, 92%);
58
+ --light-color-error-text: #ff0000;
59
--light-color-expand-collapse-toggle: #777d88;
60
+ --light-color-link: #0000ff;
61
--light-color-modal-background: rgba(255, 255, 255, 0.75);
62
--light-color-record-active: #fc3a4b;
63
--light-color-record-hover: #3578e5;
@@ -126,7 +130,11 @@
130
--dark-color-dim: #8f949d;
131
--dark-color-dimmer: #777d88;
132
--dark-color-dimmest: #4f5766;
133
+ --dark-color-error-background: #200;
134
+ --dark-color-error-border: #900;
135
+ --dark-color-error-text: #f55;
136
--dark-color-expand-collapse-toggle: #8f949d;
137
+ --dark-color-link: #61dafb;
138
--dark-color-modal-background: rgba(0, 0, 0, 0.75);
139
--dark-color-record-active: #fc3a4b;
140
--dark-color-record-hover: #a2e9fc;