diff --git a/desktop/app/src/chrome/PluginActions.tsx b/desktop/app/src/chrome/PluginActions.tsx
new file mode 100644
index 000000000..2e5bb7712
--- /dev/null
+++ b/desktop/app/src/chrome/PluginActions.tsx
@@ -0,0 +1,126 @@
+/**
+ * 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 {
+ DownloadOutlined,
+ LoadingOutlined,
+ PlusOutlined,
+} from '@ant-design/icons';
+import {Alert, Button} from 'antd';
+import {
+ BundledPluginDetails,
+ DownloadablePluginDetails,
+} from 'flipper-plugin-lib';
+import React, {useMemo} from 'react';
+import {useCallback} from 'react';
+import {useDispatch, useSelector} from 'react-redux';
+import {PluginDefinition} from '../plugin';
+import {startPluginDownload} from '../reducers/pluginDownloads';
+import {loadPlugin, switchPlugin} from '../reducers/pluginManager';
+import {
+ getActiveClient,
+ getPluginDownloadStatusMap,
+} from '../selectors/connections';
+import {Layout} from '../ui';
+import {ActivePluginListItem} from '../utils/pluginUtils';
+
+export function PluginActions({
+ activePlugin,
+ type,
+}: {
+ activePlugin: ActivePluginListItem;
+ type: 'link' | 'primary';
+}) {
+ switch (activePlugin.status) {
+ case 'disabled': {
+ return ;
+ }
+ case 'uninstalled': {
+ return ;
+ }
+ case 'unavailable': {
+ return type === 'primary' ? (
+
+ ) : null;
+ }
+ default:
+ return null;
+ }
+}
+
+function EnableButton({
+ plugin,
+ type,
+}: {
+ plugin: PluginDefinition;
+ type: 'link' | 'primary';
+}) {
+ const dispatch = useDispatch();
+ const client = useSelector(getActiveClient);
+ const enableOrDisablePlugin = useCallback(() => {
+ dispatch(switchPlugin({plugin, selectedApp: client?.query?.app}));
+ }, [dispatch, plugin, client]);
+ return (
+ }
+ onClick={enableOrDisablePlugin}
+ style={{flexGrow: type == 'primary' ? 1 : 0}}>
+ Enable Plugin
+
+ );
+}
+
+function UnavailabilityAlert({reason}: {reason: string}) {
+ return (
+
+
+
+ );
+}
+
+function InstallButton({
+ plugin,
+ type = 'primary',
+}: {
+ plugin: DownloadablePluginDetails | BundledPluginDetails;
+ type: 'link' | 'primary';
+}) {
+ const dispatch = useDispatch();
+ const installPlugin = useCallback(() => {
+ if (plugin.isBundled) {
+ dispatch(loadPlugin({plugin, enable: true, notifyIfFailed: true}));
+ } else {
+ dispatch(startPluginDownload({plugin, startedByUser: true}));
+ }
+ }, [plugin, dispatch]);
+ const downloads = useSelector(getPluginDownloadStatusMap);
+ const downloadStatus = useMemo(
+ () => downloads.get(plugin.id),
+ [downloads, plugin],
+ );
+ return (
+
+ ) : (
+
+ )
+ }
+ onClick={installPlugin}
+ style={{
+ flexGrow: type === 'primary' ? 1 : 0,
+ }}>
+ Install Plugin
+
+ );
+}
diff --git a/desktop/app/src/chrome/fb-stubs/PluginInfo.tsx b/desktop/app/src/chrome/fb-stubs/PluginInfo.tsx
index b5611d3d4..22489bdaf 100644
--- a/desktop/app/src/chrome/fb-stubs/PluginInfo.tsx
+++ b/desktop/app/src/chrome/fb-stubs/PluginInfo.tsx
@@ -7,94 +7,46 @@
* @format
*/
-import React, {useCallback, useMemo} from 'react';
-import {Label, ToggleButton, SmallText, styled, Layout} from '../../ui';
-import {useDispatch, useStore} from '../../utils/useStore';
-import {switchPlugin} from '../../reducers/pluginManager';
-import {isPluginEnabled} from '../../reducers/connections';
-import {theme} from 'flipper-plugin';
-import {PluginDefinition} from '../../plugin';
+import React from 'react';
import {useSelector} from 'react-redux';
-import {getActiveClient} from '../../selectors/connections';
+import {getActivePlugin} from '../../selectors/connections';
+import {ActivePluginListItem} from '../../utils/pluginUtils';
+import {Layout} from '../../ui';
+import {CenteredContainer} from '../../sandy-chrome/CenteredContainer';
+import {Typography} from 'antd';
+import {PluginActions} from '../PluginActions';
+import {CoffeeOutlined} from '@ant-design/icons';
-const Waiting = styled(Layout.Container)({
- width: '100%',
- height: '100%',
- flexGrow: 1,
- alignItems: 'center',
- justifyContent: 'center',
- textAlign: 'center',
-});
+const {Text, Title} = Typography;
export function PluginInfo() {
- const pluginId = useStore((state) => state.connections.selectedPlugin);
- const enabledPlugins = useStore((state) => state.connections.enabledPlugins);
- const enabledDevicePlugins = useStore(
- (state) => state.connections.enabledDevicePlugins,
- );
- const activeClient = useSelector(getActiveClient);
- const clientPlugins = useStore((state) => state.plugins.clientPlugins);
- const devicePlugins = useStore((state) => state.plugins.devicePlugins);
- const selectedClientId = activeClient?.id ?? null;
- const selectedApp = activeClient?.query.app ?? null;
- const disabledPlugin = useMemo(
- () =>
- pluginId &&
- !isPluginEnabled(
- enabledPlugins,
- enabledDevicePlugins,
- selectedClientId,
- pluginId,
- )
- ? clientPlugins.get(pluginId) ?? devicePlugins.get(pluginId)
- : undefined,
- [
- pluginId,
- enabledPlugins,
- enabledDevicePlugins,
- selectedClientId,
- clientPlugins,
- devicePlugins,
- ],
- );
- return disabledPlugin ? (
-
- ) : null;
+ const activePlugin = useSelector(getActivePlugin);
+ if (activePlugin) {
+ return ;
+ } else {
+ return null;
+ }
}
-function PluginEnabler({
- plugin,
- selectedApp,
+function PluginMarketplace({
+ activePlugin,
}: {
- plugin: PluginDefinition;
- selectedApp: string | null;
+ activePlugin: ActivePluginListItem;
}) {
- const dispatch = useDispatch();
- const enablePlugin = useCallback(() => {
- dispatch(switchPlugin({plugin, selectedApp: selectedApp ?? undefined}));
- }, [dispatch, plugin, selectedApp]);
return (
-
-
-
-
-
-
-
-
-
-
-
- Click to enable this plugin
-
-
-
+
+
+
+
+ Plugin '{activePlugin.details.title}' is {activePlugin.status}
+
+ {activePlugin.status === 'unavailable' ? (
+ {activePlugin.reason}.
+ ) : null}
+
+
+
+
+
);
}
diff --git a/desktop/app/src/sandy-chrome/appinspect/__tests__/PluginList.spec.tsx b/desktop/app/src/sandy-chrome/appinspect/__tests__/PluginList.spec.tsx
index abfd26b29..aae5a07ee 100644
--- a/desktop/app/src/sandy-chrome/appinspect/__tests__/PluginList.spec.tsx
+++ b/desktop/app/src/sandy-chrome/appinspect/__tests__/PluginList.spec.tsx
@@ -244,7 +244,7 @@ describe('basic getActiveDevice with metro present', () => {
unavailablePlugins: [
[
gateKeepedPlugin,
- "This plugin is only available to members of gatekeeper 'not for you'",
+ "Plugin 'Gatekeeped Plugin' is only available to members of gatekeeper 'not for you'",
],
[
unsupportedDevicePlugin.details,
diff --git a/desktop/app/src/utils/pluginUtils.tsx b/desktop/app/src/utils/pluginUtils.tsx
index f894b772e..fa28e2417 100644
--- a/desktop/app/src/utils/pluginUtils.tsx
+++ b/desktop/app/src/utils/pluginUtils.tsx
@@ -301,18 +301,21 @@ export function computePluginLists(
// process problematic plugins
plugins.disabledPlugins.forEach((plugin) => {
- unavailablePlugins.push([plugin, 'Plugin is disabled by configuration']);
+ unavailablePlugins.push([
+ plugin,
+ `Plugin '${plugin.title}' is disabled by configuration`,
+ ]);
});
plugins.gatekeepedPlugins.forEach((plugin) => {
unavailablePlugins.push([
plugin,
- `This plugin is only available to members of gatekeeper '${plugin.gatekeeper}'`,
+ `Plugin '${plugin.title}' is only available to members of gatekeeper '${plugin.gatekeeper}'`,
]);
});
plugins.failedPlugins.forEach(([plugin, error]) => {
unavailablePlugins.push([
plugin,
- `Flipper failed to load this plugin: '${error}'`,
+ `Plugin '${plugin.title}' failed to load: '${error}'`,
]);
});