@samitouri / QOS-React-2 / commits / 099f73710e

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;