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
This commit is contained in:
Anton Nikolaev
2021-02-16 10:46:11 -08:00
committed by Facebook GitHub Bot
parent 899fcd0783
commit 4541cdc23b
14 changed files with 281 additions and 62 deletions

View File

@@ -48,7 +48,6 @@ import {processMessageQueue} from './utils/messageQueue';
import {ToggleButton, SmallText, Layout} from './ui'; import {ToggleButton, SmallText, Layout} from './ui';
import {theme, TrackingScope, _SandyPluginRenderer} from 'flipper-plugin'; import {theme, TrackingScope, _SandyPluginRenderer} from 'flipper-plugin';
import {isDevicePluginDefinition} from './utils/pluginUtils'; import {isDevicePluginDefinition} from './utils/pluginUtils';
import ArchivedDevice from './devices/ArchivedDevice';
import {ContentContainer} from './sandy-chrome/ContentContainer'; import {ContentContainer} from './sandy-chrome/ContentContainer';
import {Alert, Typography} from 'antd'; import {Alert, Typography} from 'antd';
import {InstalledPluginDetails} from 'plugin-lib'; import {InstalledPluginDetails} from 'plugin-lib';
@@ -318,7 +317,7 @@ class PluginContainer extends PureComponent<Props, State> {
onClick={() => { onClick={() => {
this.props.starPlugin({ this.props.starPlugin({
plugin: activePlugin, plugin: activePlugin,
selectedApp: (this.props.target as Client).query.app, selectedApp: (this.props.target as Client)?.query?.app,
}); });
}} }}
large large
@@ -554,6 +553,7 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
clients, clients,
deepLinkPayload, deepLinkPayload,
userStarredPlugins, userStarredPlugins,
userStarredDevicePlugins,
}, },
pluginStates, pluginStates,
plugins: {devicePlugins, clientPlugins, installedPlugins}, plugins: {devicePlugins, clientPlugins, installedPlugins},
@@ -567,23 +567,25 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
if (selectedPlugin) { if (selectedPlugin) {
activePlugin = devicePlugins.get(selectedPlugin); activePlugin = devicePlugins.get(selectedPlugin);
target = selectedDevice;
if (selectedDevice && activePlugin) { if (selectedDevice && activePlugin) {
target = selectedDevice;
pluginKey = getPluginKey(selectedDevice.serial, activePlugin.id); pluginKey = getPluginKey(selectedDevice.serial, activePlugin.id);
pluginIsEnabled = true;
} else { } else {
target = target =
clients.find((client: Client) => client.id === selectedApp) || null; clients.find((client: Client) => client.id === selectedApp) || null;
activePlugin = clientPlugins.get(selectedPlugin); activePlugin = clientPlugins.get(selectedPlugin);
if (activePlugin && target) { if (activePlugin && target) {
pluginKey = getPluginKey(target.id, activePlugin.id); pluginKey = getPluginKey(target.id, activePlugin.id);
pluginIsEnabled = pluginIsStarred(
userStarredPlugins,
selectedApp,
activePlugin.id,
);
} }
} }
pluginIsEnabled =
activePlugin !== undefined &&
pluginIsStarred(
userStarredPlugins,
userStarredDevicePlugins,
selectedApp,
activePlugin.id,
);
} }
const isArchivedDevice = !selectedDevice const isArchivedDevice = !selectedDevice
? false ? false

View File

@@ -961,7 +961,8 @@ test('Sandy plugins support isPluginSupported + selectPlugin', async () => {
expect(pluginInstance.deactivatedStub).toBeCalledTimes(0); expect(pluginInstance.deactivatedStub).toBeCalledTimes(0);
expect(linksSeen).toEqual([]); expect(linksSeen).toEqual([]);
// open a device plugin // star and navigate to a device plugin
store.dispatch(starPlugin({plugin: definition3}));
pluginInstance.selectPlugin(definition3.id); pluginInstance.selectPlugin(definition3.id);
expect(store.getState().connections.selectedPlugin).toBe(definition3.id); expect(store.getState().connections.selectedPlugin).toBe(definition3.id);
expect(renderer.baseElement.querySelector('h1')).toMatchInlineSnapshot(` expect(renderer.baseElement.querySelector('h1')).toMatchInlineSnapshot(`

View File

@@ -37,6 +37,7 @@ Object {
"userPreferredApp": "TestApp#Android#MockAndroidDevice#serial", "userPreferredApp": "TestApp#Android#MockAndroidDevice#serial",
"userPreferredDevice": "MockAndroidDevice", "userPreferredDevice": "MockAndroidDevice",
"userPreferredPlugin": "TestPlugin", "userPreferredPlugin": "TestPlugin",
"userStarredDevicePlugins": Set {},
"userStarredPlugins": Object { "userStarredPlugins": Object {
"TestApp": Array [ "TestApp": Array [
"TestPlugin", "TestPlugin",

View File

@@ -248,7 +248,10 @@ export default class BaseDevice {
instance.destroy(); instance.destroy();
this.sandyPluginStates.delete(pluginId); 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() { disconnect() {

View File

@@ -21,6 +21,8 @@ import * as TestPlugin from '../../test-utils/TestPlugin';
import {_SandyPluginDefinition as SandyPluginDefinition} from 'flipper-plugin'; import {_SandyPluginDefinition as SandyPluginDefinition} from 'flipper-plugin';
import MockFlipper from '../../test-utils/MockFlipper'; import MockFlipper from '../../test-utils/MockFlipper';
import Client from '../../Client'; import Client from '../../Client';
import React from 'react';
import BaseDevice from '../../devices/BaseDevice';
const pluginDetails1 = TestUtils.createMockPluginDetails({ const pluginDetails1 = TestUtils.createMockPluginDetails({
id: 'plugin1', id: 'plugin1',
@@ -45,10 +47,27 @@ const pluginDetails2 = TestUtils.createMockPluginDetails({
}); });
const pluginDefinition2 = new SandyPluginDefinition(pluginDetails2, TestPlugin); 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 <h1>Plugin3</h1>;
},
});
const mockedRequirePlugin = mocked(requirePlugin); const mockedRequirePlugin = mocked(requirePlugin);
let mockFlipper: MockFlipper; let mockFlipper: MockFlipper;
let mockClient: Client; let mockClient: Client;
let mockDevice: BaseDevice;
beforeEach(async () => { beforeEach(async () => {
mockedRequirePlugin.mockImplementation( mockedRequirePlugin.mockImplementation(
@@ -59,6 +78,8 @@ beforeEach(async () => {
? pluginDefinition2 ? pluginDefinition2
: details === pluginDetails1V2 : details === pluginDetails1V2
? pluginDefinition1V2 ? pluginDefinition1V2
: details === devicePluginDetails
? devicePluginDefinition
: undefined)!, : undefined)!,
); );
mockFlipper = new MockFlipper(); mockFlipper = new MockFlipper();
@@ -66,6 +87,7 @@ beforeEach(async () => {
clientOptions: {supportedPlugins: ['plugin1', 'plugin2']}, clientOptions: {supportedPlugins: ['plugin1', 'plugin2']},
}); });
mockClient = initResult.client; mockClient = initResult.client;
mockDevice = initResult.device;
}); });
afterEach(async () => { afterEach(async () => {
@@ -199,3 +221,46 @@ test('unstar plugin', async () => {
).not.toContain('plugin1'); ).not.toContain('plugin1');
expect(mockClient.sandyPluginStates.has('plugin1')).toBeFalsy(); 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();
});

View File

@@ -12,11 +12,11 @@ import {clearPluginState} from '../reducers/pluginStates';
import type {Logger} from '../fb-interfaces/Logger'; import type {Logger} from '../fb-interfaces/Logger';
import { import {
LoadPluginActionPayload, LoadPluginActionPayload,
PluginCommand,
UninstallPluginActionPayload, UninstallPluginActionPayload,
UpdatePluginActionPayload, UpdatePluginActionPayload,
pluginCommandsProcessed, pluginCommandsProcessed,
StarPluginActionPayload, StarPluginActionPayload,
PluginCommand,
} from '../reducers/pluginManager'; } from '../reducers/pluginManager';
import { import {
getInstalledPlugins, getInstalledPlugins,
@@ -28,8 +28,8 @@ import {sideEffect} from '../utils/sideEffect';
import {requirePlugin} from './plugins'; import {requirePlugin} from './plugins';
import {showErrorNotification} from '../utils/notifications'; import {showErrorNotification} from '../utils/notifications';
import { import {
ClientPluginDefinition,
DevicePluginDefinition, DevicePluginDefinition,
FlipperDevicePlugin,
FlipperPlugin, FlipperPlugin,
PluginDefinition, PluginDefinition,
} from '../plugin'; } from '../plugin';
@@ -41,11 +41,17 @@ import {
registerInstalledPlugins, registerInstalledPlugins,
} from '../reducers/plugins'; } from '../reducers/plugins';
import {_SandyPluginDefinition} from 'flipper-plugin'; 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 {deconstructClientId} from '../utils/clientUtils';
import {clearMessageQueue} from '../reducers/pluginMessageQueue'; import {clearMessageQueue} from '../reducers/pluginMessageQueue';
import { import {
getPluginKey, getPluginKey,
isDevicePluginDefinition,
defaultEnabledBackgroundPlugins, defaultEnabledBackgroundPlugins,
} from '../utils/pluginUtils'; } from '../utils/pluginUtils';
@@ -161,7 +167,7 @@ function uninstallPlugin(store: Store, {plugin}: UninstallPluginActionPayload) {
function updatePlugin(store: Store, payload: UpdatePluginActionPayload) { function updatePlugin(store: Store, payload: UpdatePluginActionPayload) {
const {plugin, enablePlugin} = payload; const {plugin, enablePlugin} = payload;
if (isDevicePluginDefinition(plugin)) { if (isDevicePluginDefinition(plugin)) {
return updateDevicePlugin(store, plugin); return updateDevicePlugin(store, plugin, enablePlugin);
} else { } else {
return updateClientPlugin(store, plugin, enablePlugin); return updateClientPlugin(store, plugin, enablePlugin);
} }
@@ -175,8 +181,26 @@ function getSelectedAppId(store: Store) {
return selectedApp; return selectedApp;
} }
function starPlugin(store: Store, payload: StarPluginActionPayload) { function starPlugin(
const {plugin, selectedApp} = payload; 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 {connections} = store.getState();
const clients = connections.clients.filter( const clients = connections.clients.filter(
(client) => client.query.app === selectedApp, (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( function updateClientPlugin(
store: Store, store: Store,
plugin: typeof FlipperPlugin, plugin: typeof FlipperPlugin,
@@ -235,9 +277,16 @@ function updateClientPlugin(
} }
} }
function updateDevicePlugin(store: Store, plugin: DevicePluginDefinition) { function updateDevicePlugin(
const devices = store.getState().connections.devices; store: Store,
const devicesWithEnabledPlugin = devices.filter((d) => plugin: DevicePluginDefinition,
enable: boolean,
) {
if (enable) {
store.dispatch(devicePluginStarred(plugin));
}
const connections = store.getState().connections;
const devicesWithEnabledPlugin = connections.devices.filter((d) =>
d.supportsPlugin(plugin), d.supportsPlugin(plugin),
); );
devicesWithEnabledPlugin.forEach((d) => { devicesWithEnabledPlugin.forEach((d) => {
@@ -295,12 +344,3 @@ function unloadPluginModule(plugin: ActivatablePluginDetails) {
} }
unloadModule(plugin.entry); unloadModule(plugin.entry);
} }
export function isDevicePluginDefinition(
definition: PluginDefinition,
): definition is DevicePluginDefinition {
return (
(definition as any).prototype instanceof FlipperDevicePlugin ||
(definition instanceof _SandyPluginDefinition && definition.isDevicePlugin)
);
}

View File

@@ -42,6 +42,7 @@ export type State = {
userPreferredPlugin: null | string; userPreferredPlugin: null | string;
userPreferredApp: null | string; userPreferredApp: null | string;
userStarredPlugins: {[client: string]: string[]}; userStarredPlugins: {[client: string]: string[]};
userStarredDevicePlugins: Set<string>;
clients: Array<Client>; clients: Array<Client>;
uninitializedClients: Array<{ uninitializedClients: Array<{
client: UninitializedClient; client: UninitializedClient;
@@ -115,6 +116,12 @@ export type Action =
selectedApp: string; selectedApp: string;
}; };
} }
| {
type: 'DEVICE_PLUGIN_STARRED';
payload: {
plugin: PluginDefinition;
};
}
| { | {
type: 'PLUGIN_UNSTARRED'; type: 'PLUGIN_UNSTARRED';
payload: { payload: {
@@ -122,6 +129,12 @@ export type Action =
selectedApp: string; selectedApp: string;
}; };
} }
| {
type: 'DEVICE_PLUGIN_UNSTARRED';
payload: {
plugin: PluginDefinition;
};
}
| { | {
type: 'SELECT_CLIENT'; type: 'SELECT_CLIENT';
payload: string | null; payload: string | null;
@@ -140,6 +153,7 @@ const INITAL_STATE: State = {
userPreferredPlugin: null, userPreferredPlugin: null,
userPreferredApp: null, userPreferredApp: null,
userStarredPlugins: {}, userStarredPlugins: {},
userStarredDevicePlugins: new Set(),
clients: [], clients: [],
uninitializedClients: [], uninitializedClients: [],
deepLinkPayload: null, 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': { case 'PLUGIN_UNSTARRED': {
const {plugin, selectedApp} = action.payload; const {plugin, selectedApp} = action.payload;
const selectedPlugin = plugin.id; 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: default:
return state; 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 = ( export const pluginUnstarred = (
plugin: PluginDefinition, plugin: PluginDefinition,
appId: string, appId: string,
@@ -583,9 +623,13 @@ export function getSelectedPluginKey(state: State): string | undefined {
export function pluginIsStarred( export function pluginIsStarred(
userStarredPlugins: State['userStarredPlugins'], userStarredPlugins: State['userStarredPlugins'],
userStarredDevicePlugins: State['userStarredDevicePlugins'],
app: string | null, app: string | null,
pluginId: string, pluginId: string,
): boolean { ): boolean {
if (userStarredDevicePlugins.has(pluginId)) {
return true;
}
if (!app) { if (!app) {
return false; return false;
} }

View File

@@ -141,7 +141,9 @@ export default combineReducers<State, Actions>({
'userPreferredPlugin', 'userPreferredPlugin',
'userPreferredApp', 'userPreferredApp',
'userStarredPlugins', 'userStarredPlugins',
'userStarredDevicePlugins',
], ],
transforms: [setTransformer({whitelist: ['userStarredDevicePlugins']})],
}, },
connections, connections,
), ),

View File

@@ -54,7 +54,7 @@ export type UpdatePluginAction = {
export type StarPluginActionPayload = { export type StarPluginActionPayload = {
plugin: PluginDefinition; plugin: PluginDefinition;
selectedApp: string; selectedApp?: string;
}; };
export type StarPluginAction = { export type StarPluginAction = {

View File

@@ -87,6 +87,7 @@ export const PluginList = memo(function PluginList({
client, client,
plugins, plugins,
connections.userStarredPlugins, connections.userStarredPlugins,
connections.userStarredDevicePlugins,
pluginsChanged, pluginsChanged,
]); ]);
const isConnected = useValue(activeDevice?.connected, false); const isConnected = useValue(activeDevice?.connected, false);
@@ -143,14 +144,16 @@ export const PluginList = memo(function PluginList({
); );
const handleStarPlugin = useCallback( const handleStarPlugin = useCallback(
(id: string) => { (id: string) => {
const plugin = (plugins.clientPlugins.get(id) ??
plugins.devicePlugins.get(id))!;
dispatch( dispatch(
starPlugin({ starPlugin({
selectedApp: client!.query.app, selectedApp: client?.query.app,
plugin: plugins.clientPlugins.get(id)!, plugin,
}), }),
); );
}, },
[client, plugins.clientPlugins, dispatch], [client, plugins.clientPlugins, plugins.devicePlugins, dispatch],
); );
const handleInstallPlugin = useCallback( const handleInstallPlugin = useCallback(
(id: string) => { (id: string) => {
@@ -200,6 +203,18 @@ export const PluginList = memo(function PluginList({
} }
onClick={handleAppPluginClick} onClick={handleAppPluginClick}
tooltip={getPluginTooltip(plugin.details)} tooltip={getPluginTooltip(plugin.details)}
actions={
isArchived ? null : (
<ActionButton
id={plugin.id}
onClick={handleStarPlugin}
title="Disable plugin"
icon={
<MinusOutlined size={16} style={{marginRight: 0}} />
}
/>
)
}
/> />
))} ))}
</PluginGroup> </PluginGroup>

View File

@@ -16,7 +16,7 @@ import {FlipperPlugin} from '../../../plugin';
import MetroDevice from '../../../devices/MetroDevice'; import MetroDevice from '../../../devices/MetroDevice';
import BaseDevice from '../../../devices/BaseDevice'; import BaseDevice from '../../../devices/BaseDevice';
import {_SandyPluginDefinition} from 'flipper-plugin'; 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 {selectPlugin} from '../../../reducers/connections';
import {registerMetroDevice} from '../../../dispatcher/metroDevice'; import {registerMetroDevice} from '../../../dispatcher/metroDevice';
import { import {
@@ -31,6 +31,8 @@ import * as LogsPluginModule from '../../../../../plugins/logs/index';
import {createMockDownloadablePluginDetails} from '../../../utils/testUtils'; import {createMockDownloadablePluginDetails} from '../../../utils/testUtils';
import {computePluginLists} from '../../../utils/pluginUtils'; import {computePluginLists} from '../../../utils/pluginUtils';
const createMockPluginDetails = TestUtils.createMockPluginDetails;
const logsPlugin = new _SandyPluginDefinition( const logsPlugin = new _SandyPluginDefinition(
createMockPluginDetails({id: 'DeviceLogs'}), createMockPluginDetails({id: 'DeviceLogs'}),
LogsPluginModule, LogsPluginModule,
@@ -173,6 +175,7 @@ describe('basic findBestDevice with metro present', () => {
flipper.client, flipper.client,
state.plugins, state.plugins,
state.connections.userStarredPlugins, state.connections.userStarredPlugins,
state.connections.userStarredDevicePlugins,
), ),
).toEqual({ ).toEqual({
downloadablePlugins: [], downloadablePlugins: [],
@@ -284,6 +287,7 @@ describe('basic findBestDevice with metro present', () => {
flipper.client, flipper.client,
state.plugins, state.plugins,
state.connections.userStarredPlugins, state.connections.userStarredPlugins,
state.connections.userStarredDevicePlugins,
); );
expect(pluginLists).toEqual({ expect(pluginLists).toEqual({
devicePlugins: [logsPlugin], devicePlugins: [logsPlugin],
@@ -297,15 +301,15 @@ describe('basic findBestDevice with metro present', () => {
], ],
[ [
unsupportedDevicePlugin.details, 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, 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, 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], downloadablePlugins: [supportedDownloadablePlugin],
@@ -325,6 +329,7 @@ describe('basic findBestDevice with metro present', () => {
flipper.client, flipper.client,
state.plugins, state.plugins,
state.connections.userStarredPlugins, state.connections.userStarredPlugins,
state.connections.userStarredDevicePlugins,
), ),
).toMatchObject({ ).toMatchObject({
enabledPlugins: [plugin2], enabledPlugins: [plugin2],

View File

@@ -62,6 +62,23 @@ type MockOptions = Partial<{
supportedPlugins?: string[]; 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( export async function createMockFlipperWithPlugin(
pluginClazz: PluginDefinition, pluginClazz: PluginDefinition,
options?: MockOptions, options?: MockOptions,
@@ -89,14 +106,7 @@ export async function createMockFlipperWithPlugin(
backgroundPlugins: options?.asBackgroundPlugin ? [pluginClazz.id] : [], backgroundPlugins: options?.asBackgroundPlugin ? [pluginClazz.id] : [],
}); });
// enable the plugin // enable the plugin
if ( if (!isPluginEnabled(store, pluginClazz, name)) {
!isDevicePluginDefinition(pluginClazz) &&
!store
.getState()
.connections.userStarredPlugins[client.query.app]?.includes(
pluginClazz.id,
)
) {
store.dispatch( store.dispatch(
starPlugin({ starPlugin({
plugin: pluginClazz, plugin: pluginClazz,
@@ -106,7 +116,7 @@ export async function createMockFlipperWithPlugin(
} }
if (!options?.dontEnableAdditionalPlugins) { if (!options?.dontEnableAdditionalPlugins) {
options?.additionalPlugins?.forEach((plugin) => { options?.additionalPlugins?.forEach((plugin) => {
if (!isDevicePluginDefinition(plugin)) { if (!isPluginEnabled(store, plugin, name)) {
store.dispatch( store.dispatch(
starPlugin({ starPlugin({
plugin, plugin,

View File

@@ -135,6 +135,7 @@ export function processMessagesLater(
case (plugin as any).prototype instanceof FlipperDevicePlugin: case (plugin as any).prototype instanceof FlipperDevicePlugin:
case pluginIsStarred( case pluginIsStarred(
store.getState().connections.userStarredPlugins, store.getState().connections.userStarredPlugins,
store.getState().connections.userStarredDevicePlugins,
deconstructPluginKey(pluginKey).client, deconstructPluginKey(pluginKey).client,
pluginId, pluginId,
): ):

View File

@@ -84,6 +84,7 @@ export function getExportablePlugins(
client, client,
state.plugins, state.plugins,
state.connections.userStarredPlugins, state.connections.userStarredPlugins,
state.connections.userStarredDevicePlugins,
); );
return [ return [
@@ -179,16 +180,33 @@ export function computePluginLists(
client: Client | undefined, client: Client | undefined,
plugins: State['plugins'], plugins: State['plugins'],
userStarredPlugins: State['connections']['userStarredPlugins'], userStarredPlugins: State['connections']['userStarredPlugins'],
userStarredDevicePlugins: Set<string>,
_pluginsChanged?: number, // this argument is purely used to invalidate the memoization cache _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[] = [ const devicePlugins: DevicePluginDefinition[] = [
...plugins.devicePlugins.values(), ...plugins.devicePlugins.values(),
].filter((p) => device?.supportsPlugin(p)); ]
.filter((p) => device?.supportsPlugin(p))
.filter((p) => userStarredDevicePlugins.has(p.id));
const metroPlugins: DevicePluginDefinition[] = [ const metroPlugins: DevicePluginDefinition[] = [
...plugins.devicePlugins.values(), ...plugins.devicePlugins.values(),
].filter((p) => metroDevice?.supportsPlugin(p)); ]
.filter((p) => metroDevice?.supportsPlugin(p))
.filter((p) => userStarredDevicePlugins.has(p.id));
const enabledPlugins: ClientPluginDefinition[] = []; 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 unavailablePlugins: [plugin: PluginDetails, reason: string][] = [];
const downloadablePlugins: ( const downloadablePlugins: (
| DownloadablePluginDetails | DownloadablePluginDetails
@@ -203,10 +221,20 @@ export function computePluginLists(
p.details, p.details,
`Device plugin '${getPluginTitle( `Device plugin '${getPluginTitle(
p.details, 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 // process problematic plugins
@@ -244,7 +272,7 @@ export function computePluginLists(
plugin.details, plugin.details,
`Plugin '${getPluginTitle( `Plugin '${getPluginTitle(
plugin.details, 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)) { } else if (favoritePlugins.includes(plugin)) {
enabledPlugins.push(plugin); enabledPlugins.push(plugin);
@@ -252,23 +280,25 @@ export function computePluginLists(
disabledPlugins.push(plugin); disabledPlugins.push(plugin);
} }
}); });
const uninstalledMarketplacePlugins = filterNewestVersionOfEachPlugin(
[...plugins.bundledPlugins.values()],
plugins.marketplacePlugins,
).filter((p) => !plugins.loadedPlugins.has(p.id));
uninstalledMarketplacePlugins.forEach((plugin) => { uninstalledMarketplacePlugins.forEach((plugin) => {
if (client.supportsPlugin(plugin.id)) { if (client.supportsPlugin(plugin.id)) {
downloadablePlugins.push(plugin); 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<string>(
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); devicePlugins.sort(sortPluginsByName);
metroPlugins.sort(sortPluginsByName); metroPlugins.sort(sortPluginsByName);