summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/README.md4
-rw-r--r--doc/help/Slack-Import.md25
-rw-r--r--doc/install/Production-Ubuntu.md2
-rw-r--r--doc/install/SMTP-Email-Setup.md76
-rw-r--r--web/react/components/access_history_modal.jsx299
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx6
-rw-r--r--web/react/components/create_comment.jsx12
-rw-r--r--web/react/utils/markdown.jsx2
-rw-r--r--web/react/utils/text_formatting.jsx2
-rw-r--r--web/sass-files/sass/partials/_post_right.scss5
10 files changed, 359 insertions, 74 deletions
diff --git a/doc/README.md b/doc/README.md
index 7ee2bb46c..ccb702a5d 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -8,15 +8,17 @@ Get up and running quickly with Docker-based install
- [AWS Elastic Beanstalk Setup](install/Amazon-Elastic-Beanstalk.md)
- [Docker Single Container Preview Setup](install/Docker-Single-Container.md)
- [SMTP Email Setup](install/SMTP-Email-Setup.md)
+- [System Console Settings](install/Configuration-Settings.md)
#### Production Installation
Set up Mattermost in your data center
- [Software and Hardware Requirements](install/Requirements.md)
- [Production Ubuntu Setup](install/Production-Ubuntu.md)
- [SMTP Email Setup](install/SMTP-Email-Setup.md)
+- [System Console Settings](install/Configuration-Settings.md)
#### Configuration and Management
-- Configuration Settings Overview
+- [System Console Settings](install/Configuration-Settings.md)
- [GitLab SSO Configuration](integrations/Single-Sign-On/Gitlab.md)
- [Mattermost Release Numbering Scheme](install/Release-Numbering.md)
diff --git a/doc/help/Slack-Import.md b/doc/help/Slack-Import.md
index a2914570b..f834d5177 100644
--- a/doc/help/Slack-Import.md
+++ b/doc/help/Slack-Import.md
@@ -1,22 +1,29 @@
-### Slack Import (Beta)
+### Slack Import
-*Note: As a SaaS service, Slack is able to change its export format quickly. If you encounter issues not mentioned in the documentation below, please let us know by [filing an issue](https://github.com/mattermost/platform/issues).*
+*Note: As a proprietary SaaS service, Slack is able to change its export format quickly and without notice. If you encounter issues not mentioned in the documentation below, please alert the product team by [filing an issue](https://github.com/mattermost/platform/issues).*
#### Usage
-The Slack Import feature in Mattermost is in "Beta" and focus is on supporting migration of teams of less than 100 registered users. The feature can be accessed from by Team Administrators and Team Owners via the `Team Settings -> Import` menu option.
+The Slack Import feature in Mattermost is in "Beta" and focus is on supporting migration of teams of less than 100 registered users. To use:
-- **It is highly recommended that you test Slack import before applying it to an instance intended for production.** If you use Docker, you can spin up a test instance in one line (`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`). If you don't use Docker, there are [step-by-step instructions to install Mattermost in preview mode in less than 5 minutes](../install/Docker-Single-Container.md).
+1. Generate a Slack "Export" file from **Slack > Team Settings > Import/Export Data > Export > Start Export**
-Mattermost currently supports the processing of an "Export" file from Slack containing account information and public channel archives from a Slack team.
+2. In Mattermost go to **Team Settings > Import > Import from Slack**. _Team Owner_ or _Team Administrator_ role is required to access this menu option.
-- Emails and usernames from Slack are used to create new Mattermost accounts, connected to messages history in imported Slack channels. Users can activate these accounts and by going to the Password Reset screen in Mattermost to set new credentials.
-- Once logged in, users will have access to previous Slack messages shared in public channels, now imported to Mattermost.
+3. Click **Select file** to upload Slack export file and click **Import**.
-#### Limitations:
+4. Emails and usernames from Slack are used to create new Mattermost accounts
+
+5. Slack users can activate their new Mattermost accounts by using Mattermost's Password Reset screen with their email addresses from Slack to set new passwords for their Mattermost accounts
+
+6. Once logged in, the Mattermost users will have access to previous Slack messages in the public channels imported from Slack.
+
+**It is highly recommended that you test Slack import before applying it to an instance intended for production.** If you use Docker, you can spin up a test instance in one line (`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform`). If you don't use Docker, there are [step-by-step instructions](../install/Docker-Single-Container.md) to install Mattermost in preview mode in less than 5 minutes.
+
+#### Notes:
- Newly added markdown suppport in Slack's Posts 2.0 feature announced on September 28, 2015 is not yet supported.
- Slack does not export files or images your team has stored in Slack's database. Mattermost will provide links to the location of your assets in Slack's web UI.
- Slack does not export any content from private groups or direct messages that your team has stored in Slack's database.
-- The Beta release of Slack Import does not offer pre-checks or roll-back and will not import Slack accounts with username or email address collisions with existing Mattermost accounts. Also, Slack channel names with underscores will not import. Also, mentions do not yet resolve as Mattermost usernames (still shows Slack ID).
+- In Beta, Slack accounts with username or email address collisions with existing Mattermost accounts will not import and mentions do not resolve as Mattermost usernames (still shows Slack ID). No pre-check or roll-back is currently offered.
diff --git a/doc/install/Production-Ubuntu.md b/doc/install/Production-Ubuntu.md
index 05a56c412..87f8edb84 100644
--- a/doc/install/Production-Ubuntu.md
+++ b/doc/install/Production-Ubuntu.md
@@ -28,7 +28,7 @@
## Set up Mattermost Server
1. For the purposes of this guide we will assume this server has an IP address of 10.10.10.2
1. Download the latest Mattermost Server by typing:
- * ``` wget https://github.com/mattermost/platform/releases/download/v1.0.0/mattermost.tar.gz```
+ * ``` wget https://github.com/mattermost/platform/releases/download/v1.1.0/mattermost.tar.gz```
1. Unzip the Mattermost Server by typing:
* ``` tar -xvzf mattermost.tar.gz```
1. For the sake of making this guide simple we located the files at `/home/ubuntu/mattermost`. In the future we will give guidance for storing under `/opt`.
diff --git a/doc/install/SMTP-Email-Setup.md b/doc/install/SMTP-Email-Setup.md
index fcf012804..4e06d2f99 100644
--- a/doc/install/SMTP-Email-Setup.md
+++ b/doc/install/SMTP-Email-Setup.md
@@ -1,50 +1,46 @@
## SMTP Email Setup
-In some product evaluation setups email is intentionally bypassed using a `ByPassEmail=true` option. This option allows account creation and system operation without having to set up an email service (e.g. no email verification is required for account creation). This also means neither email notifications nor password reset by email are available.
-
-To enable email, turn this option off by setting `ByPassEmail=false` and configuring an SMTP email service as follows:
-
-1. **Set up an SMTP email sending service.** (If you already have credentials for a SMTP server you can skip this step.)
- 1. [Setup Amazon Simple Email Service](https://console.aws.amazon.com/ses)
- 2. From the `SMTP Settings` menu click `Create My SMTP Credentials`
- 3. Copy the `Server Name`, `Port`, `SMTP Username`, and `SMTP Password`
- 4. From the `Domains` menu setup and verify a new domain. It it also a good practice to enable `Generate DKIM Settings` for this domain.
- 5. Choose an email address like `feedback@example.com` for Mattermost to send emails from.
- 6. Test sending an email from `feedback@example.com` by clicking the `Send a Test Email` button and verify everything appears to be working correctly.
-2. **Modify the Mattermost configuration file config.json or config_docker.json with the SMTP information.**
- 1. If you're running Mattermost on Amazon Beanstalk you can shell into the instance with the following commands
- 2. `ssh ec2-user@[domain for the docker instance]`
- 3. `sudo gpasswd -a ec2-user docker`
- 4. Retrieve the name of the container with `sudo docker ps`
- 5. `sudo docker exec -ti container_name /bin/bash`
-3. **Edit the config file `vi /config_docker.json` with the settings you captured from the step above.**
- 1. See an example below and notice `ByPassEmail` has been set to `false`
- ``` bash
- "EmailSettings": {
- "ByPassEmail" : false,
- "SMTPUsername": "AKIADTOVBGERKLCBV",
- "SMTPPassword": "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY",
- "SMTPServer": "email-smtp.us-east-1.amazonaws.com:465",
- "UseTLS": true,
- "FeedbackEmail": "feedback@example.com",
- "FeedbackName": "Feedback",
- "ApplePushServer": "",
- "ApplePushCertPublic": "",
- "ApplePushCertPrivate": ""
- }
- ```
-4. **Restart Mattermost**
- 1. Find the process id with `ps -A` and look for the process named `platform`
- 2. Kill the process `kill pid`
- 3. The service should restart automatically. Verify the Mattermost service is running with `ps -A`
- 4. Current logged in users will not be affected, but upon logging out or session expiration users will be required to verify their email address.
+In product evaluation setups with single-container Docker instances, email is intentionally disabled. This allows account creation and system operation without having to set up email, but it also means email notification and password reset functionality aren't available.
+
+### How to enable email
+
+To enable email, configure an SMTP email service as follows:
+
+1. **Set up an SMTP email sending service** (if you don't yet have an SMTP service with credentials)
+ 1. Any SMTP email service can be used, you just need the following information: `Server Name`, `Port`, `SMTP Username`, and `SMTP Password`.
+ 2. If you don't have an SMTP service, here are simple instructions to set one up with [Amazon Simple Email Service (SES)](https://aws.amazon.com/ses/):
+ 2. Go to [Amazon SES console](https://console.aws.amazon.com/ses) then `SMTP Settings > Create My SMTP Credentials`
+ 3. Copy the `Server Name`, `Port`, `SMTP Username`, and `SMTP Password` for Step 2 below.
+ 4. From the `Domains` menu set up and verify a new domain, then enable `Generate DKIM Settings` for the domain.
+ 5. Choose an sender address like `mattermost@example.com` and click `Send a Test Email` to verify setup is working correctly.
+2. **Configure SMTP settings**
+ 1. Open the **System Console** by logging into an existing team and accessing "System Console" from the main menu.
+ 1. Alternatively, if a team doesn't yet exist, go to `http://dockerhost:8065/` in your browser, create a team, then from the main menu click **System Console**
+ 2. Go to the **Email Settings** tab and configure the following:
+ 1. **Allow Sign Up With Email:** `true`
+ 2. **Send Email Notifications:** `true`
+ 3. **Require Email Verification:** `true`
+ 4. **Notification Display Name:** Display name on email account sending notifications
+ 5. **Notification Email Address:** Email address displayed on email account used to send notifications
+ 6. **SMTP Username**: `SMTP Username` from Step 1
+ 7. **SMTP Password**: `SMTP Password` from Step 1
+ 8. **SMTP Server**: `SMTP Server` from Step 1
+ 9. **SMTP Port**: `SMTP Port` from Step 1
+ 10. **Connection Security**: `TLS (Recommended)`
+ 11. Then click **Save**
+
+3. **Restart Mattermost**
+ 1. Use `ps -A` to find the process ID ("pid") for service named `platform` and stop it using `kill [pid]`
+ 2. The service should restart automatically. Run `ps -A` to verify the `platform` is running again
+ 3. Use the reset password page (E.g. _example.com/teamname/reset_password_) to test that email is now working by entering your email and clicking **Reset my password**.
+ 4. Note: The next time users log out, or when their session tokens expire, each will be required to verify their email address.
### Troubleshooting SMTP
-If you have issues with your SMTP install, from your Mattermost team site go to the main menu and open `System Console > Logs` to look for error messages related to your setup. You can do a search for the error code to narrow down the issue. Sometimes ISPs require nuanced setups for SMTP and error codes can hint at how to make the proper adjustments.
+If you have issues with your SMTP install, from your Mattermost team site go to the main menu and open **System Console -> Logs** to look for error messages related to your setup. You can do a search for the error code to narrow down the issue. Sometimes ISPs require nuanced setups for SMTP and error codes can hint at how to make the proper adjustments.
-For example, if `System Console > Logs` has an error code reading:
+For example, if **System Console -> Logs** has an error code reading:
```
Connection unsuccessful: Failed to add to email address - 554 5.7.1 <unknown[IP-ADDRESS]>: Client host rejected: Access denied
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx
index 2ad4d5b00..909639859 100644
--- a/web/react/components/access_history_modal.jsx
+++ b/web/react/components/access_history_modal.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
var UserStore = require('../stores/user_store.jsx');
+var ChannelStore = require('../stores/channel_store.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var LoadingScreen = require('./loading_screen.jsx');
var Utils = require('../utils/utils.jsx');
@@ -14,8 +15,10 @@ export default class AccessHistoryModal extends React.Component {
this.handleMoreInfo = this.handleMoreInfo.bind(this);
this.onHide = this.onHide.bind(this);
this.onShow = this.onShow.bind(this);
+ this.formatAuditInfo = this.formatAuditInfo.bind(this);
+ this.handleRevokedSession = this.handleRevokedSession.bind(this);
- let state = this.getStateFromStoresForAudits();
+ const state = this.getStateFromStoresForAudits();
state.moreInfo = [];
this.state = state;
@@ -52,23 +55,269 @@ export default class AccessHistoryModal extends React.Component {
newMoreInfo[index] = true;
this.setState({moreInfo: newMoreInfo});
}
- render() {
- var accessList = [];
- var currentHistoryDate = null;
+ handleRevokedSession(sessionId) {
+ return 'The session with id ' + sessionId + ' was revoked';
+ }
+ formatAuditInfo(currentAudit) {
+ const currentActionURL = currentAudit.action.replace(/\/api\/v[1-9]/, '');
- for (var i = 0; i < this.state.audits.length; i++) {
- var currentAudit = this.state.audits[i];
- var newHistoryDate = new Date(currentAudit.create_at);
- var newDate = null;
+ let currentAuditDesc = '';
+
+ if (currentActionURL.indexOf('/channels') === 0) {
+ const channelInfo = currentAudit.extra_info.split(' ');
+ const channelNameField = channelInfo[0].split('=');
+
+ let channelURL = '';
+ let channelObj;
+ let channelName = '';
+ if (channelNameField.indexOf('name') >= 0) {
+ channelURL = channelNameField[channelNameField.indexOf('name') + 1];
+ channelObj = ChannelStore.getByName(channelURL);
+ if (channelObj) {
+ channelName = channelObj.display_name;
+ } else {
+ channelName = channelURL;
+ }
+ }
+
+ switch (currentActionURL) {
+ case '/channels/create':
+ currentAuditDesc = 'Created the ' + channelName + ' channel/group';
+ break;
+ case '/channels/create_direct':
+ currentAuditDesc = 'Established a direct message channel with ' + Utils.getDirectTeammate(channelObj.id).username;
+ break;
+ case '/channels/update':
+ currentAuditDesc = 'Updated the ' + channelName + ' channel/group name';
+ break;
+ case '/channels/update_desc':
+ currentAuditDesc = 'Updated the ' + channelName + ' channel/group description';
+ break;
+ default:
+ let userIdField = [];
+ let userId = '';
+ let username = '';
+
+ if (channelInfo[1]) {
+ userIdField = channelInfo[1].split('=');
+
+ if (userIdField.indexOf('user_id') >= 0) {
+ userId = userIdField[userIdField.indexOf('user_id') + 1];
+ username = UserStore.getProfile(userId).username;
+ }
+ }
+
+ if (/\/channels\/[A-Za-z0-9]+\/delete/.test(currentActionURL)) {
+ currentAuditDesc = 'Deleted the channel/group with the URL ' + channelURL;
+ } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(currentActionURL)) {
+ currentAuditDesc = 'Added ' + username + ' to the ' + channelName + ' channel/group';
+ } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(currentActionURL)) {
+ currentAuditDesc = 'Removed ' + username + ' from the ' + channelName + ' channel/group';
+ }
+
+ break;
+ }
+ } else if (currentActionURL.indexOf('/oauth') === 0) {
+ const oauthInfo = currentAudit.extra_info.split(' ');
+
+ switch (currentActionURL) {
+ case '/oauth/register':
+ const clientIdField = oauthInfo[0].split('=');
+
+ if (clientIdField[0] === 'client_id') {
+ currentAuditDesc = 'Attempted to register a new OAuth Application with ID ' + clientIdField[1];
+ }
+
+ break;
+ case '/oauth/allow':
+ if (oauthInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to allow a new OAuth service access';
+ } else if (oauthInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully gave a new OAuth service access';
+ } else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
+ currentAuditDesc = 'Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback';
+ }
+
+ break;
+ case '/oauth/access_token':
+ if (oauthInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to get an OAuth access token';
+ } else if (oauthInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully added a new OAuth service';
+ } else {
+ const oauthTokenFailure = oauthInfo[0].split('-');
+
+ if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
+ currentAuditDesc = 'Failed to get an OAuth access token - ' + oauthTokenFailure[1].trim();
+ }
+ }
+
+ break;
+ default:
+ break;
+ }
+ } else if (currentActionURL.indexOf('/users') === 0) {
+ const userInfo = currentAudit.extra_info.split(' ');
+
+ switch (currentActionURL) {
+ case '/users/login':
+ if (userInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to login';
+ } else if (userInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully logged in';
+ } else if (userInfo[0]) {
+ currentAuditDesc = 'FAILED login attempt';
+ }
+
+ break;
+ case '/users/revoke_session':
+ currentAuditDesc = this.handleRevokedSession(userInfo[0].split('=')[1]);
+ break;
+ case '/users/newimage':
+ currentAuditDesc = 'Updated your profile picture';
+ break;
+ case '/users/update':
+ currentAuditDesc = 'Updated the general settings of your account';
+ break;
+ case '/users/newpassword':
+ if (userInfo[0] === 'attempted') {
+ currentAuditDesc = 'Attempted to change password';
+ } else if (userInfo[0] === 'completed') {
+ currentAuditDesc = 'Successfully changed password';
+ } else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
+ currentAuditDesc = 'Failed to change password - tried to update user password who was logged in through oauth';
+ }
+
+ break;
+ case '/users/update_roles':
+ const userRoles = userInfo[0].split('=')[1];
- if (!currentHistoryDate || currentHistoryDate.toLocaleDateString() !== newHistoryDate.toLocaleDateString()) {
- currentHistoryDate = newHistoryDate;
- newDate = (<div> {currentHistoryDate.toDateString()} </div>);
+ currentAuditDesc = 'Updated user role(s) to ';
+ if (userRoles.trim()) {
+ currentAuditDesc += userRoles;
+ } else {
+ currentAuditDesc += 'member';
+ }
+
+ break;
+ case '/users/update_active':
+ const updateType = userInfo[0].split('=')[0];
+ const updateField = userInfo[0].split('=')[1];
+
+ /* Either describes account activation/deactivation or a revoked session as part of an account deactivation */
+ if (updateType === 'active') {
+ if (updateField === 'true') {
+ currentAuditDesc = 'Account made active';
+ } else if (updateField === 'false') {
+ currentAuditDesc = 'Account made inactive';
+ }
+
+ const actingUserInfo = userInfo[1].split('=');
+ if (actingUserInfo[0] === 'session_user') {
+ const actingUser = UserStore.getProfile(actingUserInfo[1]);
+ const currentUser = UserStore.getCurrentUser();
+ if (currentUser && actingUser && (Utils.isAdmin(currentUser.roles) || Utils.isSystemAdmin(currentUser.roles))) {
+ currentAuditDesc += ' by ' + actingUser.username;
+ } else if (currentUser && actingUser) {
+ currentAuditDesc += ' by an admin';
+ }
+ }
+ } else if (updateType === 'session_id') {
+ currentAuditDesc = this.handleRevokedSession(updateField);
+ }
+
+ break;
+ case '/users/send_password_reset':
+ currentAuditDesc = 'Sent an email to ' + userInfo[0].split('=')[1] + ' to reset your password';
+ break;
+ case '/users/reset_password':
+ if (userInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to reset password';
+ } else if (userInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully reset password';
+ }
+
+ break;
+ case '/users/update_notify':
+ currentAuditDesc = 'Updated your global notification settings';
+ break;
+ default:
+ break;
}
+ } else if (currentActionURL.indexOf('/hooks') === 0) {
+ const webhookInfo = currentAudit.extra_info.split(' ');
+
+ switch (currentActionURL) {
+ case '/hooks/incoming/create':
+ if (webhookInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to create a webhook';
+ } else if (webhookInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully created a webhook';
+ } else if (webhookInfo[0] === 'fail - bad channel permissions') {
+ currentAuditDesc = 'Failed to create a webhook - bad channel permissions';
+ }
- if (!currentAudit.session_id && currentAudit.action.search('/users/login') !== -1) {
- currentAudit.session_id = 'N/A (Login attempt)';
+ break;
+ case '/hooks/incoming/delete':
+ if (webhookInfo[0] === 'attempt') {
+ currentAuditDesc = 'Attempted to delete a webhook';
+ } else if (webhookInfo[0] === 'success') {
+ currentAuditDesc = 'Successfully deleted a webhook';
+ } else if (webhookInfo[0] === 'fail - inappropriate conditions') {
+ currentAuditDesc = 'Failed to delete a webhook - inappropriate conditions';
+ }
+
+ break;
+ default:
+ break;
+ }
+ } else {
+ switch (currentActionURL) {
+ case '/logout':
+ currentAuditDesc = 'Logged out of your account';
+ break;
+ case '/verify_email':
+ currentAuditDesc = 'Sucessfully verified your email address';
+ break;
+ default:
+ break;
}
+ }
+
+ /* If all else fails... */
+ if (!currentAuditDesc) {
+ /* Currently not called anywhere */
+ if (currentAudit.extra_info.indexOf('revoked_all=') >= 0) {
+ currentAuditDesc = 'Revoked all current sessions for the team';
+ } else {
+ let currentActionDesc = '';
+ if (currentActionURL && currentActionURL.lastIndexOf('/') !== -1) {
+ currentActionDesc = currentActionURL.substring(currentActionURL.lastIndexOf('/') + 1).replace('_', ' ');
+ currentActionDesc = Utils.toTitleCase(currentActionDesc);
+ }
+
+ let currentExtraInfoDesc = '';
+ if (currentAudit.extra_info) {
+ currentExtraInfoDesc = currentAudit.extra_info;
+
+ if (currentExtraInfoDesc.indexOf('=') !== -1) {
+ currentExtraInfoDesc = currentExtraInfoDesc.substring(currentExtraInfoDesc.indexOf('=') + 1);
+ }
+ }
+ currentAuditDesc = currentActionDesc + ' ' + currentExtraInfoDesc;
+ }
+ }
+
+ const currentDate = new Date(currentAudit.create_at);
+ const currentAuditInfo = currentDate.toDateString() + ' - ' + currentDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute: '2-digit'}) + ' | ' + currentAuditDesc;
+ return currentAuditInfo;
+ }
+ render() {
+ var accessList = [];
+
+ for (var i = 0; i < this.state.audits.length; i++) {
+ const currentAudit = this.state.audits[i];
+ const currentAuditInfo = this.formatAuditInfo(currentAudit);
var moreInfo = (
<a
@@ -76,15 +325,27 @@ export default class AccessHistoryModal extends React.Component {
className='theme'
onClick={this.handleMoreInfo.bind(this, i)}
>
- More info
+ {'More info'}
</a>
);
if (this.state.moreInfo[i]) {
+ if (!currentAudit.session_id) {
+ currentAudit.session_id = 'N/A';
+
+ if (currentAudit.action.search('/users/login') >= 0) {
+ if (currentAudit.extra_info === 'attempt') {
+ currentAudit.session_id += ' (Login attempt)';
+ } else {
+ currentAudit.session_id += ' (Login failure)';
+ }
+ }
+ }
+
moreInfo = (
<div>
+ <div>{'IP: ' + currentAudit.ip_address}</div>
<div>{'Session ID: ' + currentAudit.session_id}</div>
- <div>{'URL: ' + currentAudit.action.replace(/\/api\/v[1-9]/, '')}</div>
</div>
);
}
@@ -99,11 +360,9 @@ export default class AccessHistoryModal extends React.Component {
key={'accessHistoryEntryKey' + i}
className='access-history__table'
>
- <div className='access__date'>{newDate}</div>
<div className='access__report'>
- <div className='report__time'>{newHistoryDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute: '2-digit'})}</div>
+ <div className='report__time'>{currentAuditInfo}</div>
<div className='report__info'>
- <div>{'IP: ' + currentAudit.ip_address}</div>
{moreInfo}
</div>
{divider}
@@ -138,13 +397,13 @@ export default class AccessHistoryModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>&times;</span>
+ <span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'
id='myModalLabel'
>
- Access History
+ {'Access History'}
</h4>
</div>
<div
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index bc6ad1931..4c2a473b6 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -100,6 +100,7 @@ export default class AdminSidebar extends React.Component {
className='menu-icon--right menu__close'
onClick={this.removeTeam.bind(this, team.id)}
style={{cursor: 'pointer'}}
+ title='Remove team from sidebar menu'
>
{'x'}
</span>
@@ -233,7 +234,10 @@ export default class AdminSidebar extends React.Component {
href='#'
onClick={this.showTeamSelect}
>
- <i className='fa fa-plus'></i>
+ <i
+ className='fa fa-plus'
+ title='Add team to sidebar menu'
+ ></i>
</a>
</span>
</h4>
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index 680d693f1..2ac5d2179 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -255,6 +255,17 @@ export default class CreateComment extends React.Component {
postFooterClassName += ' has-error';
}
+ let uploadsInProgressText = null;
+ if (this.state.uploadsInProgress.length > 0) {
+ uploadsInProgressText = (
+ <span
+ className='pull-right post-right-comments-upload-in-progress'
+ >
+ {this.state.uploadsInProgress.length === 1 ? 'File uploading' : 'Files uploading'}
+ </span>
+ );
+ }
+
return (
<form onSubmit={this.handleSubmit}>
<div className='post-create'>
@@ -295,6 +306,7 @@ export default class CreateComment extends React.Component {
value='Add Comment'
onClick={this.handleSubmit}
/>
+ {uploadsInProgressText}
{postError}
{serverError}
</div>
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index 12d6dd424..2813798d2 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -32,7 +32,7 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
link(href, title, text) {
let outHref = href;
- if (outHref.lastIndexOf('http', 0) !== 0) {
+ if (!(/^(mailto|https?|ftp)/.test(outHref))) {
outHref = `http://${outHref}`;
}
diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx
index 2b6e6e14e..d79aeed68 100644
--- a/web/react/utils/text_formatting.jsx
+++ b/web/react/utils/text_formatting.jsx
@@ -89,7 +89,7 @@ function autolinkUrls(text, tokens) {
if (match.getType() === 'email') {
url = `mailto:${url}`;
- } else if (url.lastIndexOf('http', 0) !== 0) {
+ } else if (!(/^(mailto|https?|ftp)/.test(url))) {
url = `http://${url}`;
}
diff --git a/web/sass-files/sass/partials/_post_right.scss b/web/sass-files/sass/partials/_post_right.scss
index e4860b286..b72176a11 100644
--- a/web/sass-files/sass/partials/_post_right.scss
+++ b/web/sass-files/sass/partials/_post_right.scss
@@ -36,6 +36,11 @@
.post-create-footer {
padding-top: 10px;
}
+ .post-right-comments-upload-in-progress {
+ padding: 6px 0;
+ color: #a8adb7;
+ margin-right: 10px;
+ }
}
}