sharded-gotify/ui/src/plugin/PluginDetailView.tsx

275 lines
9.8 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 (
<Container style={{display: 'block', width: '100%', margin: '20px'}}>
<Typography variant="h5">
{Icon ? (
<span>
<Icon />
&nbsp;
</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>
);
};
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>
{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);