diff options
author | Joram Wilander <jwawilander@gmail.com> | 2017-09-05 18:12:55 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-09-05 18:12:55 -0400 |
commit | df085d273d7ed3fe0d53b78364da46cdd6429c53 (patch) | |
tree | 9fee7f44a9304498ec969a51e16e48351d361d97 | |
parent | e30e4cfe3d787e2528419b0d17973eb0fc162d56 (diff) | |
download | chat-df085d273d7ed3fe0d53b78364da46cdd6429c53.tar.gz chat-df085d273d7ed3fe0d53b78364da46cdd6429c53.tar.bz2 chat-df085d273d7ed3fe0d53b78364da46cdd6429c53.zip |
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
-rw-r--r-- | utils/config.go | 2 | ||||
-rw-r--r-- | webapp/components/admin_console/admin_sidebar.jsx | 16 | ||||
-rw-r--r-- | webapp/components/admin_console/banner.jsx | 3 | ||||
-rw-r--r-- | webapp/components/admin_console/plugin_settings/index.js | 27 | ||||
-rw-r--r-- | webapp/components/admin_console/plugin_settings/plugin_settings.jsx | 293 | ||||
-rwxr-xr-x | webapp/i18n/en.json | 15 | ||||
-rw-r--r-- | webapp/routes/route_admin_console.jsx | 5 | ||||
-rw-r--r-- | webapp/sass/routes/_admin-console.scss | 22 | ||||
-rw-r--r-- | webapp/yarn.lock | 2 |
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" |