diff --git a/src/App.js b/src/App.js index 16ccd36a4..c7a193214 100644 --- a/src/App.js +++ b/src/App.js @@ -15,6 +15,7 @@ import BugReporterDialog from './chrome/BugReporterDialog.js'; import ErrorBar from './chrome/ErrorBar.js'; import ShareSheet from './chrome/ShareSheet.js'; import SignInSheet from './chrome/SignInSheet.js'; +import ShareSheetExportFile from './chrome/ShareSheetExportFile.js'; import PluginContainer from './PluginContainer.js'; import Sheet from './chrome/Sheet.js'; import {ipcRenderer, remote} from 'electron'; @@ -24,6 +25,7 @@ import { ACTIVE_SHEET_PLUGIN_DEBUGGER, ACTIVE_SHEET_SHARE_DATA, ACTIVE_SHEET_SIGN_IN, + ACTIVE_SHEET_SHARE_DATA_IN_FILE, } from './reducers/application.js'; import type {Logger} from './fb-interfaces/Logger.js'; @@ -44,6 +46,7 @@ type Props = {| selectedDevice: ?BaseDevice, error: ?string, activeSheet: ActiveSheet, + exportFile: ?string, |}; export class App extends React.Component { @@ -76,6 +79,12 @@ export class App extends React.Component { return ; } else if (this.props.activeSheet === ACTIVE_SHEET_SIGN_IN) { return ; + } else if (this.props.activeSheet === ACTIVE_SHEET_SHARE_DATA_IN_FILE) { + const {exportFile} = this.props; + if (!exportFile) { + throw new Error('Tried to export data without passing the file path'); + } + return ; } else { // contents are added via React.Portal return null; @@ -103,12 +112,13 @@ export class App extends React.Component { export default connect( ({ - application: {leftSidebarVisible, activeSheet}, + application: {leftSidebarVisible, activeSheet, exportFile}, connections: {selectedDevice, error}, }) => ({ leftSidebarVisible, selectedDevice, activeSheet, + exportFile, error, }), )(App); diff --git a/src/MenuBar.js b/src/MenuBar.js index 4578910a5..4a3381861 100644 --- a/src/MenuBar.js +++ b/src/MenuBar.js @@ -6,22 +6,20 @@ */ import type {FlipperPlugin, FlipperDevicePlugin} from './plugin.js'; +import {showOpenDialog} from './utils/exportData.js'; import { - exportStoreToFile, - showOpenDialog, - EXPORT_FLIPPER_TRACE_EVENT, -} from './utils/exportData.js'; -import {setActiveSheet, ACTIVE_SHEET_SHARE_DATA} from './reducers/application'; + setExportDataToFileActiveSheet, + setActiveSheet, + ACTIVE_SHEET_SHARE_DATA, +} from './reducers/application'; import type {Store} from './reducers/'; import electron from 'electron'; import {ENABLE_SHAREABLE_LINK} from 'flipper'; -import {remote} from 'electron'; -const {dialog} = remote; -import os from 'os'; -import path from 'path'; -import {reportPlatformFailures} from './utils/metrics'; export type DefaultKeyboardAction = 'clear' | 'goToBottom' | 'createPaste'; export type TopLevelMenu = 'Edit' | 'View' | 'Window' | 'Help'; +const {dialog} = electron.remote; +import os from 'os'; +import path from 'path'; type MenuItem = {| label?: string, @@ -199,11 +197,11 @@ function getTemplate( title: 'FlipperExport', defaultPath: path.join(os.homedir(), 'FlipperExport.flipper'), }, - file => { - reportPlatformFailures( - exportStoreToFile(file, store), - `${EXPORT_FLIPPER_TRACE_EVENT}:UI`, - ); + async file => { + if (!file) { + return; + } + store.dispatch(setExportDataToFileActiveSheet(file)); }, ); }, diff --git a/src/__tests__/App.electron.js b/src/__tests__/App.electron.js index dece90a78..163fd428b 100644 --- a/src/__tests__/App.electron.js +++ b/src/__tests__/App.electron.js @@ -29,6 +29,7 @@ test('Empty app state matches snapshot', () => { selectedDevice={null} error={null} activeSheet={null} + exportFile={null} /> , ); diff --git a/src/chrome/ShareSheetExportFile.js b/src/chrome/ShareSheetExportFile.js new file mode 100644 index 000000000..231313fd4 --- /dev/null +++ b/src/chrome/ShareSheetExportFile.js @@ -0,0 +1,143 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + */ +import { + FlexColumn, + Button, + styled, + colors, + Text, + LoadingIndicator, + Component, + FlexRow, + Spacer, +} from 'flipper'; +import {reportPlatformFailures} from '../utils/metrics'; +import { + exportStoreToFile, + EXPORT_FLIPPER_TRACE_EVENT, +} from '../utils/exportData.js'; +import PropTypes from 'prop-types'; + +const Container = styled(FlexColumn)({ + padding: 20, + width: 500, +}); + +const Center = styled(FlexColumn)({ + alignItems: 'center', + paddingTop: 50, + paddingBottom: 50, +}); + +const Uploading = styled(Text)({ + marginTop: 15, +}); + +const ErrorMessage = styled(Text)({ + display: 'block', + marginTop: 6, + wordBreak: 'break-all', + whiteSpace: 'pre-line', + lineHeight: 1.35, +}); + +const Title = styled(Text)({ + marginBottom: 6, +}); + +const InfoText = styled(Text)({ + lineHeight: 1.35, + marginBottom: 15, +}); + +type Props = { + onHide: () => mixed, + file: string, +}; +type State = { + result: ?{ + success: boolean, + error: ?Error, + }, +}; + +export default class ShareSheetExportFile extends Component { + static contextTypes = { + store: PropTypes.object.isRequired, + }; + + state = { + result: null, + }; + + async componentDidMount() { + try { + await reportPlatformFailures( + exportStoreToFile(this.props.file, this.context.store), + `${EXPORT_FLIPPER_TRACE_EVENT}:UI`, + ); + this.setState({result: {success: true, error: null}}); + } catch (err) { + this.setState({result: {success: false, error: err}}); + } + } + + render() { + const {result} = this.state; + if (result) { + const {success, error} = result; + if (success) { + return ( + + + Data Exported Successfully + + When sharing your Flipper data, consider that the captured data + might contain sensitive information like access tokens used in + network requests. + + + + + + + + ); + } + if (error) { + return ( + + Error + + {error?.message || 'File could not be saved.'} + + + + + + + ); + } + return null; + } else { + return ( + +
+ + + Exporting Flipper trace... + +
+
+ ); + } + } +} diff --git a/src/reducers/application.js b/src/reducers/application.js index 128ededc9..3df49e1e2 100644 --- a/src/reducers/application.js +++ b/src/reducers/application.js @@ -14,6 +14,8 @@ export const ACTIVE_SHEET_PLUGIN_DEBUGGER: 'PLUGIN_DEBUGGER' = 'PLUGIN_DEBUGGER'; export const ACTIVE_SHEET_SHARE_DATA: 'SHARE_DATA' = 'SHARE_DATA'; export const ACTIVE_SHEET_SIGN_IN: 'SIGN_IN' = 'SIGN_IN'; +export const ACTIVE_SHEET_SHARE_DATA_IN_FILE: 'SHARE_DATA_IN_FILE' = + 'SHARE_DATA_IN_FILE'; export type ActiveSheet = | typeof ACTIVE_SHEET_PLUGIN_SHEET @@ -21,6 +23,7 @@ export type ActiveSheet = | typeof ACTIVE_SHEET_PLUGIN_DEBUGGER | typeof ACTIVE_SHEET_SHARE_DATA | typeof ACTIVE_SHEET_SIGN_IN + | typeof ACTIVE_SHEET_SHARE_DATA_IN_FILE | null; export type State = { @@ -29,6 +32,7 @@ export type State = { rightSidebarAvailable: boolean, windowIsFocused: boolean, activeSheet: ActiveSheet, + exportFile: ?string, sessionId: ?string, serverPorts: { insecure: number, @@ -53,6 +57,10 @@ export type Action = type: 'SET_ACTIVE_SHEET', payload: ActiveSheet, } + | { + type: typeof ACTIVE_SHEET_SHARE_DATA_IN_FILE, + payload: {file: string}, + } | { type: 'SET_SERVER_PORTS', payload: { @@ -67,6 +75,7 @@ const initialState: () => State = () => ({ rightSidebarAvailable: false, windowIsFocused: remote.getCurrentWindow().isFocused(), activeSheet: null, + exportFile: null, sessionId: uuidv1(), serverPorts: { insecure: 8089, @@ -103,6 +112,12 @@ export default function reducer(state: State, action: Action): State { ...state, activeSheet: action.payload, }; + } else if (action.type === ACTIVE_SHEET_SHARE_DATA_IN_FILE) { + return { + ...state, + activeSheet: ACTIVE_SHEET_SHARE_DATA_IN_FILE, + exportFile: action.payload.file, + }; } if (action.type === 'SET_SERVER_PORTS') { return { @@ -122,6 +137,11 @@ export const toggleAction = ( payload, }); +export const setExportDataToFileActiveSheet = (file: string): Action => ({ + type: ACTIVE_SHEET_SHARE_DATA_IN_FILE, + payload: {file}, +}); + export const setActiveSheet = (payload: ActiveSheet): Action => ({ type: 'SET_ACTIVE_SHEET', payload,