Experimental plugin system console UI (#7338)

* Add system console UI for uploading/listing/removing plugins

* Add localization strings

* Add banner to plugin settings

* Updating UI for experimental plugins (#7362)

* Text updates

* Updating UI for experimental plugin stuff (#7377)

* Properly clear file input after upload
Этот коммит содержится в:
Joram Wilander
2017-09-05 18:12:55 -04:00
коммит произвёл GitHub
родитель e30e4cfe3d
Коммит df085d273d
9 изменённых файлов: 381 добавлений и 4 удалений

Просмотреть файл

@@ -536,6 +536,8 @@ func getClientConfig(c *model.Config) map[string]string {
props["DiagnosticId"] = CfgDiagnosticId
props["DiagnosticsEnabled"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics)
props["PluginsEnabled"] = strconv.FormatBool(*c.PluginSettings.Enable)
if IsLicensed() {
License := License()

Просмотреть файл

@@ -60,6 +60,7 @@ export default class AdminSidebar extends React.Component {
let metricsSettings = null;
let complianceSettings = null;
let mfaSettings = null;
let pluginSettings = null;
let license = null;
let audits = null;
@@ -277,6 +278,20 @@ export default class AdminSidebar extends React.Component {
);
}
if (window.mm_config.PluginsEnabled === 'true' && window.mm_license.IsLicensed === 'true') {
pluginSettings = (
<AdminSidebarSection
name='plugins'
title={
<FormattedMessage
id='admin.sidebar.plugins'
defaultMessage='Plugins (experimental)'
/>
}
/>
);
}
const SHOW_CLIENT_VERSIONS = false;
let clientVersions = null;
if (SHOW_CLIENT_VERSIONS) {
@@ -562,6 +577,7 @@ export default class AdminSidebar extends React.Component {
/>
}
/>
{pluginSettings}
</AdminSidebarSection>
<AdminSidebarSection
name='files'

Просмотреть файл

@@ -1,9 +1,8 @@
import PropTypes from 'prop-types';
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
export default function Banner(props) {

Просмотреть файл

@@ -0,0 +1,27 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {uploadPlugin, removePlugin, getPlugins} from 'mattermost-redux/actions/admin';
import PluginSettings from './plugin_settings.jsx';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
plugins: state.entities.admin.plugins
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
uploadPlugin,
removePlugin,
getPlugins
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(PluginSettings);

Просмотреть файл

@@ -0,0 +1,293 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import LoadingScreen from 'components/loading_screen.jsx';
import Banner from 'components/admin_console/banner.jsx';
import * as Utils from 'utils/utils.jsx';
import React from 'react';
import PropTypes from 'prop-types';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
export default class PluginSettings extends React.Component {
static propTypes = {
/*
* The config
*/
config: PropTypes.object.isRequired,
/*
* Plugins object with ids as keys and manifests as values
*/
plugins: PropTypes.object.isRequired,
actions: PropTypes.shape({
/*
* Function to upload a plugin
*/
uploadPlugin: PropTypes.func.isRequired,
/*
* Function to remove a plugin
*/
removePlugin: PropTypes.func.isRequired,
/*
* Function to get installed plugins
*/
getPlugins: PropTypes.func.isRequired
}).isRequired
}
constructor(props) {
super(props);
this.state = {
loading: true,
fileSelected: false,
fileName: null,
serverError: null
};
}
componentDidMount() {
this.props.actions.getPlugins().then(
() => this.setState({loading: false})
);
}
handleChange = () => {
const element = this.refs.fileInput;
if (element.files.length > 0) {
this.setState({fileSelected: true, fileName: element.files[0].name});
}
}
handleSubmit = async (e) => {
e.preventDefault();
const element = this.refs.fileInput;
if (element.files.length === 0) {
return;
}
const file = element.files[0];
this.setState({uploading: true});
const {error} = await this.props.actions.uploadPlugin(file);
this.setState({fileSelected: false, fileName: null, uploading: false});
Utils.clearFileInput(element);
if (error) {
if (error.server_error_id === 'app.plugin.activate.app_error') {
this.setState({serverError: Utils.localizeMessage('admin.plugin.error.activate', 'Unable to upload the plugin. It may conflict with another plugin on your server.')});
} else if (error.server_error_id === 'app.plugin.extract.app_error') {
this.setState({serverError: Utils.localizeMessage('admin.plugin.error.extract', 'Encountered an error when extracting the plugin. Review your plugin file content and try again.')});
} else {
this.setState({serverError: error.message});
}
}
}
handleRemove = async (pluginId) => {
this.setState({removing: pluginId});
const {error} = await this.props.actions.removePlugin(pluginId);
this.setState({removing: null});
if (error) {
this.setState({serverError: error.message});
}
}
render() {
let serverError = '';
if (this.state.serverError) {
serverError = <div className='col-sm-12'><div className='form-group has-error half'><label className='control-label'>{this.state.serverError}</label></div></div>;
}
let btnClass = 'btn';
if (this.state.fileSelected) {
btnClass = 'btn btn-primary';
}
let fileName;
if (this.state.fileName) {
fileName = this.state.fileName;
}
let uploadButtonText;
if (this.state.uploading) {
uploadButtonText = (
<FormattedMessage
id='admin.plugin.uploading'
defaultMessage='Uploading...'
/>
);
} else {
uploadButtonText = (
<FormattedMessage
id='admin.plugin.upload'
defaultMessage='Upload'
/>
);
}
let activePluginsList;
let activePluginsContainer;
const plugins = Object.values(this.props.plugins);
if (this.state.loading) {
activePluginsList = <LoadingScreen/>;
} else if (plugins.length === 0) {
activePluginsContainer = (
<FormattedMessage
id='admin.plugin.no_plugins'
defaultMessage='No active plugins.'
/>
);
} else {
activePluginsList = plugins.map(
(p) => {
let removeButtonText;
if (this.state.removing === p.id) {
removeButtonText = (
<FormattedMessage
id='admin.plugin.removing'
defaultMessage='Removing...'
/>
);
} else {
removeButtonText = (
<FormattedMessage
id='admin.plugin.remove'
defaultMessage='Remove'
/>
);
}
return (
<div key={p.id}>
<div>
<strong>
<FormattedMessage
id='admin.plugin.id'
defaultMessage='ID:'
/>
</strong>
{' ' + p.id}
</div>
<div className='padding-top'>
<strong>
<FormattedMessage
id='admin.plugin.desc'
defaultMessage='Description:'
/>
</strong>
{' ' + p.description}
</div>
<div className='padding-top'>
<a
disabled={this.state.removing === p.id}
onClick={() => this.handleRemove(p.id)}
>
{removeButtonText}
</a>
</div>
<hr/>
</div>
);
}
);
activePluginsContainer = (
<div className='alert alert-transparent'>
{activePluginsList}
</div>
);
}
return (
<div className='wrapper--fixed'>
<h3 className='admin-console-header'>
<FormattedMessage
id='admin.plugin.title'
defaultMessage='Plugins (experimental)'
/>
</h3>
<Banner
title={<div/>}
description={
<FormattedHTMLMessage
id='admin.plugin.banner'
defaultMessage='Plugins are experimental stage and are not yet recommended for use in production environments. <br/><br/> Webapp plugins will require users to refresh their browsers or desktop apps before the plugin will take effect. Similarly when a plugin is removed, users will continue to see the plugin until they refresh their browser or app.'
/>
}
/>
<form
className='form-horizontal'
role='form'
>
<div className='form-group'>
<label
className='control-label col-sm-4'
>
<FormattedMessage
id='admin.plugin.uploadTitle'
defaultMessage='Upload Plugin: '
/>
</label>
<div className='col-sm-8'>
<div className='file__upload'>
<button className='btn btn-primary'>
<FormattedMessage
id='admin.plugin.choose'
defaultMessage='Choose File'
/>
</button>
<input
ref='fileInput'
type='file'
accept='.gz'
onChange={this.handleChange}
/>
</div>
<button
className={btnClass}
disabled={!this.state.fileSelected}
onClick={this.handleSubmit}
>
{uploadButtonText}
</button>
<div className='help-text no-margin'>
{fileName}
</div>
{serverError}
<p className='help-text'>
<FormattedHTMLMessage
id='admin.plugin.uploadDesc'
defaultMessage='Upload a plugin for your Mattermost server. Adding or removing a webapp plugin requires users to refresh their browser or Desktop App before taking effect. See <a href="https://about.mattermost.com/default-plugins">documentation</a> to learn more.'
/>
</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
>
<FormattedMessage
id='admin.plugin.activeTitle'
defaultMessage='Active Plugins: '
/>
</label>
<div className='col-sm-8 padding-top'>
{activePluginsContainer}
</div>
</div>
</form>
</div>
);
}
}

Просмотреть файл

@@ -565,6 +565,20 @@
"admin.ldap.userFilterTitle": "User Filter:",
"admin.ldap.usernameAttrEx": "E.g.: \"sAMAccountName\"",
"admin.ldap.usernameAttrTitle": "Username Attribute:",
"admin.plugin.banner": "Plugins are experimental and not recommended for use in production.",
"admin.plugin.uploading": "Uploading...",
"admin.plugin.upload": "Upload",
"admin.plugin.error.extract": "Encountered an error when extracting the plugin. Review your plugin file content and try again.",
"admin.plugin.error.activate": "Unable to upload the plugin. It may conflict with another plugin on your server.",
"admin.plugin.no_plugins": "No active plugins.",
"admin.plugin.removing": "Removing...",
"admin.plugin.remove": "Remove",
"admin.plugin.id": "ID:",
"admin.plugin.desc": "Description:",
"admin.plugin.title": "Plugins (Experimental)",
"admin.plugin.uploadTitle": "Upload Plugin: ",
"admin.plugin.uploadDesc": "Upload a plugin for your Mattermost server. Adding or removing a webapp plugin requires users to refresh their browser or Desktop App before taking effect. See <a href=\"https://about.mattermost.com/default-plugins\">documentation</a> to learn more.",
"admin.plugin.activeTitle": "Active Plugins: ",
"admin.license.choose": "Choose File",
"admin.license.chooseFile": "Choose File",
"admin.license.edition": "Edition: ",
@@ -885,6 +899,7 @@
"admin.sidebar.email": "Email",
"admin.sidebar.emoji": "Emoji",
"admin.sidebar.external": "External Services",
"admin.sidebar.plugins": "Plugins (Experimental)",
"admin.sidebar.files": "Files",
"admin.sidebar.general": "General",
"admin.sidebar.gitlab": "GitLab",

Просмотреть файл

@@ -30,6 +30,7 @@ import EmailSettings from 'components/admin_console/email_settings.jsx';
import PushSettings from 'components/admin_console/push_settings.jsx';
import CustomIntegrationsSettings from 'components/admin_console/custom_integrations_settings.jsx';
import ExternalServiceSettings from 'components/admin_console/external_service_settings.jsx';
import PluginSettings from 'components/admin_console/plugin_settings';
import WebrtcSettings from 'components/admin_console/webrtc_settings.jsx';
import DatabaseSettings from 'components/admin_console/database_settings.jsx';
import StorageSettings from 'components/admin_console/storage_settings.jsx';
@@ -169,6 +170,10 @@ export default (
path='jira'
component={JIRASettings}
/>
<Route
path='plugins'
component={PluginSettings}
/>
</Route>
<Route path='files'>
<IndexRedirect to='storage'/>

Просмотреть файл

@@ -114,6 +114,10 @@
.form-group {
margin-bottom: 25px;
&.half {
margin-bottom: 14px;
}
}
.file__upload {
@@ -162,8 +166,8 @@
&.remove-filename {
margin-bottom: 5px;
top: -2px;
position: relative;
top: -2px;
}
}
@@ -177,6 +181,22 @@
.fa {
margin-right: 5px;
}
&.alert-transparent {
background: $white;
border: $border-gray;
margin: 0;
padding: 8px 12px;
width: 100%;
}
hr {
margin: .8em 0;
&:last-child {
display: none;
}
}
}
}

Просмотреть файл

@@ -5065,7 +5065,7 @@ math-expression-evaluator@^1.2.14:
mattermost-redux@mattermost/mattermost-redux#master:
version "0.0.1"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/8a1736b94b5718ee939a8fb61a9a6a275c6ad703"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/e732a173be8c2547d7c8269020dff2f5e44baa26"
dependencies:
deep-equal "1.0.1"
harmony-reflect "1.5.1"