Summary: My benchmarks have shown react-emotion to be faster than the current implementation of `styled`. For this reason, I am converting all styling to [emotion](https://emotion.sh). Benchmark results: {F136839093} The syntax is very similar between the two libraries. The main difference is that emotion only allows a single function for the whole style attribute, whereas the old implementation had functions for every style-attirbute. Before: ``` { color: props => props.color, fontSize: props => props.size, } ``` After: ``` props => ({ color: props.color, fontSize: props.size, }) ``` Reviewed By: jknoxville Differential Revision: D9479893 fbshipit-source-id: 2c39e4618f7e52ceacb67bbec8ae26114025723f
405 lines
9.5 KiB
JavaScript
405 lines
9.5 KiB
JavaScript
/**
|
|
* 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 'sonar';
|
|
const {spawn} = require('child_process');
|
|
const path = require('path');
|
|
const {app, shell} = require('electron').remote;
|
|
|
|
const SONAR_PLUGIN_PATH = path.join(app.getPath('home'), '.sonar');
|
|
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:sonar&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: SONAR_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>
|
|
|
|
<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 Sonar, 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(SONAR_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 fill={true}>
|
|
{this.state.restartRequired && (
|
|
<RestartRequired onClick={this.relaunch}>
|
|
<Glyph name="arrows-circle" size={12} color={colors.white} />
|
|
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 fill={true}>
|
|
<SearchablePluginManager />
|
|
</FlexColumn>
|
|
);
|
|
}
|
|
}
|