diff --git a/desktop/app/src/plugin.tsx b/desktop/app/src/plugin.tsx
index 3e6644ccc..381cbd6ac 100644
--- a/desktop/app/src/plugin.tsx
+++ b/desktop/app/src/plugin.tsx
@@ -126,7 +126,7 @@ export abstract class FlipperBasePlugin<
static gatekeeper: string | null = null;
static entry: string | null = null;
static isDefault: boolean;
- static details: PluginDetails | undefined;
+ static details: PluginDetails;
static keyboardActions: KeyboardActions | null;
static screenshot: string | null;
static defaultPersistedState: any;
diff --git a/desktop/app/src/plugins/TableNativePlugin.tsx b/desktop/app/src/plugins/TableNativePlugin.tsx
index fdde5df5a..c05cbf851 100644
--- a/desktop/app/src/plugins/TableNativePlugin.tsx
+++ b/desktop/app/src/plugins/TableNativePlugin.tsx
@@ -34,6 +34,7 @@ import createPaste from '../fb-stubs/createPaste';
import {ReactNode} from 'react';
import React from 'react';
import {KeyboardActions} from '../MenuBar';
+import {PluginDetails} from 'flipper-plugin-lib';
type ID = string;
@@ -255,6 +256,21 @@ export default function createTableNativePlugin(id: string, title: string) {
static id = id || '';
static title = title || '';
+ static details: PluginDetails = {
+ id,
+ title,
+ icon: 'apps',
+ name: id,
+ // all hmm...
+ specVersion: 1,
+ version: 'auto',
+ dir: '',
+ source: '',
+ main: '',
+ entry: '',
+ isDefault: false,
+ };
+
static defaultPersistedState: PersistedState = {
rows: [],
datas: {},
diff --git a/desktop/app/src/sandy-chrome/appinspect/AppInspect.tsx b/desktop/app/src/sandy-chrome/appinspect/AppInspect.tsx
index 2cf0e3ed6..b6f0943db 100644
--- a/desktop/app/src/sandy-chrome/appinspect/AppInspect.tsx
+++ b/desktop/app/src/sandy-chrome/appinspect/AppInspect.tsx
@@ -7,27 +7,17 @@
* @format
*/
-import React, {useEffect, useRef, useState} from 'react';
-import {Alert, Badge, Button, Input, Menu, Tooltip, Typography} from 'antd';
+import React from 'react';
+import {Alert, Button, Input} from 'antd';
import {LeftSidebar, SidebarTitle, InfoIcon} from '../LeftSidebar';
-import {
- SettingOutlined,
- RocketOutlined,
- MailOutlined,
- AppstoreOutlined,
-} from '@ant-design/icons';
-import {Glyph, Layout, Link, styled} from '../../ui';
+import {SettingOutlined, RocketOutlined} from '@ant-design/icons';
+import {Layout, Link} from '../../ui';
import {theme} from '../theme';
import {useStore as useReduxStore} from 'react-redux';
import {showEmulatorLauncher} from './LaunchEmulator';
import {AppSelector} from './AppSelector';
-import {useDispatch, useStore} from '../../utils/useStore';
-import {getPluginTitle, sortPluginsByName} from '../../utils/pluginUtils';
-import {PluginDefinition} from '../../plugin';
-import {selectPlugin} from '../../reducers/connections';
-
-const {SubMenu} = Menu;
-const {Text} = Typography;
+import {useStore} from '../../utils/useStore';
+import {PluginList} from './PluginList';
const appTooltip = (
<>
@@ -45,7 +35,7 @@ export function AppInspect() {
const selectedDevice = useStore((state) => state.connections.selectedDevice);
return (
-
+
{appTooltip}}>
App Inspect
@@ -68,226 +58,15 @@ export function AppInspect() {
- {selectedDevice ? (
-
- ) : (
-
- )}
+
+ {selectedDevice ? (
+
+ ) : (
+
+ )}
+
);
}
-
-function PluginList() {
- // const {selectedApp, selectedDevice} = useStore((state) => state.connections);
-
- return (
-
- Plugins
-
- {}}
- defaultOpenKeys={['device']}
- mode="inline">
-
-
- Header
-
-
- }>
- }>
- Option 1
-
- }>
- Option 2
-
- Option 3
- Option 4
-
- }
- title="Navigation Two">
- Option 5
- Option 6
- Option 7
- Option 8
-
-
-
- Navigation Three
-
- }>
- Option 9
- Option 10
- Option 11
- Option 12
-
-
-
-
- );
-}
-
-function DevicePlugins(props: any) {
- const dispatch = useDispatch();
- const {selectedDevice, selectedPlugin} = useStore(
- (state) => state.connections,
- );
- const devicePlugins = useStore((state) => state.plugins.devicePlugins);
- if (selectedDevice?.devicePlugins.length === 0) {
- return null;
- }
- return (
- Device}>
- {selectedDevice!.devicePlugins
- .map((pluginName) => devicePlugins.get(pluginName)!)
- .sort(sortPluginsByName)
- .map((plugin) => (
- {
- dispatch(
- selectPlugin({
- selectedPlugin: plugin.id,
- selectedApp: null,
- deepLinkPayload: null,
- selectedDevice,
- }),
- );
- }}
- plugin={plugin}
- />
- ))}
-
- );
-}
-
-const Plugin: React.FC<{
- onClick: () => void;
- isActive: boolean;
- plugin: PluginDefinition;
- app?: string | null | undefined;
- helpRef?: any;
- provided?: any;
- onFavorite?: () => void;
- starred?: boolean; // undefined means: not starrable
-}> = function (props) {
- const {isActive, plugin, onFavorite, starred, ...rest} = props;
- const domRef = useRef(null);
-
- useEffect(() => {
- const node = domRef.current;
- if (isActive && node) {
- const rect = node.getBoundingClientRect();
- if (rect.top < 0 || rect.bottom > document.documentElement.clientHeight) {
- node.scrollIntoView();
- }
- }
- }, [isActive]);
-
- return (
-
-
- {getPluginTitle(plugin)} ({plugin.version})
- {plugin.details?.description ? (
- <>
-
-
- {plugin.details?.description}
- >
- ) : (
- ''
- )}
- >
- }
- mouseEnterDelay={1}>
-
-
-
-
- {getPluginTitle(plugin)}
-
-
- {/* {starred !== undefined && (!starred || isActive) && (
-
- )} */}
-
- );
-};
-
-const iconStyle = {
- color: theme.white,
- background: theme.primaryColor,
- borderRadius: theme.borderRadius,
- width: 24,
- height: 24,
-};
-
-// TODO: move this largely to themes/base.less to make it the default?
-// Dimensions are hardcoded as they correlate strongly
-const PluginMenu = styled(Menu)({
- border: 'none',
- '.ant-menu-inline .ant-menu-item, .ant-menu-inline .ant-menu-submenu-title ': {
- width: '100%', // reset to remove weird bonus pixel from ANT
- },
- '.ant-menu-submenu > .ant-menu-submenu-title, .ant-menu-sub.ant-menu-inline > .ant-menu-item': {
- borderRadius: theme.borderRadius,
- height: '32px',
- lineHeight: '24px',
- padding: `4px 32px 4px 8px !important`,
- '&:hover': {
- color: theme.textColorPrimary,
- background: theme.backgroundTransparentHover,
- },
- '&.ant-menu-item-selected::after': {
- border: 'none',
- },
- '&.ant-menu-item-selected': {
- color: theme.white,
- background: theme.primaryColor,
- border: 'none',
- },
- },
- '.ant-menu-submenu-inline > .ant-menu-submenu-title .ant-menu-submenu-arrow': {
- right: 8,
- },
- '.ant-badge-count': {
- color: theme.textColorPrimary,
- background: theme.backgroundTransparentHover,
- fontWeight: 'bold',
- padding: `0 10px`,
- boxShadow: 'none',
- },
- '.ant-menu-item .anticon': {
- ...iconStyle,
- lineHeight: '28px', // WUT?
- },
-});
-
-const PluginIconWrapper = styled.div({
- ...iconStyle,
- display: 'inline-flex',
- alignItems: 'center',
- justifyContent: 'center',
-});
diff --git a/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx b/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx
new file mode 100644
index 000000000..e78bc7f11
--- /dev/null
+++ b/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx
@@ -0,0 +1,477 @@
+/**
+ * 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 React, {
+ memo,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import {Badge, Button, Menu, Tooltip, Typography} from 'antd';
+import {InfoIcon, SidebarTitle} from '../LeftSidebar';
+import {PlusOutlined, MinusOutlined} from '@ant-design/icons';
+import {Glyph, Layout, styled} from '../../ui';
+import {theme} from '../theme';
+import {useDispatch, useStore} from '../../utils/useStore';
+import {getPluginTitle, sortPluginsByName} from '../../utils/pluginUtils';
+import {ClientPluginDefinition, DevicePluginDefinition} from '../../plugin';
+import {selectPlugin, starPlugin} from '../../reducers/connections';
+import Client from '../../Client';
+import {State} from '../../reducers';
+import BaseDevice from '../../devices/BaseDevice';
+import {getFavoritePlugins} from '../../chrome/mainsidebar/sidebarUtils';
+import {PluginDetails} from 'flipper-plugin-lib';
+
+const {SubMenu} = Menu;
+const {Text} = Typography;
+
+export const PluginList = memo(function PluginList() {
+ const dispatch = useDispatch();
+ const connections = useStore((state) => state.connections);
+ const plugins = useStore((state) => state.plugins);
+
+ const metroDevice = useMemo(
+ () =>
+ connections?.devices?.find(
+ (device) => device.os === 'Metro' && !device.isArchived,
+ ),
+ [connections.devices],
+ );
+ const client = useMemo(
+ () => connections.clients.find((c) => c.id === connections.selectedApp),
+ [connections.clients, connections.selectedApp],
+ );
+
+ const {
+ devicePlugins,
+ metroPlugins,
+ enabledPlugins,
+ disabledPlugins,
+ unavailablePlugins,
+ } = useMemo(
+ () =>
+ computePluginLists(
+ connections.selectedDevice!,
+ metroDevice,
+ client,
+ plugins,
+ connections.userStarredPlugins,
+ ),
+ [
+ connections.selectedDevice,
+ metroDevice,
+ plugins,
+ client,
+ connections.userStarredPlugins,
+ ],
+ );
+
+ const handleAppPluginClick = useCallback(
+ (pluginId) => {
+ dispatch(
+ selectPlugin({
+ selectedPlugin: pluginId,
+ selectedApp: connections.selectedApp,
+ deepLinkPayload: null,
+ selectedDevice: connections.selectedDevice,
+ }),
+ );
+ },
+ [dispatch, connections.selectedDevice, connections.selectedApp],
+ );
+
+ const handleMetroPluginClick = useCallback(
+ (pluginId) => {
+ dispatch(
+ selectPlugin({
+ selectedPlugin: pluginId,
+ selectedApp: connections.selectedApp,
+ deepLinkPayload: null,
+ selectedDevice: metroDevice,
+ }),
+ );
+ },
+ [dispatch, metroDevice, connections.selectedApp],
+ );
+
+ const handleStarPlugin = useCallback(
+ (id: string) => {
+ dispatch(
+ starPlugin({
+ selectedApp: client!.query.app,
+ plugin: plugins.clientPlugins.get(id)!,
+ }),
+ );
+ },
+ [client, plugins.clientPlugins, dispatch],
+ );
+
+ return (
+
+ Plugins
+
+ {}}
+ defaultOpenKeys={['device', 'enabled', 'metro']}
+ mode="inline">
+
+ {devicePlugins.map((plugin) => (
+
+ ))}
+
+
+
+ {metroPlugins.map((plugin) => (
+
+ ))}
+
+
+
+ {enabledPlugins.map((plugin) => (
+ }
+ />
+ }
+ />
+ ))}
+
+
+ {disabledPlugins.map((plugin) => (
+ }
+ />
+ }
+ disabled
+ />
+ ))}
+
+
+ {unavailablePlugins.map(([plugin, reason]) => (
+ {reason}}
+ />
+ ))}
+
+
+
+
+ );
+});
+
+function getPluginTooltip(details: PluginDetails): string {
+ return `${getPluginTitle(details)} (${details.id}@${details.version}) ${
+ details.description ?? ''
+ }`;
+}
+
+function ActionButton({
+ icon,
+ onClick,
+ id,
+}: {
+ id: string;
+ icon: React.ReactElement;
+ onClick: (id: string) => void;
+}) {
+ return (
+