url handler downloads

Summary:
Adding support for downloading archived Flipper data using a URL handler.
A URL looks like `flipper://import/?url=` and will download the file specified in the url param. While downloading the file, a spinner is shown in the app's titlebar.

Reviewed By: jknoxville

Differential Revision: D14262763

fbshipit-source-id: 6538fc78c07a48cef7b71b3f7bdbcb712d054593
This commit is contained in:
Daniel Büchele
2019-03-01 04:27:27 -08:00
committed by Facebook Github Bot
parent 3e336d2349
commit 79124891a9
10 changed files with 164 additions and 80 deletions

33
flow-typed/npm/query-string_v6.x.x.js vendored Normal file
View File

@@ -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>(A, B) => number,
|}
declare type ObjectParameter = string | number | boolean | null | void;
declare type ObjectParameters = {
[string]: ObjectParameter | $ReadOnlyArray<ObjectParameter>
}
declare type QueryParameters = {
[string]: string | Array<string> | 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,
}
}

View File

@@ -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",

View File

@@ -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<Props> {
render() {
return (
<AppTitleBar focused={this.props.windowIsFocused} className="toolbar">
<DevicesButton />
<ScreenCaptureButtons />
{this.props.downloadingImportData && (
<Importing>
<LoadingIndicator size={16} />&nbsp;Importing data...
</Importing>
)}
<Spacer />
<VersionText>
{this.props.version}
{isProduction() ? '' : '-dev'}
</VersionText>
{isAutoUpdaterEnabled() ? (
<AutoUpdateVersion version={this.props.version} />
) : null}
@@ -125,12 +139,14 @@ export default connect<Props, OwnProps, _, _, _, _>(
leftSidebarVisible,
rightSidebarVisible,
rightSidebarAvailable,
downloadingImportData,
},
}) => ({
windowIsFocused,
leftSidebarVisible,
rightSidebarVisible,
rightSidebarAvailable,
downloadingImportData,
}),
{
setActiveSheet,

View File

@@ -21,6 +21,7 @@ test('TitleBar is rendered', () => {
leftSidebarVisible={false}
rightSidebarVisible={false}
rightSidebarAvailable={false}
downloadingImportData={false}
toggleLeftSidebarVisible={() => {}}
toggleRightSidebarVisible={() => {}}
setActiveSheet={_sheet => {}}

View File

@@ -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://<client>/<pluginId>/<payload>
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://<client>/<pluginId>/<payload>
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://<client>/<pluginId>/<payload>
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) {

View File

@@ -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,

View File

@@ -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'

View File

@@ -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);
});
};

View File

@@ -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) {

View File

@@ -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"