PluginManager

Summary: Adding the plugin installer to the plugin sheet as a second tab

Reviewed By: passy

Differential Revision: D17450842

fbshipit-source-id: 211c9f15ed2614a1dd46d974b86f50c825f81fb0
This commit is contained in:
Daniel Büchele
2019-09-19 02:31:33 -07:00
committed by Facebook Github Bot
parent 039d1cca99
commit 8c623867bd
7 changed files with 65 additions and 464 deletions

View File

@@ -19,24 +19,22 @@ import ShareSheetExportFile from './chrome/ShareSheetExportFile';
import PluginContainer from './PluginContainer';
import Sheet from './chrome/Sheet';
import {ipcRenderer, remote} from 'electron';
import PluginDebugger from './chrome/PluginDebugger';
import {
ActiveSheet,
ShareType,
ACTIVE_SHEET_BUG_REPORTER,
ACTIVE_SHEET_PLUGIN_DEBUGGER,
ACTIVE_SHEET_PLUGINS,
ACTIVE_SHEET_SHARE_DATA,
ACTIVE_SHEET_SIGN_IN,
ACTIVE_SHEET_SHARE_DATA_IN_FILE,
ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT,
ACTIVE_SHEET_PLUGIN_SHEET,
ACTIVE_SHEET_PLUGIN_INSTALLER,
} from './reducers/application';
import {Logger} from './fb-interfaces/Logger';
import BugReporter from './fb-stubs/BugReporter';
import {State as Store} from './reducers/index';
import {StaticView} from './reducers/connections';
import PluginInstaller from './chrome/PluginInstaller';
import PluginManager from './chrome/PluginManager';
const version = remote.app.getVersion();
type OwnProps = {
@@ -80,16 +78,14 @@ export class App extends React.Component<Props> {
onHide={onHide}
/>
);
case ACTIVE_SHEET_PLUGIN_DEBUGGER:
return <PluginDebugger onHide={onHide} />;
case ACTIVE_SHEET_PLUGINS:
return <PluginManager onHide={onHide} />;
case ACTIVE_SHEET_SHARE_DATA:
return <ShareSheet onHide={onHide} logger={this.props.logger} />;
case ACTIVE_SHEET_SIGN_IN:
return <SignInSheet onHide={onHide} />;
case ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT:
return <ExportDataPluginSheet onHide={onHide} />;
case ACTIVE_SHEET_PLUGIN_INSTALLER:
return <PluginInstaller onHide={onHide} />;
case ACTIVE_SHEET_SHARE_DATA_IN_FILE:
return (
<ShareSheetExportFile

View File

@@ -11,7 +11,7 @@ import Client from '../Client';
import {UninitializedClient} from '../UninitializedClient';
import {FlipperBasePlugin} from '../plugin';
import {PluginNotification} from '../reducers/notifications';
import {ActiveSheet} from '../reducers/application';
import {ActiveSheet, ACTIVE_SHEET_PLUGINS} from '../reducers/application';
import {State as Store} from '../reducers';
import {
@@ -392,7 +392,7 @@ class MainSidebar extends PureComponent<Props> {
))}
</Plugins>
<PluginDebugger
onClick={() => this.props.setActiveSheet('PLUGIN_DEBUGGER')}>
onClick={() => this.props.setActiveSheet(ACTIVE_SHEET_PLUGINS)}>
<Glyph
name="question-circle"
size={16}

View File

@@ -11,8 +11,6 @@ import {TableBodyRow} from '../ui/components/table/types';
import React, {Component, Fragment} from 'react';
import {connect} from 'react-redux';
import {
FlexColumn,
Button,
Text,
ManagedTable,
styled,
@@ -24,38 +22,22 @@ import {
import StatusIndicator from '../ui/components/StatusIndicator';
import {State as Store} from '../reducers';
const Container = styled(FlexColumn)({
padding: 10,
width: 700,
});
const InfoText = styled(Text)({
lineHeight: '130%',
marginBottom: 8,
});
const Title = styled('div')({
fontWeight: 500,
marginBottom: 10,
marginTop: 8,
});
const Ellipsis = styled(Text)({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
const Row = styled(FlexColumn)({
alignItems: 'flex-end',
});
const TableContainer = styled('div')({
borderRadius: 4,
overflow: 'hidden',
border: `1px solid ${colors.macOSTitleBarButtonBorder}`,
marginTop: 10,
marginBottom: 10,
backgroundColor: colors.white,
height: 400,
display: 'flex',
@@ -77,9 +59,7 @@ type StateFromProps = {
type DispatchFromProps = {};
type OwnProps = {
onHide: () => any;
};
type OwnProps = {};
const COLUMNS = {
lamp: {
@@ -304,17 +284,7 @@ class PluginDebugger extends Component<Props> {
</Fragment>
);
}
return (
<Container>
<Title>Plugin Status</Title>
{content}
<Row>
<Button compact padded onClick={this.props.onHide}>
Close
</Button>
</Row>
</Container>
);
return content;
}
}

View File

@@ -41,12 +41,6 @@ type PluginDefinition = {
description: string;
};
const Container = styled(FlexColumn)({
width: 600,
height: 400,
background: colors.white,
});
const EllipsisText = styled(Text)({
overflow: 'hidden',
textOverflow: 'ellipsis',
@@ -75,6 +69,14 @@ const columns = {
},
};
const Container = styled(FlexColumn)({
height: 300,
backgroundColor: colors.white,
borderRadius: 4,
overflow: 'hidden',
border: `1px solid ${colors.macOSTitleBarButtonBorder}`,
});
const RestartBar = styled(FlexColumn)({
backgroundColor: colors.red,
color: colors.white,
@@ -84,7 +86,7 @@ const RestartBar = styled(FlexColumn)({
textAlign: 'center',
});
export default function(props: {onHide: () => any}) {
export default function() {
const [restartRequired, setRestartRequired] = useState(false);
const [query, setQuery] = useState('');
const rows = useNPMSearch(setRestartRequired, query, setQuery);
@@ -120,10 +122,6 @@ export default function(props: {onHide: () => any}) {
highlightedRows={new Set()}
rows={rows}
/>
<Toolbar position="bottom">
<Spacer />
<Button onClick={props.onHide}>Close</Button>
</Toolbar>
</Container>
);
}
@@ -210,7 +208,7 @@ function useNPMSearch(
(h: PluginDefinition) => ({
key: h.name,
columns: {
name: {value: <EllipsisText bold>{h.name}</EllipsisText>},
name: {value: <EllipsisText>{h.name}</EllipsisText>},
version: {
value: <EllipsisText>{h.version}</EllipsisText>,
align: 'flex-end' as 'flex-end',

View File

@@ -1,404 +0,0 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import {
PureComponent,
Button,
FlexColumn,
FlexBox,
Text,
LoadingIndicator,
ButtonGroup,
colors,
Glyph,
FlexRow,
styled,
Searchable,
} from 'flipper';
const {spawn} = require('child_process');
const path = require('path');
const {app, shell} = require('electron').remote;
const FLIPPER_PLUGIN_PATH = path.join(app.getPath('home'), '.flipper');
const DYNAMIC_PLUGINS = JSON.parse(window.process.env.PLUGINS || '[]');
type NPMModule = {
name: string,
version: string,
description?: string,
error?: Object,
};
type Status =
| 'installed'
| 'outdated'
| 'install'
| 'remove'
| 'update'
| 'uninstalled'
| 'uptodate';
type PluginT = {
name: string,
version?: string,
description?: string,
status: Status,
managed?: boolean,
entry?: string,
rootDir?: string,
};
type Props = {
searchTerm: string,
};
type State = {
plugins: {
[name: string]: PluginT,
},
restartRequired: boolean,
searchCompleted: boolean,
};
const Container = styled(FlexBox)({
width: '100%',
flexGrow: 1,
background: colors.light02,
overflowY: 'scroll',
});
const Title = styled(Text)({
fontWeight: 500,
});
const Plugin = styled(FlexColumn)({
backgroundColor: colors.white,
borderRadius: 4,
padding: 15,
margin: '0 15px 25px',
boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
});
const SectionTitle = styled('span')({
fontWeight: 'bold',
fontSize: 24,
margin: 15,
marginLeft: 20,
});
const Loading = styled(FlexBox)({
padding: 50,
alignItems: 'center',
justifyContent: 'center',
});
const RestartRequired = styled(FlexBox)({
textAlign: 'center',
justifyContent: 'center',
fontWeight: 500,
color: colors.white,
padding: 12,
backgroundColor: colors.green,
cursor: 'pointer',
});
const TitleRow = styled(FlexRow)({
alignItems: 'center',
marginBottom: 10,
fontSize: '1.1em',
});
const Description = styled(FlexRow)({
marginBottom: 15,
lineHeight: '130%',
});
const PluginGlyph = styled(Glyph)({
marginRight: 5,
});
const PluginLoading = styled(LoadingIndicator)({
marginLeft: 5,
marginTop: 5,
});
const getLatestVersion = (name: string): Promise<NPMModule> => {
return fetch(`http://registry.npmjs.org/${name}/latest`).then(res =>
res.json(),
);
};
const getPluginList = (): Promise<Array<NPMModule>> => {
return fetch(
'http://registry.npmjs.org/-/v1/search?text=keywords:flipper&size=250',
)
.then(res => res.json())
.then(res => res.objects.map(o => o.package));
};
const sortByName = (a: PluginT, b: PluginT): 1 | -1 =>
a.name > b.name ? 1 : -1;
const INSTALLED = ['installed', 'outdated', 'uptodate'];
class PluginItem extends PureComponent<
{
plugin: PluginT,
onChangeState: (action: Status) => void,
},
{
working: boolean,
},
> {
state = {
working: false,
};
npmAction = (action: Status) => {
const {name, status: initialStatus} = this.props.plugin;
this.setState({working: true});
const npm = spawn('npm', [action, name], {
cwd: FLIPPER_PLUGIN_PATH,
});
npm.stderr.on('data', e => {
console.error(e.toString());
});
npm.on('close', code => {
this.setState({working: false});
const newStatus = action === 'remove' ? 'uninstalled' : 'uptodate';
this.props.onChangeState(code !== 0 ? initialStatus : newStatus);
});
};
render() {
const {
entry,
status,
version,
description,
managed,
name,
rootDir,
} = this.props.plugin;
return (
<Plugin>
<TitleRow>
<PluginGlyph
name="apps"
size={24}
variant="outline"
color={colors.light30}
/>
<Title>{name}</Title>
&nbsp;
<Text code={true}>{version}</Text>
</TitleRow>
{description && <Description>{description}</Description>}
<FlexRow>
{managed ? (
<Text size="0.9em" color={colors.light30}>
This plugin is not managed by Flipper, but loaded from{' '}
<Text size="1em" code={true}>
{rootDir}
</Text>
</Text>
) : (
<ButtonGroup>
{status === 'outdated' && (
<Button
disabled={this.state.working}
onClick={() => this.npmAction('update')}>
Update
</Button>
)}
{INSTALLED.includes(status) ? (
<Button
disabled={this.state.working}
title={
managed === true && entry != null
? `This plugin is dynamically loaded from ${entry}`
: undefined
}
onClick={() => this.npmAction('remove')}>
Remove
</Button>
) : (
<Button
disabled={this.state.working}
onClick={() => this.npmAction('install')}>
Install
</Button>
)}
<Button
onClick={() =>
shell.openExternal(`https://www.npmjs.com/package/${name}`)
}>
Info
</Button>
</ButtonGroup>
)}
{this.state.working && <PluginLoading size={18} />}
</FlexRow>
</Plugin>
);
}
}
class PluginManager extends PureComponent<Props, State> {
state = {
plugins: DYNAMIC_PLUGINS.reduce((acc, plugin) => {
acc[plugin.name] = {
...plugin,
managed: !(plugin.entry, '').startsWith(FLIPPER_PLUGIN_PATH),
status: 'installed',
};
return acc;
}, {}),
restartRequired: false,
searchCompleted: false,
};
componentDidMount() {
Promise.all(
Object.keys(this.state.plugins)
.filter(name => this.state.plugins[name].managed)
.map(getLatestVersion),
).then((res: Array<NPMModule>) => {
const updates = {};
res.forEach(plugin => {
if (
plugin.error == null &&
this.state.plugins[plugin.name].version !== plugin.version
) {
updates[plugin.name] = {
...plugin,
...this.state.plugins[plugin.name],
status: 'outdated',
};
}
});
this.setState({
plugins: {
...this.state.plugins,
...updates,
},
});
});
getPluginList().then(pluginList => {
const plugins = {...this.state.plugins};
pluginList.forEach(plugin => {
if (plugins[plugin.name] != null) {
plugins[plugin.name] = {
...plugin,
...plugins[plugin.name],
status:
plugin.version === plugins[plugin.name].version
? 'uptodate'
: 'outdated',
};
} else {
plugins[plugin.name] = {
...plugin,
status: 'uninstalled',
};
}
});
this.setState({
plugins,
searchCompleted: true,
});
});
}
onChangePluginState = (name: string, status: Status) => {
this.setState({
plugins: {
...this.state.plugins,
[name]: {
...this.state.plugins[name],
status,
},
},
restartRequired: true,
});
};
relaunch() {
app.relaunch();
app.exit(0);
}
render() {
// $FlowFixMe
const plugins: Array<PluginT> = Object.values(this.state.plugins);
const availablePlugins = plugins.filter(
({status}) => !INSTALLED.includes(status),
);
return (
<Container>
<FlexColumn grow={true}>
{this.state.restartRequired && (
<RestartRequired onClick={this.relaunch}>
<Glyph name="arrows-circle" size={12} color={colors.white} />
&nbsp; Restart Required: Click to Restart
</RestartRequired>
)}
<SectionTitle>Installed Plugins</SectionTitle>
{plugins
.filter(
({status, name}) =>
INSTALLED.includes(status) &&
name.indexOf(this.props.searchTerm) > -1,
)
.sort(sortByName)
.map((plugin: PluginT) => (
<PluginItem
plugin={plugin}
key={plugin.name}
onChangeState={action =>
this.onChangePluginState(plugin.name, action)
}
/>
))}
<SectionTitle>Available Plugins</SectionTitle>
{availablePlugins
.filter(({name}) => name.indexOf(this.props.searchTerm) > -1)
.sort(sortByName)
.map((plugin: PluginT) => (
<PluginItem
plugin={plugin}
key={plugin.name}
onChangeState={action =>
this.onChangePluginState(plugin.name, action)
}
/>
))}
{!this.state.searchCompleted && (
<Loading>
<LoadingIndicator size={32} />
</Loading>
)}
</FlexColumn>
</Container>
);
}
}
const SearchablePluginManager = Searchable(PluginManager);
export default class extends PureComponent<{}> {
render() {
return (
<FlexColumn grow={true}>
<SearchablePluginManager />
</FlexColumn>
);
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright 2018-present Facebook.
* 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, {useState} from 'react';
import {FlexColumn, Button, styled, Tab, Tabs, TabsContainer} from 'flipper';
import PluginDebugger from './PluginDebugger';
import PluginInstaller from './PluginInstaller';
const Container = styled(FlexColumn)({
padding: 15,
width: 700,
});
const Row = styled(FlexColumn)({
alignItems: 'flex-end',
});
type Tabs = 'Plugin Status' | 'Install Plugins';
export default function(props: {onHide: () => any}) {
const [tab, setTab] = useState<Tabs>('Plugin Status');
return (
<Container>
<TabsContainer>
<Tabs
active={tab}
onActive={setTab as (s: string | null | undefined) => void}>
<Tab label="Plugin Status" />
<Tab label="Install Plugins" />
</Tabs>
{tab === 'Plugin Status' && <PluginDebugger />}
{tab === 'Install Plugins' && <PluginInstaller />}
</TabsContainer>
<Row>
<Button compact padded onClick={props.onHide}>
Close
</Button>
</Row>
</Container>
);
}

View File

@@ -12,8 +12,7 @@ import CancellableExportStatus from '../chrome/CancellableExportStatus';
import {Actions} from './';
export const ACTIVE_SHEET_PLUGIN_SHEET: 'PLUGIN_SHEET' = 'PLUGIN_SHEET';
export const ACTIVE_SHEET_BUG_REPORTER: 'BUG_REPORTER' = 'BUG_REPORTER';
export const ACTIVE_SHEET_PLUGIN_DEBUGGER: 'PLUGIN_DEBUGGER' =
'PLUGIN_DEBUGGER';
export const ACTIVE_SHEET_PLUGINS: 'PLUGINS' = 'PLUGINS';
export const ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT: 'SELECT_PLUGINS_TO_EXPORT' =
'SELECT_PLUGINS_TO_EXPORT';
export const ACTIVE_SHEET_SHARE_DATA: 'SHARE_DATA' = 'SHARE_DATA';
@@ -23,18 +22,15 @@ export const ACTIVE_SHEET_SHARE_DATA_IN_FILE: 'SHARE_DATA_IN_FILE' =
export const SET_EXPORT_STATUS_MESSAGE: 'SET_EXPORT_STATUS_MESSAGE' =
'SET_EXPORT_STATUS_MESSAGE';
export const UNSET_SHARE: 'UNSET_SHARE' = 'UNSET_SHARE';
export const ACTIVE_SHEET_PLUGIN_INSTALLER: 'ACTIVE_SHEET_PLUGIN_INSTALLER' =
'ACTIVE_SHEET_PLUGIN_INSTALLER';
export type ActiveSheet =
| typeof ACTIVE_SHEET_PLUGIN_SHEET
| typeof ACTIVE_SHEET_BUG_REPORTER
| typeof ACTIVE_SHEET_PLUGIN_DEBUGGER
| typeof ACTIVE_SHEET_PLUGINS
| typeof ACTIVE_SHEET_SHARE_DATA
| typeof ACTIVE_SHEET_SIGN_IN
| typeof ACTIVE_SHEET_SHARE_DATA_IN_FILE
| typeof ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT
| typeof ACTIVE_SHEET_PLUGIN_INSTALLER
| null;
export type LauncherMsg = {