summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--utils/config.go2
-rw-r--r--webapp/components/admin_console/admin_sidebar.jsx16
-rw-r--r--webapp/components/admin_console/banner.jsx3
-rw-r--r--webapp/components/admin_console/plugin_settings/index.js27
-rw-r--r--webapp/components/admin_console/plugin_settings/plugin_settings.jsx293
-rwxr-xr-xwebapp/i18n/en.json15
-rw-r--r--webapp/routes/route_admin_console.jsx5
-rw-r--r--webapp/sass/routes/_admin-console.scss22
-rw-r--r--webapp/yarn.lock2
9 files changed, 381 insertions, 4 deletions
diff --git a/utils/config.go b/utils/config.go
index b8ec43eb5..c77d655dc 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -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()
diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx
index 9a726c65c..4918cdac0 100644
--- a/webapp/components/admin_console/admin_sidebar.jsx
+++ b/webapp/components/admin_console/admin_sidebar.jsx
@@ -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'
diff --git a/webapp/components/admin_console/banner.jsx b/webapp/components/admin_console/banner.jsx
index 452af92e1..6395ef4a1 100644
--- a/webapp/components/admin_console/banner.jsx
+++ b/webapp/components/admin_console/banner.jsx
@@ -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) {
diff --git a/webapp/components/admin_console/plugin_settings/index.js b/webapp/components/admin_console/plugin_settings/index.js
new file mode 100644
index 000000000..469d4ee2e
--- /dev/null
+++ b/webapp/components/admin_console/plugin_settings/index.js
@@ -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);
diff --git a/webapp/components/admin_console/plugin_settings/plugin_settings.jsx b/webapp/components/admin_console/plugin_settings/plugin_settings.jsx
new file mode 100644
index 000000000..286e05c06
--- /dev/null
+++ b/webapp/components/admin_console/plugin_settings/plugin_settings.jsx
@@ -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>
+ );
+ }
+}
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 754620969..4ad2f6abf 100755
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -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",
diff --git a/webapp/routes/route_admin_console.jsx b/webapp/routes/route_admin_console.jsx
index 43245556f..80950bc06 100644
--- a/webapp/routes/route_admin_console.jsx
+++ b/webapp/routes/route_admin_console.jsx
@@ -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'/>
diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss
index ff02ca17e..7983cf131 100644
--- a/webapp/sass/routes/_admin-console.scss
+++ b/webapp/sass/routes/_admin-console.scss
@@ -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;
+ }
+ }
}
}
diff --git a/webapp/yarn.lock b/webapp/yarn.lock
index b4b8875a2..fb33ce38e 100644
--- a/webapp/yarn.lock
+++ b/webapp/yarn.lock
@@ -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"