From 4541cdc23b92f166518737d4ed2a069a40a16ea3 Mon Sep 17 00:00:00 2001 From: Anton Nikolaev Date: Tue, 16 Feb 2021 10:46:11 -0800 Subject: [PATCH] Device plugin management (2/n): enable/disable, install/uninstall Summary: *Stack summary*: this stack adds ability to manage device plugins in the same way as client plugins: install, update, uninstall, enable (star) and disable (unstar) them. *Diff summary*: implemented all plugin management actions for device plugins. Changelog: it is now possible to enable/disable and install/uninstall device plugins Reviewed By: mweststrate Differential Revision: D26337377 fbshipit-source-id: 7d1ed61a8dc5f3339e5e548c613b67bca0c27f4f --- desktop/app/src/PluginContainer.tsx | 20 ++--- .../src/__tests__/PluginContainer.node.tsx | 3 +- .../createMockFlipperWithPlugin.node.tsx.snap | 1 + desktop/app/src/devices/BaseDevice.tsx | 5 +- .../__tests__/pluginManager.node.tsx | 65 ++++++++++++++++ desktop/app/src/dispatcher/pluginManager.tsx | 76 ++++++++++++++----- desktop/app/src/reducers/connections.tsx | 44 +++++++++++ desktop/app/src/reducers/index.tsx | 2 + desktop/app/src/reducers/pluginManager.tsx | 2 +- .../sandy-chrome/appinspect/PluginList.tsx | 21 ++++- .../appinspect/__tests__/PluginList.spec.tsx | 13 +++- .../createMockFlipperWithPlugin.tsx | 28 ++++--- desktop/app/src/utils/messageQueue.tsx | 1 + desktop/app/src/utils/pluginUtils.tsx | 62 +++++++++++---- 14 files changed, 281 insertions(+), 62 deletions(-) diff --git a/desktop/app/src/PluginContainer.tsx b/desktop/app/src/PluginContainer.tsx index ba1ea043e..d4d9c5bf2 100644 --- a/desktop/app/src/PluginContainer.tsx +++ b/desktop/app/src/PluginContainer.tsx @@ -48,7 +48,6 @@ import {processMessageQueue} from './utils/messageQueue'; import {ToggleButton, SmallText, Layout} from './ui'; import {theme, TrackingScope, _SandyPluginRenderer} from 'flipper-plugin'; import {isDevicePluginDefinition} from './utils/pluginUtils'; -import ArchivedDevice from './devices/ArchivedDevice'; import {ContentContainer} from './sandy-chrome/ContentContainer'; import {Alert, Typography} from 'antd'; import {InstalledPluginDetails} from 'plugin-lib'; @@ -318,7 +317,7 @@ class PluginContainer extends PureComponent { onClick={() => { this.props.starPlugin({ plugin: activePlugin, - selectedApp: (this.props.target as Client).query.app, + selectedApp: (this.props.target as Client)?.query?.app, }); }} large @@ -554,6 +553,7 @@ export default connect( clients, deepLinkPayload, userStarredPlugins, + userStarredDevicePlugins, }, pluginStates, plugins: {devicePlugins, clientPlugins, installedPlugins}, @@ -567,23 +567,25 @@ export default connect( if (selectedPlugin) { activePlugin = devicePlugins.get(selectedPlugin); - target = selectedDevice; if (selectedDevice && activePlugin) { + target = selectedDevice; pluginKey = getPluginKey(selectedDevice.serial, activePlugin.id); - pluginIsEnabled = true; } else { target = clients.find((client: Client) => client.id === selectedApp) || null; activePlugin = clientPlugins.get(selectedPlugin); if (activePlugin && target) { pluginKey = getPluginKey(target.id, activePlugin.id); - pluginIsEnabled = pluginIsStarred( - userStarredPlugins, - selectedApp, - activePlugin.id, - ); } } + pluginIsEnabled = + activePlugin !== undefined && + pluginIsStarred( + userStarredPlugins, + userStarredDevicePlugins, + selectedApp, + activePlugin.id, + ); } const isArchivedDevice = !selectedDevice ? false diff --git a/desktop/app/src/__tests__/PluginContainer.node.tsx b/desktop/app/src/__tests__/PluginContainer.node.tsx index cdfe6457b..b697dc89d 100644 --- a/desktop/app/src/__tests__/PluginContainer.node.tsx +++ b/desktop/app/src/__tests__/PluginContainer.node.tsx @@ -961,7 +961,8 @@ test('Sandy plugins support isPluginSupported + selectPlugin', async () => { expect(pluginInstance.deactivatedStub).toBeCalledTimes(0); expect(linksSeen).toEqual([]); - // open a device plugin + // star and navigate to a device plugin + store.dispatch(starPlugin({plugin: definition3})); pluginInstance.selectPlugin(definition3.id); expect(store.getState().connections.selectedPlugin).toBe(definition3.id); expect(renderer.baseElement.querySelector('h1')).toMatchInlineSnapshot(` diff --git a/desktop/app/src/__tests__/__snapshots__/createMockFlipperWithPlugin.node.tsx.snap b/desktop/app/src/__tests__/__snapshots__/createMockFlipperWithPlugin.node.tsx.snap index 4199f1442..7ef6d1501 100644 --- a/desktop/app/src/__tests__/__snapshots__/createMockFlipperWithPlugin.node.tsx.snap +++ b/desktop/app/src/__tests__/__snapshots__/createMockFlipperWithPlugin.node.tsx.snap @@ -37,6 +37,7 @@ Object { "userPreferredApp": "TestApp#Android#MockAndroidDevice#serial", "userPreferredDevice": "MockAndroidDevice", "userPreferredPlugin": "TestPlugin", + "userStarredDevicePlugins": Set {}, "userStarredPlugins": Object { "TestApp": Array [ "TestPlugin", diff --git a/desktop/app/src/devices/BaseDevice.tsx b/desktop/app/src/devices/BaseDevice.tsx index a68c307c0..fac73b55e 100644 --- a/desktop/app/src/devices/BaseDevice.tsx +++ b/desktop/app/src/devices/BaseDevice.tsx @@ -248,7 +248,10 @@ export default class BaseDevice { instance.destroy(); this.sandyPluginStates.delete(pluginId); } - this.devicePlugins.splice(this.devicePlugins.indexOf(pluginId), 1); + const index = this.devicePlugins.indexOf(pluginId); + if (index >= 0) { + this.devicePlugins.splice(index, 1); + } } disconnect() { diff --git a/desktop/app/src/dispatcher/__tests__/pluginManager.node.tsx b/desktop/app/src/dispatcher/__tests__/pluginManager.node.tsx index 4a6a1d1a6..d60e9782f 100644 --- a/desktop/app/src/dispatcher/__tests__/pluginManager.node.tsx +++ b/desktop/app/src/dispatcher/__tests__/pluginManager.node.tsx @@ -21,6 +21,8 @@ import * as TestPlugin from '../../test-utils/TestPlugin'; import {_SandyPluginDefinition as SandyPluginDefinition} from 'flipper-plugin'; import MockFlipper from '../../test-utils/MockFlipper'; import Client from '../../Client'; +import React from 'react'; +import BaseDevice from '../../devices/BaseDevice'; const pluginDetails1 = TestUtils.createMockPluginDetails({ id: 'plugin1', @@ -45,10 +47,27 @@ const pluginDetails2 = TestUtils.createMockPluginDetails({ }); const pluginDefinition2 = new SandyPluginDefinition(pluginDetails2, TestPlugin); +const devicePluginDetails = TestUtils.createMockPluginDetails({ + id: 'device', + name: 'flipper-device', +}); +const devicePluginDefinition = new SandyPluginDefinition(devicePluginDetails, { + supportsDevice() { + return true; + }, + devicePlugin() { + return {}; + }, + Component() { + return

Plugin3

; + }, +}); + const mockedRequirePlugin = mocked(requirePlugin); let mockFlipper: MockFlipper; let mockClient: Client; +let mockDevice: BaseDevice; beforeEach(async () => { mockedRequirePlugin.mockImplementation( @@ -59,6 +78,8 @@ beforeEach(async () => { ? pluginDefinition2 : details === pluginDetails1V2 ? pluginDefinition1V2 + : details === devicePluginDetails + ? devicePluginDefinition : undefined)!, ); mockFlipper = new MockFlipper(); @@ -66,6 +87,7 @@ beforeEach(async () => { clientOptions: {supportedPlugins: ['plugin1', 'plugin2']}, }); mockClient = initResult.client; + mockDevice = initResult.device; }); afterEach(async () => { @@ -199,3 +221,46 @@ test('unstar plugin', async () => { ).not.toContain('plugin1'); expect(mockClient.sandyPluginStates.has('plugin1')).toBeFalsy(); }); + +test('star device plugin', async () => { + mockFlipper.dispatch( + loadPlugin({ + plugin: devicePluginDetails, + enable: false, + notifyIfFailed: false, + }), + ); + mockFlipper.dispatch( + starPlugin({ + plugin: devicePluginDefinition, + }), + ); + expect( + mockFlipper.getState().connections.userStarredDevicePlugins.has('device'), + ).toBeTruthy(); + expect(mockDevice.sandyPluginStates.has('device')).toBeTruthy(); +}); + +test('unstar device plugin', async () => { + mockFlipper.dispatch( + loadPlugin({ + plugin: devicePluginDetails, + enable: false, + notifyIfFailed: false, + }), + ); + mockFlipper.dispatch( + starPlugin({ + plugin: devicePluginDefinition, + }), + ); + mockFlipper.dispatch( + starPlugin({ + plugin: devicePluginDefinition, + }), + ); + expect( + mockFlipper.getState().connections.userStarredDevicePlugins.has('device'), + ).toBeFalsy(); + expect(mockDevice.sandyPluginStates.has('device')).toBeFalsy(); +}); diff --git a/desktop/app/src/dispatcher/pluginManager.tsx b/desktop/app/src/dispatcher/pluginManager.tsx index b102f87a9..cd84884b9 100644 --- a/desktop/app/src/dispatcher/pluginManager.tsx +++ b/desktop/app/src/dispatcher/pluginManager.tsx @@ -12,11 +12,11 @@ import {clearPluginState} from '../reducers/pluginStates'; import type {Logger} from '../fb-interfaces/Logger'; import { LoadPluginActionPayload, - PluginCommand, UninstallPluginActionPayload, UpdatePluginActionPayload, pluginCommandsProcessed, StarPluginActionPayload, + PluginCommand, } from '../reducers/pluginManager'; import { getInstalledPlugins, @@ -28,8 +28,8 @@ import {sideEffect} from '../utils/sideEffect'; import {requirePlugin} from './plugins'; import {showErrorNotification} from '../utils/notifications'; import { + ClientPluginDefinition, DevicePluginDefinition, - FlipperDevicePlugin, FlipperPlugin, PluginDefinition, } from '../plugin'; @@ -41,11 +41,17 @@ import { registerInstalledPlugins, } from '../reducers/plugins'; import {_SandyPluginDefinition} from 'flipper-plugin'; -import {pluginStarred, pluginUnstarred} from '../reducers/connections'; +import { + devicePluginStarred, + devicePluginUnstarred, + pluginStarred, + pluginUnstarred, +} from '../reducers/connections'; import {deconstructClientId} from '../utils/clientUtils'; import {clearMessageQueue} from '../reducers/pluginMessageQueue'; import { getPluginKey, + isDevicePluginDefinition, defaultEnabledBackgroundPlugins, } from '../utils/pluginUtils'; @@ -161,7 +167,7 @@ function uninstallPlugin(store: Store, {plugin}: UninstallPluginActionPayload) { function updatePlugin(store: Store, payload: UpdatePluginActionPayload) { const {plugin, enablePlugin} = payload; if (isDevicePluginDefinition(plugin)) { - return updateDevicePlugin(store, plugin); + return updateDevicePlugin(store, plugin, enablePlugin); } else { return updateClientPlugin(store, plugin, enablePlugin); } @@ -175,8 +181,26 @@ function getSelectedAppId(store: Store) { return selectedApp; } -function starPlugin(store: Store, payload: StarPluginActionPayload) { - const {plugin, selectedApp} = payload; +function starPlugin( + store: Store, + {plugin, selectedApp}: StarPluginActionPayload, +) { + if (isDevicePluginDefinition(plugin)) { + starDevicePlugin(store, plugin); + } else { + starClientPlugin(store, plugin, selectedApp); + } +} + +function starClientPlugin( + store: Store, + plugin: ClientPluginDefinition, + selectedApp: string | undefined, +) { + selectedApp = selectedApp ?? getSelectedAppId(store); + if (!selectedApp) { + return; + } const {connections} = store.getState(); const clients = connections.clients.filter( (client) => client.query.app === selectedApp, @@ -200,6 +224,24 @@ function starPlugin(store: Store, payload: StarPluginActionPayload) { } } +function starDevicePlugin(store: Store, plugin: DevicePluginDefinition) { + const {connections} = store.getState(); + const devicesWithPlugin = connections.devices.filter((d) => + d.supportsPlugin(plugin.details), + ); + if (connections.userStarredDevicePlugins.has(plugin.id)) { + devicesWithPlugin.forEach((d) => { + d.unloadDevicePlugin(plugin.id); + }); + store.dispatch(devicePluginUnstarred(plugin)); + } else { + devicesWithPlugin.forEach((d) => { + d.loadDevicePlugin(plugin); + }); + store.dispatch(devicePluginStarred(plugin)); + } +} + function updateClientPlugin( store: Store, plugin: typeof FlipperPlugin, @@ -235,9 +277,16 @@ function updateClientPlugin( } } -function updateDevicePlugin(store: Store, plugin: DevicePluginDefinition) { - const devices = store.getState().connections.devices; - const devicesWithEnabledPlugin = devices.filter((d) => +function updateDevicePlugin( + store: Store, + plugin: DevicePluginDefinition, + enable: boolean, +) { + if (enable) { + store.dispatch(devicePluginStarred(plugin)); + } + const connections = store.getState().connections; + const devicesWithEnabledPlugin = connections.devices.filter((d) => d.supportsPlugin(plugin), ); devicesWithEnabledPlugin.forEach((d) => { @@ -295,12 +344,3 @@ function unloadPluginModule(plugin: ActivatablePluginDetails) { } unloadModule(plugin.entry); } - -export function isDevicePluginDefinition( - definition: PluginDefinition, -): definition is DevicePluginDefinition { - return ( - (definition as any).prototype instanceof FlipperDevicePlugin || - (definition instanceof _SandyPluginDefinition && definition.isDevicePlugin) - ); -} diff --git a/desktop/app/src/reducers/connections.tsx b/desktop/app/src/reducers/connections.tsx index 9bf94b9f2..7cc65d049 100644 --- a/desktop/app/src/reducers/connections.tsx +++ b/desktop/app/src/reducers/connections.tsx @@ -42,6 +42,7 @@ export type State = { userPreferredPlugin: null | string; userPreferredApp: null | string; userStarredPlugins: {[client: string]: string[]}; + userStarredDevicePlugins: Set; clients: Array; uninitializedClients: Array<{ client: UninitializedClient; @@ -115,6 +116,12 @@ export type Action = selectedApp: string; }; } + | { + type: 'DEVICE_PLUGIN_STARRED'; + payload: { + plugin: PluginDefinition; + }; + } | { type: 'PLUGIN_UNSTARRED'; payload: { @@ -122,6 +129,12 @@ export type Action = selectedApp: string; }; } + | { + type: 'DEVICE_PLUGIN_UNSTARRED'; + payload: { + plugin: PluginDefinition; + }; + } | { type: 'SELECT_CLIENT'; payload: string | null; @@ -140,6 +153,7 @@ const INITAL_STATE: State = { userPreferredPlugin: null, userPreferredApp: null, userStarredPlugins: {}, + userStarredDevicePlugins: new Set(), clients: [], uninitializedClients: [], deepLinkPayload: null, @@ -379,6 +393,12 @@ export default (state: State = INITAL_STATE, action: Actions): State => { } }); } + case 'DEVICE_PLUGIN_STARRED': { + const {plugin} = action.payload; + return produce(state, (draft) => { + draft.userStarredDevicePlugins.add(plugin.id); + }); + } case 'PLUGIN_UNSTARRED': { const {plugin, selectedApp} = action.payload; const selectedPlugin = plugin.id; @@ -393,6 +413,12 @@ export default (state: State = INITAL_STATE, action: Actions): State => { } }); } + case 'DEVICE_PLUGIN_UNSTARRED': { + const {plugin} = action.payload; + return produce(state, (draft) => { + draft.userStarredDevicePlugins.delete(plugin.id); + }); + } default: return state; } @@ -449,6 +475,20 @@ export const pluginStarred = ( }, }); +export const devicePluginStarred = (plugin: PluginDefinition): Action => ({ + type: 'DEVICE_PLUGIN_STARRED', + payload: { + plugin, + }, +}); + +export const devicePluginUnstarred = (plugin: PluginDefinition): Action => ({ + type: 'DEVICE_PLUGIN_UNSTARRED', + payload: { + plugin, + }, +}); + export const pluginUnstarred = ( plugin: PluginDefinition, appId: string, @@ -583,9 +623,13 @@ export function getSelectedPluginKey(state: State): string | undefined { export function pluginIsStarred( userStarredPlugins: State['userStarredPlugins'], + userStarredDevicePlugins: State['userStarredDevicePlugins'], app: string | null, pluginId: string, ): boolean { + if (userStarredDevicePlugins.has(pluginId)) { + return true; + } if (!app) { return false; } diff --git a/desktop/app/src/reducers/index.tsx b/desktop/app/src/reducers/index.tsx index 84be506c5..b20329999 100644 --- a/desktop/app/src/reducers/index.tsx +++ b/desktop/app/src/reducers/index.tsx @@ -141,7 +141,9 @@ export default combineReducers({ 'userPreferredPlugin', 'userPreferredApp', 'userStarredPlugins', + 'userStarredDevicePlugins', ], + transforms: [setTransformer({whitelist: ['userStarredDevicePlugins']})], }, connections, ), diff --git a/desktop/app/src/reducers/pluginManager.tsx b/desktop/app/src/reducers/pluginManager.tsx index d5d7051d6..8fae961a1 100644 --- a/desktop/app/src/reducers/pluginManager.tsx +++ b/desktop/app/src/reducers/pluginManager.tsx @@ -54,7 +54,7 @@ export type UpdatePluginAction = { export type StarPluginActionPayload = { plugin: PluginDefinition; - selectedApp: string; + selectedApp?: string; }; export type StarPluginAction = { diff --git a/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx b/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx index 799596087..2478cc352 100644 --- a/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx +++ b/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx @@ -87,6 +87,7 @@ export const PluginList = memo(function PluginList({ client, plugins, connections.userStarredPlugins, + connections.userStarredDevicePlugins, pluginsChanged, ]); const isConnected = useValue(activeDevice?.connected, false); @@ -143,14 +144,16 @@ export const PluginList = memo(function PluginList({ ); const handleStarPlugin = useCallback( (id: string) => { + const plugin = (plugins.clientPlugins.get(id) ?? + plugins.devicePlugins.get(id))!; dispatch( starPlugin({ - selectedApp: client!.query.app, - plugin: plugins.clientPlugins.get(id)!, + selectedApp: client?.query.app, + plugin, }), ); }, - [client, plugins.clientPlugins, dispatch], + [client, plugins.clientPlugins, plugins.devicePlugins, dispatch], ); const handleInstallPlugin = useCallback( (id: string) => { @@ -200,6 +203,18 @@ export const PluginList = memo(function PluginList({ } onClick={handleAppPluginClick} tooltip={getPluginTooltip(plugin.details)} + actions={ + isArchived ? null : ( + + } + /> + ) + } /> ))} 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 9942f76d1..fe1a4ba85 100644 --- a/desktop/app/src/sandy-chrome/appinspect/__tests__/PluginList.spec.tsx +++ b/desktop/app/src/sandy-chrome/appinspect/__tests__/PluginList.spec.tsx @@ -16,7 +16,7 @@ import {FlipperPlugin} from '../../../plugin'; import MetroDevice from '../../../devices/MetroDevice'; import BaseDevice from '../../../devices/BaseDevice'; import {_SandyPluginDefinition} from 'flipper-plugin'; -import {createMockPluginDetails} from 'flipper-plugin/src/test-utils/test-utils'; +import {TestUtils} from 'flipper-plugin'; import {selectPlugin} from '../../../reducers/connections'; import {registerMetroDevice} from '../../../dispatcher/metroDevice'; import { @@ -31,6 +31,8 @@ import * as LogsPluginModule from '../../../../../plugins/logs/index'; import {createMockDownloadablePluginDetails} from '../../../utils/testUtils'; import {computePluginLists} from '../../../utils/pluginUtils'; +const createMockPluginDetails = TestUtils.createMockPluginDetails; + const logsPlugin = new _SandyPluginDefinition( createMockPluginDetails({id: 'DeviceLogs'}), LogsPluginModule, @@ -173,6 +175,7 @@ describe('basic findBestDevice with metro present', () => { flipper.client, state.plugins, state.connections.userStarredPlugins, + state.connections.userStarredDevicePlugins, ), ).toEqual({ downloadablePlugins: [], @@ -284,6 +287,7 @@ describe('basic findBestDevice with metro present', () => { flipper.client, state.plugins, state.connections.userStarredPlugins, + state.connections.userStarredDevicePlugins, ); expect(pluginLists).toEqual({ devicePlugins: [logsPlugin], @@ -297,15 +301,15 @@ describe('basic findBestDevice with metro present', () => { ], [ unsupportedDevicePlugin.details, - "Device plugin 'Unsupported Device Plugin' is not supported by the current device type.", + "Device plugin 'Unsupported Device Plugin' is not supported by the currently connected device.", ], [ unsupportedPlugin.details, - "Plugin 'Unsupported Plugin' is installed in Flipper, but not supported by the client application", + "Plugin 'Unsupported Plugin' is not supported by the client application", ], [ unsupportedDownloadablePlugin, - "Plugin 'Unsupported Uninstalled Plugin' is not installed in Flipper and not supported by the client application", + "Plugin 'Unsupported Uninstalled Plugin' is not supported by the client application and not installed in Flipper", ], ], downloadablePlugins: [supportedDownloadablePlugin], @@ -325,6 +329,7 @@ describe('basic findBestDevice with metro present', () => { flipper.client, state.plugins, state.connections.userStarredPlugins, + state.connections.userStarredDevicePlugins, ), ).toMatchObject({ enabledPlugins: [plugin2], diff --git a/desktop/app/src/test-utils/createMockFlipperWithPlugin.tsx b/desktop/app/src/test-utils/createMockFlipperWithPlugin.tsx index 0144311df..4aeca884a 100644 --- a/desktop/app/src/test-utils/createMockFlipperWithPlugin.tsx +++ b/desktop/app/src/test-utils/createMockFlipperWithPlugin.tsx @@ -62,6 +62,23 @@ type MockOptions = Partial<{ supportedPlugins?: string[]; }>; +function isPluginEnabled( + store: Store, + pluginClazz: PluginDefinition, + selectedApp: string, +) { + return ( + (!isDevicePluginDefinition(pluginClazz) && + store + .getState() + .connections.userStarredPlugins[selectedApp]?.includes( + pluginClazz.id, + )) || + (isDevicePluginDefinition(pluginClazz) && + store.getState().connections.userStarredDevicePlugins.has(pluginClazz.id)) + ); +} + export async function createMockFlipperWithPlugin( pluginClazz: PluginDefinition, options?: MockOptions, @@ -89,14 +106,7 @@ export async function createMockFlipperWithPlugin( backgroundPlugins: options?.asBackgroundPlugin ? [pluginClazz.id] : [], }); // enable the plugin - if ( - !isDevicePluginDefinition(pluginClazz) && - !store - .getState() - .connections.userStarredPlugins[client.query.app]?.includes( - pluginClazz.id, - ) - ) { + if (!isPluginEnabled(store, pluginClazz, name)) { store.dispatch( starPlugin({ plugin: pluginClazz, @@ -106,7 +116,7 @@ export async function createMockFlipperWithPlugin( } if (!options?.dontEnableAdditionalPlugins) { options?.additionalPlugins?.forEach((plugin) => { - if (!isDevicePluginDefinition(plugin)) { + if (!isPluginEnabled(store, plugin, name)) { store.dispatch( starPlugin({ plugin, diff --git a/desktop/app/src/utils/messageQueue.tsx b/desktop/app/src/utils/messageQueue.tsx index 4d5ec8b18..106317365 100644 --- a/desktop/app/src/utils/messageQueue.tsx +++ b/desktop/app/src/utils/messageQueue.tsx @@ -135,6 +135,7 @@ export function processMessagesLater( case (plugin as any).prototype instanceof FlipperDevicePlugin: case pluginIsStarred( store.getState().connections.userStarredPlugins, + store.getState().connections.userStarredDevicePlugins, deconstructPluginKey(pluginKey).client, pluginId, ): diff --git a/desktop/app/src/utils/pluginUtils.tsx b/desktop/app/src/utils/pluginUtils.tsx index e31c6e731..902e1c041 100644 --- a/desktop/app/src/utils/pluginUtils.tsx +++ b/desktop/app/src/utils/pluginUtils.tsx @@ -84,6 +84,7 @@ export function getExportablePlugins( client, state.plugins, state.connections.userStarredPlugins, + state.connections.userStarredDevicePlugins, ); return [ @@ -179,16 +180,33 @@ export function computePluginLists( client: Client | undefined, plugins: State['plugins'], userStarredPlugins: State['connections']['userStarredPlugins'], + userStarredDevicePlugins: Set, _pluginsChanged?: number, // this argument is purely used to invalidate the memoization cache ) { + const uninstalledMarketplacePlugins = filterNewestVersionOfEachPlugin( + [...plugins.bundledPlugins.values()], + plugins.marketplacePlugins, + ).filter((p) => !plugins.loadedPlugins.has(p.id)); const devicePlugins: DevicePluginDefinition[] = [ ...plugins.devicePlugins.values(), - ].filter((p) => device?.supportsPlugin(p)); + ] + .filter((p) => device?.supportsPlugin(p)) + .filter((p) => userStarredDevicePlugins.has(p.id)); const metroPlugins: DevicePluginDefinition[] = [ ...plugins.devicePlugins.values(), - ].filter((p) => metroDevice?.supportsPlugin(p)); + ] + .filter((p) => metroDevice?.supportsPlugin(p)) + .filter((p) => userStarredDevicePlugins.has(p.id)); const enabledPlugins: ClientPluginDefinition[] = []; - const disabledPlugins: ClientPluginDefinition[] = []; + const disabledPlugins: PluginDefinition[] = [ + ...plugins.devicePlugins.values(), + ] + .filter( + (p) => + device?.supportsPlugin(p.details) || + metroDevice?.supportsPlugin(p.details), + ) + .filter((p) => !userStarredDevicePlugins.has(p.id)); const unavailablePlugins: [plugin: PluginDetails, reason: string][] = []; const downloadablePlugins: ( | DownloadablePluginDetails @@ -203,10 +221,20 @@ export function computePluginLists( p.details, `Device plugin '${getPluginTitle( p.details, - )}' is not supported by the current device type.`, + )}' is not supported by the currently connected device.`, ]); } } + for (const plugin of uninstalledMarketplacePlugins.filter( + (d) => d.pluginType === 'device', + )) { + if ( + device.supportsPlugin(plugin) || + metroDevice?.supportsPlugin(plugin) + ) { + downloadablePlugins.push(plugin); + } + } } // process problematic plugins @@ -244,7 +272,7 @@ export function computePluginLists( plugin.details, `Plugin '${getPluginTitle( plugin.details, - )}' is installed in Flipper, but not supported by the client application`, + )}' is not supported by the client application`, ]); } else if (favoritePlugins.includes(plugin)) { enabledPlugins.push(plugin); @@ -252,23 +280,25 @@ export function computePluginLists( disabledPlugins.push(plugin); } }); - const uninstalledMarketplacePlugins = filterNewestVersionOfEachPlugin( - [...plugins.bundledPlugins.values()], - plugins.marketplacePlugins, - ).filter((p) => !plugins.loadedPlugins.has(p.id)); uninstalledMarketplacePlugins.forEach((plugin) => { if (client.supportsPlugin(plugin.id)) { downloadablePlugins.push(plugin); - } else { - unavailablePlugins.push([ - plugin, - `Plugin '${getPluginTitle( - plugin, - )}' is not installed in Flipper and not supported by the client application`, - ]); } }); } + const downloadablePluginSet = new Set( + downloadablePlugins.map((p) => p.id), + ); + uninstalledMarketplacePlugins + .filter((p) => !downloadablePluginSet.has(p.id)) + .forEach((plugin) => { + unavailablePlugins.push([ + plugin, + `Plugin '${getPluginTitle( + plugin, + )}' is not supported by the client application and not installed in Flipper`, + ]); + }); devicePlugins.sort(sortPluginsByName); metroPlugins.sort(sortPluginsByName);