/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ import { FlipperDevicePlugin, FlipperBasePlugin, PluginDefinition, DevicePluginDefinition, isSandyPlugin, } from '../plugin'; import {State as PluginStatesState} from '../reducers/pluginStates'; import {State as PluginsState} from '../reducers/plugins'; import {State as PluginMessageQueueState} from '../reducers/pluginMessageQueue'; import {deconstructPluginKey, deconstructClientId} from './clientUtils'; import {_SandyPluginDefinition} from 'flipper-plugin'; type Client = import('../Client').default; export const defaultEnabledBackgroundPlugins = ['Navigation']; // The navigation plugin is enabled always, to make sure the navigation features works export function pluginsClassMap( plugins: PluginsState, ): Map { return new Map([ ...plugins.clientPlugins.entries(), ...plugins.devicePlugins.entries(), ]); } export function getPluginKey( selectedAppId: string | null, baseDevice: {serial: string} | null, pluginID: string, ): string { if (selectedAppId) { return `${selectedAppId}#${pluginID}`; } if (baseDevice) { // If selected App is not defined, then the plugin is a device plugin return `${baseDevice.serial}#${pluginID}`; } return `unknown#${pluginID}`; } export function getPersistedState( pluginKey: string, persistingPlugin: typeof FlipperBasePlugin | null, pluginStates: PluginStatesState, ): PersistedState | null { if (!persistingPlugin) { return null; } const persistedState: PersistedState = { ...persistingPlugin.defaultPersistedState, ...pluginStates[pluginKey], }; return persistedState; } /** * * @param starredPlugin starredPlugin is the dictionary of client and its enabled plugin * @param client Optional paramater indicating the selected client. * @param plugins Plugins from the state which has the mapping to Plugin's Class. * Returns plugins which are enabled or which has exportPersistedState function defined for the passed client. * Note all device plugins are enabled. */ export function getEnabledOrExportPersistedStatePlugins( starredPlugin: { [client: string]: string[]; }, client: Client, plugins: PluginsState, ): Array<{id: string; label: string}> { const appName = deconstructClientId(client.id).app; const pluginsMap: Map = pluginsClassMap(plugins); // Enabled Plugins with no exportPersistedState function defined const enabledPlugins = starredPlugin[appName] ? starredPlugin[appName] .map((pluginName) => pluginsMap.get(pluginName)!) .filter(Boolean) .filter((plugin) => { return !plugin.exportPersistedState; }) .sort(sortPluginsByName) .map((plugin) => { return {id: plugin.id, label: getPluginTitle(plugin)}; }) : []; // Device Plugins const devicePlugins = Array.from(plugins.devicePlugins.keys()) .filter((plugin) => { return client.plugins.includes(plugin); }) .map((plugin) => { return { id: plugin, label: getPluginTitle(plugins.devicePlugins.get(plugin)!), }; }) .filter(Boolean); // Plugins which have defined exportPersistedState. const exportPersistedStatePlugins = client.plugins .filter((name) => { return pluginsMap.get(name)?.exportPersistedState != null; }) .map((name) => { const plugin = pluginsMap.get(name)!; return {id: plugin.id, label: getPluginTitle(plugin)}; }); return [ ...devicePlugins, ...enabledPlugins, ...exportPersistedStatePlugins, {id: 'DeviceLogs', label: 'Logs'}, ]; } /** * * @param pluginsState PluginsState of the Redux Store. * @param plugins Plugins from the state which has the mapping to Plugin's Class. * @param selectedClient Optional paramater indicating the selected client. * Returns active persistent plugin, which means plugins which has the data in redux store or has the `exportPersistedState` function defined which can return the plugin's data when called. * If the selectedClient is defined then the active persistent plugins only for the selectedClient will be returned, otherwise it will return all active persistent plugins. */ export function getActivePersistentPlugins( pluginsState: PluginStatesState, pluginsMessageQueue: PluginMessageQueueState, plugins: PluginsState, selectedClient?: Client, ): {id: string; label: string}[] { const pluginsMap = pluginsClassMap(plugins); return getPersistentPlugins(plugins) .map((pluginName) => pluginsMap.get(pluginName)!) .sort(sortPluginsByName) .filter((plugin) => { 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([ ...Object.keys(pluginsState), ...Object.keys(pluginsMessageQueue), ]), ].map((key) => deconstructPluginKey(key).pluginName); return ( (plugin && plugin.exportPersistedState != undefined) || isSandyPlugin(plugin) || pluginsWithReduxData.includes(plugin.id) ); } }) .map((plugin) => ({ id: plugin.id, label: getPluginTitle(plugin), })); } /** * Returns all enabled plugins that are potentially exportable * @param plugins */ export function getPersistentPlugins(plugins: PluginsState): Array { const pluginsMap = pluginsClassMap(plugins); [...plugins.disabledPlugins, ...plugins.gatekeepedPlugins].forEach( (plugin) => { pluginsMap.delete(plugin.name); }, ); plugins.failedPlugins.forEach(([details]) => { pluginsMap.delete(details.id); }); return Array.from(pluginsMap.keys()).filter((plugin) => { const pluginClass = pluginsMap.get(plugin); return ( plugin == 'DeviceLogs' || isSandyPlugin(pluginClass) || pluginClass?.defaultPersistedState || pluginClass?.exportPersistedState ); }); } export function getPluginTitle(pluginClass: { title?: string | null; id: string; }) { return pluginClass.title || pluginClass.id; } export function sortPluginsByName( a: PluginDefinition, b: PluginDefinition, ): number { // make sure Device plugins are sorted before normal plugins if (isDevicePluginDefinition(a) && !isDevicePluginDefinition(b)) { return -1; } if (isDevicePluginDefinition(b) && !isDevicePluginDefinition(a)) { return 1; } return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1; } export function isDevicePluginDefinition( definition: PluginDefinition, ): definition is DevicePluginDefinition { return ( (definition as any).prototype instanceof FlipperDevicePlugin || (definition instanceof _SandyPluginDefinition && definition.isDevicePlugin) ); }