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:
committed by
Facebook GitHub Bot
parent
039d3a4a08
commit
7a1b2ecc73
126
desktop/app/src/chrome/PluginActions.tsx
Normal file
126
desktop/app/src/chrome/PluginActions.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -7,94 +7,46 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import React, {useCallback, useMemo} 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 React from 'react';
|
||||
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)({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
});
|
||||
const {Text, Title} = Typography;
|
||||
|
||||
export function PluginInfo() {
|
||||
const pluginId = useStore((state) => state.connections.selectedPlugin);
|
||||
const enabledPlugins = useStore((state) => state.connections.enabledPlugins);
|
||||
const enabledDevicePlugins = useStore(
|
||||
(state) => state.connections.enabledDevicePlugins,
|
||||
);
|
||||
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;
|
||||
const activePlugin = useSelector(getActivePlugin);
|
||||
if (activePlugin) {
|
||||
return <PluginMarketplace activePlugin={activePlugin} />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function PluginEnabler({
|
||||
plugin,
|
||||
selectedApp,
|
||||
function PluginMarketplace({
|
||||
activePlugin,
|
||||
}: {
|
||||
plugin: PluginDefinition;
|
||||
selectedApp: string | null;
|
||||
activePlugin: ActivePluginListItem;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const enablePlugin = useCallback(() => {
|
||||
dispatch(switchPlugin({plugin, selectedApp: selectedApp ?? undefined}));
|
||||
}, [dispatch, plugin, selectedApp]);
|
||||
return (
|
||||
<Layout.Container grow>
|
||||
<Waiting>
|
||||
<Layout.Container>
|
||||
<Layout.Horizontal>
|
||||
<Label
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
color: theme.textColorSecondary,
|
||||
textTransform: 'uppercase',
|
||||
}}>
|
||||
{plugin.title}
|
||||
</Label>
|
||||
</Layout.Horizontal>
|
||||
</Layout.Container>
|
||||
<Layout.Container>
|
||||
<ToggleButton toggled={false} onClick={enablePlugin} large />
|
||||
</Layout.Container>
|
||||
<Layout.Container>
|
||||
<SmallText>Click to enable this plugin</SmallText>
|
||||
</Layout.Container>
|
||||
</Waiting>
|
||||
</Layout.Container>
|
||||
<CenteredContainer>
|
||||
<Layout.Container center gap style={{maxWidth: 350}}>
|
||||
<CoffeeOutlined style={{fontSize: '24px'}} />
|
||||
<Title level={4}>
|
||||
Plugin '{activePlugin.details.title}' is {activePlugin.status}
|
||||
</Title>
|
||||
{activePlugin.status === 'unavailable' ? (
|
||||
<Text style={{textAlign: 'center'}}>{activePlugin.reason}.</Text>
|
||||
) : null}
|
||||
<Layout.Horizontal gap>
|
||||
<PluginActions activePlugin={activePlugin} type="link" />
|
||||
</Layout.Horizontal>
|
||||
</Layout.Container>
|
||||
</CenteredContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ describe('basic getActiveDevice with metro present', () => {
|
||||
unavailablePlugins: [
|
||||
[
|
||||
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,
|
||||
|
||||
@@ -301,18 +301,21 @@ export function computePluginLists(
|
||||
|
||||
// process problematic plugins
|
||||
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) => {
|
||||
unavailablePlugins.push([
|
||||
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]) => {
|
||||
unavailablePlugins.push([
|
||||
plugin,
|
||||
`Flipper failed to load this plugin: '${error}'`,
|
||||
`Plugin '${plugin.title}' failed to load: '${error}'`,
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user