Make sure Sandy plugins are selectable during export

Summary:
This diff makes sure Sandy plugins show up as well in the plugin selector when making exports (and in support form as well).

Also verified that this works with the Sandy updated section plugin.

Note that persisted state now didn't need any changes in the plugin code to work :)

Commented / simplified the calculation of available plugins a little bit and fixed a confusing issue where two different redux stores where created in one unit test, which caused an issue in the new implementation.

Reviewed By: jknoxville

Differential Revision: D22434301

fbshipit-source-id: c911196bc5b105309e82204188f124f40aab487a
This commit is contained in:
Michel Weststrate
2020-07-14 09:04:59 -07:00
committed by Facebook GitHub Bot
parent 44f99eb304
commit 6fe477f19b
7 changed files with 102 additions and 75 deletions

View File

@@ -40,31 +40,22 @@ function getStore(selectedPlugins: Array<string>) {
debug: () => {}, debug: () => {},
trackTimeSince: () => {}, trackTimeSince: () => {},
}; };
let mockStore = configureStore([])();
const selectedDevice = new BaseDevice( const selectedDevice = new BaseDevice(
'serial', 'serial',
'emulator', 'emulator',
'TestiPhone', 'TestiPhone',
'iOS', 'iOS',
); );
const client = new Client(
generateClientIdentifier(selectedDevice, 'app'),
{app: 'app', os: 'iOS', device: 'TestiPhone', device_id: 'serial'},
null,
logger,
// @ts-ignore
mockStore,
['TestPlugin', 'TestDevicePlugin'],
);
const clientId = generateClientIdentifier(selectedDevice, 'app');
const pluginStates: {[key: string]: any} = {}; const pluginStates: {[key: string]: any} = {};
pluginStates[`${client.id}#TestDevicePlugin`] = { pluginStates[`${clientId}#TestDevicePlugin`] = {
msg: 'Test Device plugin', msg: 'Test Device plugin',
}; };
pluginStates[`${client.id}#TestPlugin`] = { pluginStates[`${clientId}#TestPlugin`] = {
msg: 'Test plugin', msg: 'Test plugin',
}; };
mockStore = configureStore([])({ const mockStore = configureStore([])({
application: {share: {closeOnFinish: false, type: 'link'}}, application: {share: {closeOnFinish: false, type: 'link'}},
plugins: { plugins: {
clientPlugins: new Map([['TestPlugin', TestPlugin]]), clientPlugins: new Map([['TestPlugin', TestPlugin]]),
@@ -76,8 +67,23 @@ function getStore(selectedPlugins: Array<string>) {
}, },
pluginStates, pluginStates,
pluginMessageQueue: [], pluginMessageQueue: [],
connections: {selectedApp: client.id, clients: [client]}, connections: {selectedApp: clientId, clients: []},
}); });
const client = new Client(
clientId,
{app: 'app', os: 'iOS', device: 'TestiPhone', device_id: 'serial'},
null,
logger,
// @ts-ignore
mockStore,
['TestPlugin', 'TestDevicePlugin'],
);
mockStore.dispatch({
type: 'NEW_CLIENT',
payload: client,
});
return mockStore; return mockStore;
} }

View File

@@ -95,6 +95,7 @@ function mockPluginState(
function mockPluginDefinition(name: string): PluginDetails { function mockPluginDefinition(name: string): PluginDetails {
return { return {
name, name,
id: name,
out: 'out', out: 'out',
}; };
} }

View File

@@ -17,7 +17,6 @@ import {
import {State as PluginStatesState} from '../reducers/pluginStates'; import {State as PluginStatesState} from '../reducers/pluginStates';
import {State as PluginsState} from '../reducers/plugins'; import {State as PluginsState} from '../reducers/plugins';
import {State as PluginMessageQueueState} from '../reducers/pluginMessageQueue'; import {State as PluginMessageQueueState} from '../reducers/pluginMessageQueue';
import {PluginDetails} from 'flipper-plugin-lib';
import {deconstructPluginKey, deconstructClientId} from './clientUtils'; import {deconstructPluginKey, deconstructClientId} from './clientUtils';
type Client = import('../Client').default; type Client = import('../Client').default;
@@ -27,14 +26,10 @@ export const defaultEnabledBackgroundPlugins = ['Navigation']; // The navigation
export function pluginsClassMap( export function pluginsClassMap(
plugins: PluginsState, plugins: PluginsState,
): Map<string, PluginDefinition> { ): Map<string, PluginDefinition> {
const pluginsMap: Map<string, PluginDefinition> = new Map([]); return new Map<string, PluginDefinition>([
plugins.clientPlugins.forEach((val, key) => { ...plugins.clientPlugins.entries(),
pluginsMap.set(key, val); ...plugins.devicePlugins.entries(),
}); ]);
plugins.devicePlugins.forEach((val, key) => {
pluginsMap.set(key, val);
});
return pluginsMap;
} }
export function getPluginKey( export function getPluginKey(
@@ -141,65 +136,70 @@ export function getActivePersistentPlugins(
plugins: PluginsState, plugins: PluginsState,
selectedClient?: Client, selectedClient?: Client,
): {id: string; label: string}[] { ): {id: string; label: string}[] {
const pluginsMap: Map<string, PluginDefinition> = pluginsClassMap(plugins); const pluginsMap = pluginsClassMap(plugins);
return getPersistentPlugins(plugins) return getPersistentPlugins(plugins)
.map((pluginName) => pluginsMap.get(pluginName)!) .map((pluginName) => pluginsMap.get(pluginName)!)
.sort(sortPluginsByName) .sort(sortPluginsByName)
.map((plugin) => { .filter((plugin) => {
const keys = [ if (plugin.id == 'DeviceLogs') {
return true;
}
if (selectedClient) {
const pluginKey = getPluginKey(
selectedClient.id,
{serial: selectedClient.query.device_id},
plugin.id,
);
// If there is a selected client, active persistent plugins are those that (can) have persisted state
return (
selectedClient.isEnabledPlugin(plugin.id) &&
// this plugin can fetch and export state
(plugin.exportPersistedState ||
// this plugin has some persisted state already
pluginsState[pluginKey] ||
pluginsMessageQueue[pluginKey] ||
// this plugin has some persistable sandy state
selectedClient.sandyPluginStates.get(plugin.id)?.isPersistable())
);
}
{
// If there is no selected client, active persistent plugin is the plugin which is just persistent.
const pluginsWithReduxData = [
...new Set([ ...new Set([
...Object.keys(pluginsState), ...Object.keys(pluginsState),
...Object.keys(pluginsMessageQueue), ...Object.keys(pluginsMessageQueue),
]), ]),
] ].map((key) => deconstructPluginKey(key).pluginName);
.filter((k) => !selectedClient || k.includes(selectedClient.id)) return (
.map((key) => deconstructPluginKey(key).pluginName);
let result = plugin.id == 'DeviceLogs';
const pluginsWithExportPersistedState =
plugin && plugin.exportPersistedState != undefined;
const pluginsWithReduxData = keys.includes(plugin.id);
if (!result && selectedClient) {
// If there is a selected client, active persistent plugin is the plugin which is active for selectedClient and also persistent.
result =
selectedClient.plugins.includes(plugin.id) &&
(pluginsWithExportPersistedState || pluginsWithReduxData);
} else if (!result && !selectedClient) {
// If there is no selected client, active persistent plugin is the plugin which is just persistent.
result =
(plugin && plugin.exportPersistedState != undefined) || (plugin && plugin.exportPersistedState != undefined) ||
keys.includes(plugin.id); isSandyPlugin(plugin) ||
pluginsWithReduxData.includes(plugin.id)
);
} }
return (result })
? { .map((plugin) => ({
id: plugin.id, id: plugin.id,
label: getPluginTitle(plugin), label: getPluginTitle(plugin),
} }));
: undefined)!;
})
.filter(Boolean);
} }
/**
* Returns all enabled plugins that are potentially exportable
* @param plugins
*/
export function getPersistentPlugins(plugins: PluginsState): Array<string> { export function getPersistentPlugins(plugins: PluginsState): Array<string> {
const pluginsMap: Map<string, PluginDefinition> = pluginsClassMap(plugins); const pluginsMap = pluginsClassMap(plugins);
const arr: Array<PluginDetails> = plugins.disabledPlugins.concat( [...plugins.disabledPlugins, ...plugins.gatekeepedPlugins].forEach(
plugins.gatekeepedPlugins, (plugin) => {
);
arr.forEach((plugin: PluginDetails) => {
if (pluginsMap.has(plugin.name)) {
pluginsMap.delete(plugin.name); pluginsMap.delete(plugin.name);
} },
);
plugins.failedPlugins.forEach(([details]) => {
pluginsMap.delete(details.id);
}); });
plugins.failedPlugins.forEach((plugin: [PluginDetails, string]) => { return Array.from(pluginsMap.keys()).filter((plugin) => {
if (plugin[0] && plugin[0].name && pluginsMap.has(plugin[0].name)) {
pluginsMap.delete(plugin[0].name);
}
});
const activePlugins = [...pluginsMap.keys()];
return activePlugins.filter((plugin) => {
const pluginClass = pluginsMap.get(plugin); const pluginClass = pluginsMap.get(plugin);
return ( return (
plugin == 'DeviceLogs' || plugin == 'DeviceLogs' ||

View File

@@ -35,9 +35,6 @@ export function plugin(client: FlipperClient<Events, Methods>) {
}, },
); );
// TODO: add tests for sending and receiving data T68683442
// including typescript assertions
client.onConnect(connectStub); client.onConnect(connectStub);
client.onDisconnect(disconnectStub); client.onDisconnect(disconnectStub);
client.onDestroy(destroyStub); client.onDestroy(destroyStub);

View File

@@ -217,6 +217,10 @@ export class SandyPluginInstance {
); );
} }
isPersistable(): boolean {
return Object.keys(this.rootStates).length > 0;
}
private assertNotDestroyed() { private assertNotDestroyed() {
if (this.destroyed) { if (this.destroyed) {
throw new Error('Plugin has been destroyed already'); throw new Error('Plugin has been destroyed already');

View File

@@ -119,4 +119,23 @@ test('It can have selection and render details', async () => {
// Sidebar should be visible now // Sidebar should be visible now
expect(await renderer.findByText('Extras')).not.toBeNull(); expect(await renderer.findByText('Extras')).not.toBeNull();
// Verify export
expect(plugin.exportState()).toMatchInlineSnapshot(`
Object {
"rows": Object {
"1": Object {
"id": 1,
"title": "Dolphin",
"url": "http://dolphin.png",
},
"2": Object {
"id": 2,
"title": "Turtle",
"url": "http://turtle.png",
},
},
"selection": "2",
}
`);
}); });

View File

@@ -50,8 +50,8 @@ type PersistedState = {
}; };
export function plugin(client: FlipperClient<Events, {}>) { export function plugin(client: FlipperClient<Events, {}>) {
const rows = createState<PersistedState>({}); const rows = createState<PersistedState>({}, {persist: 'rows'});
const selectedID = createState<string | null>(null); const selectedID = createState<string | null>(null, {persist: 'selection'});
client.onMessage('newRow', (row) => { client.onMessage('newRow', (row) => {
rows.update((draft) => { rows.update((draft) => {