From 8a7323b9f81680445da973cb4fc3e9aaf14649e0 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Thu, 22 Oct 2020 09:37:26 -0700 Subject: [PATCH] Render plugin list Summary: This diff adds the rough navigation to open pugins, there are some rough egdes still, and tests will be added later, but wanted to keep diffs small, and land the feature early to get some initial dogfooding going on early. Note that we now also show all disabled plugins to help people with trouble shooting. Reviewed By: nikoant Differential Revision: D24418411 fbshipit-source-id: 1402d69efe2e52bc2c81336cfb4f4c9928ea4d80 --- desktop/app/src/plugin.tsx | 2 +- desktop/app/src/plugins/TableNativePlugin.tsx | 16 + .../sandy-chrome/appinspect/AppInspect.tsx | 249 +-------- .../sandy-chrome/appinspect/PluginList.tsx | 477 ++++++++++++++++++ desktop/app/src/utils/pluginUtils.tsx | 5 +- desktop/static/icons.json | 113 +++-- desktop/themes/light.less | 2 +- 7 files changed, 596 insertions(+), 268 deletions(-) create mode 100644 desktop/app/src/sandy-chrome/appinspect/PluginList.tsx 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 ( +