@samitouri / QOS-React-2 / commits / 9642d26674

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;