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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
e30e4cfe3d
Коммит
df085d273d
@@ -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) {
|
||||
|
||||
27
webapp/components/admin_console/plugin_settings/index.js
Обычный файл
27
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);
|
||||
293
webapp/components/admin_console/plugin_settings/plugin_settings.jsx
Обычный файл
293
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Ссылка в новой задаче
Block a user