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

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