main
js 158 lines 4.62 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import * as React from 'react';
11 import {Fragment, useContext, useCallback, useRef} from 'react';
12 import {ProfilerContext} from './ProfilerContext';
13 import {ModalDialogContext} from '../ModalDialog';
14 import Button from '../Button';
15 import ButtonIcon from '../ButtonIcon';
16 import {StoreContext} from '../context';
17 import {
18 prepareProfilingDataExport,
19 prepareProfilingDataFrontendFromExport,
20 } from './utils';
21 import {downloadFile} from '../utils';
22 import isArray from 'shared/isArray';
23 import hasOwnProperty from 'shared/hasOwnProperty';
24
25 import styles from './ProfilingImportExportButtons.css';
26
27 import type {ProfilingDataExport} from './types';
28
29 export default function ProfilingImportExportButtons(): React.Node {
30 const {isProfiling, profilingData, rootID} = useContext(ProfilerContext);
31 const store = useContext(StoreContext);
32 const {profilerStore} = store;
33
34 const inputRef = useRef<HTMLInputElement | null>(null);
35 const downloadRef = useRef<HTMLAnchorElement | null>(null);
36
37 const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);
38
39 const doesHaveInMemoryData = profilerStore.didRecordCommits;
40
41 const downloadData = useCallback(() => {
42 if (rootID === null) {
43 return;
44 }
45
46 const anchorElement = downloadRef.current;
47
48 if (profilingData !== null && anchorElement !== null) {
49 const profilingDataExport = prepareProfilingDataExport(profilingData);
50 const date = new Date();
51 const dateString = date
52 .toLocaleDateString(undefined, {
53 year: 'numeric',
54 month: '2-digit',
55 day: '2-digit',
56 })
57 .replace(/\//g, '-');
58 const timeString = date
59 .toLocaleTimeString(undefined, {
60 hour12: false,
61 })
62 .replace(/:/g, '-');
63 downloadFile(
64 anchorElement,
65 `profiling-data.${dateString}.${timeString}.json`,
66 JSON.stringify(profilingDataExport, null, 2),
67 );
68 }
69 }, [rootID, profilingData]);
70
71 const clickInputElement = useCallback(() => {
72 if (inputRef.current !== null) {
73 inputRef.current.click();
74 }
75 }, []);
76
77 const showImportError = (message: string | null) => {
78 modalDialogDispatch({
79 id: 'ProfilingImportExportButtons',
80 type: 'SHOW',
81 title: 'Import failed',
82 content: (
83 <Fragment>
84 <div>The profiling data you selected cannot be imported.</div>
85 {message !== null && (
86 <div className={styles.ErrorMessage}>{message}</div>
87 )}
88 </Fragment>
89 ),
90 });
91 };
92
93 // TODO (profiling) We should probably use a transition for this and suspend while loading the file.
94 // Local files load so fast it's probably not very noticeable though.
95 const handleChange = () => {
96 const input = inputRef.current;
97 if (input !== null && input.files.length > 0) {
98 const file = input.files[0];
99
100 // TODO (profiling) Handle fileReader errors.
101 const fileReader = new FileReader();
102 fileReader.addEventListener('load', () => {
103 const raw = fileReader.result as any as string;
104
105 let json;
106 try {
107 json = JSON.parse(raw);
108 } catch (error) {
109 showImportError(error !== null ? error.message : null);
110 return;
111 }
112
113 if (isArray(json) || !hasOwnProperty.call(json, 'version')) {
114 showImportError(
115 'This file does not look like a profile exported from the React DevTools profiler.',
116 );
117 return;
118 }
119
120 try {
121 const profilingDataExport = json as any as ProfilingDataExport;
122 profilerStore.profilingData =
123 prepareProfilingDataFrontendFromExport(profilingDataExport);
124 } catch (error) {
125 showImportError(error !== null ? error.message : null);
126 }
127 });
128 fileReader.readAsText(file);
129 }
130 };
131
132 return (
133 <Fragment>
134 <div className={styles.VRule} />
135 <input
136 ref={inputRef}
137 className={styles.Input}
138 type="file"
139 accept=".json"
140 onChange={handleChange}
141 tabIndex={-1}
142 />
143 <a ref={downloadRef} className={styles.Input} />
144 <Button
145 disabled={isProfiling}
146 onClick={clickInputElement}
147 title="Load profile...">
148 <ButtonIcon type="import" />
149 </Button>
150 <Button
151 disabled={isProfiling || !doesHaveInMemoryData}
152 onClick={downloadData}
153 title="Save profile...">
154 <ButtonIcon type="export" />
155 </Button>
156 </Fragment>
157 );
158 }