Serialize Store

Summary:
This diff adds the capability to export the flipper data to a file. With this diff you can click on the "Export Flipper" option from the "Edit" menu in menubar. It will export it in the file at this location

`~/.flipper/MessageLogs.json`

We do not exactly export the store, but just the important part of it. We export in the following format

```
{
fileVersion: '1.0',
device: {
   os: 'iOS',
   title: 'iPhone 7',
   serial: '',
   deviceType: 'physical',
  },
clients: [
   {
    query: {
       app: 'Facebook',
      },
      d: '12345678'
     },
   {
    query: {
       app: 'Instagram',
      },
    id: '12345678'
   }
],
store: {
   pluginState: {},
   notifications: {}
  }
}

```

In next diff I will add the capability to select the folder to export the file too.

Reviewed By: danielbuechele

Differential Revision: D13751963

fbshipit-source-id: 7d3d49c6adf8145b2181d2332c7dbd589155cec3
This commit is contained in:
Pritesh Nandgaonkar
2019-01-28 15:33:56 -08:00
committed by Facebook Github Bot
parent fc1d32a3f9
commit 5ef970e5b5
7 changed files with 118 additions and 11 deletions

View File

@@ -73,7 +73,6 @@ export default class Client extends EventEmitter {
store: Store, store: Store,
) { ) {
super(); super();
this.connected = true; this.connected = true;
this.plugins = []; this.plugins = [];
this.connection = conn; this.connection = conn;
@@ -104,6 +103,7 @@ export default class Client extends EventEmitter {
}, },
}); });
} }
getDevice = (): ?BaseDevice => getDevice = (): ?BaseDevice =>
this.store this.store
.getState() .getState()
@@ -272,7 +272,7 @@ export default class Client extends EventEmitter {
} }
toJSON() { toJSON() {
return `<Client#${this.id}>`; return {id: this.id, query: this.query};
} }
subscribe( subscribe(

View File

@@ -6,7 +6,8 @@
*/ */
import type {FlipperPlugin, FlipperDevicePlugin} from './plugin.js'; import type {FlipperPlugin, FlipperDevicePlugin} from './plugin.js';
import {exportStoreToFile} from './utils/exportData.js';
import type {Store} from './reducers/';
import electron from 'electron'; import electron from 'electron';
export type DefaultKeyboardAction = 'clear' | 'goToBottom' | 'createPaste'; export type DefaultKeyboardAction = 'clear' | 'goToBottom' | 'createPaste';
@@ -64,9 +65,13 @@ function actionHandler(action: string) {
export function setupMenuBar( export function setupMenuBar(
plugins: Array<Class<FlipperPlugin<> | FlipperDevicePlugin<>>>, plugins: Array<Class<FlipperPlugin<> | FlipperDevicePlugin<>>>,
store: Store,
) { ) {
const template = getTemplate(electron.remote.app, electron.remote.shell); const template = getTemplate(
electron.remote.app,
electron.remote.shell,
store,
);
// collect all keyboard actions from all plugins // collect all keyboard actions from all plugins
const registeredActions: Set<?KeyboardAction> = new Set( const registeredActions: Set<?KeyboardAction> = new Set(
plugins plugins
@@ -169,8 +174,24 @@ export function activateMenuItems(
); );
} }
function getTemplate(app: Object, shell: Object): Array<MenuItem> { function getTemplate(
app: Object,
shell: Object,
store: Store,
): Array<MenuItem> {
const template = [ const template = [
{
label: 'File',
submenu: [
{
label: 'Export Data',
role: 'export',
click: function(item: Object, focusedWindow: Object) {
exportStoreToFile(store);
},
},
],
},
{ {
label: 'Edit', label: 'Edit',
submenu: [ submenu: [

View File

@@ -68,7 +68,12 @@ export default class BaseDevice {
} }
toJSON() { toJSON() {
return `<${this.constructor.name}#${this.title}#${this.serial}>`; return {
os: this.os,
title: this.title,
deviceType: this.deviceType,
serial: this.serial,
};
} }
teardown() {} teardown() {}

View File

@@ -59,10 +59,13 @@ export default (store: Store, logger: Logger) => {
store.subscribe(() => { store.subscribe(() => {
const newState = store.getState().plugins; const newState = store.getState().plugins;
if (state !== newState) { if (state !== newState) {
setupMenuBar([ setupMenuBar(
[
...newState.devicePlugins.values(), ...newState.devicePlugins.values(),
...newState.clientPlugins.values(), ...newState.clientPlugins.values(),
]); ],
store,
);
} }
state = newState; state = newState;
}); });

View File

@@ -354,6 +354,7 @@ export const selectPlugin = (payload: {|
type: 'SELECT_PLUGIN', type: 'SELECT_PLUGIN',
payload, payload,
}); });
export const userPreferredPlugin = (payload: string): Action => ({ export const userPreferredPlugin = (payload: string): Action => ({
type: 'SELECT_USER_PREFERRED_PLUGIN', type: 'SELECT_USER_PREFERRED_PLUGIN',
payload, payload,

View File

@@ -11,6 +11,7 @@ import connections from './connections.js';
import pluginStates from './pluginStates.js'; import pluginStates from './pluginStates.js';
import notifications from './notifications.js'; import notifications from './notifications.js';
import plugins from './plugins.js'; import plugins from './plugins.js';
import {persistReducer} from 'redux-persist'; import {persistReducer} from 'redux-persist';
import storage from 'redux-persist/lib/storage/index.js'; import storage from 'redux-persist/lib/storage/index.js';

76
src/utils/exportData.js Normal file
View File

@@ -0,0 +1,76 @@
/**
* 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 fs from 'fs';
import os from 'os';
import path from 'path';
const exportFilePath = path.join(
os.homedir(),
'.flipper',
'FlipperExport.json',
);
export const exportStoreToFile = (data: Store): Promise<void> => {
const state = data.getState();
const json = {
fileVersion: '1.0.0',
device: {},
clients: [],
store: {
pluginStates: {},
activeNotifications: [],
},
};
const device = state.connections.selectedDevice;
if (device) {
const {serial} = device;
json.device = device.toJSON();
const clients = state.connections.clients
.filter(client => {
return client.query.device_id === serial;
})
.map(client => {
return client.toJSON();
});
json.clients = clients;
const allPluginStates = state.pluginStates;
let pluginStates = {};
for (let key in allPluginStates) {
const filteredClients = clients.filter(client => {
let keyArray = key.split('#');
keyArray.pop(); // Remove the last entry related to plugin
return client.id.includes(keyArray.join('#'));
});
if (filteredClients.length > 0) {
pluginStates = {...pluginStates, [key]: allPluginStates[key]};
json.store.pluginStates = pluginStates;
}
}
const allActiveNotifications = state.notifications.activeNotifications;
let activeNotifications = allActiveNotifications.filter(notif => {
const filteredClients = clients.filter(client =>
client.id.includes(notif.client),
);
return filteredClients.length > 0;
});
json.store.activeNotifications = activeNotifications;
return new Promise((resolve, reject) => {
fs.writeFile(exportFilePath, JSON.stringify(json), err => {
if (err) {
reject(err);
}
resolve();
});
});
}
console.error('Make sure a device is connected');
return new Promise.reject(new Error('No device is selected'));
};