Commit 16a01961 authored by catalintomai's avatar catalintomai Committed by Catalin Tomai
Browse files

Add metric warning support (announcement bar and DM) (#5447)



* Admin Advisory: Add warning for number of active users metric status
Co-authored-by: default avatarCatalin Tomai <catalin.tomai@mattermost.com>
parent 71686044
......@@ -449,6 +449,14 @@ export function handleEvent(msg) {
handleGroupNotAssociatedToChannelEvent(msg);
break;
case SocketEvents.WARN_METRIC_STATUS_RECEIVED:
handleWarnMetricStatusReceivedEvent(msg);
break;
case SocketEvents.WARN_METRIC_STATUS_REMOVED:
handleWarnMetricStatusRemovedEvent(msg);
break;
default:
}
......@@ -1220,3 +1228,20 @@ function handleGroupNotAssociatedToChannelEvent(msg) {
data: {channelID: msg.broadcast.channel_id, groups: [{id: msg.data.group_id}]},
});
}
function handleWarnMetricStatusReceivedEvent(msg) {
store.dispatch(batchActions([
{
type: GeneralTypes.WARN_METRIC_STATUS_RECEIVED,
data: JSON.parse(msg.data.warnMetricStatus),
},
{
type: ActionTypes.SHOW_NOTICE,
data: [AnnouncementBarMessages.NUMBER_OF_ACTIVE_USERS_WARN_METRIC_STATUS],
},
]));
}
function handleWarnMetricStatusRemovedEvent(msg) {
store.dispatch({type: GeneralTypes.WARN_METRIC_STATUS_REMOVED, data: {id: msg.data.warnMetricId}});
}
......@@ -31,6 +31,9 @@ exports[`components/AnnouncementBar should match snapshot, bar not showing 1`] =
<injectIntl(FormattedMarkdownMessage)
id="text"
/>
<span
className="announcement-bar__link"
/>
</span>
</OverlayTrigger>
</div>
......@@ -67,6 +70,9 @@ exports[`components/AnnouncementBar should match snapshot, bar showing 1`] = `
<injectIntl(FormattedMarkdownMessage)
id="text"
/>
<span
className="announcement-bar__link"
/>
</span>
</OverlayTrigger>
</div>
......@@ -103,6 +109,9 @@ exports[`components/AnnouncementBar should match snapshot, bar showing, no dismi
<injectIntl(FormattedMarkdownMessage)
id="text"
/>
<span
className="announcement-bar__link"
/>
</span>
</OverlayTrigger>
</div>
......@@ -139,6 +148,9 @@ exports[`components/AnnouncementBar should match snapshot, dismissal 1`] = `
<injectIntl(FormattedMarkdownMessage)
id="text"
/>
<span
className="announcement-bar__link"
/>
</span>
</OverlayTrigger>
</div>
......@@ -175,6 +187,9 @@ exports[`components/AnnouncementBar should match snapshot, dismissal 2`] = `
<injectIntl(FormattedMarkdownMessage)
id="text"
/>
<span
className="announcement-bar__link"
/>
</span>
</OverlayTrigger>
</div>
......@@ -211,6 +226,9 @@ exports[`components/AnnouncementBar should match snapshot, dismissal 3`] = `
<injectIntl(FormattedMarkdownMessage)
id="text"
/>
<span
className="announcement-bar__link"
/>
</span>
</OverlayTrigger>
</div>
......@@ -247,6 +265,9 @@ exports[`components/AnnouncementBar should match snapshot, props change 1`] = `
<injectIntl(FormattedMarkdownMessage)
id="text"
/>
<span
className="announcement-bar__link"
/>
</span>
</OverlayTrigger>
</div>
......@@ -283,6 +304,9 @@ exports[`components/AnnouncementBar should match snapshot, props change 2`] = `
<injectIntl(FormattedMarkdownMessage)
id="text"
/>
<span
className="announcement-bar__link"
/>
</span>
</OverlayTrigger>
</div>
......@@ -319,6 +343,9 @@ exports[`components/AnnouncementBar should match snapshot, props change 3`] = `
<injectIntl(FormattedMarkdownMessage)
id="text"
/>
<span
className="announcement-bar__link"
/>
</span>
</OverlayTrigger>
</div>
......@@ -355,6 +382,9 @@ exports[`components/AnnouncementBar should match snapshot, props change 4`] = `
<injectIntl(FormattedMarkdownMessage)
id="text"
/>
<span
className="announcement-bar__link"
/>
</span>
</OverlayTrigger>
</div>
......
......@@ -20,6 +20,7 @@ export default class AnnouncementBarController extends React.PureComponent {
canViewSystemErrors: PropTypes.bool.isRequired,
latestError: PropTypes.object,
totalUsers: PropTypes.number,
warnMetricsStatus: PropTypes.object,
actions: PropTypes.shape({
dismissError: PropTypes.func.isRequired,
}).isRequired,
......@@ -61,6 +62,7 @@ export default class AnnouncementBarController extends React.PureComponent {
canViewSystemErrors={this.props.canViewSystemErrors}
totalUsers={this.props.totalUsers}
user={this.props.user}
warnMetricsStatus={this.props.warnMetricsStatus}
/>
</React.Fragment>
);
......
......@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
import {FormattedMessage, injectIntl} from 'react-intl';
import {isLicenseExpired, isLicenseExpiring, isLicensePastGracePeriod} from 'utils/license_utils.jsx';
import {AnnouncementBarTypes, AnnouncementBarMessages} from 'utils/constants';
import {AnnouncementBarTypes, AnnouncementBarMessages, WarnMetricTypes} from 'utils/constants';
import {intlShape} from 'utils/react_intl';
import {t} from 'utils/i18n';
......@@ -17,6 +17,9 @@ import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import AnnouncementBar from '../default_announcement_bar';
import TextDismissableBar from '../text_dismissable_bar';
import ackIcon from 'images/icons/check-circle-outline.svg';
import alertIcon from 'images/icons/round-white-info-icon.svg';
const RENEWAL_LINK = 'https://mattermost.com/renew/';
class ConfigurationAnnouncementBar extends React.PureComponent {
......@@ -28,7 +31,10 @@ class ConfigurationAnnouncementBar extends React.PureComponent {
canViewSystemErrors: PropTypes.bool.isRequired,
totalUsers: PropTypes.number,
dismissedExpiringLicense: PropTypes.bool,
dismissedNumberOfActiveUsersWarnMetricStatus: PropTypes.bool,
dismissedNumberOfActiveUsersWarnMetricStatusAck: PropTypes.bool,
siteURL: PropTypes.string.isRequired,
warnMetricsStatus: PropTypes.object,
actions: PropTypes.shape({
dismissNotice: PropTypes.func.isRequired,
}).isRequired,
......@@ -38,6 +44,77 @@ class ConfigurationAnnouncementBar extends React.PureComponent {
this.props.actions.dismissNotice(AnnouncementBarMessages.LICENSE_EXPIRING);
}
dismissNumberOfActiveUsersWarnMetric = () => {
this.props.actions.dismissNotice(AnnouncementBarMessages.NUMBER_OF_ACTIVE_USERS_WARN_METRIC_STATUS);
}
dismissNumberOfActiveUsersWarnMetricAck = () => {
this.props.actions.dismissNotice(AnnouncementBarMessages.NUMBER_OF_ACTIVE_USERS_WARN_METRIC_STATUS_ACK);
}
getNoticeForWarnMetric = (warnMetricStatus) => {
if (!warnMetricStatus) {
return null;
}
var message = '';
var type = '';
var showModal = false;
var dismissFunc = null;
var isDismissed = null;
switch (warnMetricStatus.id) {
case WarnMetricTypes.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500:
if (warnMetricStatus.acked) {
message = (
<React.Fragment>
<img
className='advisor-icon'
src={ackIcon}
/>
<FormattedMarkdownMessage
id={t('announcement_bar.error.number_active_users_warn_metric_status_ack.text')}
defaultMessage={'Your trial has started! Go to the [System Console](/admin_console/environment/web_server) to check out the new features.'}
/>
</React.Fragment>
);
type = AnnouncementBarTypes.ADVISOR_ACK;
showModal = false;
dismissFunc = this.dismissNumberOfActiveUsersWarnMetricAck;
isDismissed = this.props.dismissedNumberOfActiveUsersWarnMetricStatusAck;
} else {
message = (
<React.Fragment>
<img
className='advisor-icon'
src={alertIcon}
/>
<FormattedMarkdownMessage
id={t('announcement_bar.error.number_active_users_warn_metric_status.text')}
defaultMessage={'You now have over {limit} users. We strongly recommend that you upgrade to our Enterprise edition.'}
values={{
limit: warnMetricStatus.limit,
}}
/>
</React.Fragment>
);
type = AnnouncementBarTypes.ADVISOR;
showModal = true;
dismissFunc = this.dismissNumberOfActiveUsersWarnMetric;
isDismissed = this.props.dismissedNumberOfActiveUsersWarnMetricStatus;
}
return {
Message: message,
DismissFunc: dismissFunc,
IsDismissed: isDismissed,
Type: type,
ShowModal: showModal,
};
default:
return null;
}
}
render() {
// System administrators
if (this.props.canViewSystemErrors) {
......@@ -95,6 +172,27 @@ class ConfigurationAnnouncementBar extends React.PureComponent {
/>
);
}
if (this.props.license.IsLicensed === 'false' && this.props.warnMetricsStatus) {
for (const status of Object.values(this.props.warnMetricsStatus)) {
var notice = this.getNoticeForWarnMetric(status);
if (!notice || notice.IsDismissed) {
continue;
}
return (
<AnnouncementBar
showCloseButton={true}
handleClose={notice.DismissFunc}
type={notice.Type}
showModal={notice.ShowModal}
modalButtonText={t('announcement_bar.error.warn_metric_status.link')}
modalButtonDefaultText={'Learn More'}
warnMetricStatus={status}
message={notice.Message}
/>
);
}
}
} else {
// Regular users
if (isLicensePastGracePeriod(this.props.license)) { //eslint-disable-line no-lonely-if
......
......@@ -14,6 +14,8 @@ function mapStateToProps(state) {
return {
siteURL: getSiteURL(state),
dismissedExpiringLicense: Boolean(state.views.notice.hasBeenDismissed[AnnouncementBarMessages.LICENSE_EXPIRING]),
dismissedNumberOfActiveUsersWarnMetricStatus: Boolean(state.views.notice.hasBeenDismissed[AnnouncementBarMessages.NUMBER_OF_ACTIVE_USERS_WARN_METRIC_STATUS]),
dismissedNumberOfActiveUsersWarnMetricStatusAck: Boolean(state.views.notice.hasBeenDismissed[AnnouncementBarMessages.NUMBER_OF_ACTIVE_USERS_WARN_METRIC_STATUS_ACK]),
};
}
......
......@@ -2,12 +2,20 @@
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import {Tooltip} from 'react-bootstrap';
import {Constants, AnnouncementBarTypes} from 'utils/constants';
import {Constants, AnnouncementBarTypes, ModalIdentifiers} from 'utils/constants';
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import OverlayTrigger from 'components/overlay_trigger';
import WarnMetricAckModal from 'components/warn_metric_ack_modal';
import ToggleModalButtonRedux from 'components/toggle_modal_button_redux';
import {trackEvent} from 'actions/diagnostics_actions.jsx';
export default class AnnouncementBar extends React.PureComponent {
static propTypes = {
......@@ -18,6 +26,10 @@ export default class AnnouncementBar extends React.PureComponent {
message: PropTypes.node.isRequired,
handleClose: PropTypes.func,
announcementBarCount: PropTypes.number.isRequired,
showModal: PropTypes.bool,
modalButtonText: PropTypes.string,
modalButtonDefaultText: PropTypes.string,
warnMetricStatus: PropTypes.object,
actions: PropTypes.shape({
incrementAnnouncementBarCount: PropTypes.func.isRequired,
decrementAnnouncementBarCount: PropTypes.func.isRequired,
......@@ -71,6 +83,10 @@ export default class AnnouncementBar extends React.PureComponent {
barClass = 'announcement-bar announcement-bar-critical';
} else if (this.props.type === AnnouncementBarTypes.SUCCESS) {
barClass = 'announcement-bar announcement-bar-success';
} else if (this.props.type === AnnouncementBarTypes.ADVISOR) {
barClass = 'announcement-bar announcement-bar-advisor';
} else if (this.props.type === AnnouncementBarTypes.ADVISOR_ACK) {
barClass = 'announcement-bar announcement-bar-advisor-ack';
}
let closeButton;
......@@ -93,7 +109,6 @@ export default class AnnouncementBar extends React.PureComponent {
<FormattedMarkdownMessage id={this.props.message}/>
);
}
const announcementTooltip = (
<Tooltip id='announcement-bar__tooltip'>
{message}
......@@ -112,6 +127,30 @@ export default class AnnouncementBar extends React.PureComponent {
>
<span>
{message}
<span className='announcement-bar__link'>
{this.props.showModal &&
<FormattedMessage
id={this.props.modalButtonText}
defaultMessage={this.props.modalButtonDefaultText}
>
{(linkmessage) => (
<ToggleModalButtonRedux
accessibilityLabel={linkmessage}
className={'color--link--adminack'}
dialogType={WarnMetricAckModal}
onClick={() => trackEvent('admin', 'click_warn_metric_learn_more')}
modalId={ModalIdentifiers.WARN_METRIC_ACK}
dialogProps={{
warnMetricStatus: this.props.warnMetricStatus,
closeParentComponent: this.props.handleClose,
}}
>
{linkmessage}
</ToggleModalButtonRedux>
)}
</FormattedMessage>
}
</span>
</span>
</OverlayTrigger>
{closeButton}
......
......@@ -3,10 +3,10 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {Permissions} from 'mattermost-redux/constants';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
import {Permissions} from 'mattermost-redux/constants';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getConfig, getLicense, warnMetricsStatus as getWarnMetricsStatus} from 'mattermost-redux/selectors/entities/general';
import {getDisplayableErrors} from 'mattermost-redux/selectors/errors';
import {dismissError} from 'mattermost-redux/actions/errors';
import {getStandardAnalytics} from 'mattermost-redux/actions/admin';
......@@ -21,6 +21,8 @@ function mapStateToProps(state) {
const config = getConfig(state);
const user = getCurrentUser(state);
const errors = getDisplayableErrors(state);
const warnMetricsStatus = getWarnMetricsStatus(state);
const totalUsers = state.entities.admin.analytics.TOTAL_USERS;
let latestError = null;
if (errors && errors.length >= 1) {
......@@ -34,6 +36,7 @@ function mapStateToProps(state) {
canViewSystemErrors,
latestError,
totalUsers,
warnMetricsStatus,
};
}
......
......@@ -5,17 +5,22 @@ exports[`components/post_view/message_attachments/action_button.jsx should match
data-action-cookie="cookie-contents"
data-action-id="action_id_1"
key="action_id_1"
onClick={[MockFunction]}
onClick={[Function]}
>
<Connect(Markdown)
message="action_name_1"
options={
Object {
"autolinkedUrlSchemes": Array [],
"markdown": false,
"mentionHighlight": false,
<LoadingWrapper
loading={true}
text={null}
>
<Connect(Markdown)
message="action_name_1"
options={
Object {
"autolinkedUrlSchemes": Array [],
"markdown": false,
"mentionHighlight": false,
}
}
}
/>
/>
</LoadingWrapper>
</button>
`;
......@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import {changeOpacity} from 'mattermost-redux/utils/theme_utils';
import LoadingWrapper from 'components/widgets/loading/loading_wrapper';
import Markdown from 'components/markdown';
export default class ActionButton extends React.PureComponent {
......@@ -13,6 +14,8 @@ export default class ActionButton extends React.PureComponent {
handleAction: PropTypes.func.isRequired,
disabled: PropTypes.bool,
theme: PropTypes.object.isRequired,
actionExecuting: PropTypes.bool,
actionExecutingMessage: PropTypes.string,
}
getStatusColors(theme) {
......@@ -53,17 +56,22 @@ export default class ActionButton extends React.PureComponent {
data-action-cookie={action.cookie}
disabled={disabled}
key={action.id}
onClick={handleAction}
onClick={(e) => handleAction(e, this.props.action.options)}
style={customButtonStyle}
>
<Markdown
message={action.name}
options={{
mentionHighlight: false,
markdown: false,
autolinkedUrlSchemes: [],
}}
/>
<LoadingWrapper
loading={this.props.actionExecuting}
text={this.props.actionExecutingMessage}
>
<Markdown
message={action.name}
options={{
mentionHighlight: false,
markdown: false,
autolinkedUrlSchemes: [],
}}
/>
</LoadingWrapper>
</button>
);
}
......
......@@ -109,6 +109,8 @@ exports[`components/post_view/MessageAttachment should call actions.doPostAction
"name": "action_name_1",
}
}
actionExecuting={false}
actionExecutingMessage={null}
handleAction={[Function]}
key="action_id_1"
/>
......@@ -664,6 +666,8 @@ exports[`components/post_view/MessageAttachment should match value on renderPost
"name": "action_name_1",
}
}
actionExecuting={false}
actionExecutingMessage={null}
handleAction={[Function]}
/>
<Memo(Connect(ActionButton))
......@@ -673,6 +677,8 @@ exports[`components/post_view/MessageAttachment should match value on renderPost
"name": "action_name_2",
}
}
actionExecuting={false}
actionExecutingMessage={null}
handleAction={[Function]}
/>
<Memo(Connect(ActionMenu))
......
......@@ -9,6 +9,7 @@ import truncate from 'lodash/truncate';
import {isUrlSafe} from 'utils/url';
import {Constants} from 'utils/constants';
import * as Utils from 'utils/utils';
import LinkOnlyRenderer from 'utils/markdown/link_only_renderer';
import ExternalImage from 'components/external_image';
import Markdown from 'components/markdown';
......@@ -17,7 +18,8 @@ import SizeAwareImage from 'components/size_aware_image';
import ActionButton from '../action_button';
import ActionMenu from '../action_menu';
import LinkOnlyRenderer from 'utils/markdown/link_only_renderer';
import {trackEvent} from 'actions/diagnostics_actions';
export default class MessageAttachment extends React.PureComponent {
static propTypes = {
......@@ -54,6 +56,8 @@ export default class MessageAttachment extends React.PureComponent {
this.state = {
checkOverflow: 0,
actionExecuting: false,
actionExecutingMessage: null,
};
this.imageProps = {
......@@ -130,6 +134,8 @@ export default class MessageAttachment extends React.PureComponent {
action={action}
disabled={action.disabled}
handleAction={this.handleAction}
actionExecuting={this.state.actionExecuting}
actionExecutingMessage={this.state.actionExecutingMessage}
/>,
);
break;
......@@ -145,14 +151,46 @@ export default class MessageAttachment extends React.PureComponent {
);
};
handleAction = (e) => {
handleAction = (e, actionOptions) => {
e.preventDefault();
var actionExecutingMessage = this.getActionOption(actionOptions, 'ActionExecutingMessage');
if (actionExecutingMessage) {
this.setState({actionExecuting: true, actionExecutingMessage: actionExecutingMessage.value});
}
var trackOption = this.getActionOption(actionOptions, 'TrackEventId');
if (trackOption) {
trackEvent('admin', 'click_warn_metric_bot_id', {metric: trackOption.value});
}
const actionId = e.currentTarget.getAttribute('data-action-id');
const actionCookie = e.currentTarget.getAttribute('data-action-cookie');
this.props.actions.doPostActionWithCookie(this.props.postId, actionId, actionCookie);
this.props.actions.doPostActionWithCookie(this.props.postId, actionId, actionCookie).then(() => {
this.handleCustomActions(actionOptions);
if (actionExecutingMessage) {
this.setState({actionExecuting: false, actionExecutingMessage: null});
}
});
};
handleCustomActions = (actionOptions) => {
var extUrlOption = this.getActionOption(actionOptions, 'WarnMetricMailtoUrl');
if (extUrlOption) {
const mailtoPayload = JSON.parse(extUrlOption.value);
window.location.href = 'mailto:' + mailtoPayload.mail_recipient + '?cc=' + mailtoPayload.mail_cc + '&subject=' + encodeURIComponent(mailtoPayload.mail_subject) + '&body=' + encodeURIComponent(mailtoPayload.mail_body);
}
}