Allow "uninstallation" of bundled plugins

Summary:
Allow "uninstallation" of bundled plugins which means we mark them as uninstalled and not auto-updating anymore. Uninstalled bundled plugins are shown in "Detected in App" section together with Marketplace plugins where user can install them back.

Changelog:
Plugins can be uninstalled from sidebar in new Sandy UI.

Reviewed By: passy

Differential Revision: D25557789

fbshipit-source-id: 751cad68456313c069af639584541086efc7102b
This commit is contained in:
Anton Nikolaev
2020-12-15 09:28:58 -08:00
committed by Facebook GitHub Bot
parent 756edf9860
commit 3d6afdb529
7 changed files with 74 additions and 39 deletions

View File

@@ -49,6 +49,7 @@ Object {
exports[`can create a Fake flipper 2`] = ` exports[`can create a Fake flipper 2`] = `
Object { Object {
"bundledPlugins": Map {},
"clientPlugins": Map { "clientPlugins": Map {
"TestPlugin" => [Function], "TestPlugin" => [Function],
}, },

View File

@@ -20,6 +20,7 @@ import {
addDisabledPlugins, addDisabledPlugins,
addFailedPlugins, addFailedPlugins,
registerLoadedPlugins, registerLoadedPlugins,
registerBundledPlugins,
} from '../reducers/plugins'; } from '../reducers/plugins';
import GK from '../fb-stubs/GK'; import GK from '../fb-stubs/GK';
import {FlipperBasePlugin} from '../plugin'; import {FlipperBasePlugin} from '../plugin';
@@ -33,7 +34,7 @@ import semver from 'semver';
import { import {
ActivatablePluginDetails, ActivatablePluginDetails,
BundledPluginDetails, BundledPluginDetails,
InstalledPluginDetails, PluginDetails,
} from 'flipper-plugin-lib'; } from 'flipper-plugin-lib';
import {tryCatchReportPluginFailures, reportUsage} from '../utils/metrics'; import {tryCatchReportPluginFailures, reportUsage} from '../utils/metrics';
import * as FlipperPluginSDK from 'flipper-plugin'; import * as FlipperPluginSDK from 'flipper-plugin';
@@ -64,19 +65,21 @@ export default async (store: Store, logger: Logger) => {
const uninstalledPlugins = store.getState().pluginManager.uninstalledPlugins; const uninstalledPlugins = store.getState().pluginManager.uninstalledPlugins;
const bundledPlugins = getBundledPlugins();
const loadedPlugins = filterNewestVersionOfEachPlugin( const loadedPlugins = filterNewestVersionOfEachPlugin(
getBundledPlugins(), bundledPlugins,
await getDynamicPlugins(), await getDynamicPlugins(),
); ).filter((p) => !uninstalledPlugins.has(p.name));
const initialPlugins: PluginDefinition[] = loadedPlugins const initialPlugins: PluginDefinition[] = loadedPlugins
.filter((p) => !uninstalledPlugins.has(p.name))
.map(reportVersion) .map(reportVersion)
.filter(checkDisabled(disabledPlugins)) .filter(checkDisabled(disabledPlugins))
.filter(checkGK(gatekeepedPlugins)) .filter(checkGK(gatekeepedPlugins))
.map(createRequirePluginFunction(failedPlugins)) .map(createRequirePluginFunction(failedPlugins))
.filter(notNull); .filter(notNull);
store.dispatch(registerBundledPlugins(bundledPlugins));
store.dispatch(registerLoadedPlugins(loadedPlugins)); store.dispatch(registerLoadedPlugins(loadedPlugins));
store.dispatch(addGatekeepedPlugins(gatekeepedPlugins)); store.dispatch(addGatekeepedPlugins(gatekeepedPlugins));
store.dispatch(addDisabledPlugins(disabledPlugins)); store.dispatch(addDisabledPlugins(disabledPlugins));
@@ -108,11 +111,11 @@ function reportVersion(pluginDetails: ActivatablePluginDetails) {
return pluginDetails; return pluginDetails;
} }
export function filterNewestVersionOfEachPlugin( export function filterNewestVersionOfEachPlugin<
bundledPlugins: BundledPluginDetails[], T1 extends PluginDetails,
dynamicPlugins: InstalledPluginDetails[], T2 extends PluginDetails
): ActivatablePluginDetails[] { >(bundledPlugins: T1[], dynamicPlugins: T2[]): (T1 | T2)[] {
const pluginByName: {[key: string]: ActivatablePluginDetails} = {}; const pluginByName: {[key: string]: T1 | T2} = {};
for (const plugin of bundledPlugins) { for (const plugin of bundledPlugins) {
pluginByName[plugin.name] = plugin; pluginByName[plugin.name] = plugin;
} }

View File

@@ -33,6 +33,7 @@ test('add clientPlugin', () => {
devicePlugins: new Map(), devicePlugins: new Map(),
clientPlugins: new Map(), clientPlugins: new Map(),
loadedPlugins: new Map(), loadedPlugins: new Map(),
bundledPlugins: new Map(),
gatekeepedPlugins: [], gatekeepedPlugins: [],
failedPlugins: [], failedPlugins: [],
disabledPlugins: [], disabledPlugins: [],
@@ -50,6 +51,7 @@ test('add devicePlugin', () => {
devicePlugins: new Map(), devicePlugins: new Map(),
clientPlugins: new Map(), clientPlugins: new Map(),
loadedPlugins: new Map(), loadedPlugins: new Map(),
bundledPlugins: new Map(),
gatekeepedPlugins: [], gatekeepedPlugins: [],
failedPlugins: [], failedPlugins: [],
disabledPlugins: [], disabledPlugins: [],
@@ -67,6 +69,7 @@ test('do not add plugin twice', () => {
devicePlugins: new Map(), devicePlugins: new Map(),
clientPlugins: new Map(), clientPlugins: new Map(),
loadedPlugins: new Map(), loadedPlugins: new Map(),
bundledPlugins: new Map(),
gatekeepedPlugins: [], gatekeepedPlugins: [],
failedPlugins: [], failedPlugins: [],
disabledPlugins: [], disabledPlugins: [],
@@ -99,6 +102,7 @@ test('add gatekeeped plugin', () => {
devicePlugins: new Map(), devicePlugins: new Map(),
clientPlugins: new Map(), clientPlugins: new Map(),
loadedPlugins: new Map(), loadedPlugins: new Map(),
bundledPlugins: new Map(),
gatekeepedPlugins: [], gatekeepedPlugins: [],
failedPlugins: [], failedPlugins: [],
disabledPlugins: [], disabledPlugins: [],

View File

@@ -11,6 +11,7 @@ import {DevicePluginMap, ClientPluginMap, PluginDefinition} from '../plugin';
import { import {
DownloadablePluginDetails, DownloadablePluginDetails,
ActivatablePluginDetails, ActivatablePluginDetails,
BundledPluginDetails,
} from 'flipper-plugin-lib'; } from 'flipper-plugin-lib';
import {Actions} from '.'; import {Actions} from '.';
import produce from 'immer'; import produce from 'immer';
@@ -20,6 +21,7 @@ export type State = {
devicePlugins: DevicePluginMap; devicePlugins: DevicePluginMap;
clientPlugins: ClientPluginMap; clientPlugins: ClientPluginMap;
loadedPlugins: Map<string, ActivatablePluginDetails>; loadedPlugins: Map<string, ActivatablePluginDetails>;
bundledPlugins: Map<string, BundledPluginDetails>;
gatekeepedPlugins: Array<ActivatablePluginDetails>; gatekeepedPlugins: Array<ActivatablePluginDetails>;
disabledPlugins: Array<ActivatablePluginDetails>; disabledPlugins: Array<ActivatablePluginDetails>;
failedPlugins: Array<[ActivatablePluginDetails, string]>; failedPlugins: Array<[ActivatablePluginDetails, string]>;
@@ -57,12 +59,17 @@ export type Action =
| { | {
type: 'REGISTER_LOADED_PLUGINS'; type: 'REGISTER_LOADED_PLUGINS';
payload: Array<ActivatablePluginDetails>; payload: Array<ActivatablePluginDetails>;
}
| {
type: 'REGISTER_BUNDLED_PLUGINS';
payload: Array<BundledPluginDetails>;
}; };
const INITIAL_STATE: State = { const INITIAL_STATE: State = {
devicePlugins: new Map(), devicePlugins: new Map(),
clientPlugins: new Map(), clientPlugins: new Map(),
loadedPlugins: new Map(), loadedPlugins: new Map(),
bundledPlugins: new Map(),
gatekeepedPlugins: [], gatekeepedPlugins: [],
disabledPlugins: [], disabledPlugins: [],
failedPlugins: [], failedPlugins: [],
@@ -119,6 +126,11 @@ export default function reducer(
...state, ...state,
loadedPlugins: new Map(action.payload.map((p) => [p.id, p])), loadedPlugins: new Map(action.payload.map((p) => [p.id, p])),
}; };
} else if (action.type === 'REGISTER_BUNDLED_PLUGINS') {
return {
...state,
bundledPlugins: new Map(action.payload.map((p) => [p.id, p])),
};
} else { } else {
return state; return state;
} }
@@ -168,3 +180,10 @@ export const registerLoadedPlugins = (
type: 'REGISTER_LOADED_PLUGINS', type: 'REGISTER_LOADED_PLUGINS',
payload, payload,
}); });
export const registerBundledPlugins = (
payload: Array<BundledPluginDetails>,
): Action => ({
type: 'REGISTER_BUNDLED_PLUGINS',
payload,
});

View File

@@ -35,7 +35,9 @@ import {
PluginDownloadStatus, PluginDownloadStatus,
startPluginDownload, startPluginDownload,
} from '../../reducers/pluginDownloads'; } from '../../reducers/pluginDownloads';
import {uninstallPlugin} from '../../reducers/pluginManager'; import {activatePlugin, uninstallPlugin} from '../../reducers/pluginManager';
import {BundledPluginDetails} from 'plugin-lib/lib';
import {filterNewestVersionOfEachPlugin} from '../../dispatcher/plugins';
const {SubMenu} = Menu; const {SubMenu} = Menu;
const {Text} = Typography; const {Text} = Typography;
@@ -71,8 +73,14 @@ export const PluginList = memo(function PluginList({
const isArchived = !!activeDevice?.isArchived; const isArchived = !!activeDevice?.isArchived;
const annotatedDownloadablePlugins = useMemoize< const annotatedDownloadablePlugins = useMemoize<
[Record<string, DownloadablePluginState>, DownloadablePluginDetails[]], [
[plugin: DownloadablePluginDetails, downloadStatus?: PluginDownloadStatus][] Record<string, DownloadablePluginState>,
(DownloadablePluginDetails | BundledPluginDetails)[],
],
[
plugin: DownloadablePluginDetails | BundledPluginDetails,
downloadStatus?: PluginDownloadStatus,
][]
>( >(
(downloads, downloadablePlugins) => { (downloads, downloadablePlugins) => {
const downloadMap = new Map( const downloadMap = new Map(
@@ -126,12 +134,11 @@ export const PluginList = memo(function PluginList({
const handleInstallPlugin = useCallback( const handleInstallPlugin = useCallback(
(id: string) => { (id: string) => {
const plugin = downloadablePlugins.find((p) => p.id === id)!; const plugin = downloadablePlugins.find((p) => p.id === id)!;
dispatch( if (plugin.isBundled) {
startPluginDownload({ dispatch(activatePlugin({plugin, enable: true, notifyIfFailed: true}));
plugin, } else {
startedByUser: true, dispatch(startPluginDownload({plugin, startedByUser: true}));
}), }
);
}, },
[downloadablePlugins, dispatch], [downloadablePlugins, dispatch],
); );
@@ -229,19 +236,14 @@ export const PluginList = memo(function PluginList({
tooltip={getPluginTooltip(plugin.details)} tooltip={getPluginTooltip(plugin.details)}
actions={ actions={
<> <>
{!plugin.details.isBundled && (
<ActionButton <ActionButton
id={plugin.id} id={plugin.id}
title="Uninstall plugin" title="Uninstall plugin"
onClick={handleUninstallPlugin} onClick={handleUninstallPlugin}
icon={ icon={
<DeleteOutlined <DeleteOutlined size={16} style={{marginRight: 0}} />
size={16}
style={{marginRight: 0}}
/>
} }
/> />
)}
<ActionButton <ActionButton
id={plugin.id} id={plugin.id}
title="Enable plugin" title="Enable plugin"
@@ -465,7 +467,10 @@ export function computePluginLists(
const enabledPlugins: ClientPluginDefinition[] = []; const enabledPlugins: ClientPluginDefinition[] = [];
const disabledPlugins: ClientPluginDefinition[] = []; const disabledPlugins: ClientPluginDefinition[] = [];
const unavailablePlugins: [plugin: PluginDetails, reason: string][] = []; const unavailablePlugins: [plugin: PluginDetails, reason: string][] = [];
const downloadablePlugins: DownloadablePluginDetails[] = []; const downloadablePlugins: (
| DownloadablePluginDetails
| BundledPluginDetails
)[] = [];
if (device) { if (device) {
// find all device plugins that aren't part of the current device / metro // find all device plugins that aren't part of the current device / metro
@@ -528,13 +533,10 @@ export function computePluginLists(
disabledPlugins.push(plugin); disabledPlugins.push(plugin);
} }
}); });
const installedPluginIds = new Set<string>([ const uninstalledMarketplacePlugins = filterNewestVersionOfEachPlugin(
...clientPlugins.map((p) => p.id), [...plugins.bundledPlugins.values()],
...unavailablePlugins.map(([p]) => p.id), plugins.marketplacePlugins,
]); ).filter((p) => !plugins.loadedPlugins.has(p.id));
const uninstalledMarketplacePlugins = plugins.marketplacePlugins.filter(
(p) => !installedPluginIds.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);

View File

@@ -200,6 +200,7 @@ function uninstallPlugin(state: StoreState, plugin: PluginDefinition) {
unloadPluginModule(plugin.details); unloadPluginModule(plugin.details);
draft.plugins.clientPlugins.delete(plugin.id); draft.plugins.clientPlugins.delete(plugin.id);
draft.plugins.devicePlugins.delete(plugin.id); draft.plugins.devicePlugins.delete(plugin.id);
draft.plugins.loadedPlugins.delete(plugin.id);
draft.pluginManager.uninstalledPlugins.add(plugin.details.name); draft.pluginManager.uninstalledPlugins.add(plugin.details.name);
}); });
} }

View File

@@ -767,6 +767,7 @@ test('test determinePluginsToProcess for mutilple clients having plugins present
['RandomPlugin', TestPlugin.details], ['RandomPlugin', TestPlugin.details],
['TestDevicePlugin', TestDevicePlugin.details], ['TestDevicePlugin', TestDevicePlugin.details],
]), ]),
bundledPlugins: new Map(),
gatekeepedPlugins: [], gatekeepedPlugins: [],
disabledPlugins: [], disabledPlugins: [],
failedPlugins: [], failedPlugins: [],
@@ -838,6 +839,7 @@ test('test determinePluginsToProcess for no selected plugin present in any clien
['RandomPlugin', TestPlugin.details], ['RandomPlugin', TestPlugin.details],
['TestDevicePlugin', TestDevicePlugin.details], ['TestDevicePlugin', TestDevicePlugin.details],
]), ]),
bundledPlugins: new Map(),
gatekeepedPlugins: [], gatekeepedPlugins: [],
disabledPlugins: [], disabledPlugins: [],
failedPlugins: [], failedPlugins: [],
@@ -886,6 +888,7 @@ test('test determinePluginsToProcess for multiple clients on same device', async
['TestPlugin', TestPlugin.details], ['TestPlugin', TestPlugin.details],
['TestDevicePlugin', TestDevicePlugin.details], ['TestDevicePlugin', TestDevicePlugin.details],
]), ]),
bundledPlugins: new Map(),
gatekeepedPlugins: [], gatekeepedPlugins: [],
disabledPlugins: [], disabledPlugins: [],
failedPlugins: [], failedPlugins: [],
@@ -972,6 +975,7 @@ test('test determinePluginsToProcess for multiple clients on different device',
['TestPlugin', TestPlugin.details], ['TestPlugin', TestPlugin.details],
['TestDevicePlugin', TestDevicePlugin.details], ['TestDevicePlugin', TestDevicePlugin.details],
]), ]),
bundledPlugins: new Map(),
gatekeepedPlugins: [], gatekeepedPlugins: [],
disabledPlugins: [], disabledPlugins: [],
failedPlugins: [], failedPlugins: [],
@@ -1055,6 +1059,7 @@ test('test determinePluginsToProcess to ignore archived clients', async () => {
['TestPlugin', TestPlugin.details], ['TestPlugin', TestPlugin.details],
['TestDevicePlugin', TestDevicePlugin.details], ['TestDevicePlugin', TestDevicePlugin.details],
]), ]),
bundledPlugins: new Map(),
gatekeepedPlugins: [], gatekeepedPlugins: [],
disabledPlugins: [], disabledPlugins: [],
failedPlugins: [], failedPlugins: [],