| 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 | } |