Unify computation of available plugins

Summary:
While trying to change something, discovered we have 3 different mechanisms in our code base to compute active plugins; the plugin list component, support form, and export flipper trace form had all their own, subtly different implementations of computing which plugins are available to the user.

Also removed some hardcoded exceptions for e.g. Logs plugin, which in the next diff and onward will be just a vanilla plugin without special casing

Unified that, which some how went a bit deeper than hoped, trough some hoops in in circular deps. Also unified to use the same testing utils, to avoid some gobbling objects manually together, with resulted in a bunch of unexpected NPEs. Found out that we actually still have unit tests using Flow in the process :-P. Converted one to TS.

Reviewed By: nikoant

Differential Revision: D26103172

fbshipit-source-id: 2fce2577d97d98543cb9312b3d013f24faee43aa
This commit is contained in:
Michel Weststrate
2021-02-01 11:40:20 -08:00
committed by Facebook GitHub Bot
parent 5320015776
commit e1daa449ba
17 changed files with 556 additions and 792 deletions

View File

@@ -938,6 +938,7 @@ test('Sandy plugins support isPluginSupported + selectPlugin', async () => {
definition,
{
additionalPlugins: [definition2, definition3],
dontEnableAdditionalPlugins: true,
},
);

View File

@@ -13,7 +13,7 @@ import {ShareType} from '../reducers/application';
import {State as Store} from '../reducers';
import {ActiveSheet} from '../reducers/application';
import {selectedPlugins as actionForSelectedPlugins} from '../reducers/plugins';
import {getActivePersistentPlugins} from '../utils/pluginUtils';
import {getExportablePlugins} from '../utils/pluginUtils';
import {
ACTIVE_SHEET_SHARE_DATA,
setActiveSheet as getActiveSheetAction,
@@ -103,25 +103,18 @@ class ExportDataPluginSheet extends Component<Props, {}> {
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({
application: {share},
plugins,
pluginStates,
pluginMessageQueue,
connections: {selectedApp, clients},
}) => {
const selectedClient = clients.find((o) => {
return o.id === selectedApp;
(state) => {
const selectedClient = state.connections.clients.find((o) => {
return o.id === state.connections.selectedApp;
});
const availablePluginsToExport = getActivePersistentPlugins(
pluginStates,
pluginMessageQueue,
plugins,
const availablePluginsToExport = getExportablePlugins(
state,
state.connections.selectedDevice ?? undefined,
selectedClient,
);
return {
share,
selectedPlugins: plugins.selectedPlugins,
share: state.application.share,
selectedPlugins: state.plugins.selectedPlugins,
availablePluginsToExport,
};
},

View File

@@ -159,6 +159,9 @@ class RowComponent extends Component<RowComponentProps> {
}
}
/**
* @deprecated use Ant Design instead
*/
export default class ListView extends Component<Props, State> {
state: State = {selectedElements: new Set([])};
static getDerivedStateFromProps(props: Props, _state: State) {

View File

@@ -8,96 +8,79 @@
*/
import React from 'react';
import Client from '../../Client';
import {create, act, ReactTestRenderer} from 'react-test-renderer';
import configureStore from 'redux-mock-store';
import {Provider} from 'react-redux';
import {default as BaseDevice} from '../../devices/BaseDevice';
import ExportDataPluginSheet from '../ExportDataPluginSheet';
import {FlipperPlugin, FlipperDevicePlugin} from '../../plugin';
import {getExportablePlugins, getPluginKey} from '../../utils/pluginUtils';
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
import {setPluginState} from '../../reducers/pluginStates';
function generateClientIdentifier(device: BaseDevice, app: string): string {
const {os, deviceType, serial} = device;
const identifier = `${app}#${os}#${deviceType}#${serial}`;
return identifier;
class TestPlugin extends FlipperPlugin<any, any, any> {
static details = {
title: 'TestPlugin',
id: 'TestPlugin',
} as any;
}
class TestPlugin extends FlipperPlugin<any, any, any> {}
TestPlugin.title = 'TestPlugin';
TestPlugin.id = 'TestPlugin';
TestPlugin.defaultPersistedState = {msg: 'Test plugin'};
class TestDevicePlugin extends FlipperDevicePlugin<any, any, any> {}
class TestDevicePlugin extends FlipperDevicePlugin<any, any, any> {
static details = {
title: 'TestDevicePlugin',
id: 'TestDevicePlugin',
} as any;
static supportsDevice() {
return true;
}
}
TestDevicePlugin.title = 'TestDevicePlugin';
TestDevicePlugin.id = 'TestDevicePlugin';
TestDevicePlugin.defaultPersistedState = {msg: 'TestDevicePlugin'};
function getStore(selectedPlugins: Array<string>) {
const logger = {
track: () => {},
info: () => {},
warn: () => {},
error: () => {},
debug: () => {},
trackTimeSince: () => {},
};
const selectedDevice = new BaseDevice(
'serial',
'emulator',
'TestiPhone',
'iOS',
);
const clientId = generateClientIdentifier(selectedDevice, 'app');
const pluginStates: {[key: string]: any} = {};
pluginStates[`${clientId}#TestDevicePlugin`] = {
msg: 'Test Device plugin',
};
pluginStates[`${clientId}#TestPlugin`] = {
msg: 'Test plugin',
};
const mockStore = configureStore([])({
application: {share: {closeOnFinish: false, type: 'link'}},
plugins: {
clientPlugins: new Map([['TestPlugin', TestPlugin]]),
devicePlugins: new Map([['TestDevicePlugin', TestDevicePlugin]]),
gatekeepedPlugins: [],
disabledPlugins: [],
failedPlugins: [],
selectedPlugins,
},
pluginStates,
pluginMessageQueue: [],
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'],
selectedDevice,
);
mockStore.dispatch({
type: 'NEW_CLIENT',
payload: client,
});
return mockStore;
}
test('SettingsSheet snapshot with nothing enabled', async () => {
let root: ReactTestRenderer;
const {
store,
togglePlugin,
client,
device,
pluginKey,
} = await createMockFlipperWithPlugin(TestPlugin, {
additionalPlugins: [TestDevicePlugin],
});
togglePlugin();
store.dispatch(
setPluginState({
pluginKey,
state: {test: '1'},
}),
);
expect(getExportablePlugins(store.getState(), device, client)).toEqual([]);
// makes device plugin visible
store.dispatch(
setPluginState({
pluginKey: getPluginKey(undefined, device, 'TestDevicePlugin'),
state: {test: '1'},
}),
);
expect(getExportablePlugins(store.getState(), device, client)).toEqual([
{
id: 'TestDevicePlugin',
label: 'TestDevicePlugin',
},
]);
await act(async () => {
root = create(
<Provider store={getStore([])}>
<Provider store={store}>
<ExportDataPluginSheet onHide={() => {}} />
</Provider>,
);
@@ -108,9 +91,42 @@ test('SettingsSheet snapshot with nothing enabled', async () => {
test('SettingsSheet snapshot with one plugin enabled', async () => {
let root: ReactTestRenderer;
const {store, device, client, pluginKey} = await createMockFlipperWithPlugin(
TestPlugin,
{
additionalPlugins: [TestDevicePlugin],
},
);
// enabled, but no data
expect(getExportablePlugins(store.getState(), device, client)).toEqual([]);
store.dispatch(
setPluginState({
pluginKey,
state: {test: '1'},
}),
);
store.dispatch(
setPluginState({
pluginKey: getPluginKey(undefined, device, 'TestDevicePlugin'),
state: {test: '1'},
}),
);
expect(getExportablePlugins(store.getState(), device, client)).toEqual([
{
id: 'TestDevicePlugin',
label: 'TestDevicePlugin',
},
{
id: 'TestPlugin',
label: 'TestPlugin',
},
]);
await act(async () => {
root = create(
<Provider store={getStore(['TestPlugin'])}>
<Provider store={store}>
<ExportDataPluginSheet onHide={() => {}} />
</Provider>,
);

View File

@@ -59,47 +59,6 @@ exports[`SettingsSheet snapshot with nothing enabled 1`] = `
/>
</div>
</div>
<div
className="css-18abd42-View-FlexBox-FlexColumn ecr18to0"
>
<div
className="css-auhar3-TooltipContainer e1abcqbd0"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<div
className="css-1jrm6r3"
>
<div
className="css-wospjg-View-FlexBox-FlexRow epz0qe20"
style={
Object {
"alignItems": "center",
}
}
>
<span
className="css-xsnw23-Text e19o3fcp0"
>
TestPlugin
</span>
<div
className="css-t4wmtk-View-FlexBox-Spacer e13mj6h80"
/>
<input
checked={false}
className="css-1pxrk7-CheckboxContainer e28aqfo0"
disabled={false}
onChange={[Function]}
type="checkbox"
/>
</div>
</div>
<div
className="css-1p0wwd3-View"
/>
</div>
</div>
</div>
</div>
<div
@@ -233,7 +192,7 @@ exports[`SettingsSheet snapshot with one plugin enabled 1`] = `
className="css-t4wmtk-View-FlexBox-Spacer e13mj6h80"
/>
<input
checked={true}
checked={false}
className="css-1pxrk7-CheckboxContainer e28aqfo0"
disabled={false}
onChange={[Function]}
@@ -277,7 +236,7 @@ exports[`SettingsSheet snapshot with one plugin enabled 1`] = `
>
<button
className="ant-btn ant-btn-primary"
disabled={false}
disabled={true}
onClick={[Function]}
type="button"
>

View File

@@ -7,13 +7,12 @@
* @format
*/
import {Store} from '../reducers/index';
import {Logger} from '../fb-interfaces/Logger';
import {PluginDefinition} from '../plugin';
import type {Store} from '../reducers/index';
import type {Logger} from '../fb-interfaces/Logger';
import type {PluginDefinition} from '../plugin';
import React from 'react';
import ReactDOM from 'react-dom';
import adbkit from 'adbkit';
import * as Flipper from '../index';
import {
registerPlugins,
addGatekeepedPlugins,
@@ -52,7 +51,7 @@ export default async (store: Store, logger: Logger) => {
const globalObject: any = typeof window === 'undefined' ? global : window;
globalObject.React = React;
globalObject.ReactDOM = ReactDOM;
globalObject.Flipper = Flipper;
globalObject.Flipper = require('../index');
globalObject.adbkit = adbkit;
globalObject.FlipperPlugin = FlipperPluginSDK;
globalObject.Immer = Immer;

View File

@@ -13,15 +13,15 @@ import {produce} from 'immer';
import type BaseDevice from '../devices/BaseDevice';
import MacDevice from '../devices/MacDevice';
import type Client from '../Client';
import {UninitializedClient} from '../UninitializedClient';
import type {UninitializedClient} from '../UninitializedClient';
import {isEqual} from 'lodash';
import {performance} from 'perf_hooks';
import {Actions} from '.';
import type {Actions} from '.';
import {WelcomeScreenStaticView} from '../sandy-chrome/WelcomeScreen';
import {getPluginKey, isDevicePluginDefinition} from '../utils/pluginUtils';
import {deconstructClientId} from '../utils/clientUtils';
import {PluginDefinition} from '../plugin';
import {RegisterPluginAction} from './plugins';
import type {PluginDefinition} from '../plugin';
import type {RegisterPluginAction} from './plugins';
import MetroDevice from '../devices/MetroDevice';
import {Logger} from 'flipper-plugin';

View File

@@ -7,7 +7,7 @@
* @format
*/
import {Actions} from '.';
import type {Actions} from '.';
import {deconstructPluginKey} from '../utils/clientUtils';
export type State = {
@@ -31,10 +31,8 @@ export type Action =
payload: {clientId: string; devicePlugins: Set<string>};
};
const INITIAL_STATE: State = {};
export default function reducer(
state: State | undefined = INITIAL_STATE,
state: State | undefined = {},
action: Actions,
): State {
if (action.type === 'SET_PLUGIN_STATE') {

View File

@@ -7,13 +7,17 @@
* @format
*/
import {DevicePluginMap, ClientPluginMap, PluginDefinition} from '../plugin';
import {
import type {
DevicePluginMap,
ClientPluginMap,
PluginDefinition,
} from '../plugin';
import type {
DownloadablePluginDetails,
ActivatablePluginDetails,
BundledPluginDetails,
} from 'flipper-plugin-lib';
import {Actions} from '.';
import type {Actions} from '.';
import produce from 'immer';
import {isDevicePluginDefinition} from '../utils/pluginUtils';
@@ -65,20 +69,18 @@ export type Action =
payload: Array<BundledPluginDetails>;
};
const INITIAL_STATE: State = {
devicePlugins: new Map(),
clientPlugins: new Map(),
loadedPlugins: new Map(),
bundledPlugins: new Map(),
gatekeepedPlugins: [],
disabledPlugins: [],
failedPlugins: [],
selectedPlugins: [],
marketplacePlugins: [],
};
export default function reducer(
state: State | undefined = INITIAL_STATE,
state: State | undefined = {
devicePlugins: new Map(),
clientPlugins: new Map(),
loadedPlugins: new Map(),
bundledPlugins: new Map(),
gatekeepedPlugins: [],
disabledPlugins: [],
failedPlugins: [],
selectedPlugins: [],
marketplacePlugins: [],
},
action: Actions,
): State {
if (action.type === 'REGISTER_PLUGINS') {

View File

@@ -13,18 +13,14 @@ import {deconstructClientId} from '../utils/clientUtils';
import {starPlugin as setStarPlugin} from './connections';
import {showStatusUpdatesForDuration} from '../utils/promiseTimeout';
import {selectedPlugins as setSelectedPlugins} from './plugins';
import {getEnabledOrExportPersistedStatePlugins} from '../utils/pluginUtils';
import {addStatusMessage, removeStatusMessage} from './application';
import constants from '../fb-stubs/constants';
import {getInstance} from '../fb-stubs/Logger';
import {logPlatformSuccessRate} from '../utils/metrics';
import {getActivePersistentPlugins} from '../utils/pluginUtils';
import {getExportablePlugins} from '../utils/pluginUtils';
export const SUPPORT_FORM_PREFIX = 'support-form-v2';
import {State as PluginStatesState} from './pluginStates';
import {State as PluginsState} from '../reducers/plugins';
import {State as PluginMessageQueueState} from '../reducers/pluginMessageQueue';
import Client from '../Client';
import {OS} from '../devices/BaseDevice';
import BaseDevice, {OS} from '../devices/BaseDevice';
const {DEFAULT_SUPPORT_GROUP} = constants;
@@ -197,13 +193,11 @@ export class Group {
selectedGroup: this,
}),
);
const pluginsList = selectedClient
? getEnabledOrExportPersistedStatePlugins(
store.getState().connections.userStarredPlugins,
selectedClient,
store.getState().plugins,
)
: [];
const pluginsList = getExportablePlugins(
store.getState(),
store.getState().connections.selectedDevice ?? undefined,
selectedClient,
);
store.dispatch(
setSelectedPlugins(
@@ -225,17 +219,11 @@ export class Group {
}
getWarningMessage(
plugins: PluginsState,
pluginsState: PluginStatesState,
pluginsMessageQueue: PluginMessageQueueState,
state: Parameters<typeof getExportablePlugins>[0],
device: BaseDevice | undefined,
client: Client,
): string | null {
const activePersistentPlugins = getActivePersistentPlugins(
pluginsState,
pluginsMessageQueue,
plugins,
client,
);
const activePersistentPlugins = getExportablePlugins(state, device, client);
const emptyPlugins: Array<string> = [];
for (const plugin of this.requiredPlugins) {
if (

View File

@@ -20,17 +20,15 @@ import {
import {Glyph, Layout, styled} from '../../ui';
import {theme, NUX, Tracked} from 'flipper-plugin';
import {useDispatch, useStore} from '../../utils/useStore';
import {getPluginTitle, sortPluginsByName} from '../../utils/pluginUtils';
import {
ClientPluginDefinition,
DevicePluginDefinition,
PluginDefinition,
} from '../../plugin';
computePluginLists,
getPluginTitle,
getPluginTooltip,
} from '../../utils/pluginUtils';
import {selectPlugin, starPlugin} from '../../reducers/connections';
import Client from '../../Client';
import {State} from '../../reducers';
import BaseDevice from '../../devices/BaseDevice';
import {PluginDetails, DownloadablePluginDetails} from 'flipper-plugin-lib';
import {DownloadablePluginDetails} from 'flipper-plugin-lib';
import {useMemoize} from '../../utils/useMemoize';
import MetroDevice from '../../devices/MetroDevice';
import {
@@ -40,7 +38,6 @@ import {
} from '../../reducers/pluginDownloads';
import {activatePlugin, uninstallPlugin} from '../../reducers/pluginManager';
import {BundledPluginDetails} from 'plugin-lib';
import {filterNewestVersionOfEachPlugin} from '../../dispatcher/plugins';
import {reportUsage} from '../../utils/metrics';
const {SubMenu} = Menu;
@@ -465,132 +462,6 @@ const PluginGroup = memo(function PluginGroup({
);
});
function getPluginTooltip(details: PluginDetails): string {
return `${getPluginTitle(details)} (${details.id}@${details.version}) ${
details.description ?? ''
}`;
}
export function computePluginLists(
device: BaseDevice | undefined,
metroDevice: BaseDevice | undefined,
client: Client | undefined,
plugins: State['plugins'],
userStarredPlugins: State['connections']['userStarredPlugins'],
_pluginsChanged?: number, // this argument is purely used to invalidate the memoization cache
) {
const devicePlugins: DevicePluginDefinition[] =
device?.devicePlugins.map((name) => plugins.devicePlugins.get(name)!) ?? [];
const metroPlugins: DevicePluginDefinition[] =
metroDevice?.devicePlugins.map(
(name) => plugins.devicePlugins.get(name)!,
) ?? [];
const enabledPlugins: ClientPluginDefinition[] = [];
const disabledPlugins: ClientPluginDefinition[] = [];
const unavailablePlugins: [plugin: PluginDetails, reason: string][] = [];
const downloadablePlugins: (
| DownloadablePluginDetails
| BundledPluginDetails
)[] = [];
if (device) {
// find all device plugins that aren't part of the current device / metro
const detectedDevicePlugins = new Set([
...device.devicePlugins,
...(metroDevice?.devicePlugins ?? []),
]);
for (const [name, definition] of plugins.devicePlugins.entries()) {
if (!detectedDevicePlugins.has(name)) {
unavailablePlugins.push([
definition.details,
`Device plugin '${getPluginTitle(
definition.details,
)}' is not supported by the current device type.`,
]);
}
}
}
// process problematic plugins
plugins.disabledPlugins.forEach((plugin) => {
unavailablePlugins.push([plugin, 'Plugin is disabled by configuration']);
});
plugins.gatekeepedPlugins.forEach((plugin) => {
unavailablePlugins.push([
plugin,
`This plugin is only available to members of gatekeeper '${plugin.gatekeeper}'`,
]);
});
plugins.failedPlugins.forEach(([plugin, error]) => {
unavailablePlugins.push([
plugin,
`Flipper failed to load this plugin: '${error}'`,
]);
});
// process all client plugins
if (device && client) {
const clientPlugins = Array.from(plugins.clientPlugins.values()).sort(
sortPluginsByName,
);
const favoritePlugins = getFavoritePlugins(
device,
client,
clientPlugins,
client && userStarredPlugins[client.query.app],
true,
);
clientPlugins.forEach((plugin) => {
if (!client.supportsPlugin(plugin.id)) {
unavailablePlugins.push([
plugin.details,
`Plugin '${getPluginTitle(
plugin.details,
)}' is installed in Flipper, but not supported by the client application`,
]);
} else if (favoritePlugins.includes(plugin)) {
enabledPlugins.push(plugin);
} else {
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`,
]);
}
});
}
devicePlugins.sort(sortPluginsByName);
metroPlugins.sort(sortPluginsByName);
unavailablePlugins.sort(([a], [b]) => {
return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1;
});
downloadablePlugins.sort((a, b) => {
return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1;
});
return {
devicePlugins,
metroPlugins,
enabledPlugins,
disabledPlugins,
unavailablePlugins,
downloadablePlugins,
};
}
// Dimensions are hardcoded as they correlate strongly
const PluginMenu = styled(Menu)({
userSelect: 'none',
@@ -652,28 +523,3 @@ function iconStyle(disabled: boolean) {
height: 24,
};
}
function getFavoritePlugins(
device: BaseDevice,
client: Client,
allPlugins: PluginDefinition[],
starredPlugins: undefined | string[],
returnFavoredPlugins: boolean, // if false, unfavoried plugins are returned
): PluginDefinition[] {
if (device.isArchived) {
if (!returnFavoredPlugins) {
return [];
}
// for archived plugins, all stored plugins are enabled
return allPlugins.filter(
(plugin) => client.plugins.indexOf(plugin.id) !== -1,
);
}
if (!starredPlugins || !starredPlugins.length) {
return returnFavoredPlugins ? [] : allPlugins;
}
return allPlugins.filter((plugin) => {
const idx = starredPlugins.indexOf(plugin.id);
return idx === -1 ? !returnFavoredPlugins : returnFavoredPlugins;
});
}

View File

@@ -11,7 +11,6 @@ import {
createMockFlipperWithPlugin,
MockFlipperResult,
} from '../../../test-utils/createMockFlipperWithPlugin';
import {computePluginLists} from '../PluginList';
import {findBestClient, findBestDevice, findMetroDevice} from '../AppInspect';
import {FlipperPlugin} from '../../../plugin';
import MetroDevice from '../../../devices/MetroDevice';
@@ -29,6 +28,7 @@ import {
// eslint-disable-next-line
import * as LogsPluginModule from '../../../../../plugins/logs/index';
import {createMockDownloadablePluginDetails} from '../../../utils/testUtils';
import {computePluginLists} from '../../../utils/pluginUtils';
const logsPlugin = new _SandyPluginDefinition(
createMockPluginDetails({id: 'DeviceLogs'}),

View File

@@ -47,6 +47,7 @@ export type MockFlipperResult = {
createDevice(serial: string): BaseDevice;
createClient(device: BaseDevice, name: string): Promise<Client>;
logger: Logger;
togglePlugin(plugin?: string): void;
};
type MockOptions = Partial<{
@@ -56,6 +57,7 @@ type MockOptions = Partial<{
*/
onSend?: (pluginId: string, method: string, params?: object) => any;
additionalPlugins?: PluginDefinition[];
dontEnableAdditionalPlugins?: true;
}>;
export async function createMockFlipperWithPlugin(
@@ -108,7 +110,12 @@ export async function createMockFlipperWithPlugin(
null, // create a stub connection to avoid this plugin to be archived?
logger,
store,
isDevicePluginDefinition(pluginClazz) ? [] : [pluginClazz.id],
[
...(isDevicePluginDefinition(pluginClazz) ? [] : [pluginClazz.id]),
...(options?.dontEnableAdditionalPlugins
? []
: options?.additionalPlugins?.map((p) => p.id) ?? []),
],
device,
);
@@ -159,6 +166,18 @@ export async function createMockFlipperWithPlugin(
}),
);
}
if (!options?.dontEnableAdditionalPlugins) {
options?.additionalPlugins?.forEach((plugin) => {
if (!isDevicePluginDefinition(plugin)) {
store.dispatch(
starPlugin({
plugin,
selectedApp: client.query.app,
}),
);
}
});
}
await client.init();
// As convenience, by default we select the new client, star the plugin, and select it
@@ -204,6 +223,20 @@ export async function createMockFlipperWithPlugin(
createClient,
logger,
pluginKey: getPluginKey(client.id, device, pluginClazz.id),
togglePlugin(id?: string) {
const plugin = id
? store.getState().plugins.clientPlugins.get(id)
: pluginClazz;
if (!plugin) {
throw new Error('unknown plugin ' + id);
}
store.dispatch(
starPlugin({
plugin,
selectedApp: client.query.app,
}),
);
},
};
}

View File

@@ -1,293 +0,0 @@
/**
* 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 {
getPersistentPlugins,
getActivePersistentPlugins,
} from '../pluginUtils.tsx';
import type {State as PluginsState} from '../../reducers/plugins.tsx';
import type {State as PluginStatesState} from '../../reducers/pluginStates.tsx';
import type {PluginDetails} from 'flipper-plugin-lib';
import type {State as PluginMessageQueueState} from '../../reducers/pluginStates.tsx';
import {FlipperBasePlugin} from '../../plugin';
import type {ReduxState} from '../../reducers/index.tsx';
function createMockFlipperPluginWithDefaultPersistedState(id: string) {
return class MockFlipperPluginWithDefaultPersistedState extends FlipperBasePlugin<
*,
*,
{msg: string},
> {
static id = id;
static defaultPersistedState = {msg: 'MockFlipperPluginWithPersistedState'};
};
}
function createMockFlipperPluginWithExportPersistedState(id: string) {
return class MockFlipperPluginWithExportPersistedState extends FlipperBasePlugin<
*,
*,
{msg: string},
> {
static id = id;
static exportPersistedState = (
callClient: (string, ?Object) => Promise<Object>,
persistedState: ?{msg: string},
store: ?ReduxState,
supportsMethod?: (string) => Promise<boolean>,
): Promise<?{msg: string}> => {
return Promise.resolve({
msg: 'MockFlipperPluginWithExportPersistedState',
});
};
};
}
function createMockFlipperPluginWithNoPersistedState(id: string) {
return class MockFlipperPluginWithNoPersistedState extends FlipperBasePlugin<
*,
*,
*,
> {
static id = id;
};
}
function mockPluginState(
gatekeepedPlugins: Array<PluginDetails>,
disabledPlugins: Array<PluginDetails>,
failedPlugins: Array<[PluginDetails, string]>,
): PluginsState {
return {
devicePlugins: new Map([
[
'DevicePlugin1',
createMockFlipperPluginWithDefaultPersistedState('DevicePlugin1'),
],
[
'DevicePlugin2',
createMockFlipperPluginWithDefaultPersistedState('DevicePlugin2'),
],
]),
clientPlugins: new Map([
[
'ClientPlugin1',
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin1'),
],
[
'ClientPlugin2',
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin2'),
],
]),
gatekeepedPlugins,
disabledPlugins,
failedPlugins,
selectedPlugins: [],
};
}
function mockPluginDefinition(name: string): PluginDetails {
return {
name,
id: name,
out: 'out',
};
}
test('getPersistentPlugins with the plugins getting excluded', () => {
const state = mockPluginState(
[mockPluginDefinition('DevicePlugin1')],
[mockPluginDefinition('ClientPlugin1')],
[[mockPluginDefinition('DevicePlugin2'), 'DevicePlugin2']],
);
const list = getPersistentPlugins(state);
expect(list).toEqual(['ClientPlugin2']);
});
test('getPersistentPlugins with no plugins getting excluded', () => {
const state = mockPluginState([], [], []);
const list = getPersistentPlugins(state);
expect(list).toEqual([
'ClientPlugin1',
'ClientPlugin2',
'DevicePlugin1',
'DevicePlugin2',
]);
});
test('getPersistentPlugins, where the plugins with exportPersistedState not getting excluded', () => {
const state: PluginsState = {
devicePlugins: new Map([
[
'DevicePlugin1',
createMockFlipperPluginWithExportPersistedState('DevicePlugin1'),
],
[
'DevicePlugin2',
createMockFlipperPluginWithExportPersistedState('DevicePlugin2'),
],
]),
clientPlugins: new Map([
[
'ClientPlugin1',
createMockFlipperPluginWithExportPersistedState('ClientPlugin1'),
],
[
'ClientPlugin2',
createMockFlipperPluginWithExportPersistedState('ClientPlugin2'),
],
]),
gatekeepedPlugins: [],
disabledPlugins: [],
failedPlugins: [],
selectedPlugins: [],
};
const list = getPersistentPlugins(state);
expect(list).toEqual([
'ClientPlugin1',
'ClientPlugin2',
'DevicePlugin1',
'DevicePlugin2',
]);
});
test('getPersistentPlugins, where the non persistent plugins getting excluded', () => {
const state: PluginsState = {
devicePlugins: new Map([
[
'DevicePlugin1',
createMockFlipperPluginWithNoPersistedState('DevicePlugin1'),
],
[
'DevicePlugin2',
createMockFlipperPluginWithDefaultPersistedState('DevicePlugin2'),
],
]),
clientPlugins: new Map([
[
'ClientPlugin1',
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin1'),
],
[
'ClientPlugin2',
createMockFlipperPluginWithNoPersistedState('ClientPlugin2'),
],
]),
gatekeepedPlugins: [],
disabledPlugins: [],
failedPlugins: [],
selectedPlugins: [],
};
const list = getPersistentPlugins(state);
expect(list).toEqual(['ClientPlugin1', 'DevicePlugin2']);
});
test('getActivePersistentPlugins, where the non persistent plugins getting excluded', () => {
const state: PluginsState = {
devicePlugins: new Map([
[
'DevicePlugin1',
createMockFlipperPluginWithNoPersistedState('DevicePlugin1'),
],
[
'DevicePlugin2',
createMockFlipperPluginWithDefaultPersistedState('DevicePlugin2'),
],
]),
clientPlugins: new Map([
[
'ClientPlugin1',
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin1'),
],
[
'ClientPlugin2',
createMockFlipperPluginWithNoPersistedState('ClientPlugin2'),
],
]),
gatekeepedPlugins: [],
disabledPlugins: [],
failedPlugins: [],
selectedPlugins: [],
};
const plugins: PluginStatesState = {
'serial#app#DevicePlugin1': {msg: 'DevicePlugin1'},
'serial#app#DevicePlugin2': {msg: 'DevicePlugin2'},
'serial#app#ClientPlugin1': {msg: 'ClientPlugin1'},
'serial#app#ClientPlugin2': {msg: 'ClientPlugin2'},
};
const queues: PluginMessageQueueState = {};
const list = getActivePersistentPlugins(plugins, queues, state);
expect(list).toEqual([
{
id: 'ClientPlugin1',
label: 'ClientPlugin1',
},
{
id: 'DevicePlugin2',
label: 'DevicePlugin2',
},
]);
});
test('getActivePersistentPlugins, where the plugins not in pluginState or queue gets excluded', () => {
const state: PluginsState = {
devicePlugins: new Map([
[
'DevicePlugin1',
createMockFlipperPluginWithDefaultPersistedState('DevicePlugin1'),
],
[
'DevicePlugin2',
createMockFlipperPluginWithDefaultPersistedState('DevicePlugin2'),
],
]),
clientPlugins: new Map([
[
'ClientPlugin1',
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin1'),
],
[
'ClientPlugin2',
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin2'),
],
[
'ClientPlugin3',
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin3'),
],
]),
gatekeepedPlugins: [],
disabledPlugins: [],
failedPlugins: [],
selectedPlugins: [],
};
const plugins: PluginStatesState = {
'serial#app#DevicePlugin1': {msg: 'DevicePlugin1'},
'serial#app#ClientPlugin2': {msg: 'ClientPlugin2'},
};
const queues: PluginMessageQueueState = {
'serial#app#ClientPlugin3': [
{method: 'msg', params: {msg: 'ClientPlugin3'}},
],
};
const list = getActivePersistentPlugins(plugins, queues, state);
expect(list).toEqual([
{
id: 'ClientPlugin2',
label: 'ClientPlugin2',
},
{
id: 'ClientPlugin3',
label: 'ClientPlugin3',
},
{
id: 'DevicePlugin1',
label: 'DevicePlugin1',
},
]);
});

View File

@@ -0,0 +1,144 @@
/**
* 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 {getExportablePlugins, getPluginKey} from '../pluginUtils';
import {FlipperPlugin, FlipperDevicePlugin} from '../../plugin';
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
function createMockFlipperPluginWithDefaultPersistedState(id: string) {
return class MockFlipperPluginWithDefaultPersistedState extends FlipperPlugin<
any,
any,
any
> {
static id = id;
static defaultPersistedState = {msg: 'MockFlipperPluginWithPersistedState'};
['constructor']: any;
subscriptions = null as any;
client = null as any;
realClient = null as any;
getDevice = null as any;
};
}
function createMockDeviceFlipperPlugin(id: string) {
return class MockFlipperDevicePlugin extends FlipperDevicePlugin<
any,
any,
any
> {
static id = id;
['constructor']: any;
static supportsDevice() {
return true;
}
};
}
function createMockFlipperPluginWithExportPersistedState(id: string) {
return class MockFlipperPluginWithExportPersistedState extends FlipperPlugin<
any,
any,
any
> {
static id = id;
static exportPersistedState = (): Promise<any> => {
return Promise.resolve({
msg: 'MockFlipperPluginWithExportPersistedState',
});
};
['constructor']: any;
};
}
function createMockFlipperPluginWithNoPersistedState(id: string) {
return class MockFlipperPluginWithNoPersistedState extends FlipperPlugin<
any,
any,
any
> {
static id = id;
['constructor']: any;
};
}
test('getActivePersistentPlugins, where the non persistent plugins getting excluded', async () => {
const {store, device, client} = await createMockFlipperWithPlugin(
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin1'),
{
additionalPlugins: [
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin2'),
createMockFlipperPluginWithNoPersistedState('ClientPlugin3'),
createMockFlipperPluginWithNoPersistedState('ClientPlugin4'),
createMockFlipperPluginWithExportPersistedState('ClientPlugin5'),
],
},
);
const state = store.getState();
state.pluginStates = {
[getPluginKey(client.id, device, 'ClientPlugin1')]: {msg: 'DevicePlugin1'},
[getPluginKey(client.id, device, 'ClientPlugin4')]: {msg: 'ClientPlugin2'},
};
const list = getExportablePlugins(state, device, client);
expect(list).toEqual([
{
id: 'ClientPlugin1',
label: 'ClientPlugin1',
},
{
id: 'ClientPlugin4',
label: 'ClientPlugin4',
},
{
id: 'ClientPlugin5',
label: 'ClientPlugin5',
},
]);
});
test('getActivePersistentPlugins, where the plugins not in pluginState or queue gets excluded', async () => {
const {store, device, client} = await createMockFlipperWithPlugin(
createMockFlipperPluginWithDefaultPersistedState('Plugin1'),
{
additionalPlugins: [
createMockDeviceFlipperPlugin('DevicePlugin2'),
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin1'),
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin2'),
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin3'),
],
},
);
const state = store.getState();
state.pluginStates = {
[getPluginKey(client.id, device, 'ClientPlugin2')]: {msg: 'ClientPlugin2'},
};
state.pluginMessageQueue = {
[getPluginKey(client.id, device, 'ClientPlugin3')]: [
{method: 'msg', params: {msg: 'ClientPlugin3'}},
],
};
const list = getExportablePlugins(store.getState(), device, client);
expect(list).toEqual([
{
id: 'ClientPlugin2', // has state
label: 'ClientPlugin2',
},
{
id: 'ClientPlugin3', // queued
label: 'ClientPlugin3',
},
]);
});

View File

@@ -7,8 +7,8 @@
* @format
*/
import Client from '../Client';
import BaseDevice from '../devices/BaseDevice';
import type Client from '../Client';
import type BaseDevice from '../devices/BaseDevice';
/* A Client uniuely identifies an app running on some device.

View File

@@ -13,14 +13,20 @@ import {
PluginDefinition,
DevicePluginDefinition,
isSandyPlugin,
ClientPluginDefinition,
} 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 type {State} from '../reducers';
import type {State as PluginStatesState} from '../reducers/pluginStates';
import type {State as PluginsState} from '../reducers/plugins';
import {_SandyPluginDefinition} from 'flipper-plugin';
type Client = import('../Client').default;
import type BaseDevice from '../devices/BaseDevice';
import type Client from '../Client';
import type {
BundledPluginDetails,
DownloadablePluginDetails,
PluginDetails,
} from 'flipper-plugin-lib';
import {filterNewestVersionOfEachPlugin} from '../dispatcher/plugins';
export const defaultEnabledBackgroundPlugins = ['Navigation']; // The navigation plugin is enabled always, to make sure the navigation features works
@@ -34,8 +40,8 @@ export function pluginsClassMap(
}
export function getPluginKey(
selectedAppId: string | null,
baseDevice: {serial: string} | null,
selectedAppId: string | null | undefined,
baseDevice: {serial: string} | null | undefined,
pluginID: string,
): string {
if (selectedAppId) {
@@ -64,153 +70,71 @@ export function getPersistedState<PersistedState>(
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<string, PluginDefinition> = 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,
export function getExportablePlugins(
state: Pick<
State,
'plugins' | 'connections' | 'pluginStates' | 'pluginMessageQueue'
>,
device: BaseDevice | undefined | null,
client?: 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),
}));
const availablePlugins = computePluginLists(
device ?? undefined,
undefined,
client,
state.plugins,
state.connections.userStarredPlugins,
);
return [
...availablePlugins.devicePlugins.filter((plugin) => {
return isExportablePlugin(state, device, client, plugin);
}),
...availablePlugins.enabledPlugins.filter((plugin) => {
return isExportablePlugin(state, device, client, plugin);
}),
].map((p) => ({
id: p.id,
label: getPluginTitle(p),
}));
}
/**
* Returns all enabled plugins that are potentially exportable
* @param plugins
*/
export function getPersistentPlugins(plugins: PluginsState): Array<string> {
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
);
});
function isExportablePlugin(
{
pluginStates,
pluginMessageQueue,
}: Pick<State, 'pluginStates' | 'pluginMessageQueue'>,
device: BaseDevice | undefined | null,
client: Client | undefined,
plugin: PluginDefinition,
): boolean {
// can generate an export when requested
if (plugin.exportPersistedState) {
return true;
}
const pluginKey = isDevicePluginDefinition(plugin)
? getPluginKey(undefined, device, plugin.id)
: getPluginKey(client?.id, undefined, plugin.id);
// plugin has exportable redux state
if (pluginStates[pluginKey]) {
return true;
}
// plugin has exportable sandy state
if (client?.sandyPluginStates.get(plugin.id)?.isPersistable()) {
return true;
}
if (device?.sandyPluginStates.get(plugin.id)?.isPersistable()) {
return true;
}
// plugin has pending messages and a persisted state reducer or isSandy
if (
pluginMessageQueue[pluginKey] &&
((plugin as any).defaultPersistedState || isSandyPlugin(plugin))
) {
return true;
}
// nothing to serialize
return false;
}
export function getPluginTitle(pluginClass: {
@@ -242,3 +166,154 @@ export function isDevicePluginDefinition(
(definition instanceof _SandyPluginDefinition && definition.isDevicePlugin)
);
}
export function getPluginTooltip(details: PluginDetails): string {
return `${getPluginTitle(details)} (${details.id}@${details.version}) ${
details.description ?? ''
}`;
}
export function computePluginLists(
device: BaseDevice | undefined,
metroDevice: BaseDevice | undefined,
client: Client | undefined,
plugins: State['plugins'],
userStarredPlugins: State['connections']['userStarredPlugins'],
_pluginsChanged?: number, // this argument is purely used to invalidate the memoization cache
) {
const devicePlugins: DevicePluginDefinition[] =
device?.devicePlugins.map((name) => plugins.devicePlugins.get(name)!) ?? [];
const metroPlugins: DevicePluginDefinition[] =
metroDevice?.devicePlugins.map(
(name) => plugins.devicePlugins.get(name)!,
) ?? [];
const enabledPlugins: ClientPluginDefinition[] = [];
const disabledPlugins: ClientPluginDefinition[] = [];
const unavailablePlugins: [plugin: PluginDetails, reason: string][] = [];
const downloadablePlugins: (
| DownloadablePluginDetails
| BundledPluginDetails
)[] = [];
if (device) {
// find all device plugins that aren't part of the current device / metro
const detectedDevicePlugins = new Set([
...device.devicePlugins,
...(metroDevice?.devicePlugins ?? []),
]);
for (const [name, definition] of plugins.devicePlugins.entries()) {
if (!detectedDevicePlugins.has(name)) {
unavailablePlugins.push([
definition.details,
`Device plugin '${getPluginTitle(
definition.details,
)}' is not supported by the current device type.`,
]);
}
}
}
// process problematic plugins
plugins.disabledPlugins.forEach((plugin) => {
unavailablePlugins.push([plugin, 'Plugin is disabled by configuration']);
});
plugins.gatekeepedPlugins.forEach((plugin) => {
unavailablePlugins.push([
plugin,
`This plugin is only available to members of gatekeeper '${plugin.gatekeeper}'`,
]);
});
plugins.failedPlugins.forEach(([plugin, error]) => {
unavailablePlugins.push([
plugin,
`Flipper failed to load this plugin: '${error}'`,
]);
});
// process all client plugins
if (device && client) {
const clientPlugins = Array.from(plugins.clientPlugins.values()).sort(
sortPluginsByName,
);
const favoritePlugins = getFavoritePlugins(
device,
client,
clientPlugins,
client && userStarredPlugins[client.query.app],
true,
);
clientPlugins.forEach((plugin) => {
if (!client.supportsPlugin(plugin.id)) {
unavailablePlugins.push([
plugin.details,
`Plugin '${getPluginTitle(
plugin.details,
)}' is installed in Flipper, but not supported by the client application`,
]);
} else if (favoritePlugins.includes(plugin)) {
enabledPlugins.push(plugin);
} else {
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`,
]);
}
});
}
devicePlugins.sort(sortPluginsByName);
metroPlugins.sort(sortPluginsByName);
unavailablePlugins.sort(([a], [b]) => {
return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1;
});
downloadablePlugins.sort((a, b) => {
return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1;
});
return {
devicePlugins,
metroPlugins,
enabledPlugins,
disabledPlugins,
unavailablePlugins,
downloadablePlugins,
};
}
function getFavoritePlugins(
device: BaseDevice,
client: Client,
allPlugins: PluginDefinition[],
starredPlugins: undefined | string[],
returnFavoredPlugins: boolean, // if false, unfavoried plugins are returned
): PluginDefinition[] {
if (device.isArchived) {
if (!returnFavoredPlugins) {
return [];
}
// for archived plugins, all stored plugins are enabled
return allPlugins.filter(
(plugin) => client.plugins.indexOf(plugin.id) !== -1,
);
}
if (!starredPlugins || !starredPlugins.length) {
return returnFavoredPlugins ? [] : allPlugins;
}
return allPlugins.filter((plugin) => {
const idx = starredPlugins.indexOf(plugin.id);
return idx === -1 ? !returnFavoredPlugins : returnFavoredPlugins;
});
}