Show import error dialog if profiling data import fails
Brian Vaughn committed
May 7, 2019 at 14:38 UTC
278661715c28b01b9c32b775679e9636baad2b18
14 files changed
+197
-44
src/__tests__/__snapshots__/profiling-test.js.snap
+4
-4
@@ -266,7 +266,7 @@ Object {
266
"interactionCount": 0,
267
"rootID": 1,
268
},
269
- "version": 1,
269
+ "version": 2,
270
}
271
`;
272
@@ -516,7 +516,7 @@ Object {
516
"interactionCount": 0,
517
"rootID": 1,
518
},
519
- "version": 1,
519
+ "version": 2,
520
}
521
`;
522
@@ -714,7 +714,7 @@ Object {
714
"interactionCount": 2,
715
"rootID": 1,
716
},
717
- "version": 1,
717
+ "version": 2,
718
}
719
`;
720
@@ -915,6 +915,6 @@ Object {
915
"interactionCount": 0,
916
"rootID": 1,
917
},
918
- "version": 1,
918
+ "version": 2,
919
}
920
`;
src/__tests__/profiling-test.js
+13
@@ -69,6 +69,19 @@ describe('profiling', () => {
69
TestRenderer = utils.requireTestRenderer();
70
});
71
72
+ it('should throw if importing older/unsupported data', () => {
73
+ const {
74
+ prepareProfilingImport,
75
+ } = require('src/devtools/views/Profiler/utils');
76
+ expect(() =>
77
+ prepareProfilingImport(
78
+ JSON.stringify({
79
+ version: 0,
80
+ })
81
+ )
82
+ ).toThrow('Unsupported profiler export version "0"');
83
+ });
84
+
85
describe('ProfilingSummary', () => {
86
it('should be collected for each commit', async done => {
87
const Parent = ({ count }) => {
src/backend/renderer.js
+2
-1
@@ -18,6 +18,7 @@ import {
18
ElementTypeRoot,
19
ElementTypeSuspense,
20
} from 'src/types';
21
+import { PROFILER_EXPORT_VERSION } from 'src/constants';
22
import {
23
getDisplayName,
24
getSavedComponentFilters,
@@ -2058,7 +2059,7 @@ export function attach(
2059
}
2060
}
2061
return {
2061
- version: 1,
2062
+ version: PROFILER_EXPORT_VERSION,
2063
profilingSummary: getProfilingSummary(rootID),
2064
commitDetails,
2065
interactions: getInteractions(rootID),
src/constants.js
+2
@@ -15,3 +15,5 @@ export const SESSION_STORAGE_LAST_SELECTION_KEY =
15
'React::DevTools::lastSelection';
16
17
export const __DEBUG__ = false;
18
+
19
+export const PROFILER_EXPORT_VERSION = 2;
src/devtools/views/Profiler/CommitFilterModal.css
renamed
src/devtools/views/Profiler/CommitFilterModal.js
renamed
+8
-6
@@ -1,16 +1,13 @@
1
// @flow
2
3
import React, { useCallback, useContext, useEffect, useRef } from 'react';
4
+import { CommitFilterModalContext } from './CommitFilterModalContext';
5
import { ProfilerContext } from './ProfilerContext';
6
import { useModalDismissSignal } from '../hooks';
7
7
-import styles from './FilterModal.css';
8
+import styles from './CommitFilterModal.css';
9
9
-type Props = {|
10
- dismissModal: () => void,
11
-|};
12
-
13
-export default function FilterModal({ dismissModal }: Props) {
10
+export default function FilterModal(_: {||}) {
11
const {
12
isCommitFilterEnabled,
13
minCommitDuration,
@@ -18,6 +15,11 @@ export default function FilterModal({ dismissModal }: Props) {
15
setMinCommitDuration,
16
} = useContext(ProfilerContext);
17
18
+ const { setIsModalShowing } = useContext(CommitFilterModalContext);
19
+ const dismissModal = useCallback(() => setIsModalShowing(false), [
20
+ setIsModalShowing,
21
+ ]);
22
+
23
const handleNumberChange = useCallback(
24
(event: SyntheticEvent<HTMLInputElement>) => {
25
const newValue = parseFloat(event.currentTarget.value);
src/devtools/views/Profiler/CommitFilterModalContext.js
+6
-6
@@ -3,8 +3,8 @@
3
import React, { createContext, useMemo, useState } from 'react';
4
5
type Context = {|
6
- isFilterModalShowing: boolean,
7
- setIsFilterModalShowing: (value: boolean) => void,
6
+ isModalShowing: boolean,
7
+ setIsModalShowing: (value: boolean) => void,
8
|};
9
10
const CommitFilterModalContext = createContext<Context>(((null: any): Context));
@@ -15,14 +15,14 @@ type Props = {|
15
|};
16
17
function CommitFilterModalContextController({ children }: Props) {
18
- const [isFilterModalShowing, setIsFilterModalShowing] = useState(false);
18
+ const [isModalShowing, setIsModalShowing] = useState(false);
19
20
const value = useMemo(
21
() => ({
22
- isFilterModalShowing,
23
- setIsFilterModalShowing,
22
+ isModalShowing,
23
+ setIsModalShowing,
24
}),
25
- [isFilterModalShowing]
25
+ [isModalShowing]
26
);
27
28
return (
src/devtools/views/Profiler/ImportFailedModal.css
new
+39
@@ -0,0 +1,39 @@
1
+.Background {
2
+ position: absolute;
3
+ width: 100%;
4
+ height: 100%;
5
+ display: flex;
6
+ align-items: flex-start;
7
+ justify-content: center;
8
+ padding: 1rem;
9
+ background-color: var(--color-modal-background);
10
+ overflow: auto;
11
+}
12
+
13
+.Dialog {
14
+ position: relative;
15
+ z-index: 3;
16
+ min-width: 20rem;
17
+ max-width: 100%;
18
+ display: inline-block;
19
+ background-color: var(--color-background);
20
+ padding: 0.5rem;
21
+ border: 1px solid var(--color-border);
22
+ border-radius: 0.25rem;
23
+}
24
+
25
+.Header {
26
+ font-size: var(--font-size-sans-large);
27
+ margin-bottom: 0.5rem;
28
+}
29
+
30
+.Buttons {
31
+ text-align: right;
32
+}
33
+
34
+.ErrorMessage {
35
+ margin: 0.5rem 0;
36
+ color: var(--color-dim);
37
+ font-family: var(--font-family-monospace);
38
+ font-size: var(--font-size-monospace-normal);
39
+}
src/devtools/views/Profiler/ImportFailedModal.js
new
+35
@@ -0,0 +1,35 @@
1
+// @flow
2
+
3
+import React, { useCallback, useContext, useRef } from 'react';
4
+import { ImportFailedModalContext } from './ImportFailedModalContext';
5
+import Button from '../Button';
6
+import { useModalDismissSignal } from '../hooks';
7
+
8
+import styles from './ImportFailedModal.css';
9
+
10
+export default function ImportFailedModal(_: {||}) {
11
+ const { importError, setImportError } = useContext(ImportFailedModalContext);
12
+ const dismissModal = useCallback(() => setImportError(null), [
13
+ setImportError,
14
+ ]);
15
+ const modalRef = useRef<HTMLDivElement | null>(null);
16
+
17
+ useModalDismissSignal(modalRef, dismissModal);
18
+
19
+ return (
20
+ <div className={styles.Background}>
21
+ <div className={styles.Dialog} ref={modalRef}>
22
+ <div className={styles.Header}>Import failed</div>
23
+ <div>The profiling data you selected cannot be imported.</div>
24
+ {importError !== null && (
25
+ <div className={styles.ErrorMessage}>{importError.message}</div>
26
+ )}
27
+ <div className={styles.Buttons}>
28
+ <Button autoFocus onClick={dismissModal}>
29
+ Okay
30
+ </Button>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ );
35
+}
src/devtools/views/Profiler/ImportFailedModalContext.js
new
+37
@@ -0,0 +1,37 @@
1
+// @flow
2
+
3
+import React, { createContext, useMemo, useState } from 'react';
4
+
5
+type Context = {|
6
+ importError: Error | null,
7
+ isModalShowing: boolean,
8
+ setImportError: (error: Error | null) => void,
9
+|};
10
+
11
+const ImportFailedModalContext = createContext<Context>(((null: any): Context));
12
+ImportFailedModalContext.displayName = 'ImportFailedModalContext';
13
+
14
+type Props = {|
15
+ children: React$Node,
16
+|};
17
+
18
+function ImportFailedModalContextController({ children }: Props) {
19
+ const [importError, setImportError] = useState<Error | null>(null);
20
+
21
+ const value = useMemo(
22
+ () => ({
23
+ importError,
24
+ isModalShowing: !!importError,
25
+ setImportError,
26
+ }),
27
+ [importError]
28
+ );
29
+
30
+ return (
31
+ <ImportFailedModalContext.Provider value={value}>
32
+ {children}
33
+ </ImportFailedModalContext.Provider>
34
+ );
35
+}
36
+
37
+export { ImportFailedModalContext, ImportFailedModalContextController };
src/devtools/views/Profiler/Profiler.js
+32
-17
@@ -1,16 +1,21 @@
1
// @flow
2
3
-import React, { Suspense, useCallback, useContext } from 'react';
3
+import React, { Suspense, useContext } from 'react';
4
import {
5
CommitFilterModalContext,
6
CommitFilterModalContextController,
7
} from './CommitFilterModalContext';
8
+import {
9
+ ImportFailedModalContext,
10
+ ImportFailedModalContextController,
11
+} from './ImportFailedModalContext';
12
import { ProfilerContext } from './ProfilerContext';
13
import TabBar from '../TabBar';
14
import ClearProfilingDataButton from './ClearProfilingDataButton';
15
import CommitFlamegraph from './CommitFlamegraph';
16
import CommitRanked from './CommitRanked';
13
-import FilterModal from './FilterModal';
17
+import CommitFilterModal from './CommitFilterModal';
18
+import ImportFailedModal from './ImportFailedModal';
19
import Interactions from './Interactions';
20
import RecordToggle from './RecordToggle';
21
import ReloadAndProfileButton from './ReloadAndProfileButton';
@@ -35,16 +40,20 @@ function Profiler({ supportsProfiling }: Props) {
40
41
if (isProfiling || !rootHasProfilingData) {
42
return (
38
- <NonSuspendingProfiler
39
- hasProfilingData={hasProfilingData}
40
- isProfiling={isProfiling}
41
- supportsProfiling={supportsProfiling}
42
- />
43
+ <ImportFailedModalContextController>
44
+ <NonSuspendingProfiler
45
+ hasProfilingData={hasProfilingData}
46
+ isProfiling={isProfiling}
47
+ supportsProfiling={supportsProfiling}
48
+ />
49
+ </ImportFailedModalContextController>
50
);
51
} else {
52
return (
53
<CommitFilterModalContextController>
47
- <SuspendingProfiler />
54
+ <ImportFailedModalContextController>
55
+ <SuspendingProfiler />
56
+ </ImportFailedModalContextController>
57
</CommitFilterModalContextController>
58
);
59
}
@@ -74,6 +83,10 @@ function NonSuspendingProfiler({
83
view = <NoProfilingDataForRoot />;
84
}
85
86
+ const { isModalShowing: isImportFailedModalShowing } = useContext(
87
+ ImportFailedModalContext
88
+ );
89
+
90
return (
91
<div className={styles.Profiler}>
92
<div className={styles.LeftColumn}>
@@ -92,7 +105,10 @@ function NonSuspendingProfiler({
105
tabs={tabs}
106
/>
107
</div>
95
- <div className={styles.Content}>{view}</div>
108
+ <div className={styles.Content}>
109
+ {view}
110
+ {isImportFailedModalShowing && <ImportFailedModal />}
111
+ </div>
112
</div>
113
</div>
114
);
@@ -113,13 +129,13 @@ function SuspendingProfiler() {
129
const { selectedFiberID, selectedTabID, selectTab } = useContext(
130
ProfilerContext
131
);
116
- const { isFilterModalShowing, setIsFilterModalShowing } = useContext(
132
+
133
+ const { isModalShowing: isFilterModalShowing } = useContext(
134
CommitFilterModalContext
135
);
119
-
120
- const dismissFilterModal = useCallback(() => setIsFilterModalShowing(false), [
121
- setIsFilterModalShowing,
122
- ]);
136
+ const { isModalShowing: isImportFailedModalShowing } = useContext(
137
+ ImportFailedModalContext
138
+ );
139
140
let view = null;
141
switch (selectedTabID) {
@@ -178,9 +194,8 @@ function SuspendingProfiler() {
194
</div>
195
<div className={styles.Content}>
196
<Suspense fallback={<ContentFallback />}>{view}</Suspense>
181
- {isFilterModalShowing && (
182
- <FilterModal dismissModal={dismissFilterModal} />
183
- )}
197
+ {isFilterModalShowing && <CommitFilterModal />}
198
+ {isImportFailedModalShowing && <ImportFailedModal />}
199
</div>
200
</div>
201
<div className={styles.RightColumn}>
src/devtools/views/Profiler/ProfilingImportExportButtons.js
+11
-6
@@ -2,6 +2,7 @@
2
3
import React, { Fragment, useContext, useCallback, useRef } from 'react';
4
import { ProfilerContext } from './ProfilerContext';
5
+import { ImportFailedModalContext } from './ImportFailedModalContext';
6
import Button from '../Button';
7
import ButtonIcon from '../ButtonIcon';
8
import { BridgeContext, StoreContext } from '../context';
@@ -20,6 +21,8 @@ export default function ProfilingImportExportButtons() {
21
22
const inputRef = useRef<HTMLInputElement | null>(null);
23
24
+ const { setImportError } = useContext(ImportFailedModalContext);
25
+
26
const downloadData = useCallback(() => {
27
if (rendererID === null || rootID === null) {
28
return;
@@ -53,16 +56,18 @@ export default function ProfilingImportExportButtons() {
56
if (input !== null && input.files.length > 0) {
57
const fileReader = new FileReader();
58
fileReader.addEventListener('load', () => {
56
- const raw = ((fileReader.result: any): string);
57
- const data = prepareProfilingImport(raw);
58
-
59
- // TODO (profiling) Catch possible version check error and show dialog.
59
+ try {
60
+ const raw = ((fileReader.result: any): string);
61
+ const data = prepareProfilingImport(raw);
62
61
- store.importedProfilingData = ((data: any): ImportedProfilingData);
63
+ store.importedProfilingData = ((data: any): ImportedProfilingData);
64
+ } catch (error) {
65
+ setImportError(error);
66
+ }
67
});
68
fileReader.readAsText(input.files[0]);
69
}
65
- }, [store]);
70
+ }, [store, setImportError]);
71
72
return (
73
<Fragment>
src/devtools/views/Profiler/ToggleCommitFilterModalButton.js
+3
-3
@@ -6,10 +6,10 @@ import Button from '../Button';
6
import ButtonIcon from '../ButtonIcon';
7
8
export default function ToggleCommitFilterModalButton() {
9
- const { setIsFilterModalShowing } = useContext(CommitFilterModalContext);
9
+ const { setIsModalShowing } = useContext(CommitFilterModalContext);
10
11
- const showFilterModal = useCallback(() => setIsFilterModalShowing(true), [
12
- setIsFilterModalShowing,
11
+ const showFilterModal = useCallback(() => setIsModalShowing(true), [
12
+ setIsModalShowing,
13
]);
14
15
return (
src/devtools/views/Profiler/utils.js
+5
-1
@@ -1,5 +1,7 @@
1
// @flow
2
3
+import { PROFILER_EXPORT_VERSION } from 'src/constants';
4
+
5
import type {
6
CommitDetailsFrontend,
7
CommitTreeFrontend,
@@ -82,7 +84,9 @@ export const prepareProfilingExport = (
84
export const prepareProfilingImport = (raw: string) => {
85
const parsed = JSON.parse(raw);
86
85
- // TODO (profiling) Version check; throw if older version.
87
+ if (parsed.version !== PROFILER_EXPORT_VERSION) {
88
+ throw Error(`Unsupported profiler export version "${parsed.version}".`);
89
+ }
90
91
const entries = [];
92
Object.values(parsed.profilingSnapshots).forEach(snapshot => {