diff options
-rw-r--r-- | api/admin.go | 5 | ||||
-rw-r--r-- | api/command_loadtest.go | 45 | ||||
-rw-r--r-- | api/command_loadtest_test.go | 41 | ||||
-rw-r--r-- | config/config.json | 1 | ||||
-rw-r--r-- | einterfaces/ldap.go | 1 | ||||
-rw-r--r-- | i18n/en.json | 8 | ||||
-rw-r--r-- | model/config.go | 8 | ||||
-rw-r--r-- | tests/test-slack-attachments.json | 56 | ||||
-rw-r--r-- | utils/config.go | 16 | ||||
-rw-r--r-- | webapp/components/admin_console/ldap_settings.jsx | 100 | ||||
-rw-r--r-- | webapp/components/admin_console/team_users.jsx | 8 | ||||
-rw-r--r-- | webapp/components/admin_console/user_item.jsx | 30 | ||||
-rw-r--r-- | webapp/components/post_attachment.jsx | 131 | ||||
-rw-r--r-- | webapp/i18n/en.json | 21 | ||||
-rw-r--r-- | webapp/i18n/es.json | 14 | ||||
-rw-r--r-- | webapp/sass/responsive/_mobile.scss | 5 | ||||
-rw-r--r-- | webapp/sass/routes/_admin-console.scss | 14 | ||||
-rw-r--r-- | webapp/sass/routes/_backstage.scss | 16 |
18 files changed, 356 insertions, 164 deletions
diff --git a/api/admin.go b/api/admin.go index 2990691a6..7b041619e 100644 --- a/api/admin.go +++ b/api/admin.go @@ -148,6 +148,11 @@ func saveConfig(c *Context, w http.ResponseWriter, r *http.Request) { return } + if err := utils.ValidateLdapFilter(cfg); err != nil { + c.Err = err + return + } + c.LogAudit("") utils.SaveConfig(utils.CfgFileName, cfg) diff --git a/api/command_loadtest.go b/api/command_loadtest.go index c76bc713f..63598c06e 100644 --- a/api/command_loadtest.go +++ b/api/command_loadtest.go @@ -49,6 +49,11 @@ var usage = `Mattermost load testing commands to help configure the system Example: /loadtest http://www.example.com/sample_file.md + Json - Add a post using the JSON file as payload to the current channel. + /loadtest json url + + Example + /loadtest json http://www.example.com/sample_body.json ` @@ -105,7 +110,9 @@ func (me *LoadTestProvider) DoCommand(c *Context, channelId string, message stri if strings.HasPrefix(message, "url") { return me.UrlCommand(c, channelId, message) } - + if strings.HasPrefix(message, "json") { + return me.JsonCommand(c, channelId, message) + } return me.HelpCommand(c, channelId, message) } @@ -330,6 +337,42 @@ func (me *LoadTestProvider) UrlCommand(c *Context, channelId string, message str return &model.CommandResponse{Text: "Loading data...", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } +func (me *LoadTestProvider) JsonCommand(c *Context, channelId string, message string) *model.CommandResponse { + url := strings.TrimSpace(strings.TrimPrefix(message, "json")) + if len(url) == 0 { + return &model.CommandResponse{Text: "Command must contain a url", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + + // provide a shortcut to easily access tests stored in doc/developer/tests + if !strings.HasPrefix(url, "http") { + url = "https://raw.githubusercontent.com/mattermost/platform/master/tests/" + url + + if path.Ext(url) == "" { + url += ".json" + } + } + + var contents io.ReadCloser + if r, err := http.Get(url); err != nil { + return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } else if r.StatusCode > 400 { + return &model.CommandResponse{Text: "Unable to get file", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } else { + contents = r.Body + } + + post := model.PostFromJson(contents) + post.ChannelId = channelId + if post.Message == "" { + post.Message = message + } + + if _, err := CreatePost(c, post, false); err != nil { + return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + return &model.CommandResponse{Text: "Loading data...", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} +} + func parseRange(command string, cmd string) (utils.Range, bool) { tokens := strings.Fields(strings.TrimPrefix(command, cmd)) var begin int diff --git a/api/command_loadtest_test.go b/api/command_loadtest_test.go index ef370cf19..4988050a1 100644 --- a/api/command_loadtest_test.go +++ b/api/command_loadtest_test.go @@ -221,3 +221,44 @@ func TestLoadTestUrlCommands(t *testing.T) { time.Sleep(2 * time.Second) } + +func TestLoadTestJsonCommands(t *testing.T) { + Setup() + // enable testing to use /loadtest but don't save it since we don't want to overwrite config.json + enableTesting := utils.Cfg.ServiceSettings.EnableTesting + defer func() { + utils.Cfg.ServiceSettings.EnableTesting = enableTesting + }() + + utils.Cfg.ServiceSettings.EnableTesting = true + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + channel := &model.Channel{DisplayName: "00", Name: "00" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel = Client.Must(Client.CreateChannel(channel)).Data.(*model.Channel) + + command := "/loadtest json " + if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Command must contain a url" { + t.Fatal("/loadtest url with no url should've failed") + } + + command = "/loadtest json http://missingfiletonwhere/path/asdf/qwerty" + if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Unable to get file" { + t.Log(r.Text) + t.Fatal("/loadtest url with invalid url should've failed") + } + + command = "/loadtest url https://secure.beldienst.nl/test.json" // Chicken-egg so will replace with mattermost/platform URL soon + if r := Client.Must(Client.Command(channel.Id, command, false)).Data.(*model.CommandResponse); r.Text != "Loading data..." { + t.Fatal("/loadtest url for README.md should've executed") + } + + time.Sleep(2 * time.Second) +} diff --git a/config/config.json b/config/config.json index 62dcfcffc..27c697be0 100644 --- a/config/config.json +++ b/config/config.json @@ -135,6 +135,7 @@ "BaseDN": "", "BindUsername": "", "BindPassword": "", + "UserFilter": "", "FirstNameAttribute": "", "LastNameAttribute": "", "EmailAttribute": "", diff --git a/einterfaces/ldap.go b/einterfaces/ldap.go index 2977ab812..3917b42f8 100644 --- a/einterfaces/ldap.go +++ b/einterfaces/ldap.go @@ -12,6 +12,7 @@ type LdapInterface interface { GetUser(id string) (*model.User, *model.AppError) CheckPassword(id string, password string) *model.AppError SwitchToEmail(userId, ldapId, ldapPassword string) *model.AppError + ValidateFilter(filter string) *model.AppError } var theLdapInterface LdapInterface diff --git a/i18n/en.json b/i18n/en.json index a4c84a462..6cdeeaad2 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1824,6 +1824,14 @@ "translation": "User not registered on LDAP server" }, { + "id": "ent.ldap.do_login.user_filtered.app_error", + "translation": "User is not permitted to use Mattermost. (LDAP user filter)" + }, + { + "id": "ent.ldap.validate_filter.app_error", + "translation": "Invalid LDAP Filter" + }, + { "id": "ent.mfa.activate.authenticate.app_error", "translation": "Error attempting to authenticate MFA token" }, diff --git a/model/config.go b/model/config.go index e7ab07f8c..666b2770b 100644 --- a/model/config.go +++ b/model/config.go @@ -169,6 +169,9 @@ type LdapSettings struct { BindUsername *string BindPassword *string + // Filtering + UserFilter *string + // User Mapping FirstNameAttribute *string LastNameAttribute *string @@ -366,6 +369,11 @@ func (o *Config) SetDefaults() { *o.LdapSettings.Enable = false } + if o.LdapSettings.UserFilter == nil { + o.LdapSettings.UserFilter = new(string) + *o.LdapSettings.UserFilter = "" + } + if o.ServiceSettings.SessionLengthWebInDays == nil { o.ServiceSettings.SessionLengthWebInDays = new(int) *o.ServiceSettings.SessionLengthWebInDays = 30 diff --git a/tests/test-slack-attachments.json b/tests/test-slack-attachments.json new file mode 100644 index 000000000..1c499b4ca --- /dev/null +++ b/tests/test-slack-attachments.json @@ -0,0 +1,56 @@ +{ + "message": "Hello world", + "props": { + "attachments": [ + { + "color": "#7CD197", + "fields": [ + { + "short": false, + "title": "Area", + "value": "Testing with a very long piece of text that will take up the whole width of the table. This is one more sentence to really make it a long field." + }, + { + "short": true, + "title": "Iteration", + "value": "Testing" + }, + { + "short": true, + "title": "State", + "value": "New" + }, + { + "short": false, + "title": "Reason", + "value": "New defect reported" + }, + { + "short": false, + "title": "Random field", + "value": "This is a field which is not marked as short so it should be rendered on a separate row" + }, + { + "short": true, + "title": "Short 1", + "value": "Short field" + }, + { + "short": true, + "title": "Short 2", + "value": "Another one" + } + ], + "mrkdwn_in": [ + "pretext" + ], + "pretext": "Some text here", + "text": "This is the text of the attachment. It should appear just above the image", + "thumb_url": "https://slack.global.ssl.fastly.net/7bf4/img/services/jenkins-ci_128.png", + "title": "A slack attachment", + "title_link": "https://www.google.com" + } + ] + }, + "type": "slack_attachment" +} diff --git a/utils/config.go b/utils/config.go index 93c8ffc7c..d8f52ce49 100644 --- a/utils/config.go +++ b/utils/config.go @@ -13,6 +13,7 @@ import ( l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/einterfaces" "github.com/mattermost/platform/model" ) @@ -167,6 +168,11 @@ func LoadConfig(fileName string) { map[string]interface{}{"Filename": fileName, "Error": err.Message})) } + if err := ValidateLdapFilter(&config); err != nil { + panic(T("utils.config.load_config.validating.panic", + map[string]interface{}{"Filename": fileName, "Error": err.Message})) + } + configureLog(&config.LogSettings) TestConnection(&config) @@ -243,3 +249,13 @@ func getClientConfig(c *model.Config) map[string]string { return props } + +func ValidateLdapFilter(cfg *model.Config) *model.AppError { + ldapInterface := einterfaces.GetLdapInterface() + if *cfg.LdapSettings.Enable && ldapInterface != nil && *cfg.LdapSettings.UserFilter != "" { + if err := ldapInterface.ValidateFilter(*cfg.LdapSettings.UserFilter); err != nil { + return err + } + } + return nil +} diff --git a/webapp/components/admin_console/ldap_settings.jsx b/webapp/components/admin_console/ldap_settings.jsx index 7996a3aab..190f707b9 100644 --- a/webapp/components/admin_console/ldap_settings.jsx +++ b/webapp/components/admin_console/ldap_settings.jsx @@ -4,56 +4,14 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; import * as Client from 'utils/client.jsx'; +import * as Utils from 'utils/utils.jsx'; import * as AsyncClient from 'utils/async_client.jsx'; -import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl'; +import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; const DEFAULT_LDAP_PORT = 389; const DEFAULT_QUERY_TIMEOUT = 60; -var holders = defineMessages({ - serverEx: { - id: 'admin.ldap.serverEx', - defaultMessage: 'Ex "10.0.0.23"' - }, - portEx: { - id: 'admin.ldap.portEx', - defaultMessage: 'Ex "389"' - }, - baseEx: { - id: 'admin.ldap.baseEx', - defaultMessage: 'Ex "ou=Unit Name,dc=corp,dc=example,dc=com"' - }, - firstnameAttrEx: { - id: 'admin.ldap.firstnameAttrEx', - defaultMessage: 'Ex "givenName"' - }, - lastnameAttrEx: { - id: 'admin.ldap.lastnameAttrEx', - defaultMessage: 'Ex "sn"' - }, - emailAttrEx: { - id: 'admin.ldap.emailAttrEx', - defaultMessage: 'Ex "mail" or "userPrincipalName"' - }, - usernameAttrEx: { - id: 'admin.ldap.usernameAttrEx', - defaultMessage: 'Ex "sAMAccountName"' - }, - idAttrEx: { - id: 'admin.ldap.idAttrEx', - defaultMessage: 'Ex "sAMAccountName"' - }, - queryEx: { - id: 'admin.ldap.queryEx', - defaultMessage: 'Ex "60"' - }, - saving: { - id: 'admin.ldap.saving', - defaultMessage: 'Saving Config...' - } -}); - import React from 'react'; class LdapSettings extends React.Component { @@ -102,6 +60,7 @@ class LdapSettings extends React.Component { config.LdapSettings.EmailAttribute = this.refs.EmailAttribute.value.trim(); config.LdapSettings.UsernameAttribute = this.refs.UsernameAttribute.value.trim(); config.LdapSettings.IdAttribute = this.refs.IdAttribute.value.trim(); + config.LdapSettings.UserFilter = this.refs.UserFilter.value.trim(); let QueryTimeout = DEFAULT_QUERY_TIMEOUT; if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.QueryTimeout).value, 10))) { @@ -129,7 +88,6 @@ class LdapSettings extends React.Component { ); } render() { - const {formatMessage} = this.props.intl; let serverError = ''; if (this.state.serverError) { serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; @@ -251,7 +209,7 @@ class LdapSettings extends React.Component { className='form-control' id='LdapServer' ref='LdapServer' - placeholder={formatMessage(holders.serverEx)} + placeholder={Utils.localizeMessage('admin.ldap.serverEx', 'Ex "10.0.0.23"')} defaultValue={this.props.config.LdapSettings.LdapServer} onChange={this.handleChange} disabled={!this.state.enable} @@ -280,7 +238,7 @@ class LdapSettings extends React.Component { className='form-control' id='LdapPort' ref='LdapPort' - placeholder={formatMessage(holders.portEx)} + placeholder={Utils.localizeMessage('admin.ldap.portEx', 'Ex "389"')} defaultValue={this.props.config.LdapSettings.LdapPort} onChange={this.handleChange} disabled={!this.state.enable} @@ -309,7 +267,7 @@ class LdapSettings extends React.Component { className='form-control' id='BaseDN' ref='BaseDN' - placeholder={formatMessage(holders.baseEx)} + placeholder={Utils.localizeMessage('admin.ldap.baseEx', 'Ex "ou=Unit Name,dc=corp,dc=example,dc=com"')} defaultValue={this.props.config.LdapSettings.BaseDN} onChange={this.handleChange} disabled={!this.state.enable} @@ -383,6 +341,35 @@ class LdapSettings extends React.Component { <div className='form-group'> <label className='control-label col-sm-4' + htmlFor='UserFilter' + > + <FormattedMessage + id='admin.ldap.userFilterTitle' + defaultMessage='User Filter:' + /> + </label> + <div className='col-sm-8'> + <input + type='text' + className='form-control' + id='UserFilter' + ref='UserFilter' + placeholder={Utils.localizeMessage('admin.ldap.userFilterEx', 'Ex. "(objectClass=user)"')} + defaultValue={this.props.config.LdapSettings.UserFilter} + onChange={this.handleChange} + disabled={!this.state.enable} + /> + <p className='help-text'> + <FormattedMessage + id='admin.ldap.userFilterDisc' + defaultMessage='LDAP Filter to use when searching for user objects.' + /> + </p> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' htmlFor='FirstNameAttribute' > <FormattedMessage @@ -396,7 +383,7 @@ class LdapSettings extends React.Component { className='form-control' id='FirstNameAttribute' ref='FirstNameAttribute' - placeholder={formatMessage(holders.firstnameAttrEx)} + placeholder={Utils.localizeMessage('admin.ldap.firstnameAttrEx', 'Ex "givenName"')} defaultValue={this.props.config.LdapSettings.FirstNameAttribute} onChange={this.handleChange} disabled={!this.state.enable} @@ -425,7 +412,7 @@ class LdapSettings extends React.Component { className='form-control' id='LastNameAttribute' ref='LastNameAttribute' - placeholder={formatMessage(holders.lastnameAttrEx)} + placeholder={Utils.localizeMessage('admin.ldap.lastnameAttrEx', 'Ex "sn"')} defaultValue={this.props.config.LdapSettings.LastNameAttribute} onChange={this.handleChange} disabled={!this.state.enable} @@ -454,7 +441,7 @@ class LdapSettings extends React.Component { className='form-control' id='EmailAttribute' ref='EmailAttribute' - placeholder={formatMessage(holders.emailAttrEx)} + placeholder={Utils.localizeMessage('admin.ldap.emailAttrEx', 'Ex "mail" or "userPrincipalName"')} defaultValue={this.props.config.LdapSettings.EmailAttribute} onChange={this.handleChange} disabled={!this.state.enable} @@ -483,7 +470,7 @@ class LdapSettings extends React.Component { className='form-control' id='UsernameAttribute' ref='UsernameAttribute' - placeholder={formatMessage(holders.usernameAttrEx)} + placeholder={Utils.localizeMessage('admin.ldap.usernameAttrEx', 'Ex "sAMAccountName"')} defaultValue={this.props.config.LdapSettings.UsernameAttribute} onChange={this.handleChange} disabled={!this.state.enable} @@ -512,7 +499,7 @@ class LdapSettings extends React.Component { className='form-control' id='IdAttribute' ref='IdAttribute' - placeholder={formatMessage(holders.idAttrEx)} + placeholder={Utils.localizeMessage('admin.ldap.idAttrEx', 'Ex "sAMAccountName"')} defaultValue={this.props.config.LdapSettings.IdAttribute} onChange={this.handleChange} disabled={!this.state.enable} @@ -541,7 +528,7 @@ class LdapSettings extends React.Component { className='form-control' id='QueryTimeout' ref='QueryTimeout' - placeholder={formatMessage(holders.queryEx)} + placeholder={Utils.localizeMessage('admin.ldap.queryEx', 'Ex "60"')} defaultValue={this.props.config.LdapSettings.QueryTimeout} onChange={this.handleChange} disabled={!this.state.enable} @@ -563,7 +550,7 @@ class LdapSettings extends React.Component { className={saveClass} onClick={this.handleSubmit} id='save-button' - data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)} + data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + Utils.localizeMessage('admin.ldap.saving', 'Saving Config...')} > <FormattedMessage id='admin.ldap.save' @@ -581,8 +568,7 @@ LdapSettings.defaultProps = { }; LdapSettings.propTypes = { - intl: intlShape.isRequired, config: React.PropTypes.object }; -export default injectIntl(LdapSettings); +export default LdapSettings; diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx index 1bf3f785b..8b37bd237 100644 --- a/webapp/components/admin_console/team_users.jsx +++ b/webapp/components/admin_console/team_users.jsx @@ -165,11 +165,9 @@ export default class UserList extends React.Component { className='form-horizontal' role='form' > - <table className='more-modal__list member-list-holder'> - <tbody> - {memberList} - </tbody> - </table> + <div className='more-modal__list member-list-holder'> + {memberList} + </div> </form> <ResetPasswordModal user={this.state.user} diff --git a/webapp/components/admin_console/user_item.jsx b/webapp/components/admin_console/user_item.jsx index c00050584..5bd05d063 100644 --- a/webapp/components/admin_console/user_item.jsx +++ b/webapp/components/admin_console/user_item.jsx @@ -365,16 +365,18 @@ export default class UserItem extends React.Component { } return ( - <tr> - <td className='more-modal__row'> - <img - className='more-modal__image pull-left' - src={`/api/v1/users/${user.id}/image?time=${user.update_at}`} - height='36' - width='36' - /> - <span className='more-modal__name'>{Utils.getDisplayName(user)}</span> - <span className='more-modal__description'>{email}</span> + <div className='more-modal__row'> + <img + className='more-modal__image pull-left' + src={`/api/v1/users/${user.id}/image?time=${user.update_at}`} + height='36' + width='36' + /> + <div className='more-modal__details'> + <div className='more-modal__name'>{Utils.getDisplayName(user)}</div> + <div className='more-modal__description'>{email}</div> + </div> + <div className='more-modal__actions'> <div className='dropdown member-drop'> <a href='#' @@ -409,10 +411,10 @@ export default class UserItem extends React.Component { </li> </ul> </div> - {makeDemoteModal} - {serverError} - </td> - </tr> + </div> + {makeDemoteModal} + {serverError} + </div> ); } } diff --git a/webapp/components/post_attachment.jsx b/webapp/components/post_attachment.jsx index 7c4125c27..1c3df6ea2 100644 --- a/webapp/components/post_attachment.jsx +++ b/webapp/components/post_attachment.jsx @@ -87,75 +87,82 @@ class PostAttachment extends React.Component { return ''; } - const compactTable = fields.filter((field) => field.short).length > 0; - let tHead; - let tBody; - - if (compactTable) { - let headerCols = []; - let bodyCols = []; - - fields.forEach((field, i) => { - headerCols.push( - <th - className='attachment___field-caption' - key={'attachment__field-caption-' + i} + let fieldTables = []; + + let headerCols = []; + let bodyCols = []; + let rowPos = 0; + let lastWasLong = false; + let nrTables = 0; + + fields.forEach((field, i) => { + if (rowPos === 2 || !(field.short === true) || lastWasLong) { + fieldTables.push( + <table + className='attachment___fields' + key={'attachment__table__' + nrTables} > - {field.title} - </th> + <thead> + <tr> + {headerCols} + </tr> + </thead> + <tbody> + <tr> + {bodyCols} + </tr> + </tbody> + </table> ); - bodyCols.push( - <td - className='attachment___field' - key={'attachment__field-' + i} - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(field.value || '')}} - > - </td> - ); - }); - - tHead = ( - <tr> - {headerCols} - </tr> + headerCols = []; + bodyCols = []; + rowPos = 0; + nrTables += 1; + lastWasLong = false; + } + headerCols.push( + <th + className='attachment___field-caption' + key={'attachment__field-caption-' + i + '__' + nrTables} + width='50%' + > + {field.title} + </th> ); - tBody = ( - <tr> - {bodyCols} - </tr> + bodyCols.push( + <td + className='attachment___field' + key={'attachment__field-' + i + '__' + nrTables} + dangerouslySetInnerHTML={{__html: TextFormatting.formatText(field.value || '')}} + > + </td> ); - } else { - tBody = []; - - fields.forEach((field, i) => { - tBody.push( - <tr key={'attachment__field-' + i}> - <td - className='attachment___field-caption' - > - {field.title} - </td> - <td - className='attachment___field' - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(field.value || '')}} - > - </td> - </tr> + rowPos += 1; + lastWasLong = !(field.short === true); + }); + if (headerCols.length > 0) { // Flush last fields + fieldTables.push( + <table + className='attachment___fields' + key={'attachment__table__' + nrTables} + > + <thead> + <tr> + {headerCols} + </tr> + </thead> + <tbody> + <tr> + {bodyCols} + </tr> + </tbody> + </table> ); - }); } - return ( - <table - className='attachment___fields' - > - <thead> - {tHead} - </thead> - <tbody> - {tBody} - </tbody> - </table> + <div> + {fieldTables} + </div> ); } diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index b66b079e7..7914fd1c7 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -39,8 +39,8 @@ "add_command.displayName": "Display Name", "add_command.header": "Add Slash Command", "add_command.iconUrl": "Response Icon", - "add_command.iconUrl.placeholder": "https://www.example.com/myicon.png", "add_command.iconUrl.help": "Choose a profile picture override for the post responses to this slash command. Enter the URL of a .png or .jpg file at least 128 pixels by 128 pixels.", + "add_command.iconUrl.placeholder": "https://www.example.com/myicon.png", "add_command.method": "Request Method", "add_command.method.get": "GET", "add_command.method.help": "The type of command request issued to the Request URL.", @@ -50,13 +50,13 @@ "add_command.trigger.help2": "Reserved: /echo, /join, /logout, /me, /shrug", "add_command.trigger.placeholder": "Command trigger e.g. \"hello\" not including the slash", "add_command.triggerRequired": "A trigger word is required", - "add_command.username": "Response Username", - "add_command.username.help": "Choose a username override for responses for this slash command. Usernames can consist of up to 22 characters consisting of lowercase letters, numbers and they symbols \"-\", \"_\", and \".\" .", - "add_command.username.placeholder": "Username", "add_command.url": "Request URL", "add_command.url.help": "The callback URL to receive the HTTP POST or GET event request when the slash command is run.", "add_command.url.placeholder": "Must start with http:// or https://", "add_command.urlRequired": "A request URL is required", + "add_command.username": "Response Username", + "add_command.username.help": "Choose a username override for responses for this slash command. Usernames can consist of up to 22 characters consisting of lowercase letters, numbers and they symbols \"-\", \"_\", and \".\" .", + "add_command.username.placeholder": "Username", "add_incoming_webhook.cancel": "Cancel", "add_incoming_webhook.channel": "Channel", "add_incoming_webhook.channelRequired": "A valid channel is required", @@ -64,9 +64,9 @@ "add_incoming_webhook.header": "Add Incoming Webhook", "add_incoming_webhook.name": "Name", "add_incoming_webhook.save": "Save", - "add_integration.header": "Add Integration", "add_integration.command.description": "Create slash commands to send events to external integrations and receive a response.", "add_integration.command.title": "Slash Command", + "add_integration.header": "Add Integration", "add_integration.incomingWebhook.description": "Create webhook URLs for use in external integrations.", "add_integration.incomingWebhook.title": "Incoming Webhook", "add_integration.outgoingWebhook.description": "Create webhooks to send new message events to an external integration.", @@ -289,6 +289,9 @@ "admin.ldap.uernameAttrDesc": "The attribute in the LDAP server that will be used to populate the username field in Mattermost. This may be the same as the ID Attribute.", "admin.ldap.usernameAttrEx": "Ex \"sAMAccountName\"", "admin.ldap.usernameAttrTitle": "Username Attribute:", + "admin.ldap.userFilterTitle": "User Filter:", + "admin.ldap.userFilterEx": "Ex. \"(objectClass=user)\"", + "admin.ldap.userFilterDisc": "LDAP Filter to use when searching for user objects.", "admin.licence.keyMigration": "If you’re migrating servers you may need to remove your license key from this server in order to install it on a new server. To start, <a href=\"http://mattermost.com\" target=\"_blank\">disable all Enterprise Edition features on this server</a>. This will enable the ability to remove the license key and downgrade this server from Enterprise Edition to Team Edition.", "admin.license.choose": "Choose File", "admin.license.chooseFile": "Choose File", @@ -620,7 +623,7 @@ "backstage_navbar.backToMattermost": "Back to {siteName}", "backstage_sidebar.integrations": "Integrations", "backstage_sidebar.integrations.add": "Add Integration", - "backstage_sidebar.integrations.add.command": "Outgoing Webhook", + "backstage_sidebar.integrations.add.command": "Slash Command", "backstage_sidebar.integrations.add.incomingWebhook": "Incoming Webhook", "backstage_sidebar.integrations.add.outgoingWebhook": "Outgoing Webhook", "backstage_sidebar.integrations.installed": "Installed Integrations", @@ -847,11 +850,11 @@ "get_team_invite_link_modal.title": "Team Invite Link", "installed_integrations.add": "Add Integration", "installed_integrations.allFilter": "All ({count})", + "installed_integrations.commandType": "(Slash Command)", + "installed_integrations.commandsFilter": "Slash Commands ({count})", "installed_integrations.creation": "Created by {creator} on {createAt, date, full}", "installed_integrations.delete": "Delete", "installed_integrations.header": "Installed Integrations", - "installed_integrations.commandType": "(Slash Command)", - "installed_integrations.commandsFilter": "Slash Commands ({count})", "installed_integrations.incomingWebhookType": "(Incoming Webhook)", "installed_integrations.incomingWebhooksFilter": "Incoming Webhooks ({count})", "installed_integrations.outgoingWebhookType": "(Outgoing Webhook)", @@ -1420,4 +1423,4 @@ "web.footer.terms": "Terms", "web.header.back": "Back", "web.root.singup_info": "All team communication in one place, searchable and accessible anywhere" -} +}
\ No newline at end of file diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json index ab44f1ea1..f3c075b48 100644 --- a/webapp/i18n/es.json +++ b/webapp/i18n/es.json @@ -27,6 +27,7 @@ "activity_log_modal.android": "Android", "activity_log_modal.androidNativeApp": "Android App Nativa", "activity_log_modal.iphoneNativeApp": "iPhone App Nativa", + "add_command.autocomplete": "Autocompletar", "add_command.autocomplete.help": "Mostrar este comando en la lista de auto completado.", "add_command.autocompleteDescription": "Descripción del Autocompletado", "add_command.autocompleteDescription.help": "Descripción corta opcional para la lista de autocompletado del comando de barra.", @@ -34,8 +35,12 @@ "add_command.autocompleteHint": "Pista del Autocompletado", "add_command.autocompleteHint.help": "Pista opcional que aparece como paramentros necesarios en la lista de autocompletado para el comando.", "add_command.autocompleteHint.placeholder": "Ejemplo: [Nombre del Paciente]", + "add_command.description": "Descripción", + "add_command.displayName": "Nombre a mostrar", + "add_command.header": "Agragar un Comando de Barra", "add_command.iconUrl": "Icono de Respuesta", "add_command.iconUrl.help": "Escoge una imagen de perfil que reemplazara los mensajes publicados por este comando de barra. Ingresa el URL de un archivo .png o .jpg de al menos 128 x 128 pixels.", + "add_command.iconUrl.placeholder": "https://www.example.com/myicon.png", "add_command.method": "Método de Solicitud", "add_command.method.get": "GET", "add_command.method.help": "El tipo de comando que se utiliza al hacer una solicitud al URL.", @@ -44,9 +49,11 @@ "add_command.trigger.help1": "Ejemplos: /paciente, /cliente, /empleado", "add_command.trigger.help2": "Reservadas: /echo, /join, /logout, /me, /shrug", "add_command.trigger.placeholder": "Gatillador del Comando ej. \"hola\" no se debe incluir la barra", + "add_command.triggerRequired": "Se requiere una palabra gatilladora", "add_command.url": "URL de Solicitud", "add_command.url.help": "El URL para recibir el evento de la solicitud HTTP POST o GET cuando se ejecuta el comando de barra.", "add_command.url.placeholder": "Debe comenzar con http:// o https://", + "add_command.urlRequired": "Se requiere un URL a donde llegará la solicitud", "add_command.username": "Nombre de usuario de Respuesta", "add_command.username.help": "Escoge un nombre de usuario que reemplazara los mensajes publicados por este comando de barra. Los nombres de usuario pueden tener hasta 22 caracteres y contener letras en minúsculas, números y los siguientes símbolos \"-\", \"_\", y \".\" .", "add_command.username.placeholder": "Nombre de usuario", @@ -57,6 +64,8 @@ "add_incoming_webhook.header": "Agregar un Webhook de Entrada", "add_incoming_webhook.name": "Nombre", "add_incoming_webhook.save": "Guardar", + "add_integration.command.description": "Crea un comando de barra para enviar eventos a una integración externa y recibir una respuesta.", + "add_integration.command.title": "Comando de Barra", "add_integration.header": "Agregar Integración", "add_integration.incomingWebhook.description": "Crea webhook URLs para utilizarlas con integraciones externas.", "add_integration.incomingWebhook.title": "Webhook de Entrada", @@ -611,6 +620,7 @@ "backstage_navbar.backToMattermost": "Volver a {siteName}", "backstage_sidebar.integrations": "Integraciones", "backstage_sidebar.integrations.add": "Agregar Integración", + "backstage_sidebar.integrations.add.command": "Comando de Barra", "backstage_sidebar.integrations.add.incomingWebhook": "Webhook de Entrada", "backstage_sidebar.integrations.add.outgoingWebhook": "Webhook de Salida", "backstage_sidebar.integrations.installed": "Integraciones Instaladas", @@ -837,6 +847,8 @@ "get_team_invite_link_modal.title": "Enlace de Invitación al Equipo", "installed_integrations.add": "Agregar Integración", "installed_integrations.allFilter": "Todos ({count})", + "installed_integrations.commandType": "(Comando de Barra)", + "installed_integrations.commandsFilter": "Comandos de Barra ({count})", "installed_integrations.creation": "Creado por {creator} el {createAt, date, full}", "installed_integrations.delete": "Eliminar", "installed_integrations.header": "Integraciones Instaladas", @@ -1408,4 +1420,4 @@ "web.footer.terms": "Términos", "web.header.back": "Atrás", "web.root.singup_info": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte" -} +}
\ No newline at end of file diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss index c518f3729..7ed1e5b3e 100644 --- a/webapp/sass/responsive/_mobile.scss +++ b/webapp/sass/responsive/_mobile.scss @@ -726,7 +726,7 @@ top: 0; z-index: 0; } - + &.move--right { @include translate3d(290px, 0, 0); @@ -883,8 +883,9 @@ .backstage-list__item { display: block; + .item-actions, .actions { - margin-top: 10px; + margin-top: 15px; padding: 0; } } diff --git a/webapp/sass/routes/_admin-console.scss b/webapp/sass/routes/_admin-console.scss index 1878ba19c..73e8f816c 100644 --- a/webapp/sass/routes/_admin-console.scss +++ b/webapp/sass/routes/_admin-console.scss @@ -357,11 +357,15 @@ } .member-list-holder { - .member-role, - .member-drop { - position: absolute; - right: 15px; - top: 8px; + background: $white; + margin-bottom: 4em; + overflow: visible; + padding: 5px 0; + + .more-modal__row { + &:last-child { + border: none; + } } } } diff --git a/webapp/sass/routes/_backstage.scss b/webapp/sass/routes/_backstage.scss index 9685f3aef..9b115a132 100644 --- a/webapp/sass/routes/_backstage.scss +++ b/webapp/sass/routes/_backstage.scss @@ -199,27 +199,27 @@ body { .item-details__row + .item-details__row { @include clearfix; - margin-top: 10px; text-overflow: ellipsis; } .item-details__name { font-weight: 600; - margin-bottom: 1em; } .item-details__type { margin-left: 6px; } - .item-details__description { - color: $dark-gray; - margin-bottom: 1em; - } - + .item-details__description, .item-details__creation { color: $dark-gray; - margin-bottom: 1em; + display: inline-block; + margin-top: 10px; + vertical-align: top; + + &:empty { + display: none; + } } .item-actions { |