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:
committed by
Facebook GitHub Bot
parent
756edf9860
commit
3d6afdb529
@@ -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],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: [],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user