Export and import all the nodes
Summary: This diff does the following - Support to export the entire view hierarchy for iOS - Android is not supported yet - This diff adds a call `getAllNodes` to the client side of iOS, which returns the entire view hierarchy - Currently the search doesn't work on the imported layout plugin data. Also the imported layout plugin data doesn't expand the way it does when component is mounted, reason being the client passed to the plugin is not functional for the archived case I will work on fixing the last points in the next diffs stacked on the current one. For Android: - Currently the export function will export whatever is currently displayed on the Flipper app, not the entire view hierarchy Support for Android will also come up in later diffs. Reviewed By: jknoxville Differential Revision: D14209157 fbshipit-source-id: 3ad3e39edfd994913dc19cc239bfbbe011a9757c
This commit is contained in:
committed by
Facebook Github Bot
parent
99ea11b8e6
commit
af317eed2b
@@ -97,10 +97,8 @@ function startFlipper({
|
||||
const logger = initLogger(store, {isHeadless: true});
|
||||
dispatcher(store, logger);
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
originalConsole.log(
|
||||
JSON.stringify(serializeStore(store.getState()), null, 2),
|
||||
);
|
||||
process.on('SIGINT', async () => {
|
||||
originalConsole.log(JSON.stringify(await serializeStore(store), null, 2));
|
||||
process.exit();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,6 +86,10 @@
|
||||
[connection receive:@"getRoot" withBlock:^(NSDictionary *params, id<FlipperResponder> responder) {
|
||||
FlipperPerformBlockOnMainThread(^{ [weakSelf onCallGetRoot: responder]; }, responder);
|
||||
}];
|
||||
|
||||
[connection receive:@"getAllNodes" withBlock:^(NSDictionary *params, id<FlipperResponder> responder) {
|
||||
FlipperPerformBlockOnMainThread(^{ [weakSelf onCallGetAllNodesWithResponder: responder]; }, responder);
|
||||
}];
|
||||
|
||||
[connection receive:@"getNodes" withBlock:^(NSDictionary *params, id<FlipperResponder> responder) {
|
||||
FlipperPerformBlockOnMainThread(^{ [weakSelf onCallGetNodes: params[@"ids"] withResponder: responder]; }, responder);
|
||||
@@ -134,6 +138,29 @@
|
||||
[responder success: rootNode];
|
||||
}
|
||||
|
||||
- (void)populateAllNodesFromNode:(nonnull NSString *)identifier inDictionary:(nonnull NSMutableDictionary<NSString*, NSDictionary*> *)mutableDict {
|
||||
NSDictionary *nodeDict = [self getNode:identifier];
|
||||
mutableDict[identifier] = nodeDict;
|
||||
NSArray *arr = nodeDict[@"children"];
|
||||
for (NSString *childIdentifier in arr) {
|
||||
[self populateAllNodesFromNode:childIdentifier inDictionary:mutableDict];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
- (void)onCallGetAllNodesWithResponder:(nonnull id<FlipperResponder>)responder {
|
||||
NSMutableArray<NSDictionary*> *allNodes = @[].mutableCopy;
|
||||
NSString *identifier = [self trackObject: _rootNode];
|
||||
NSDictionary *rootNode = [self getNode: identifier];
|
||||
if (!rootNode) {
|
||||
return [responder error:@{@"error": [NSString stringWithFormat:@"getNode returned nil for the rootNode %@, while getting all the nodes", identifier]}];
|
||||
}
|
||||
[allNodes addObject:rootNode];
|
||||
NSMutableDictionary *allNodesDict = @{}.mutableCopy;
|
||||
[self populateAllNodesFromNode:identifier inDictionary:allNodesDict];
|
||||
[responder success:@{@"allNodes": @{@"rootElement": identifier, @"elements": allNodesDict}}];
|
||||
}
|
||||
|
||||
- (void)onCallGetNodes:(NSArray<NSDictionary *> *)nodeIds withResponder:(id<FlipperResponder>)responder {
|
||||
NSMutableArray<NSDictionary *> *elements = [NSMutableArray new];
|
||||
|
||||
@@ -417,6 +444,10 @@
|
||||
return objectIdentifier;
|
||||
}
|
||||
|
||||
- (BOOL)runInBackground {
|
||||
return true;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
||||
@@ -171,7 +171,9 @@ export default class Client extends EventEmitter {
|
||||
|
||||
// get the supported plugins
|
||||
async getPlugins(): Promise<Plugins> {
|
||||
const plugins = await this.rawCall('getPlugins').then(data => data.plugins);
|
||||
const plugins = await this.rawCall('getPlugins', false).then(
|
||||
data => data.plugins,
|
||||
);
|
||||
this.plugins = plugins;
|
||||
return plugins;
|
||||
}
|
||||
@@ -345,7 +347,11 @@ export default class Client extends EventEmitter {
|
||||
methodCallbacks.delete(callback);
|
||||
}
|
||||
|
||||
rawCall(method: string, params?: Object): Promise<Object> {
|
||||
rawCall(
|
||||
method: string,
|
||||
fromPlugin: boolean,
|
||||
params?: Object,
|
||||
): Promise<Object> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = this.messageIdCounter++;
|
||||
const metadata: RequestMetadata = {
|
||||
@@ -378,13 +384,13 @@ export default class Client extends EventEmitter {
|
||||
|
||||
const mark = this.getPerformanceMark(metadata);
|
||||
performance.mark(mark);
|
||||
if (this.isAcceptingMessagesFromPlugin(plugin)) {
|
||||
if (!fromPlugin || this.isAcceptingMessagesFromPlugin(plugin)) {
|
||||
this.connection &&
|
||||
this.connection
|
||||
.requestResponse({data: JSON.stringify(data)})
|
||||
.subscribe({
|
||||
onComplete: payload => {
|
||||
if (this.isAcceptingMessagesFromPlugin(plugin)) {
|
||||
if (!fromPlugin || this.isAcceptingMessagesFromPlugin(plugin)) {
|
||||
const logEventName = this.getLogEventName(data);
|
||||
this.logger.trackTimeSince(mark, logEventName);
|
||||
const response: {|
|
||||
@@ -452,9 +458,14 @@ export default class Client extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
call(api: string, method: string, params?: Object): Promise<Object> {
|
||||
call(
|
||||
api: string,
|
||||
method: string,
|
||||
fromPlugin: boolean,
|
||||
params?: Object,
|
||||
): Promise<Object> {
|
||||
return reportPluginFailures(
|
||||
this.rawCall('execute', {api, method, params}),
|
||||
this.rawCall('execute', fromPlugin, {api, method, params}),
|
||||
`Call-${method}`,
|
||||
api,
|
||||
);
|
||||
@@ -474,7 +485,7 @@ export default class Client extends EventEmitter {
|
||||
if (this.sdkVersion < 2) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return this.rawCall('isMethodSupported', {api, method}).then(
|
||||
return this.rawCall('isMethodSupported', true, {api, method}).then(
|
||||
response => response.isSupported,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {FlipperPlugin, FlipperDevicePlugin} from './plugin.js';
|
||||
import type {Logger} from './fb-interfaces/Logger';
|
||||
import BaseDevice from './devices/BaseDevice.js';
|
||||
import type {Props as PluginProps} from './plugin';
|
||||
|
||||
import {pluginKey as getPluginKey} from './reducers/pluginStates';
|
||||
import Client from './Client.js';
|
||||
import {
|
||||
ErrorBoundary,
|
||||
@@ -168,7 +168,7 @@ export default connect<Props, OwnProps, _, _, _, _>(
|
||||
}
|
||||
target = selectedDevice;
|
||||
if (activePlugin) {
|
||||
pluginKey = `${selectedDevice.serial}#${activePlugin.id}`;
|
||||
pluginKey = getPluginKey(selectedDevice.serial, activePlugin.id);
|
||||
} else {
|
||||
target = clients.find((client: Client) => client.id === selectedApp);
|
||||
activePlugin = clientPlugins.get(selectedPlugin);
|
||||
@@ -177,7 +177,7 @@ export default connect<Props, OwnProps, _, _, _, _>(
|
||||
`Plugin "${selectedPlugin || ''}" could not be found.`,
|
||||
);
|
||||
}
|
||||
pluginKey = `${target.id}#${activePlugin.id}`;
|
||||
pluginKey = getPluginKey(target.id, activePlugin.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@ export {
|
||||
FlipperBasePlugin,
|
||||
FlipperPlugin,
|
||||
FlipperDevicePlugin,
|
||||
callClient,
|
||||
} from './plugin.js';
|
||||
export type {PluginClient} from './plugin.js';
|
||||
export {default as Client} from './Client.js';
|
||||
export {clipboard} from 'electron';
|
||||
export * from './fb-stubs/constants.js';
|
||||
export * from './fb-stubs/createPaste.js';
|
||||
|
||||
@@ -19,6 +19,15 @@ import IOSDevice from './devices/IOSDevice';
|
||||
|
||||
const invariant = require('invariant');
|
||||
|
||||
// This function is intended to be called from outside of the plugin.
|
||||
// If you want to `call` from the plugin use, this.client.call
|
||||
export function callClient(
|
||||
client: Client,
|
||||
id: string,
|
||||
): (string, ?Object) => Promise<Object> {
|
||||
return (method, params) => client.call(id, method, false, params);
|
||||
}
|
||||
|
||||
export type PluginClient = {|
|
||||
send: (method: string, params?: Object) => void,
|
||||
call: (method: string, params?: Object) => Promise<any>,
|
||||
@@ -69,6 +78,11 @@ export class FlipperBasePlugin<
|
||||
method: string,
|
||||
data: Object,
|
||||
) => $Shape<PersistedState>;
|
||||
static exportPersistedState: ?(
|
||||
callClient: (string, ?Object) => Promise<Object>,
|
||||
persistedState: ?PersistedState,
|
||||
store: ?Store,
|
||||
) => Promise<?PersistedState>;
|
||||
static getActiveNotifications: ?(
|
||||
persistedState: PersistedState,
|
||||
) => Array<Notification>;
|
||||
@@ -160,7 +174,7 @@ export class FlipperPlugin<S = *, A = *, P = *> extends FlipperBasePlugin<
|
||||
// $FlowFixMe props.target will be instance of Client
|
||||
this.realClient = props.target;
|
||||
this.client = {
|
||||
call: (method, params) => this.realClient.call(id, method, params),
|
||||
call: (method, params) => this.realClient.call(id, method, true, params),
|
||||
send: (method, params) => this.realClient.send(id, method, params),
|
||||
subscribe: (method, callback) => {
|
||||
this.subscriptions.push({
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {ElementID, Element, ElementSearchResultSet} from 'flipper';
|
||||
import type {ElementID, Element, ElementSearchResultSet, Store} from 'flipper';
|
||||
|
||||
import {
|
||||
FlexColumn,
|
||||
@@ -51,6 +51,22 @@ const BetaBar = styled(Toolbar)({
|
||||
});
|
||||
|
||||
export default class Layout extends FlipperPlugin<State, void, PersistedState> {
|
||||
static exportPersistedState = (
|
||||
callClient: (string, ?Object) => Promise<Object>,
|
||||
persistedState: ?PersistedState,
|
||||
store: ?Store,
|
||||
): Promise<?PersistedState> => {
|
||||
const defaultPromise = Promise.resolve(persistedState);
|
||||
if (!store) {
|
||||
return defaultPromise;
|
||||
}
|
||||
const selectedDevice = store.getState().connections.selectedDevice;
|
||||
if (selectedDevice && selectedDevice.os === 'iOS') {
|
||||
return callClient('getAllNodes').then(({allNodes}) => allNodes);
|
||||
}
|
||||
return defaultPromise;
|
||||
};
|
||||
|
||||
static defaultPersistedState = {
|
||||
rootElement: null,
|
||||
rootAXElement: null,
|
||||
@@ -68,11 +84,6 @@ export default class Layout extends FlipperPlugin<State, void, PersistedState> {
|
||||
searchResults: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// reset the data, as it might be outdated
|
||||
this.props.setPersistedState(Layout.defaultPersistedState);
|
||||
}
|
||||
|
||||
init() {
|
||||
// persist searchActive state when moving between plugins to prevent multiple
|
||||
// TouchOverlayViews since we can't edit the view heirarchy in onDisconnect
|
||||
|
||||
@@ -9,6 +9,10 @@ export type State = {
|
||||
[pluginKey: string]: Object,
|
||||
};
|
||||
|
||||
export const pluginKey = (serial: string, pluginName: string): string => {
|
||||
return `${serial}#${pluginName}`;
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: 'SET_PLUGIN_STATE',
|
||||
|
||||
@@ -10,8 +10,8 @@ import type {State as PluginStates} from '../reducers/pluginStates';
|
||||
import type {PluginNotification} from '../reducers/notifications.js';
|
||||
import type {ClientExport} from '../Client.js';
|
||||
import type {State as PluginStatesState} from '../reducers/pluginStates';
|
||||
import type {State} from '../reducers/index';
|
||||
import {FlipperDevicePlugin} from '../plugin.js';
|
||||
import {pluginKey} from '../reducers/pluginStates';
|
||||
import {FlipperDevicePlugin, FlipperPlugin, callClient} from '../plugin.js';
|
||||
import {default as BaseDevice} from '../devices/BaseDevice';
|
||||
import {default as ArchivedDevice} from '../devices/ArchivedDevice';
|
||||
import {default as Client} from '../Client';
|
||||
@@ -174,17 +174,51 @@ export const processStore = (
|
||||
return null;
|
||||
};
|
||||
|
||||
export function serializeStore(state: State): ?ExportType {
|
||||
const {activeNotifications} = state.notifications;
|
||||
const {selectedDevice, clients} = state.connections;
|
||||
export async function serializeStore(store: Store): Promise<?ExportType> {
|
||||
const state = store.getState();
|
||||
const {clients} = state.connections;
|
||||
const {pluginStates} = state;
|
||||
const {devicePlugins} = state.plugins;
|
||||
const {plugins} = state;
|
||||
const newPluginState = {...pluginStates};
|
||||
// TODO: T39612653 Make Client mockable. Currently rsocket logic is tightly coupled.
|
||||
// Not passing the entire state as currently Client is not mockable.
|
||||
|
||||
const pluginsMap: Map<
|
||||
string,
|
||||
Class<FlipperDevicePlugin<> | FlipperPlugin<>>,
|
||||
> = new Map([]);
|
||||
plugins.clientPlugins.forEach((val, key) => {
|
||||
pluginsMap.set(key, val);
|
||||
});
|
||||
plugins.devicePlugins.forEach((val, key) => {
|
||||
pluginsMap.set(key, val);
|
||||
});
|
||||
for (let client of clients) {
|
||||
for (let plugin of client.plugins) {
|
||||
const pluginClass: ?Class<
|
||||
FlipperDevicePlugin<> | FlipperPlugin<>,
|
||||
> = plugin ? pluginsMap.get(plugin) : null;
|
||||
const exportState = pluginClass ? pluginClass.exportPersistedState : null;
|
||||
if (exportState) {
|
||||
const key = pluginKey(client.id, plugin);
|
||||
const data = await exportState(
|
||||
callClient(client, plugin),
|
||||
newPluginState[key],
|
||||
store,
|
||||
);
|
||||
newPluginState[key] = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const {activeNotifications} = store.getState().notifications;
|
||||
const {selectedDevice} = store.getState().connections;
|
||||
const {devicePlugins} = store.getState().plugins;
|
||||
|
||||
return processStore(
|
||||
activeNotifications,
|
||||
selectedDevice,
|
||||
pluginStates,
|
||||
newPluginState,
|
||||
clients.map(client => client.toJSON()),
|
||||
devicePlugins,
|
||||
uuid.v4(),
|
||||
@@ -193,21 +227,21 @@ export function serializeStore(state: State): ?ExportType {
|
||||
|
||||
export const exportStoreToFile = (
|
||||
exportFilePath: string,
|
||||
data: Store,
|
||||
store: Store,
|
||||
): Promise<void> => {
|
||||
const json = serializeStore(data.getState());
|
||||
if (json) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.writeFile(exportFilePath, serialize(json), err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const json = await serializeStore(store);
|
||||
if (!json) {
|
||||
console.error('Make sure a device is connected');
|
||||
reject('No device is selected');
|
||||
}
|
||||
fs.writeFile(exportFilePath, serialize(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'));
|
||||
});
|
||||
};
|
||||
|
||||
export const importFileToStore = (file: string, store: Store) => {
|
||||
|
||||
Reference in New Issue
Block a user