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:
committed by
Facebook Github Bot
parent
3e336d2349
commit
79124891a9
33
flow-typed/npm/query-string_v6.x.x.js
vendored
Normal file
33
flow-typed/npm/query-string_v6.x.x.js
vendored
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,6 +92,7 @@
|
|||||||
"pkg": "^4.3.7",
|
"pkg": "^4.3.7",
|
||||||
"promise-retry": "^1.1.1",
|
"promise-retry": "^1.1.1",
|
||||||
"prop-types": "^15.6.0",
|
"prop-types": "^15.6.0",
|
||||||
|
"query-string": "^6.2.0",
|
||||||
"react": "16",
|
"react": "16",
|
||||||
"react-color": "^2.11.7",
|
"react-color": "^2.11.7",
|
||||||
"react-debounce-render": "^4.0.3",
|
"react-debounce-render": "^4.0.3",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
Spacer,
|
Spacer,
|
||||||
styled,
|
styled,
|
||||||
Text,
|
Text,
|
||||||
|
LoadingIndicator,
|
||||||
} from 'flipper';
|
} from 'flipper';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import {
|
import {
|
||||||
@@ -61,6 +62,7 @@ type Props = {|
|
|||||||
leftSidebarVisible: boolean,
|
leftSidebarVisible: boolean,
|
||||||
rightSidebarVisible: boolean,
|
rightSidebarVisible: boolean,
|
||||||
rightSidebarAvailable: boolean,
|
rightSidebarAvailable: boolean,
|
||||||
|
downloadingImportData: boolean,
|
||||||
toggleLeftSidebarVisible: (visible?: boolean) => void,
|
toggleLeftSidebarVisible: (visible?: boolean) => void,
|
||||||
toggleRightSidebarVisible: (visible?: boolean) => void,
|
toggleRightSidebarVisible: (visible?: boolean) => void,
|
||||||
setActiveSheet: (sheet: ActiveSheet) => void,
|
setActiveSheet: (sheet: ActiveSheet) => void,
|
||||||
@@ -72,17 +74,29 @@ const VersionText = styled(Text)({
|
|||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const Importing = styled(FlexRow)({
|
||||||
|
color: colors.light50,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginLeft: 10,
|
||||||
|
});
|
||||||
|
|
||||||
class TitleBar extends Component<Props> {
|
class TitleBar extends Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<AppTitleBar focused={this.props.windowIsFocused} className="toolbar">
|
<AppTitleBar focused={this.props.windowIsFocused} className="toolbar">
|
||||||
<DevicesButton />
|
<DevicesButton />
|
||||||
<ScreenCaptureButtons />
|
<ScreenCaptureButtons />
|
||||||
|
{this.props.downloadingImportData && (
|
||||||
|
<Importing>
|
||||||
|
<LoadingIndicator size={16} /> Importing data...
|
||||||
|
</Importing>
|
||||||
|
)}
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<VersionText>
|
<VersionText>
|
||||||
{this.props.version}
|
{this.props.version}
|
||||||
{isProduction() ? '' : '-dev'}
|
{isProduction() ? '' : '-dev'}
|
||||||
</VersionText>
|
</VersionText>
|
||||||
|
|
||||||
{isAutoUpdaterEnabled() ? (
|
{isAutoUpdaterEnabled() ? (
|
||||||
<AutoUpdateVersion version={this.props.version} />
|
<AutoUpdateVersion version={this.props.version} />
|
||||||
) : null}
|
) : null}
|
||||||
@@ -125,12 +139,14 @@ export default connect<Props, OwnProps, _, _, _, _>(
|
|||||||
leftSidebarVisible,
|
leftSidebarVisible,
|
||||||
rightSidebarVisible,
|
rightSidebarVisible,
|
||||||
rightSidebarAvailable,
|
rightSidebarAvailable,
|
||||||
|
downloadingImportData,
|
||||||
},
|
},
|
||||||
}) => ({
|
}) => ({
|
||||||
windowIsFocused,
|
windowIsFocused,
|
||||||
leftSidebarVisible,
|
leftSidebarVisible,
|
||||||
rightSidebarVisible,
|
rightSidebarVisible,
|
||||||
rightSidebarAvailable,
|
rightSidebarAvailable,
|
||||||
|
downloadingImportData,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
setActiveSheet,
|
setActiveSheet,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ test('TitleBar is rendered', () => {
|
|||||||
leftSidebarVisible={false}
|
leftSidebarVisible={false}
|
||||||
rightSidebarVisible={false}
|
rightSidebarVisible={false}
|
||||||
rightSidebarAvailable={false}
|
rightSidebarAvailable={false}
|
||||||
|
downloadingImportData={false}
|
||||||
toggleLeftSidebarVisible={() => {}}
|
toggleLeftSidebarVisible={() => {}}
|
||||||
toggleRightSidebarVisible={() => {}}
|
toggleRightSidebarVisible={() => {}}
|
||||||
setActiveSheet={_sheet => {}}
|
setActiveSheet={_sheet => {}}
|
||||||
|
|||||||
@@ -8,9 +8,12 @@
|
|||||||
import {remote, ipcRenderer} from 'electron';
|
import {remote, ipcRenderer} from 'electron';
|
||||||
import type {Store} from '../reducers/index.js';
|
import type {Store} from '../reducers/index.js';
|
||||||
import type {Logger} from '../fb-interfaces/Logger.js';
|
import type {Logger} from '../fb-interfaces/Logger.js';
|
||||||
|
import {toggleAction} from '../reducers/application';
|
||||||
import {parseFlipperPorts} from '../utils/environmentVariables';
|
import {parseFlipperPorts} from '../utils/environmentVariables';
|
||||||
import {importFileToStore} from '../utils/exportData';
|
import {importDataToStore, importFileToStore} from '../utils/exportData';
|
||||||
import {selectPlugin, userPreferredPlugin} from '../reducers/connections';
|
import {selectPlugin} from '../reducers/connections';
|
||||||
|
import qs from 'query-string';
|
||||||
|
|
||||||
export const uriComponents = (url: string) => {
|
export const uriComponents = (url: string) => {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return [];
|
return [];
|
||||||
@@ -42,11 +45,29 @@ export default (store: Store, logger: Logger) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcRenderer.on('flipper-deeplink', (event, url) => {
|
ipcRenderer.on('flipper-protocol-handler', (event, url) => {
|
||||||
// flipper://<client>/<pluginId>/<payload>
|
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);
|
const match = uriComponents(url);
|
||||||
if (match.length > 1) {
|
if (match.length > 1) {
|
||||||
store.dispatch(
|
// flipper://<client>/<pluginId>/<payload>
|
||||||
|
return store.dispatch(
|
||||||
selectPlugin({
|
selectPlugin({
|
||||||
selectedApp: match[0],
|
selectedApp: match[0],
|
||||||
selectedPlugin: match[1],
|
selectedPlugin: match[1],
|
||||||
@@ -60,14 +81,6 @@ export default (store: Store, logger: Logger) => {
|
|||||||
importFileToStore(url, store);
|
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) {
|
if (process.env.FLIPPER_PORTS) {
|
||||||
const portOverrides = parseFlipperPorts(process.env.FLIPPER_PORTS);
|
const portOverrides = parseFlipperPorts(process.env.FLIPPER_PORTS);
|
||||||
if (portOverrides) {
|
if (portOverrides) {
|
||||||
|
|||||||
@@ -316,7 +316,6 @@ export function processEntry(
|
|||||||
entry: DeviceLogEntry,
|
entry: DeviceLogEntry,
|
||||||
} {
|
} {
|
||||||
const {icon, style} = LOG_TYPES[(entry.type: string)] || LOG_TYPES.debug;
|
const {icon, style} = LOG_TYPES[(entry.type: string)] || LOG_TYPES.debug;
|
||||||
|
|
||||||
// build the item, it will either be batched or added straight away
|
// build the item, it will either be batched or added straight away
|
||||||
return {
|
return {
|
||||||
entry,
|
entry,
|
||||||
|
|||||||
@@ -30,13 +30,15 @@ export type State = {
|
|||||||
insecure: number,
|
insecure: number,
|
||||||
secure: number,
|
secure: number,
|
||||||
},
|
},
|
||||||
|
downloadingImportData: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
type BooleanActionType =
|
type BooleanActionType =
|
||||||
| 'leftSidebarVisible'
|
| 'leftSidebarVisible'
|
||||||
| 'rightSidebarVisible'
|
| 'rightSidebarVisible'
|
||||||
| 'rightSidebarAvailable'
|
| 'rightSidebarAvailable'
|
||||||
| 'windowIsFocused';
|
| 'windowIsFocused'
|
||||||
|
| 'downloadingImportData';
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
| {
|
| {
|
||||||
@@ -66,6 +68,7 @@ const initialState: () => State = () => ({
|
|||||||
insecure: 8089,
|
insecure: 8089,
|
||||||
secure: 8088,
|
secure: 8088,
|
||||||
},
|
},
|
||||||
|
downloadingImportData: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function reducer(state: State, action: Action): State {
|
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 === 'leftSidebarVisible' ||
|
||||||
action.type === 'rightSidebarVisible' ||
|
action.type === 'rightSidebarVisible' ||
|
||||||
action.type === 'rightSidebarAvailable' ||
|
action.type === 'rightSidebarAvailable' ||
|
||||||
action.type === 'windowIsFocused'
|
action.type === 'windowIsFocused' ||
|
||||||
|
action.type === 'downloadingImportData'
|
||||||
) {
|
) {
|
||||||
const newValue =
|
const newValue =
|
||||||
typeof action.payload === 'undefined'
|
typeof action.payload === 'undefined'
|
||||||
|
|||||||
@@ -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) => {
|
export const importFileToStore = (file: string, store: Store) => {
|
||||||
fs.readFile(file, 'utf8', (err, data) => {
|
fs.readFile(file, 'utf8', (err, data) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const json = deserialize(data);
|
importDataToStore(data, store);
|
||||||
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],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ app.on('will-finish-launching', () => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
deeplinkURL = url;
|
deeplinkURL = url;
|
||||||
if (win) {
|
if (win) {
|
||||||
win.webContents.send('flipper-deeplink', deeplinkURL);
|
win.webContents.send('flipper-protocol-handler', deeplinkURL);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
app.on('open-file', (event, path) => {
|
app.on('open-file', (event, path) => {
|
||||||
@@ -163,7 +163,7 @@ app.on('ready', function() {
|
|||||||
|
|
||||||
ipcMain.on('componentDidMount', event => {
|
ipcMain.on('componentDidMount', event => {
|
||||||
if (deeplinkURL) {
|
if (deeplinkURL) {
|
||||||
win.webContents.send('flipper-deeplink-preferred-plugin', deeplinkURL);
|
win.webContents.send('flipper-protocol-handler', deeplinkURL);
|
||||||
deeplinkURL = null;
|
deeplinkURL = null;
|
||||||
}
|
}
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
|
|||||||
13
yarn.lock
13
yarn.lock
@@ -5785,6 +5785,14 @@ qs@~6.5.1, qs@~6.5.2:
|
|||||||
version "6.5.2"
|
version "6.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
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:
|
querystring@0.2.0, querystring@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
|
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
|
||||||
@@ -6863,6 +6871,11 @@ stream-meter@1.0.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
readable-stream "^2.1.4"
|
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:
|
string-length@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
|
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
|
||||||
|
|||||||
Reference in New Issue
Block a user