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
This commit is contained in:
committed by
Facebook GitHub Bot
parent
2c6c7fb46c
commit
8a7323b9f8
@@ -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;
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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 (
|
||||
<LeftSidebar>
|
||||
<Layout.Top scrollable>
|
||||
<Layout.Top>
|
||||
<Layout.Container borderBottom>
|
||||
<SidebarTitle actions={<InfoIcon>{appTooltip}</InfoIcon>}>
|
||||
App Inspect
|
||||
@@ -68,226 +58,15 @@ export function AppInspect() {
|
||||
</Layout.Vertical>
|
||||
</Layout.Container>
|
||||
<Layout.Container padv={theme.space.large}>
|
||||
<Layout.ScrollContainer vertical>
|
||||
{selectedDevice ? (
|
||||
<PluginList />
|
||||
) : (
|
||||
<Alert message="No device or app selected" type="info" />
|
||||
)}
|
||||
</Layout.ScrollContainer>
|
||||
</Layout.Container>
|
||||
</Layout.Top>
|
||||
</LeftSidebar>
|
||||
);
|
||||
}
|
||||
|
||||
function PluginList() {
|
||||
// const {selectedApp, selectedDevice} = useStore((state) => state.connections);
|
||||
|
||||
return (
|
||||
<Layout.Container>
|
||||
<SidebarTitle>Plugins</SidebarTitle>
|
||||
<Layout.Container padv={theme.space.small} padh={theme.space.tiny}>
|
||||
<PluginMenu
|
||||
inlineIndent={8}
|
||||
onClick={() => {}}
|
||||
defaultOpenKeys={['device']}
|
||||
mode="inline">
|
||||
<DevicePlugins key="device" />
|
||||
<SubMenu
|
||||
key="sub1"
|
||||
title={
|
||||
<Layout.Right center>
|
||||
<Text strong>Header</Text>
|
||||
<Badge count={8}></Badge>
|
||||
</Layout.Right>
|
||||
}>
|
||||
<Menu.Item key="1" icon={<AppstoreOutlined />}>
|
||||
Option 1
|
||||
</Menu.Item>
|
||||
<Menu.Item key="2" icon={<AppstoreOutlined />}>
|
||||
Option 2
|
||||
</Menu.Item>
|
||||
<Menu.Item key="3">Option 3</Menu.Item>
|
||||
<Menu.Item key="4">Option 4</Menu.Item>
|
||||
</SubMenu>
|
||||
<SubMenu
|
||||
key="sub2"
|
||||
icon={<AppstoreOutlined />}
|
||||
title="Navigation Two">
|
||||
<Menu.Item key="5">Option 5</Menu.Item>
|
||||
<Menu.Item key="6">Option 6</Menu.Item>
|
||||
<Menu.Item key="7">Option 7</Menu.Item>
|
||||
<Menu.Item key="8">Option 8</Menu.Item>
|
||||
</SubMenu>
|
||||
<SubMenu
|
||||
key="sub4"
|
||||
title={
|
||||
<span>
|
||||
<SettingOutlined />
|
||||
<span>Navigation Three</span>
|
||||
</span>
|
||||
}>
|
||||
<Menu.Item key="9">Option 9</Menu.Item>
|
||||
<Menu.Item key="10">Option 10</Menu.Item>
|
||||
<Menu.Item key="11">Option 11</Menu.Item>
|
||||
<Menu.Item key="12">Option 12</Menu.Item>
|
||||
</SubMenu>
|
||||
</PluginMenu>
|
||||
</Layout.Container>
|
||||
</Layout.Container>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<SubMenu {...props} title={<Text strong>Device</Text>}>
|
||||
{selectedDevice!.devicePlugins
|
||||
.map((pluginName) => devicePlugins.get(pluginName)!)
|
||||
.sort(sortPluginsByName)
|
||||
.map((plugin) => (
|
||||
<Plugin
|
||||
key={plugin.id}
|
||||
isActive={plugin.id === selectedPlugin}
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
selectPlugin({
|
||||
selectedPlugin: plugin.id,
|
||||
selectedApp: null,
|
||||
deepLinkPayload: null,
|
||||
selectedDevice,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
plugin={plugin}
|
||||
/>
|
||||
))}
|
||||
</SubMenu>
|
||||
);
|
||||
}
|
||||
|
||||
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<HTMLDivElement>(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 (
|
||||
<Menu.Item
|
||||
{...rest}
|
||||
active={isActive}
|
||||
onClick={props.onClick}
|
||||
disabled={starred === false}>
|
||||
<Tooltip
|
||||
placement="right"
|
||||
title={
|
||||
<>
|
||||
{getPluginTitle(plugin)} ({plugin.version})
|
||||
{plugin.details?.description ? (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
{plugin.details?.description}
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
}
|
||||
mouseEnterDelay={1}>
|
||||
<Layout.Horizontal center gap={10} ref={domRef}>
|
||||
<PluginIconWrapper>
|
||||
<Glyph size={16} name={plugin.icon || 'apps'} color="white" />
|
||||
</PluginIconWrapper>
|
||||
{getPluginTitle(plugin)}
|
||||
</Layout.Horizontal>
|
||||
</Tooltip>
|
||||
{/* {starred !== undefined && (!starred || isActive) && (
|
||||
<ToggleButton
|
||||
onClick={onFavorite}
|
||||
toggled={starred}
|
||||
tooltip="Click to enable / disable this plugin"
|
||||
/>
|
||||
)} */}
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
477
desktop/app/src/sandy-chrome/appinspect/PluginList.tsx
Normal file
477
desktop/app/src/sandy-chrome/appinspect/PluginList.tsx
Normal file
@@ -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<BaseDevice | undefined>(
|
||||
() =>
|
||||
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 (
|
||||
<Layout.Container>
|
||||
<SidebarTitle>Plugins</SidebarTitle>
|
||||
<Layout.Container padv={theme.space.small} padh={theme.space.tiny}>
|
||||
<PluginMenu
|
||||
inlineIndent={8}
|
||||
onClick={() => {}}
|
||||
defaultOpenKeys={['device', 'enabled', 'metro']}
|
||||
mode="inline">
|
||||
<PluginGroup key="device" title="Device">
|
||||
{devicePlugins.map((plugin) => (
|
||||
<PluginEntry
|
||||
key={plugin.id}
|
||||
plugin={plugin.details}
|
||||
active={plugin.id === connections.selectedPlugin}
|
||||
onClick={handleAppPluginClick}
|
||||
tooltip={getPluginTooltip(plugin.details)}
|
||||
/>
|
||||
))}
|
||||
</PluginGroup>
|
||||
|
||||
<PluginGroup key="metro" title="React Native">
|
||||
{metroPlugins.map((plugin) => (
|
||||
<PluginEntry
|
||||
key={plugin.id}
|
||||
plugin={plugin.details}
|
||||
active={plugin.id === connections.selectedPlugin}
|
||||
onClick={handleMetroPluginClick}
|
||||
tooltip={getPluginTooltip(plugin.details)}
|
||||
/>
|
||||
))}
|
||||
</PluginGroup>
|
||||
|
||||
<PluginGroup key="enabled" title="Enabled">
|
||||
{enabledPlugins.map((plugin) => (
|
||||
<PluginEntry
|
||||
key={plugin.id}
|
||||
plugin={plugin.details}
|
||||
active={plugin.id === connections.selectedPlugin}
|
||||
onClick={handleAppPluginClick}
|
||||
tooltip={getPluginTooltip(plugin.details)}
|
||||
actions={
|
||||
<ActionButton
|
||||
id={plugin.id}
|
||||
onClick={handleStarPlugin}
|
||||
icon={<MinusOutlined size={16} style={{marginRight: 0}} />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</PluginGroup>
|
||||
<PluginGroup key="disabled" title="Disabled">
|
||||
{disabledPlugins.map((plugin) => (
|
||||
<PluginEntry
|
||||
key={plugin.id}
|
||||
plugin={plugin.details}
|
||||
active={plugin.id === connections.selectedPlugin}
|
||||
tooltip={getPluginTooltip(plugin.details)}
|
||||
actions={
|
||||
<ActionButton
|
||||
id={plugin.id}
|
||||
onClick={handleStarPlugin}
|
||||
icon={<PlusOutlined size={16} style={{marginRight: 0}} />}
|
||||
/>
|
||||
}
|
||||
disabled
|
||||
/>
|
||||
))}
|
||||
</PluginGroup>
|
||||
<PluginGroup key="unavailable" title="Unavailable plugins">
|
||||
{unavailablePlugins.map(([plugin, reason]) => (
|
||||
<PluginEntry
|
||||
key={plugin.id}
|
||||
plugin={plugin}
|
||||
tooltip={`${getPluginTitle(plugin)} (${plugin.id}@${
|
||||
plugin.version
|
||||
}): ${reason}`}
|
||||
disabled
|
||||
actions={<InfoIcon>{reason}</InfoIcon>}
|
||||
/>
|
||||
))}
|
||||
</PluginGroup>
|
||||
</PluginMenu>
|
||||
</Layout.Container>
|
||||
</Layout.Container>
|
||||
);
|
||||
});
|
||||
|
||||
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 (
|
||||
<Button
|
||||
size="small"
|
||||
icon={icon}
|
||||
style={{border: 'none', color: theme.textColorPrimary}}
|
||||
onClick={() => {
|
||||
onClick(id);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const PluginEntry = memo(function PluginEntry({
|
||||
plugin,
|
||||
disabled,
|
||||
tooltip,
|
||||
onClick,
|
||||
active,
|
||||
actions,
|
||||
...rest
|
||||
}: {
|
||||
plugin: {id: string; title: string; icon?: string; version: string};
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
tooltip: string;
|
||||
onClick?: (id: string) => void;
|
||||
actions?: React.ReactElement;
|
||||
}) {
|
||||
const [hovering, setHovering] = useState(false);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
setHovering(true);
|
||||
}, []);
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setHovering(false);
|
||||
}, []);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onClick?.(plugin.id);
|
||||
}, [onClick, plugin.id]);
|
||||
|
||||
const domRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const node = domRef.current;
|
||||
if (active && node) {
|
||||
const rect = node.getBoundingClientRect();
|
||||
if (rect.top < 0 || rect.bottom > document.documentElement.clientHeight) {
|
||||
node.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
key={plugin.id}
|
||||
active={active}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
{...rest}>
|
||||
<Layout.Horizontal
|
||||
center
|
||||
gap={10}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}>
|
||||
<PluginIconWrapper disabled={disabled} ref={domRef}>
|
||||
<Glyph size={16} name={plugin.icon || 'apps'} color="white" />
|
||||
</PluginIconWrapper>
|
||||
<Tooltip placement="right" title={tooltip} mouseEnterDelay={1}>
|
||||
<Text style={{flex: 1}}>{getPluginTitle(plugin)}</Text>
|
||||
</Tooltip>
|
||||
{hovering && actions}
|
||||
</Layout.Horizontal>
|
||||
</Menu.Item>
|
||||
);
|
||||
});
|
||||
|
||||
const PluginGroup = memo(function PluginGroup({
|
||||
title,
|
||||
children,
|
||||
...rest
|
||||
}: {title: string; children: React.ReactElement[]} & Record<string, any>) {
|
||||
if (children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SubMenu
|
||||
{...rest}
|
||||
title={
|
||||
<Layout.Right center>
|
||||
<Text strong>{title}</Text>
|
||||
<Badge
|
||||
count={children.length}
|
||||
style={{
|
||||
marginRight: 20,
|
||||
}}
|
||||
/>
|
||||
</Layout.Right>
|
||||
}>
|
||||
{children}
|
||||
</SubMenu>
|
||||
);
|
||||
});
|
||||
|
||||
function computePluginLists(
|
||||
device: BaseDevice,
|
||||
metroDevice: BaseDevice | undefined,
|
||||
client: Client | undefined,
|
||||
plugins: State['plugins'],
|
||||
userStarredPlugins: State['connections']['userStarredPlugins'],
|
||||
) {
|
||||
const devicePlugins: DevicePluginDefinition[] = device.devicePlugins.map(
|
||||
(name) => plugins.devicePlugins.get(name)!,
|
||||
);
|
||||
const metroPlugins: DevicePluginDefinition[] =
|
||||
metroDevice?.devicePlugins.map(
|
||||
(name) => plugins.devicePlugins.get(name)!,
|
||||
) ?? [];
|
||||
const enabledPlugins: ClientPluginDefinition[] = [];
|
||||
const disabledPlugins: ClientPluginDefinition[] = [];
|
||||
const unavailablePlugins: [plugin: PluginDetails, reason: string][] = [];
|
||||
|
||||
{
|
||||
// find all device plugins that aren't part of the current device / metro
|
||||
const detectedDevicePlugins = new Set([
|
||||
...device.devicePlugins,
|
||||
...(metroDevice?.devicePlugins ?? []),
|
||||
]);
|
||||
for (const [name, definition] of plugins.devicePlugins.entries()) {
|
||||
if (!detectedDevicePlugins.has(name)) {
|
||||
unavailablePlugins.push([
|
||||
definition.details,
|
||||
`Device plugin '${getPluginTitle(
|
||||
definition.details,
|
||||
)}' is not supported by the current device type.`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process all client plugins
|
||||
if (client) {
|
||||
const clientPlugins = Array.from(plugins.clientPlugins.values()).sort(
|
||||
sortPluginsByName,
|
||||
);
|
||||
const favoritePlugins = getFavoritePlugins(
|
||||
device,
|
||||
client,
|
||||
clientPlugins,
|
||||
client && userStarredPlugins[client.query.app],
|
||||
true,
|
||||
);
|
||||
|
||||
client &&
|
||||
clientPlugins.forEach((plugin) => {
|
||||
if (!client.plugins.includes(plugin.id)) {
|
||||
unavailablePlugins.push([
|
||||
plugin.details,
|
||||
`Plugin '${getPluginTitle(
|
||||
plugin.details,
|
||||
)}' is not loaded by the client application`,
|
||||
]);
|
||||
} else if (favoritePlugins.includes(plugin)) {
|
||||
enabledPlugins.push(plugin);
|
||||
} else {
|
||||
disabledPlugins.push(plugin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// process problematic plugins
|
||||
plugins.disabledPlugins.forEach((plugin) => {
|
||||
unavailablePlugins.push([plugin, 'Plugin is disabled by configuration']);
|
||||
});
|
||||
plugins.gatekeepedPlugins.forEach((plugin) => {
|
||||
unavailablePlugins.push([
|
||||
plugin,
|
||||
`This plugin is only available to members of gatekeeper '${plugin.gatekeeper}'`,
|
||||
]);
|
||||
});
|
||||
plugins.failedPlugins.forEach(([plugin, error]) => {
|
||||
unavailablePlugins.push([
|
||||
plugin,
|
||||
`Flipper failed to load this plugin: '${error}'`,
|
||||
]);
|
||||
});
|
||||
|
||||
devicePlugins.sort(sortPluginsByName);
|
||||
metroPlugins.sort(sortPluginsByName);
|
||||
unavailablePlugins.sort(([a], [b]) => {
|
||||
return getPluginTitle(a) > getPluginTitle(b) ? 1 : -1;
|
||||
});
|
||||
|
||||
return {
|
||||
devicePlugins,
|
||||
metroPlugins,
|
||||
enabledPlugins,
|
||||
disabledPlugins,
|
||||
unavailablePlugins,
|
||||
};
|
||||
}
|
||||
|
||||
// Dimensions are hardcoded as they correlate strongly
|
||||
const PluginMenu = styled(Menu)({
|
||||
userSelect: 'none',
|
||||
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 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-item-selected .ant-typography': {
|
||||
color: theme.white,
|
||||
},
|
||||
},
|
||||
'.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',
|
||||
},
|
||||
});
|
||||
|
||||
const PluginIconWrapper = styled.div<{disabled?: boolean}>(({disabled}) => ({
|
||||
...iconStyle(!!disabled),
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}));
|
||||
|
||||
function iconStyle(disabled: boolean) {
|
||||
return {
|
||||
color: theme.white,
|
||||
background: disabled ? theme.disabledColor : theme.primaryColor,
|
||||
borderRadius: theme.borderRadius,
|
||||
width: 24,
|
||||
height: 24,
|
||||
};
|
||||
}
|
||||
@@ -211,7 +211,10 @@ export function getPersistentPlugins(plugins: PluginsState): Array<string> {
|
||||
});
|
||||
}
|
||||
|
||||
export function getPluginTitle(pluginClass: PluginDefinition) {
|
||||
export function getPluginTitle(pluginClass: {
|
||||
title?: string | null;
|
||||
id: string;
|
||||
}) {
|
||||
return pluginClass.title || pluginClass.id;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
12
|
||||
],
|
||||
"bird": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"borders": [
|
||||
16
|
||||
@@ -37,13 +38,15 @@
|
||||
12
|
||||
],
|
||||
"brush-paint": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"bug": [
|
||||
12
|
||||
],
|
||||
"building-city": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"camcorder": [
|
||||
12,
|
||||
@@ -111,7 +114,8 @@
|
||||
24
|
||||
],
|
||||
"dashboard": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"data-table": [
|
||||
16
|
||||
@@ -120,7 +124,8 @@
|
||||
12
|
||||
],
|
||||
"directions": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"dots-3-circle-outline": [
|
||||
16
|
||||
@@ -135,7 +140,8 @@
|
||||
12
|
||||
],
|
||||
"flash-default": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"info-circle": [
|
||||
12,
|
||||
@@ -143,13 +149,15 @@
|
||||
24
|
||||
],
|
||||
"internet": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"life-event-major": [
|
||||
16
|
||||
],
|
||||
"magic-wand": [
|
||||
12,
|
||||
16,
|
||||
20
|
||||
],
|
||||
"magnifying-glass": [
|
||||
@@ -158,7 +166,8 @@
|
||||
24
|
||||
],
|
||||
"messages": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"minus-circle": [
|
||||
12
|
||||
@@ -172,10 +181,12 @@
|
||||
32
|
||||
],
|
||||
"network": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"news-feed": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"pause": [
|
||||
16
|
||||
@@ -187,7 +198,8 @@
|
||||
16
|
||||
],
|
||||
"profile": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"question-circle-outline": [
|
||||
16
|
||||
@@ -205,6 +217,7 @@
|
||||
],
|
||||
"rocket": [
|
||||
12,
|
||||
16,
|
||||
20
|
||||
],
|
||||
"settings": [
|
||||
@@ -241,7 +254,8 @@
|
||||
16
|
||||
],
|
||||
"thought-bubble": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"tools": [
|
||||
12,
|
||||
@@ -249,7 +263,8 @@
|
||||
20
|
||||
],
|
||||
"translate": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"trash-outline": [
|
||||
16
|
||||
@@ -259,10 +274,12 @@
|
||||
16
|
||||
],
|
||||
"tree": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"trending": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"triangle-down": [
|
||||
12,
|
||||
@@ -273,13 +290,16 @@
|
||||
12
|
||||
],
|
||||
"underline": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"washing-machine": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"watch-tv": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"gears-two": [
|
||||
16
|
||||
@@ -315,25 +335,30 @@
|
||||
16
|
||||
],
|
||||
"messenger-code": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"book": [
|
||||
12
|
||||
],
|
||||
"list-arrow-up": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"cat": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"duplicate": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"profile-circle-outline": [
|
||||
16
|
||||
],
|
||||
"card-person": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"pencil-outline": [
|
||||
16
|
||||
@@ -366,10 +391,12 @@
|
||||
16
|
||||
],
|
||||
"paper-stack": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"weather-cold": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"mobile-cross": [
|
||||
16
|
||||
@@ -390,13 +417,15 @@
|
||||
16
|
||||
],
|
||||
"marketplace": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"workflow": [
|
||||
12
|
||||
],
|
||||
"sankey-diagram": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"media-stack": [
|
||||
16
|
||||
@@ -411,7 +440,8 @@
|
||||
16
|
||||
],
|
||||
"log": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"triangle-up": [
|
||||
16,
|
||||
@@ -434,10 +464,12 @@
|
||||
16
|
||||
],
|
||||
"scuba": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"line-chart": [
|
||||
12
|
||||
12,
|
||||
16
|
||||
],
|
||||
"caution-circle": [
|
||||
12
|
||||
@@ -459,5 +491,26 @@
|
||||
],
|
||||
"sushi": [
|
||||
12
|
||||
],
|
||||
"arrows-up-down": [
|
||||
16
|
||||
],
|
||||
"style-effects": [
|
||||
16
|
||||
],
|
||||
"stopwatch": [
|
||||
16
|
||||
],
|
||||
"database": [
|
||||
16
|
||||
],
|
||||
"bar-chart": [
|
||||
16
|
||||
],
|
||||
"augmented-reality": [
|
||||
16
|
||||
],
|
||||
"app-flash": [
|
||||
16
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
// Based on: https://www.figma.com/file/4e6BMdm2SuZ1L7FSuOPQVC/Flipper?node-id=620%3A84614
|
||||
|
||||
// Link Text & Icon
|
||||
@primary-color: #722ED1;
|
||||
@primary-color: @purple-6;
|
||||
// Success
|
||||
@success-color: @green-7;
|
||||
// Negative
|
||||
|
||||
Reference in New Issue
Block a user