Uninstall plugins from sidebar
Summary: Added UI for uninstalling plugins from sidebar. To avoid confusion between "disable" and "uninstall" and to reduce possibility of errors when plugins uninstalled accidentally by misclicks, I made it only possible to uninstall plugins after they are disabled. So for enabled plugins two steps are required for uninstalling. Reviewed By: mweststrate Differential Revision: D25454117 fbshipit-source-id: 28e67dc1ff2d39ad67e6d2770302a996affd9723
This commit is contained in:
committed by
Facebook GitHub Bot
parent
97d37abbb2
commit
df03ccbeab
@@ -9,11 +9,17 @@
|
|||||||
|
|
||||||
import {Store} from '../reducers/index';
|
import {Store} from '../reducers/index';
|
||||||
import {Logger} from '../fb-interfaces/Logger';
|
import {Logger} from '../fb-interfaces/Logger';
|
||||||
import {registerInstalledPlugins} from '../reducers/pluginManager';
|
import {
|
||||||
|
pluginFilesRemoved,
|
||||||
|
registerInstalledPlugins,
|
||||||
|
} from '../reducers/pluginManager';
|
||||||
import {
|
import {
|
||||||
getInstalledPlugins,
|
getInstalledPlugins,
|
||||||
cleanupOldInstalledPluginVersions,
|
cleanupOldInstalledPluginVersions,
|
||||||
|
removePlugin,
|
||||||
} from 'flipper-plugin-lib';
|
} from 'flipper-plugin-lib';
|
||||||
|
import {sideEffect} from '../utils/sideEffect';
|
||||||
|
import pMap from 'p-map';
|
||||||
|
|
||||||
const maxInstalledPluginVersionsToKeep = 2;
|
const maxInstalledPluginVersionsToKeep = 2;
|
||||||
|
|
||||||
@@ -28,4 +34,26 @@ export default (store: Store, _logger: Logger) => {
|
|||||||
window.requestIdleCallback(() => {
|
window.requestIdleCallback(() => {
|
||||||
refreshInstalledPlugins(store);
|
refreshInstalledPlugins(store);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
sideEffect(
|
||||||
|
store,
|
||||||
|
{
|
||||||
|
name: 'removeUninstalledPluginFiles',
|
||||||
|
throttleMs: 1000,
|
||||||
|
fireImmediately: true,
|
||||||
|
},
|
||||||
|
(state) => state.pluginManager.removedPlugins,
|
||||||
|
(removedPlugins) => {
|
||||||
|
pMap(removedPlugins, (p) => {
|
||||||
|
removePlugin(p.name)
|
||||||
|
.then(() => pluginFilesRemoved(p))
|
||||||
|
.catch((e) =>
|
||||||
|
console.error(
|
||||||
|
`Error while removing files of uninstalled plugin ${p.title}`,
|
||||||
|
e,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).then(() => refreshInstalledPlugins(store));
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ export default function createTableNativePlugin(id: string, title: string) {
|
|||||||
source: '',
|
source: '',
|
||||||
main: '',
|
main: '',
|
||||||
entry: '',
|
entry: '',
|
||||||
isDefault: false,
|
isDefault: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultPersistedState: PersistedState = {
|
static defaultPersistedState: PersistedState = {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {PluginDetails} from 'flipper-plugin-lib';
|
|||||||
|
|
||||||
test('reduce empty registerInstalledPlugins', () => {
|
test('reduce empty registerInstalledPlugins', () => {
|
||||||
const result = reducer(undefined, registerInstalledPlugins([]));
|
const result = reducer(undefined, registerInstalledPlugins([]));
|
||||||
expect(result).toEqual({installedPlugins: []});
|
expect(result).toEqual({installedPlugins: [], removedPlugins: []});
|
||||||
});
|
});
|
||||||
|
|
||||||
const EXAMPLE_PLUGIN = {
|
const EXAMPLE_PLUGIN = {
|
||||||
@@ -33,8 +33,9 @@ test('reduce registerInstalledPlugins, clear again', () => {
|
|||||||
const result = reducer(undefined, registerInstalledPlugins([EXAMPLE_PLUGIN]));
|
const result = reducer(undefined, registerInstalledPlugins([EXAMPLE_PLUGIN]));
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
installedPlugins: [EXAMPLE_PLUGIN],
|
installedPlugins: [EXAMPLE_PLUGIN],
|
||||||
|
removedPlugins: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const result2 = reducer(result, registerInstalledPlugins([]));
|
const result2 = reducer(result, registerInstalledPlugins([]));
|
||||||
expect(result2).toEqual({installedPlugins: []});
|
expect(result2).toEqual({installedPlugins: [], removedPlugins: []});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,18 +9,33 @@
|
|||||||
|
|
||||||
import {Actions} from './';
|
import {Actions} from './';
|
||||||
import {PluginDetails} from 'flipper-plugin-lib';
|
import {PluginDetails} from 'flipper-plugin-lib';
|
||||||
|
import {produce} from 'immer';
|
||||||
|
import {PluginDefinition} from '../plugin';
|
||||||
|
|
||||||
export type State = {
|
export type State = {
|
||||||
installedPlugins: PluginDetails[];
|
installedPlugins: PluginDetails[];
|
||||||
|
removedPlugins: PluginDetails[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Action = {
|
export type Action =
|
||||||
|
| {
|
||||||
type: 'REGISTER_INSTALLED_PLUGINS';
|
type: 'REGISTER_INSTALLED_PLUGINS';
|
||||||
payload: PluginDetails[];
|
payload: PluginDetails[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'PLUGIN_FILES_REMOVED';
|
||||||
|
payload: PluginDetails;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// Implemented by rootReducer in `store.tsx`
|
||||||
|
type: 'UNINSTALL_PLUGIN';
|
||||||
|
payload: PluginDefinition;
|
||||||
};
|
};
|
||||||
|
|
||||||
const INITIAL_STATE: State = {
|
const INITIAL_STATE: State = {
|
||||||
installedPlugins: [],
|
installedPlugins: [],
|
||||||
|
// plugins which were uninstalled recently and require file cleanup
|
||||||
|
removedPlugins: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function reducer(
|
export default function reducer(
|
||||||
@@ -32,6 +47,13 @@ export default function reducer(
|
|||||||
...state,
|
...state,
|
||||||
installedPlugins: action.payload,
|
installedPlugins: action.payload,
|
||||||
};
|
};
|
||||||
|
} else if (action.type === 'PLUGIN_FILES_REMOVED') {
|
||||||
|
const plugin = action.payload;
|
||||||
|
return produce(state, (draft) => {
|
||||||
|
draft.removedPlugins = draft.removedPlugins.filter(
|
||||||
|
(p) => p.id === plugin.id,
|
||||||
|
);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -41,3 +63,13 @@ export const registerInstalledPlugins = (payload: PluginDetails[]): Action => ({
|
|||||||
type: 'REGISTER_INSTALLED_PLUGINS',
|
type: 'REGISTER_INSTALLED_PLUGINS',
|
||||||
payload,
|
payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const pluginFilesRemoved = (payload: PluginDetails): Action => ({
|
||||||
|
type: 'PLUGIN_FILES_REMOVED',
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const uninstallPlugin = (payload: PluginDefinition): Action => ({
|
||||||
|
type: 'UNINSTALL_PLUGIN',
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import React, {memo, useCallback, useEffect, useRef, useState} from 'react';
|
import React, {memo, useCallback, useEffect, useRef, useState} from 'react';
|
||||||
import {Badge, Button, Menu, Tooltip, Typography} from 'antd';
|
import {Badge, Button, Menu, Tooltip, Typography} from 'antd';
|
||||||
import {InfoIcon, SidebarTitle} from '../LeftSidebar';
|
import {InfoIcon, SidebarTitle} from '../LeftSidebar';
|
||||||
import {PlusOutlined, MinusOutlined} from '@ant-design/icons';
|
import {PlusOutlined, MinusOutlined, DeleteOutlined} from '@ant-design/icons';
|
||||||
import {Glyph, Layout, styled} from '../../ui';
|
import {Glyph, Layout, styled} from '../../ui';
|
||||||
import {theme, NUX, Tracked} from 'flipper-plugin';
|
import {theme, NUX, Tracked} from 'flipper-plugin';
|
||||||
import {useDispatch, useStore} from '../../utils/useStore';
|
import {useDispatch, useStore} from '../../utils/useStore';
|
||||||
@@ -26,6 +26,7 @@ import {useMemoize} from '../../utils/useMemoize';
|
|||||||
import MetroDevice from '../../devices/MetroDevice';
|
import MetroDevice from '../../devices/MetroDevice';
|
||||||
import {DownloadablePluginDetails} from 'plugin-lib/lib';
|
import {DownloadablePluginDetails} from 'plugin-lib/lib';
|
||||||
import {startPluginDownload} from '../../reducers/pluginDownloads';
|
import {startPluginDownload} from '../../reducers/pluginDownloads';
|
||||||
|
import {uninstallPlugin} from '../../reducers/pluginManager';
|
||||||
|
|
||||||
const {SubMenu} = Menu;
|
const {SubMenu} = Menu;
|
||||||
const {Text} = Typography;
|
const {Text} = Typography;
|
||||||
@@ -108,6 +109,13 @@ export const PluginList = memo(function PluginList({
|
|||||||
},
|
},
|
||||||
[uninstalledPlugins, dispatch],
|
[uninstalledPlugins, dispatch],
|
||||||
);
|
);
|
||||||
|
const handleUninstallPlugin = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
const plugin = disabledPlugins.find((p) => p.id === id)!;
|
||||||
|
dispatch(uninstallPlugin(plugin));
|
||||||
|
},
|
||||||
|
[disabledPlugins, dispatch],
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Layout.Container>
|
<Layout.Container>
|
||||||
<SidebarTitle>Plugins</SidebarTitle>
|
<SidebarTitle>Plugins</SidebarTitle>
|
||||||
@@ -194,12 +202,29 @@ export const PluginList = memo(function PluginList({
|
|||||||
scrollTo={plugin.id === connections.selectedPlugin}
|
scrollTo={plugin.id === connections.selectedPlugin}
|
||||||
tooltip={getPluginTooltip(plugin.details)}
|
tooltip={getPluginTooltip(plugin.details)}
|
||||||
actions={
|
actions={
|
||||||
|
<>
|
||||||
|
{!plugin.details.isDefault && (
|
||||||
|
<ActionButton
|
||||||
|
id={plugin.id}
|
||||||
|
title="Uninstall plugin"
|
||||||
|
onClick={handleUninstallPlugin}
|
||||||
|
icon={
|
||||||
|
<DeleteOutlined
|
||||||
|
size={16}
|
||||||
|
style={{marginRight: 0}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
id={plugin.id}
|
id={plugin.id}
|
||||||
title="Enable plugin"
|
title="Enable plugin"
|
||||||
onClick={handleStarPlugin}
|
onClick={handleStarPlugin}
|
||||||
icon={<PlusOutlined size={16} style={{marginRight: 0}} />}
|
icon={
|
||||||
|
<PlusOutlined size={16} style={{marginRight: 0}} />
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ export function rootReducer(
|
|||||||
} else {
|
} else {
|
||||||
return updateClientPlugin(state, plugin, enablePlugin);
|
return updateClientPlugin(state, plugin, enablePlugin);
|
||||||
}
|
}
|
||||||
|
} else if (action.type === 'UNINSTALL_PLUGIN' && state) {
|
||||||
|
const plugin = action.payload;
|
||||||
|
return uninstallPlugin(state, plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise
|
// otherwise
|
||||||
@@ -173,6 +176,24 @@ function updateClientPlugin(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uninstallPlugin(state: StoreState, plugin: PluginDefinition) {
|
||||||
|
const clients = state.connections.clients;
|
||||||
|
return produce(state, (draft) => {
|
||||||
|
clients.forEach((client) => {
|
||||||
|
stopPlugin(client, plugin.id);
|
||||||
|
const pluginKey = getPluginKey(
|
||||||
|
client.id,
|
||||||
|
{serial: client.query.device_id},
|
||||||
|
plugin.id,
|
||||||
|
);
|
||||||
|
delete draft.pluginMessageQueue[pluginKey];
|
||||||
|
});
|
||||||
|
cleanupPluginStates(draft.pluginStates, plugin.id);
|
||||||
|
draft.plugins.clientPlugins.delete(plugin.id);
|
||||||
|
draft.pluginManager.removedPlugins.push(plugin.details);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateDevicePlugin(state: StoreState, plugin: DevicePluginDefinition) {
|
function updateDevicePlugin(state: StoreState, plugin: DevicePluginDefinition) {
|
||||||
const devices = state.connections.devices;
|
const devices = state.connections.devices;
|
||||||
return produce(state, (draft) => {
|
return produce(state, (draft) => {
|
||||||
|
|||||||
@@ -140,8 +140,12 @@ export async function getInstalledPlugins(): Promise<PluginDetails[]> {
|
|||||||
versionDirs
|
versionDirs
|
||||||
.filter(([_, versionDirs]) => versionDirs.length > 0)
|
.filter(([_, versionDirs]) => versionDirs.length > 0)
|
||||||
.map(([_, versionDirs]) => versionDirs[0]),
|
.map(([_, versionDirs]) => versionDirs[0]),
|
||||||
(latestVersionDir) => getPluginDetailsFromDir(latestVersionDir),
|
(latestVersionDir) =>
|
||||||
);
|
getPluginDetailsFromDir(latestVersionDir).catch((err) => {
|
||||||
|
console.error(`Failed to load plugin from ${latestVersionDir}`, err);
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
).then((plugins) => plugins.filter(notNull));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanupOldInstalledPluginVersions(
|
export async function cleanupOldInstalledPluginVersions(
|
||||||
@@ -172,11 +176,20 @@ export async function moveInstalledPluginsFromLegacyDir() {
|
|||||||
fs
|
fs
|
||||||
.lstat(dir)
|
.lstat(dir)
|
||||||
.then((lstat) => lstat.isDirectory())
|
.then((lstat) => lstat.isDirectory())
|
||||||
.catch(() => Promise.resolve(false)),
|
.catch(() => false),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.then((dirs) =>
|
.then((dirs) =>
|
||||||
pmap(dirs, (dir) => getPluginDetailsFromDir(dir).catch(() => null)),
|
pmap(dirs, (dir) =>
|
||||||
|
getPluginDetailsFromDir(dir).catch(async (err) => {
|
||||||
|
console.error(
|
||||||
|
`Failed to load plugin from ${dir} on moving legacy plugins. Removing it.`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
fs.remove(dir);
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.then((plugins) =>
|
.then((plugins) =>
|
||||||
pmap(plugins.filter(notNull), (plugin) =>
|
pmap(plugins.filter(notNull), (plugin) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user