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 ( +