287 lines
10 KiB
TypeScript
287 lines
10 KiB
TypeScript
import React, {Component} from 'react';
|
|
import {RouteComponentProps} from 'react-router';
|
|
import {Markdown} from '../common/Markdown';
|
|
import {UnControlled as CodeMirror} from 'react-codemirror2';
|
|
import 'codemirror/lib/codemirror.css';
|
|
import 'codemirror/theme/material.css';
|
|
import 'codemirror/mode/yaml/yaml';
|
|
import Info from '@material-ui/icons/Info';
|
|
import Build from '@material-ui/icons/Build';
|
|
import Subject from '@material-ui/icons/Subject';
|
|
import Refresh from '@material-ui/icons/Refresh';
|
|
import Button from '@material-ui/core/Button';
|
|
import Typography from '@material-ui/core/Typography';
|
|
import DefaultPage from '../common/DefaultPage';
|
|
import * as config from '../config';
|
|
import Container from '../common/Container';
|
|
import {inject, Stores} from '../inject';
|
|
import {IPlugin} from '../types';
|
|
|
|
type IProps = RouteComponentProps<{id: string}>;
|
|
|
|
interface IState {
|
|
displayText: string | null;
|
|
currentConfig: string | null;
|
|
}
|
|
|
|
class PluginDetailView extends Component<IProps & Stores<'pluginStore'>, IState> {
|
|
private pluginID: number = parseInt(this.props.match.params.id, 10);
|
|
private pluginInfo = () => this.props.pluginStore.getByID(this.pluginID);
|
|
|
|
public state: IState = {
|
|
displayText: null,
|
|
currentConfig: null,
|
|
};
|
|
|
|
public componentWillMount() {
|
|
this.refreshFeatures();
|
|
}
|
|
|
|
public componentWillReceiveProps(nextProps: IProps & Stores<'pluginStore'>) {
|
|
this.pluginID = parseInt(nextProps.match.params.id, 10);
|
|
this.refreshFeatures();
|
|
}
|
|
|
|
private refreshFeatures() {
|
|
return Promise.all([this.refreshConfigurer(), this.refreshDisplayer()]);
|
|
}
|
|
|
|
private async refreshConfigurer() {
|
|
const {
|
|
props: {pluginStore},
|
|
} = this;
|
|
if (this.pluginInfo().capabilities.indexOf('configurer') !== -1) {
|
|
const response = await pluginStore.requestConfig(this.pluginID);
|
|
this.setState({currentConfig: response});
|
|
}
|
|
}
|
|
|
|
private async refreshDisplayer() {
|
|
const {
|
|
props: {pluginStore},
|
|
} = this;
|
|
if (this.pluginInfo().capabilities.indexOf('displayer') !== -1) {
|
|
const response = await pluginStore.requestDisplay(this.pluginID);
|
|
this.setState({displayText: response});
|
|
}
|
|
}
|
|
|
|
public render() {
|
|
const pluginInfo = this.pluginInfo();
|
|
const {name, capabilities} = pluginInfo;
|
|
return (
|
|
<DefaultPage title={name} maxWidth={1000}>
|
|
<PanelWrapper name={'Plugin Info'} icon={Info}>
|
|
<PluginInfo pluginInfo={pluginInfo} />
|
|
</PanelWrapper>
|
|
{capabilities.indexOf('configurer') !== -1 ? (
|
|
<PanelWrapper
|
|
name={'Configurer'}
|
|
description={'This is the configuration panel for this plugin.'}
|
|
icon={Build}
|
|
refresh={this.refreshConfigurer.bind(this)}>
|
|
<ConfigurerPanel
|
|
pluginInfo={pluginInfo}
|
|
initialConfig={
|
|
this.state.currentConfig !== null
|
|
? this.state.currentConfig
|
|
: 'Loading...'
|
|
}
|
|
save={async (newConfig) => {
|
|
await this.props.pluginStore.changeConfig(this.pluginID, newConfig);
|
|
await this.refreshFeatures();
|
|
}}
|
|
/>
|
|
</PanelWrapper>
|
|
) : null}{' '}
|
|
{capabilities.indexOf('displayer') !== -1 ? (
|
|
<PanelWrapper
|
|
name={'Displayer'}
|
|
description={'This is the information generated by the plugin.'}
|
|
refresh={this.refreshDisplayer.bind(this)}
|
|
icon={Subject}>
|
|
<DisplayerPanel
|
|
pluginInfo={pluginInfo}
|
|
displayText={
|
|
this.state.displayText !== null
|
|
? this.state.displayText
|
|
: 'Loading...'
|
|
}
|
|
/>
|
|
</PanelWrapper>
|
|
) : null}
|
|
</DefaultPage>
|
|
);
|
|
}
|
|
}
|
|
|
|
interface IPanelWrapperProps {
|
|
name: string;
|
|
description?: string;
|
|
refresh?: () => Promise<void>;
|
|
icon?: React.ComponentType;
|
|
}
|
|
|
|
const PanelWrapper: React.FC<IPanelWrapperProps> = ({
|
|
name,
|
|
description,
|
|
refresh,
|
|
icon,
|
|
children,
|
|
}) => {
|
|
const Icon = icon;
|
|
return (
|
|
<div
|
|
style={{
|
|
width: '100%',
|
|
paddingLeft: '16px',
|
|
paddingRight: '16px',
|
|
}}>
|
|
<Container
|
|
style={{
|
|
display: 'block',
|
|
width: '100%',
|
|
margin: '12px 0px',
|
|
}}>
|
|
<Typography variant="h5">
|
|
{Icon ? (
|
|
<span>
|
|
<Icon />
|
|
|
|
</span>
|
|
) : null}
|
|
{name}
|
|
{refresh ? (
|
|
<Button
|
|
style={{float: 'right'}}
|
|
onClick={() => {
|
|
refresh();
|
|
}}>
|
|
<Refresh />
|
|
</Button>
|
|
) : null}
|
|
</Typography>
|
|
{description ? <Typography variant="subtitle1">{description}</Typography> : null}
|
|
<hr />
|
|
<div className={name.toLowerCase().trim().replace(/ /g, '-')}>{children}</div>
|
|
</Container>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface IConfigurerPanelProps {
|
|
pluginInfo: IPlugin;
|
|
initialConfig: string;
|
|
save: (newConfig: string) => Promise<void>;
|
|
}
|
|
class ConfigurerPanel extends Component<IConfigurerPanelProps, {unsavedChanges: string | null}> {
|
|
public state = {unsavedChanges: null};
|
|
|
|
public render() {
|
|
return (
|
|
<div>
|
|
<CodeMirror
|
|
value={this.props.initialConfig}
|
|
options={{
|
|
mode: 'yaml',
|
|
theme: 'material',
|
|
lineNumbers: true,
|
|
}}
|
|
onChange={(_, _1, value) => {
|
|
let newConf: string | null = value;
|
|
if (value === this.props.initialConfig) {
|
|
newConf = null;
|
|
}
|
|
this.setState({unsavedChanges: newConf});
|
|
}}
|
|
/>
|
|
<br />
|
|
<Button
|
|
variant="contained"
|
|
color="primary"
|
|
fullWidth={true}
|
|
disabled={
|
|
this.state.unsavedChanges === null ||
|
|
this.state.unsavedChanges === this.props.initialConfig
|
|
}
|
|
className="config-save"
|
|
onClick={() => {
|
|
const newConfig = this.state.unsavedChanges;
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
this.props.save(newConfig!).then(() => {
|
|
this.setState({unsavedChanges: null});
|
|
});
|
|
}}>
|
|
<Typography variant="button">Save</Typography>
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
interface IDisplayerPanelProps {
|
|
pluginInfo: IPlugin;
|
|
displayText: string;
|
|
}
|
|
const DisplayerPanel: React.FC<IDisplayerPanelProps> = ({displayText}) => (
|
|
<Typography variant="body2">
|
|
<Markdown>{displayText}</Markdown>
|
|
</Typography>
|
|
);
|
|
|
|
class PluginInfo extends Component<{pluginInfo: IPlugin}> {
|
|
public render() {
|
|
const {
|
|
props: {
|
|
pluginInfo: {name, author, modulePath, website, license, capabilities, id, token},
|
|
},
|
|
} = this;
|
|
return (
|
|
<div style={{wordWrap: 'break-word'}}>
|
|
{name ? (
|
|
<Typography variant="body2" className="name">
|
|
Name: <span>{name}</span>
|
|
</Typography>
|
|
) : null}
|
|
{author ? (
|
|
<Typography variant="body2" className="author">
|
|
Author: <span>{author}</span>
|
|
</Typography>
|
|
) : null}
|
|
<Typography variant="body2" className="module-path">
|
|
Module Path: <span>{modulePath}</span>
|
|
</Typography>
|
|
{website ? (
|
|
<Typography variant="body2" className="website">
|
|
Website: <span>{website}</span>
|
|
</Typography>
|
|
) : null}
|
|
{license ? (
|
|
<Typography variant="body2" className="license">
|
|
License: <span>{license}</span>
|
|
</Typography>
|
|
) : null}
|
|
<Typography variant="body2" className="capabilities">
|
|
Capabilities: <span>{capabilities.join(', ')}</span>
|
|
</Typography>
|
|
{capabilities.indexOf('webhooker') !== -1 ? (
|
|
<Typography variant="body2">
|
|
Custom Route Prefix:{' '}
|
|
{((url) => (
|
|
<a
|
|
href={url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="custom-route">
|
|
{url}
|
|
</a>
|
|
))(`${config.get('url')}plugin/${id}/custom/${token}/`)}
|
|
</Typography>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default inject('pluginStore')(PluginDetailView);
|