Basic plugin info view for open-source builds

Summary: Show a new view when a disabled/uninstalled plugin is selected in open-source Flipper build. I'm going to improve it later with additional info from npm packages metadata. For now it just shows status of the plugin and button to install/enable it.

Reviewed By: mweststrate

Differential Revision: D29482937

fbshipit-source-id: 45d207d3f6e846c354184f2b5fd911751d3164b0
This commit is contained in:
Anton Nikolaev
2021-06-30 04:15:30 -07:00
committed by Facebook GitHub Bot
parent 039d3a4a08
commit 7a1b2ecc73
4 changed files with 165 additions and 84 deletions

View File

@@ -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 <EnableButton plugin={activePlugin.definition} type={type} />;
}
case 'uninstalled': {
return <InstallButton plugin={activePlugin.details} type={type} />;
}
case 'unavailable': {
return type === 'primary' ? (
<UnavailabilityAlert reason={activePlugin.reason} />
) : 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 (
<Button
type={type}
icon={<PlusOutlined />}
onClick={enableOrDisablePlugin}
style={{flexGrow: type == 'primary' ? 1 : 0}}>
Enable Plugin
</Button>
);
}
function UnavailabilityAlert({reason}: {reason: string}) {
return (
<Layout.Container center>
<Alert message={reason} type="warning" />
</Layout.Container>
);
}
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 (
<Button
type={type}
disabled={!!downloadStatus}
icon={
downloadStatus ? (
<LoadingOutlined size={16} />
) : (
<DownloadOutlined size={16} />
)
}
onClick={installPlugin}
style={{
flexGrow: type === 'primary' ? 1 : 0,
}}>
Install Plugin
</Button>
);
}

View File

@@ -7,94 +7,46 @@
* @format * @format
*/ */
import React, {useCallback, useMemo} from 'react'; import React 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 {useSelector} from 'react-redux'; 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)({ const {Text, Title} = Typography;
width: '100%',
height: '100%',
flexGrow: 1,
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
});
export function PluginInfo() { export function PluginInfo() {
const pluginId = useStore((state) => state.connections.selectedPlugin); const activePlugin = useSelector(getActivePlugin);
const enabledPlugins = useStore((state) => state.connections.enabledPlugins); if (activePlugin) {
const enabledDevicePlugins = useStore( return <PluginMarketplace activePlugin={activePlugin} />;
(state) => state.connections.enabledDevicePlugins, } else {
); return null;
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 ? (
<PluginEnabler plugin={disabledPlugin} selectedApp={selectedApp} />
) : null;
} }
function PluginEnabler({ function PluginMarketplace({
plugin, activePlugin,
selectedApp,
}: { }: {
plugin: PluginDefinition; activePlugin: ActivePluginListItem;
selectedApp: string | null;
}) { }) {
const dispatch = useDispatch();
const enablePlugin = useCallback(() => {
dispatch(switchPlugin({plugin, selectedApp: selectedApp ?? undefined}));
}, [dispatch, plugin, selectedApp]);
return ( return (
<Layout.Container grow> <CenteredContainer>
<Waiting> <Layout.Container center gap style={{maxWidth: 350}}>
<Layout.Container> <CoffeeOutlined style={{fontSize: '24px'}} />
<Layout.Horizontal> <Title level={4}>
<Label Plugin '{activePlugin.details.title}' is {activePlugin.status}
style={{ </Title>
fontSize: '16px', {activePlugin.status === 'unavailable' ? (
color: theme.textColorSecondary, <Text style={{textAlign: 'center'}}>{activePlugin.reason}.</Text>
textTransform: 'uppercase', ) : null}
}}> <Layout.Horizontal gap>
{plugin.title} <PluginActions activePlugin={activePlugin} type="link" />
</Label>
</Layout.Horizontal> </Layout.Horizontal>
</Layout.Container> </Layout.Container>
<Layout.Container> </CenteredContainer>
<ToggleButton toggled={false} onClick={enablePlugin} large />
</Layout.Container>
<Layout.Container>
<SmallText>Click to enable this plugin</SmallText>
</Layout.Container>
</Waiting>
</Layout.Container>
); );
} }

View File

@@ -244,7 +244,7 @@ describe('basic getActiveDevice with metro present', () => {
unavailablePlugins: [ unavailablePlugins: [
[ [
gateKeepedPlugin, 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, unsupportedDevicePlugin.details,

View File

@@ -301,18 +301,21 @@ export function computePluginLists(
// process problematic plugins // process problematic plugins
plugins.disabledPlugins.forEach((plugin) => { 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) => { plugins.gatekeepedPlugins.forEach((plugin) => {
unavailablePlugins.push([ unavailablePlugins.push([
plugin, 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]) => { plugins.failedPlugins.forEach(([plugin, error]) => {
unavailablePlugins.push([ unavailablePlugins.push([
plugin, plugin,
`Flipper failed to load this plugin: '${error}'`, `Plugin '${plugin.title}' failed to load: '${error}'`,
]); ]);
}); });