diff --git a/desktop/app/src/PluginContainer.tsx b/desktop/app/src/PluginContainer.tsx index 1c05f1bde..fdd6927b4 100644 --- a/desktop/app/src/PluginContainer.tsx +++ b/desktop/app/src/PluginContainer.tsx @@ -11,7 +11,6 @@ import { FlipperPlugin, FlipperDevicePlugin, Props as PluginProps, - PluginDefinition, } from './plugin'; import {Logger} from './fb-interfaces/Logger'; import BaseDevice from './devices/BaseDevice'; @@ -28,11 +27,7 @@ import { VBox, View, } from './ui'; -import { - StaticView, - setStaticView, - isPluginEnabled, -} from './reducers/connections'; +import {StaticView, setStaticView} from './reducers/connections'; import {switchPlugin} from './reducers/pluginManager'; import React, {PureComponent} from 'react'; import {connect, ReactReduxContext} from 'react-redux'; @@ -46,7 +41,12 @@ import {IdlerImpl} from './utils/Idler'; import {processMessageQueue} from './utils/messageQueue'; import {Layout} from './ui'; import {theme, TrackingScope, _SandyPluginRenderer} from 'flipper-plugin'; -import {isDevicePluginDefinition, isSandyPlugin} from './utils/pluginUtils'; +import { + ActivePluginListItem, + isDevicePlugin, + isDevicePluginDefinition, + isSandyPlugin, +} from './utils/pluginUtils'; import {ContentContainer} from './sandy-chrome/ContentContainer'; import {Alert, Typography} from 'antd'; import {InstalledPluginDetails} from 'flipper-plugin-lib'; @@ -55,6 +55,7 @@ import {loadPlugin} from './reducers/pluginManager'; import {produce} from 'immer'; import {reportUsage} from './utils/metrics'; import PluginInfo from './chrome/fb-stubs/PluginInfo'; +import {getActiveClient, getActivePlugin} from './selectors/connections'; const {Text, Link} = Typography; @@ -107,14 +108,13 @@ type OwnProps = { type StateFromProps = { pluginState: Object; - activePlugin: PluginDefinition | undefined; + activePlugin: ActivePluginListItem | null; target: Client | BaseDevice | null; pluginKey: string | null; deepLinkPayload: unknown; selectedApp: string | null; isArchivedDevice: boolean; pendingMessages: Message[] | undefined; - pluginIsEnabled: boolean; settingsState: Settings; latestInstalledVersion: InstalledPluginDetails | undefined; }; @@ -202,14 +202,13 @@ class PluginContainer extends PureComponent { const {deepLinkPayload, target, activePlugin} = this.props; if (deepLinkPayload && activePlugin && target) { target.sandyPluginStates - .get(activePlugin.id) + .get(activePlugin.details.id) ?.triggerDeepLink(deepLinkPayload); } } processMessageQueue() { - const {pluginKey, pendingMessages, activePlugin, pluginIsEnabled, target} = - this.props; + const {pluginKey, pendingMessages, activePlugin, target} = this.props; if (pluginKey !== this.pluginBeingProcessed) { this.pluginBeingProcessed = pluginKey; this.cancelCurrentQueue(); @@ -219,23 +218,27 @@ class PluginContainer extends PureComponent { }), ); // device plugins don't have connections so no message queues - if (!activePlugin || isDevicePluginDefinition(activePlugin)) { + if ( + !activePlugin || + activePlugin.status !== 'enabled' || + isDevicePluginDefinition(activePlugin.definition) + ) { return; } if ( - pluginIsEnabled && target instanceof Client && activePlugin && - (isSandyPlugin(activePlugin) || activePlugin.persistedStateReducer) && + (isSandyPlugin(activePlugin.definition) || + activePlugin.definition.persistedStateReducer) && pluginKey && pendingMessages?.length ) { const start = Date.now(); this.idler = new IdlerImpl(); processMessageQueue( - isSandyPlugin(activePlugin) - ? target.sandyPluginStates.get(activePlugin.id)! - : activePlugin, + isSandyPlugin(activePlugin.definition) + ? target.sandyPluginStates.get(activePlugin.definition.id)! + : activePlugin.definition, pluginKey, this.store, (progress) => { @@ -246,18 +249,22 @@ class PluginContainer extends PureComponent { ); }, this.idler, - ).then((completed) => { - const duration = Date.now() - start; - this.props.logger.track( - 'duration', - 'queue-processing-before-plugin-open', - { - completed, - duration, - }, - activePlugin.id, + ) + .then((completed) => { + const duration = Date.now() - start; + this.props.logger.track( + 'duration', + 'queue-processing-before-plugin-open', + { + completed, + duration, + }, + activePlugin.definition.id, + ); + }) + .catch((err) => + console.error('Error while processing plugin message queue', err), ); - }); } } } @@ -269,13 +276,11 @@ class PluginContainer extends PureComponent { } render() { - const {activePlugin, pluginKey, target, pendingMessages, pluginIsEnabled} = - this.props; + const {activePlugin, pluginKey, target, pendingMessages} = this.props; if (!activePlugin || !target || !pluginKey) { return null; } - - if (!pluginIsEnabled) { + if (activePlugin.status !== 'enabled') { return this.renderPluginInfo(); } if (!pendingMessages || pendingMessages.length === 0) { @@ -303,7 +308,7 @@ class PluginContainer extends PureComponent { @@ -367,7 +372,12 @@ class PluginContainer extends PureComponent { isSandy, latestInstalledVersion, } = this.props; - if (!activePlugin || !target || !pluginKey) { + if ( + !activePlugin || + !target || + !pluginKey || + activePlugin.status !== 'enabled' + ) { console.warn(`No selected plugin. Rendering empty!`); return this.renderNoPluginActive(); } @@ -378,10 +388,13 @@ class PluginContainer extends PureComponent { !this.state.autoUpdateAlertSuppressed.has( `${latestInstalledVersion.name}@${latestInstalledVersion.version}`, ) && - semver.gt(latestInstalledVersion.version, activePlugin.version); - if (isSandyPlugin(activePlugin)) { + semver.gt( + latestInstalledVersion.version, + activePlugin.definition.version, + ); + if (isSandyPlugin(activePlugin.definition)) { // Make sure we throw away the container for different pluginKey! - const instance = target.sandyPluginStates.get(activePlugin.id); + const instance = target.sandyPluginStates.get(activePlugin.definition.id); if (!instance) { // happens if we selected a plugin that is not enabled on a specific app or not supported on a specific device. return this.renderNoPluginActive(); @@ -403,9 +416,9 @@ class PluginContainer extends PureComponent { key: pluginKey, logger: this.props.logger, selectedApp, - persistedState: activePlugin.defaultPersistedState + persistedState: activePlugin.definition.defaultPersistedState ? { - ...activePlugin.defaultPersistedState, + ...activePlugin.definition.defaultPersistedState, ...pluginState, } : pluginState, @@ -438,8 +451,8 @@ class PluginContainer extends PureComponent { settingsState, }; pluginElement = ( - - {React.createElement(activePlugin, props)} + + {React.createElement(activePlugin.definition, props)} ); } @@ -450,7 +463,7 @@ class PluginContainer extends PureComponent { - Plugin "{activePlugin.title}" v + Plugin "{activePlugin.definition.title}" v {latestInstalledVersion?.version} is downloaded and ready to install. Reload to start using the new version. @@ -475,7 +488,7 @@ class PluginContainer extends PureComponent { {pluginElement} @@ -487,7 +500,7 @@ class PluginContainer extends PureComponent { {pluginElement} @@ -499,54 +512,33 @@ class PluginContainer extends PureComponent { } export default connect( - ({ - connections: { - selectedPlugin, - selectedDevice, - selectedApp, - clients, - deepLinkPayload, - enabledPlugins, - enabledDevicePlugins, - }, - pluginStates, - plugins: {devicePlugins, clientPlugins, installedPlugins}, - pluginMessageQueue, - settingsState, - }) => { - let pluginKey = null; - let target = null; - let activePlugin: PluginDefinition | undefined; - let pluginIsEnabled = false; - - if (selectedPlugin) { - activePlugin = devicePlugins.get(selectedPlugin); - if (selectedDevice && activePlugin) { + (state: Store) => { + let pluginKey: string | null = null; + let target: BaseDevice | Client | null = null; + const { + connections: {selectedDevice, selectedApp, deepLinkPayload}, + pluginStates, + plugins: {installedPlugins}, + pluginMessageQueue, + settingsState, + } = state; + const selectedClient = getActiveClient(state); + const activePlugin = getActivePlugin(state); + if (activePlugin) { + if (selectedDevice && isDevicePlugin(activePlugin)) { target = selectedDevice; - pluginKey = getPluginKey(selectedDevice.serial, activePlugin.id); - } else { - target = - clients.find((client: Client) => client.id === selectedApp) || null; - activePlugin = clientPlugins.get(selectedPlugin); - if (activePlugin && target) { - pluginKey = getPluginKey(target.id, activePlugin.id); - } - } - pluginIsEnabled = - activePlugin !== undefined && - isPluginEnabled( - enabledPlugins, - enabledDevicePlugins, - selectedApp, - activePlugin.id, + pluginKey = getPluginKey( + selectedDevice.serial, + activePlugin.details.id, ); + } else if (selectedClient) { + target = selectedClient; + pluginKey = getPluginKey(selectedClient.id, activePlugin.details.id); + } } const isArchivedDevice = !selectedDevice ? false : selectedDevice.isArchived; - if (isArchivedDevice) { - pluginIsEnabled = true; - } const pendingMessages = pluginKey ? pluginMessageQueue[pluginKey] @@ -554,17 +546,16 @@ export default connect( const s: StateFromProps = { pluginState: pluginStates[pluginKey as string], - activePlugin: activePlugin, + activePlugin, target, deepLinkPayload, pluginKey, isArchivedDevice, selectedApp: selectedApp || null, pendingMessages, - pluginIsEnabled, settingsState, latestInstalledVersion: installedPlugins.get( - activePlugin?.packageName ?? '', + activePlugin?.details?.name ?? '', ), }; return s; diff --git a/desktop/app/src/__tests__/PluginContainer.node.tsx b/desktop/app/src/__tests__/PluginContainer.node.tsx index 3ccceb3bf..32dae6310 100644 --- a/desktop/app/src/__tests__/PluginContainer.node.tsx +++ b/desktop/app/src/__tests__/PluginContainer.node.tsx @@ -38,6 +38,10 @@ class TestPlugin extends FlipperPlugin { count: 0, }; + static details = TestUtils.createMockPluginDetails({ + id: 'TestPlugin', + }); + static persistedStateReducer( persistedState: PersistedState, method: string, @@ -636,7 +640,7 @@ test('PluginContainer can render Sandy device plugins', async () => { }; const definition = new _SandyPluginDefinition( - TestUtils.createMockPluginDetails(), + TestUtils.createMockPluginDetails({pluginType: 'device'}), { supportsDevice: () => true, devicePlugin, @@ -749,7 +753,7 @@ test('PluginContainer + Sandy device plugin supports deeplink', async () => { }; const definition = new _SandyPluginDefinition( - TestUtils.createMockPluginDetails(), + TestUtils.createMockPluginDetails({pluginType: 'device'}), { devicePlugin, supportsDevice: () => true, @@ -922,7 +926,7 @@ test('Sandy plugins support isPluginSupported + selectPlugin', async () => { }, ); const definition3 = new _SandyPluginDefinition( - TestUtils.createMockPluginDetails({id: 'device'}), + TestUtils.createMockPluginDetails({id: 'device', pluginType: 'device'}), { supportsDevice() { return true; diff --git a/desktop/app/src/sandy-chrome/appinspect/__tests__/PluginList.spec.tsx b/desktop/app/src/sandy-chrome/appinspect/__tests__/PluginList.spec.tsx index da05528c5..abfd26b29 100644 --- a/desktop/app/src/sandy-chrome/appinspect/__tests__/PluginList.spec.tsx +++ b/desktop/app/src/sandy-chrome/appinspect/__tests__/PluginList.spec.tsx @@ -248,15 +248,15 @@ describe('basic getActiveDevice with metro present', () => { ], [ unsupportedDevicePlugin.details, - "Device plugin 'Unsupported Device Plugin' is not supported by the currently connected device.", + "Device plugin 'Unsupported Device Plugin' is not supported by the selected device 'MockAndroidDevice' (Android)", ], [ unsupportedPlugin.details, - "Plugin 'Unsupported Plugin' is not supported by the client application", + "Plugin 'Unsupported Plugin' is not supported by the selected application 'TestApp' (Android)", ], [ unsupportedDownloadablePlugin, - "Plugin 'Unsupported Uninstalled Plugin' is not supported by the client application and not installed in Flipper", + "Plugin 'Unsupported Uninstalled Plugin' is not supported by the selected application 'TestApp' (Android) and not installed in Flipper", ], ], downloadablePlugins: [supportedDownloadablePlugin], diff --git a/desktop/app/src/selectors/connections.tsx b/desktop/app/src/selectors/connections.tsx index 754af7bcf..8a95b7cd2 100644 --- a/desktop/app/src/selectors/connections.tsx +++ b/desktop/app/src/selectors/connections.tsx @@ -7,33 +7,16 @@ * @format */ -import { - PluginDetails, - DownloadablePluginDetails, - BundledPluginDetails, -} from 'flipper-plugin-lib'; import MetroDevice from '../devices/MetroDevice'; -import { - DevicePluginDefinition, - ClientPluginDefinition, - PluginDefinition, -} from '../plugin'; import {State} from '../reducers'; import { computePluginLists, computeExportablePlugins, + computeActivePluginList, } from '../utils/pluginUtils'; import createSelector from './createSelector'; -export type PluginLists = { - devicePlugins: DevicePluginDefinition[]; - metroPlugins: DevicePluginDefinition[]; - enabledPlugins: ClientPluginDefinition[]; - disabledPlugins: PluginDefinition[]; - unavailablePlugins: [plugin: PluginDetails, reason: string][]; - downloadablePlugins: (DownloadablePluginDetails | BundledPluginDetails)[]; -}; - +const getSelectedPluginId = (state: State) => state.connections.selectedPlugin; const getSelectedApp = (state: State) => state.connections.selectedApp || state.connections.userPreferredApp; const getSelectedDevice = (state: State) => state.connections.selectedDevice; @@ -135,3 +118,18 @@ export const getExportablePlugins = createSelector( getPluginLists, computeExportablePlugins, ); +export const getActivePluginList = createSelector( + getPluginLists, + computeActivePluginList, +); + +export const getActivePlugin = createSelector( + getSelectedPluginId, + getActivePluginList, + (pluginId, pluginList) => { + if (!pluginId) { + return null; + } + return pluginList[pluginId] ?? null; + }, +); diff --git a/desktop/app/src/utils/pluginUtils.tsx b/desktop/app/src/utils/pluginUtils.tsx index cb58fcd25..f894b772e 100644 --- a/desktop/app/src/utils/pluginUtils.tsx +++ b/desktop/app/src/utils/pluginUtils.tsx @@ -21,12 +21,44 @@ import {_SandyPluginDefinition} from 'flipper-plugin'; import type BaseDevice from '../devices/BaseDevice'; import type Client from '../Client'; import type { + ActivatablePluginDetails, BundledPluginDetails, DownloadablePluginDetails, PluginDetails, } from 'flipper-plugin-lib'; import {getLatestCompatibleVersionOfEachPlugin} from '../dispatcher/plugins'; -import {PluginLists} from '../selectors/connections'; + +export type PluginLists = { + devicePlugins: DevicePluginDefinition[]; + metroPlugins: DevicePluginDefinition[]; + enabledPlugins: ClientPluginDefinition[]; + disabledPlugins: PluginDefinition[]; + unavailablePlugins: [plugin: PluginDetails, reason: string][]; + downloadablePlugins: (DownloadablePluginDetails | BundledPluginDetails)[]; +}; + +export type ActivePluginListItem = + | { + status: 'enabled'; + details: ActivatablePluginDetails; + definition: PluginDefinition; + } + | { + status: 'disabled'; + details: ActivatablePluginDetails; + definition: PluginDefinition; + } + | { + status: 'uninstalled'; + details: DownloadablePluginDetails | BundledPluginDetails; + } + | { + status: 'unavailable'; + details: PluginDetails; + reason: string; + }; + +export type ActivePluginList = Record; export const defaultEnabledBackgroundPlugins = ['Navigation']; // The navigation plugin is enabled always, to make sure the navigation features works @@ -157,6 +189,16 @@ export function sortPluginsByName( return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1; } +export function isDevicePlugin(activePlugin: ActivePluginListItem) { + if (activePlugin.details.pluginType === 'device') { + return true; + } + return ( + (activePlugin.status === 'enabled' || activePlugin.status === 'disabled') && + isDevicePluginDefinition(activePlugin.definition) + ); +} + export function isDevicePluginDefinition( definition: PluginDefinition, ): definition is DevicePluginDefinition { @@ -239,7 +281,9 @@ export function computePluginLists( p.details, `Device plugin '${getPluginTitle( p.details, - )}' is not supported by the currently connected device.`, + )}' is not supported by the selected device '${device.title}' (${ + device.os + })`, ]); } } @@ -290,7 +334,9 @@ export function computePluginLists( plugin.details, `Plugin '${getPluginTitle( plugin.details, - )}' is not supported by the client application`, + )}' is not supported by the selected application '${ + client.query.app + }' (${client.query.os})`, ]); } else if (favoritePlugins.includes(plugin)) { enabledPlugins.push(plugin); @@ -312,13 +358,21 @@ export function computePluginLists( .forEach((plugin) => { unavailablePlugins.push([ plugin, - `Plugin '${getPluginTitle( - plugin, - )}' is not supported by the client application and not installed in Flipper`, + `Plugin '${getPluginTitle(plugin)}' is not supported by the selected ${ + plugin.pluginType === 'device' ? 'device' : 'application' + } '${ + (plugin.pluginType === 'device' + ? device?.title + : client?.query.app) ?? 'unknown' + }' (${ + plugin.pluginType === 'device' ? device?.os : client?.query.os + }) and not installed in Flipper`, ]); }); + enabledPlugins.sort(sortPluginsByName); devicePlugins.sort(sortPluginsByName); + disabledPlugins.sort(sortPluginsByName); metroPlugins.sort(sortPluginsByName); unavailablePlugins.sort(([a], [b]) => { return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1; @@ -359,3 +413,48 @@ function getFavoritePlugins( return idx === -1 ? !returnFavoredPlugins : returnFavoredPlugins; }); } + +export function computeActivePluginList({ + enabledPlugins, + devicePlugins, + disabledPlugins, + downloadablePlugins, + unavailablePlugins, +}: PluginLists) { + const pluginList: ActivePluginList = {}; + for (const plugin of enabledPlugins) { + pluginList[plugin.id] = { + status: 'enabled', + details: plugin.details, + definition: plugin, + }; + } + for (const plugin of devicePlugins) { + pluginList[plugin.id] = { + status: 'enabled', + details: plugin.details, + definition: plugin, + }; + } + for (const plugin of disabledPlugins) { + pluginList[plugin.id] = { + status: 'disabled', + details: plugin.details, + definition: plugin, + }; + } + for (const plugin of downloadablePlugins) { + pluginList[plugin.id] = { + status: 'uninstalled', + details: plugin, + }; + } + for (const [plugin, reason] of unavailablePlugins) { + pluginList[plugin.id] = { + status: 'unavailable', + details: plugin, + reason, + }; + } + return pluginList; +}