diff --git a/flow-typed/npm/query-string_v6.x.x.js b/flow-typed/npm/query-string_v6.x.x.js new file mode 100644 index 000000000..852eb52be --- /dev/null +++ b/flow-typed/npm/query-string_v6.x.x.js @@ -0,0 +1,33 @@ +// flow-typed signature: 9096718d5c8abc7d9ae9aadbe0ce565a +// flow-typed version: 6d8676cf5a/query-string_v6.x.x/flow_>=v0.32.x + +declare module 'query-string' { + declare type ArrayFormat = 'none' | 'bracket' | 'index' + declare type ParseOptions = {| + arrayFormat?: ArrayFormat, + |} + + declare type StringifyOptions = {| + arrayFormat?: ArrayFormat, + encode?: boolean, + strict?: boolean, + sort?: false | (A, B) => number, + |} + + declare type ObjectParameter = string | number | boolean | null | void; + + declare type ObjectParameters = { + [string]: ObjectParameter | $ReadOnlyArray + } + + declare type QueryParameters = { + [string]: string | Array | null + } + + declare module.exports: { + extract(str: string): string, + parse(str: string, opts?: ParseOptions): QueryParameters, + parseUrl(str: string, opts?: ParseOptions): { url: string, query: QueryParameters }, + stringify(obj: ObjectParameters, opts?: StringifyOptions): string, + } +} diff --git a/package.json b/package.json index 63849efc1..70344313d 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "pkg": "^4.3.7", "promise-retry": "^1.1.1", "prop-types": "^15.6.0", + "query-string": "^6.2.0", "react": "16", "react-color": "^2.11.7", "react-debounce-render": "^4.0.3", diff --git a/src/chrome/TitleBar.js b/src/chrome/TitleBar.js index cc9c20385..0f5a09ea3 100644 --- a/src/chrome/TitleBar.js +++ b/src/chrome/TitleBar.js @@ -16,6 +16,7 @@ import { Spacer, styled, Text, + LoadingIndicator, } from 'flipper'; import {connect} from 'react-redux'; import { @@ -61,6 +62,7 @@ type Props = {| leftSidebarVisible: boolean, rightSidebarVisible: boolean, rightSidebarAvailable: boolean, + downloadingImportData: boolean, toggleLeftSidebarVisible: (visible?: boolean) => void, toggleRightSidebarVisible: (visible?: boolean) => void, setActiveSheet: (sheet: ActiveSheet) => void, @@ -72,17 +74,29 @@ const VersionText = styled(Text)({ marginTop: 2, }); +const Importing = styled(FlexRow)({ + color: colors.light50, + alignItems: 'center', + marginLeft: 10, +}); + class TitleBar extends Component { render() { return ( + {this.props.downloadingImportData && ( + +  Importing data... + + )} {this.props.version} {isProduction() ? '' : '-dev'} + {isAutoUpdaterEnabled() ? ( ) : null} @@ -125,12 +139,14 @@ export default connect( leftSidebarVisible, rightSidebarVisible, rightSidebarAvailable, + downloadingImportData, }, }) => ({ windowIsFocused, leftSidebarVisible, rightSidebarVisible, rightSidebarAvailable, + downloadingImportData, }), { setActiveSheet, diff --git a/src/chrome/__tests__/TitleBar.electron.js b/src/chrome/__tests__/TitleBar.electron.js index 61be1564d..32badda47 100644 --- a/src/chrome/__tests__/TitleBar.electron.js +++ b/src/chrome/__tests__/TitleBar.electron.js @@ -21,6 +21,7 @@ test('TitleBar is rendered', () => { leftSidebarVisible={false} rightSidebarVisible={false} rightSidebarAvailable={false} + downloadingImportData={false} toggleLeftSidebarVisible={() => {}} toggleRightSidebarVisible={() => {}} setActiveSheet={_sheet => {}} diff --git a/src/dispatcher/application.js b/src/dispatcher/application.js index 1ca86336c..27c6031ed 100644 --- a/src/dispatcher/application.js +++ b/src/dispatcher/application.js @@ -8,9 +8,12 @@ import {remote, ipcRenderer} from 'electron'; import type {Store} from '../reducers/index.js'; import type {Logger} from '../fb-interfaces/Logger.js'; +import {toggleAction} from '../reducers/application'; import {parseFlipperPorts} from '../utils/environmentVariables'; -import {importFileToStore} from '../utils/exportData'; -import {selectPlugin, userPreferredPlugin} from '../reducers/connections'; +import {importDataToStore, importFileToStore} from '../utils/exportData'; +import {selectPlugin} from '../reducers/connections'; +import qs from 'query-string'; + export const uriComponents = (url: string) => { if (!url) { return []; @@ -42,11 +45,29 @@ export default (store: Store, logger: Logger) => { }); }); - ipcRenderer.on('flipper-deeplink', (event, url) => { - // flipper://// + ipcRenderer.on('flipper-protocol-handler', (event, url) => { + if (url.startsWith('flipper://import')) { + const {search} = new URL(url); + const download = qs.parse(search)?.url; + store.dispatch(toggleAction('downloadingImportData', true)); + return ( + download && + fetch(String(download)) + .then(res => res.text()) + .then(data => importDataToStore(data, store)) + .then(() => { + store.dispatch(toggleAction('downloadingImportData', false)); + }) + .catch((e: Error) => { + console.error(e); + store.dispatch(toggleAction('downloadingImportData', false)); + }) + ); + } const match = uriComponents(url); if (match.length > 1) { - store.dispatch( + // flipper://// + return store.dispatch( selectPlugin({ selectedApp: match[0], selectedPlugin: match[1], @@ -60,14 +81,6 @@ export default (store: Store, logger: Logger) => { importFileToStore(url, store); }); - ipcRenderer.on('flipper-deeplink-preferred-plugin', (event, url) => { - // flipper://// - const match = uriComponents(url); - if (match.length > 1) { - store.dispatch(userPreferredPlugin(match[1])); - } - }); - if (process.env.FLIPPER_PORTS) { const portOverrides = parseFlipperPorts(process.env.FLIPPER_PORTS); if (portOverrides) { diff --git a/src/plugins/logs/index.js b/src/plugins/logs/index.js index bdcca1ade..cadd36c82 100644 --- a/src/plugins/logs/index.js +++ b/src/plugins/logs/index.js @@ -316,7 +316,6 @@ export function processEntry( entry: DeviceLogEntry, } { const {icon, style} = LOG_TYPES[(entry.type: string)] || LOG_TYPES.debug; - // build the item, it will either be batched or added straight away return { entry, diff --git a/src/reducers/application.js b/src/reducers/application.js index 1c8d4be2c..4a551e45f 100644 --- a/src/reducers/application.js +++ b/src/reducers/application.js @@ -30,13 +30,15 @@ export type State = { insecure: number, secure: number, }, + downloadingImportData: boolean, }; type BooleanActionType = | 'leftSidebarVisible' | 'rightSidebarVisible' | 'rightSidebarAvailable' - | 'windowIsFocused'; + | 'windowIsFocused' + | 'downloadingImportData'; export type Action = | { @@ -66,6 +68,7 @@ const initialState: () => State = () => ({ insecure: 8089, secure: 8088, }, + downloadingImportData: false, }); export default function reducer(state: State, action: Action): State { @@ -74,7 +77,8 @@ export default function reducer(state: State, action: Action): State { action.type === 'leftSidebarVisible' || action.type === 'rightSidebarVisible' || action.type === 'rightSidebarAvailable' || - action.type === 'windowIsFocused' + action.type === 'windowIsFocused' || + action.type === 'downloadingImportData' ) { const newValue = typeof action.payload === 'undefined' diff --git a/src/utils/exportData.js b/src/utils/exportData.js index 4ad9a25d7..ae0ffbc1d 100644 --- a/src/utils/exportData.js +++ b/src/utils/exportData.js @@ -244,73 +244,77 @@ export const exportStoreToFile = ( }); }; +export function importDataToStore(data: string, store: Store) { + const json = deserialize(data); + const {device, clients} = json; + const {serial, deviceType, title, os, logs} = device; + const archivedDevice = new ArchivedDevice( + serial, + deviceType, + title, + os, + logs ? logs : [], + ); + const devices = store.getState().connections.devices; + const matchedDevices = devices.filter( + availableDevice => availableDevice.serial === serial, + ); + if (matchedDevices.length > 0) { + store.dispatch({ + type: 'SELECT_DEVICE', + payload: matchedDevices[0], + }); + return; + } + store.dispatch({ + type: 'REGISTER_DEVICE', + payload: archivedDevice, + }); + store.dispatch({ + type: 'SELECT_DEVICE', + payload: archivedDevice, + }); + + const {pluginStates} = json.store; + const keys = Object.keys(pluginStates); + clients.forEach(client => { + const clientPlugins = keys + .filter(key => { + const arr = key.split('#'); + arr.pop(); + const clientPlugin = arr.join('#'); + return client.id === clientPlugin; + }) + .map(client => client.split('#').pop()); + store.dispatch({ + type: 'NEW_CLIENT', + payload: new Client( + client.id, + client.query, + null, + getInstance(), + store, + clientPlugins, + ), + }); + }); + keys.forEach(key => { + store.dispatch({ + type: 'SET_PLUGIN_STATE', + payload: { + pluginKey: key, + state: pluginStates[key], + }, + }); + }); +} + export const importFileToStore = (file: string, store: Store) => { fs.readFile(file, 'utf8', (err, data) => { if (err) { console.error(err); return; } - const json = deserialize(data); - const {device, clients} = json; - const {serial, deviceType, title, os, logs} = device; - const archivedDevice = new ArchivedDevice( - serial, - deviceType, - title, - os, - logs ? logs : [], - ); - const devices = store.getState().connections.devices; - const matchedDevices = devices.filter( - availableDevice => availableDevice.serial === serial, - ); - if (matchedDevices.length > 0) { - store.dispatch({ - type: 'SELECT_DEVICE', - payload: matchedDevices[0], - }); - return; - } - store.dispatch({ - type: 'REGISTER_DEVICE', - payload: archivedDevice, - }); - store.dispatch({ - type: 'SELECT_DEVICE', - payload: archivedDevice, - }); - - const {pluginStates} = json.store; - const keys = Object.keys(pluginStates); - clients.forEach(client => { - const clientPlugins = keys - .filter(key => { - const arr = key.split('#'); - arr.pop(); - const clientPlugin = arr.join('#'); - return client.id === clientPlugin; - }) - .map(client => client.split('#').pop()); - store.dispatch({ - type: 'NEW_CLIENT', - payload: new Client( - client.id, - client.query, - null, - getInstance(), - store, - clientPlugins, - ), - }); - }); - keys.forEach(key => { - store.dispatch({ - type: 'SET_PLUGIN_STATE', - payload: { - pluginKey: key, - state: pluginStates[key], - }, - }); - }); + importDataToStore(data, store); }); }; diff --git a/static/index.js b/static/index.js index a22252936..d424932d3 100644 --- a/static/index.js +++ b/static/index.js @@ -131,7 +131,7 @@ app.on('will-finish-launching', () => { event.preventDefault(); deeplinkURL = url; if (win) { - win.webContents.send('flipper-deeplink', deeplinkURL); + win.webContents.send('flipper-protocol-handler', deeplinkURL); } }); app.on('open-file', (event, path) => { @@ -163,7 +163,7 @@ app.on('ready', function() { ipcMain.on('componentDidMount', event => { if (deeplinkURL) { - win.webContents.send('flipper-deeplink-preferred-plugin', deeplinkURL); + win.webContents.send('flipper-protocol-handler', deeplinkURL); deeplinkURL = null; } if (filePath) { diff --git a/yarn.lock b/yarn.lock index 2941cdb73..f60fda87a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5785,6 +5785,14 @@ qs@~6.5.1, qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" +query-string@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.2.0.tgz#468edeb542b7e0538f9f9b1aeb26f034f19c86e1" + integrity sha512-5wupExkIt8RYL4h/FE+WTg3JHk62e6fFPWtAZA9J5IWK1PfTfKkMS93HBUHcFpeYi9KsY5pFbh+ldvEyaz5MyA== + dependencies: + decode-uri-component "^0.2.0" + strict-uri-encode "^2.0.0" + querystring@0.2.0, querystring@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" @@ -6863,6 +6871,11 @@ stream-meter@1.0.4: dependencies: readable-stream "^2.1.4" +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= + string-length@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"