diff options
Diffstat (limited to 'web/react/components/admin_console')
9 files changed, 542 insertions, 85 deletions
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index efd163017..360ae3ef3 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -11,6 +11,7 @@ import * as Utils from '../../utils/utils.jsx'; import EmailSettingsTab from './email_settings.jsx'; import LogSettingsTab from './log_settings.jsx'; import LogsTab from './logs.jsx'; +import AuditsTab from './audits.jsx'; import FileSettingsTab from './image_settings.jsx'; import PrivacySettingsTab from './privacy_settings.jsx'; import RateSettingsTab from './rate_settings.jsx'; @@ -138,6 +139,8 @@ export default class AdminController extends React.Component { tab = <LogSettingsTab config={this.state.config} />; } else if (this.state.selected === 'logs') { tab = <LogsTab />; + } else if (this.state.selected === 'audits') { + tab = <AuditsTab />; } else if (this.state.selected === 'image_settings') { tab = <FileSettingsTab config={this.state.config} />; } else if (this.state.selected === 'privacy_settings') { diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index d6bae1feb..642bfe9d7 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -214,6 +214,24 @@ export default class AdminSidebar extends React.Component { ); } + let audits; + if (global.window.mm_license.IsLicensed === 'true') { + audits = ( + <li> + <a + href='#' + className={this.isSelected('audits')} + onClick={this.handleClick.bind(this, 'audits', null)} + > + <FormattedMessage + id='admin.sidebar.audits' + defaultMessage='Audits' + /> + </a> + </li> + ); + } + return ( <div className='sidebar--left sidebar--collapsable'> <div> @@ -448,6 +466,7 @@ export default class AdminSidebar extends React.Component { /> </a> </li> + {audits} </ul> </li> </ul> diff --git a/web/react/components/admin_console/analytics.jsx b/web/react/components/admin_console/analytics.jsx index a22c26c34..0a159d2e3 100644 --- a/web/react/components/admin_console/analytics.jsx +++ b/web/react/components/admin_console/analytics.jsx @@ -4,11 +4,60 @@ import * as Utils from '../../utils/utils.jsx'; import Constants from '../../utils/constants.jsx'; import LineChart from './line_chart.jsx'; +import DoughnutChart from './doughnut_chart.jsx'; +import StatisticCount from './statistic_count.jsx'; var Tooltip = ReactBootstrap.Tooltip; var OverlayTrigger = ReactBootstrap.OverlayTrigger; -import {FormattedMessage} from 'mm-intl'; +import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl'; + +const holders = defineMessages({ + analyticsTotalUsers: { + id: 'admin.analytics.totalUsers', + defaultMessage: 'Total Users' + }, + analyticsPublicChannels: { + id: 'admin.analytics.publicChannels', + defaultMessage: 'Public Channels' + }, + analyticsPrivateGroups: { + id: 'admin.analytics.privateGroups', + defaultMessage: 'Private Groups' + }, + analyticsTotalPosts: { + id: 'admin.analytics.totalPosts', + defaultMessage: 'Total Posts' + }, + analyticsFilePosts: { + id: 'admin.analytics.totalFilePosts', + defaultMessage: 'Posts with Files' + }, + analyticsHashtagPosts: { + id: 'admin.analytics.totalHashtagPosts', + defaultMessage: 'Posts with Hashtags' + }, + analyticsIncomingHooks: { + id: 'admin.analytics.totalIncomingWebhooks', + defaultMessage: 'Incoming Webhooks' + }, + analyticsOutgoingHooks: { + id: 'admin.analytics.totalOutgoingWebhooks', + defaultMessage: 'Outgoing Webhooks' + }, + analyticsChannelTypes: { + id: 'admin.analytics.channelTypes', + defaultMessage: 'Channel Types' + }, + analyticsTextPosts: { + id: 'admin.analytics.textPosts', + defaultMessage: 'Posts with Text-only' + }, + analyticsPostTypes: { + id: 'admin.analytics.postTypes', + defaultMessage: 'Posts, Files and Hashtags' + } +}); export default class Analytics extends React.Component { constructor(props) { @@ -18,6 +67,8 @@ export default class Analytics extends React.Component { } render() { // in the future, break down these into smaller components + const {formatMessage} = this.props.intl; + var serverError = ''; if (this.props.serverError) { serverError = <div className='form-group has-error'><label className='control-label'>{this.props.serverError}</label></div>; @@ -30,77 +81,129 @@ export default class Analytics extends React.Component { /> ); - var totalCount = ( - <div className='col-sm-3'> - <div className='total-count'> - <div className='title'> - <FormattedMessage - id='admin.analytics.totalUsers' - defaultMessage='Total Users' - /> - <i className='fa fa-users'/></div> - <div className='content'>{this.props.uniqueUserCount == null ? loading : this.props.uniqueUserCount}</div> + let firstRow; + let extraGraphs; + if (this.props.showAdvanced) { + firstRow = ( + <div className='row'> + <StatisticCount + title={formatMessage(holders.analyticsTotalUsers)} + icon='fa-users' + count={this.props.uniqueUserCount} + /> + <StatisticCount + title={formatMessage(holders.analyticsTotalPosts)} + icon='fa-comment' + count={this.props.postCount} + /> + <StatisticCount + title={formatMessage(holders.analyticsIncomingHooks)} + icon='fa-arrow-down' + count={this.props.incomingWebhookCount} + /> + <StatisticCount + title={formatMessage(holders.analyticsOutgoingHooks)} + icon='fa-arrow-up' + count={this.props.outgoingWebhookCount} + /> </div> - </div> - ); + ); - var openChannelCount = ( - <div className='col-sm-3'> - <div className='total-count'> - <div className='title'> - <FormattedMessage - id='admin.analytics.publicChannels' - defaultMessage='Public Channels' - /> - <i className='fa fa-globe'/></div> - <div className='content'>{this.props.channelOpenCount == null ? loading : this.props.channelOpenCount}</div> - </div> - </div> - ); + const channelTypeData = [ + { + value: this.props.channelOpenCount, + color: '#46BFBD', + highlight: '#5AD3D1', + label: formatMessage(holders.analyticsPublicChannels) + }, + { + value: this.props.channelPrivateCount, + color: '#FDB45C', + highlight: '#FFC870', + label: formatMessage(holders.analyticsPrivateGroups) + } + ]; - var openPrivateCount = ( - <div className='col-sm-3'> - <div className='total-count'> - <div className='title'> - <FormattedMessage - id='admin.analytics.privateGroups' - defaultMessage='Private Groups' - /> - <i className='fa fa-lock'/></div> - <div className='content'>{this.props.channelPrivateCount == null ? loading : this.props.channelPrivateCount}</div> - </div> - </div> - ); + const postTypeData = [ + { + value: this.props.filePostCount, + color: '#46BFBD', + highlight: '#5AD3D1', + label: formatMessage(holders.analyticsFilePosts) + }, + { + value: this.props.filePostCount, + color: '#F7464A', + highlight: '#FF5A5E', + label: formatMessage(holders.analyticsHashtagPosts) + }, + { + value: this.props.postCount - this.props.filePostCount - this.props.hashtagPostCount, + color: '#FDB45C', + highlight: '#FFC870', + label: formatMessage(holders.analyticsTextPosts) + } + ]; - var postCount = ( - <div className='col-sm-3'> - <div className='total-count'> - <div className='title'> - <FormattedMessage - id='admin.analytics.totalPosts' - defaultMessage='Total Posts' - /> - <i className='fa fa-comment'/></div> - <div className='content'>{this.props.postCount == null ? loading : this.props.postCount}</div> + extraGraphs = ( + <div className='row'> + <DoughnutChart + title={formatMessage(holders.analyticsChannelTypes)} + data={channelTypeData} + width='300' + height='225' + /> + <DoughnutChart + title={formatMessage(holders.analyticsPostTypes)} + data={postTypeData} + width='300' + height='225' + /> </div> - </div> - ); + ); + } else { + firstRow = ( + <div className='row'> + <StatisticCount + title={formatMessage(holders.analyticsTotalUsers)} + icon='fa-users' + count={this.props.uniqueUserCount} + /> + <StatisticCount + title={formatMessage(holders.analyticsPublicChannels)} + icon='fa-globe' + count={this.props.channelOpenCount} + /> + <StatisticCount + title={formatMessage(holders.analyticsPrivateGroups)} + icon='fa-lock' + count={this.props.channelPrivateCount} + /> + <StatisticCount + title={formatMessage(holders.analyticsTotalPosts)} + icon='fa-comment' + count={this.props.postCount} + /> + </div> + ); + } - var postCountsByDay = ( - <div className='col-sm-12'> - <div className='total-count by-day'> - <div className='title'> - <FormattedMessage - id='admin.analytics.totalPosts' - defaultMessage='Total Posts' - /> + let postCountsByDay; + if (this.props.postCountsDay == null) { + postCountsByDay = ( + <div className='col-sm-12'> + <div className='total-count by-day'> + <div className='title'> + <FormattedMessage + id='admin.analytics.totalPosts' + defaultMessage='Total Posts' + /> + </div> + <div className='content'>{loading}</div> </div> - <div className='content'>{loading}</div> </div> - </div> - ); - - if (this.props.postCountsDay != null) { + ); + } else { let content; if (this.props.postCountsDay.labels.length === 0) { content = ( @@ -137,21 +240,22 @@ export default class Analytics extends React.Component { ); } - var usersWithPostsByDay = ( - <div className='col-sm-12'> - <div className='total-count by-day'> - <div className='title'> - <FormattedMessage - id='admin.analytics.activeUsers' - defaultMessage='Active Users With Posts' - /> + let usersWithPostsByDay; + if (this.props.userCountsWithPostsDay == null) { + usersWithPostsByDay = ( + <div className='col-sm-12'> + <div className='total-count by-day'> + <div className='title'> + <FormattedMessage + id='admin.analytics.activeUsers' + defaultMessage='Active Users With Posts' + /> + </div> + <div className='content'>{loading}</div> </div> - <div className='content'>{loading}</div> </div> - </div> - ); - - if (this.props.userCountsWithPostsDay != null) { + ); + } else { let content; if (this.props.userCountsWithPostsDay.labels.length === 0) { content = ( @@ -312,12 +416,8 @@ export default class Analytics extends React.Component { /> </h3> {serverError} - <div className='row'> - {totalCount} - {postCount} - {openChannelCount} - {openPrivateCount} - </div> + {firstRow} + {extraGraphs} <div className='row'> {postCountsByDay} </div> @@ -347,10 +447,16 @@ Analytics.defaultProps = { }; Analytics.propTypes = { + intl: intlShape.isRequired, title: React.PropTypes.string, channelOpenCount: React.PropTypes.number, channelPrivateCount: React.PropTypes.number, postCount: React.PropTypes.number, + showAdvanced: React.PropTypes.bool, + filePostCount: React.PropTypes.number, + hashtagPostCount: React.PropTypes.number, + incomingWebhookCount: React.PropTypes.number, + outgoingWebhookCount: React.PropTypes.number, postCountsDay: React.PropTypes.object, userCountsWithPostsDay: React.PropTypes.object, recentActiveUsers: React.PropTypes.array, @@ -358,3 +464,5 @@ Analytics.propTypes = { uniqueUserCount: React.PropTypes.number, serverError: React.PropTypes.string }; + +export default injectIntl(Analytics); diff --git a/web/react/components/admin_console/audits.jsx b/web/react/components/admin_console/audits.jsx new file mode 100644 index 000000000..866539b3d --- /dev/null +++ b/web/react/components/admin_console/audits.jsx @@ -0,0 +1,94 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import LoadingScreen from '../loading_screen.jsx'; +import AuditTable from '../audit_table.jsx'; + +import AdminStore from '../../stores/admin_store.jsx'; + +import * as AsyncClient from '../../utils/async_client.jsx'; + +import {FormattedMessage} from 'mm-intl'; + +export default class Audits extends React.Component { + constructor(props) { + super(props); + + this.onAuditListenerChange = this.onAuditListenerChange.bind(this); + this.reload = this.reload.bind(this); + + this.state = { + audits: AdminStore.getAudits() + }; + } + + componentDidMount() { + AdminStore.addAuditChangeListener(this.onAuditListenerChange); + AsyncClient.getServerAudits(); + } + + componentWillUnmount() { + AdminStore.removeAuditChangeListener(this.onAuditListenerChange); + } + + onAuditListenerChange() { + this.setState({ + audits: AdminStore.getAudits() + }); + } + + reload() { + AdminStore.saveAudits(null); + this.setState({ + audits: null + }); + + AsyncClient.getServerAudits(); + } + + render() { + var content = null; + + if (global.window.mm_license.IsLicensed !== 'true') { + return <div/>; + } + + if (this.state.audits === null) { + content = <LoadingScreen />; + } else { + content = ( + <div style={{margin: '10px'}}> + <AuditTable + audits={this.state.audits} + oneLine={true} + showUserId={true} + /> + </div> + ); + } + + return ( + <div className='panel'> + <h3> + <FormattedMessage + id='admin.audits.title' + defaultMessage='Server Audits' + /> + </h3> + <button + type='submit' + className='btn btn-primary' + onClick={this.reload} + > + <FormattedMessage + id='admin.audits.reload' + defaultMessage='Reload' + /> + </button> + <div className='log__panel'> + {content} + </div> + </div> + ); + } +} diff --git a/web/react/components/admin_console/doughnut_chart.jsx b/web/react/components/admin_console/doughnut_chart.jsx new file mode 100644 index 000000000..e2dc01528 --- /dev/null +++ b/web/react/components/admin_console/doughnut_chart.jsx @@ -0,0 +1,77 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'mm-intl'; + +export default class DoughnutChart extends React.Component { + constructor(props) { + super(props); + + this.initChart = this.initChart.bind(this); + this.chart = null; + } + + componentDidMount() { + this.initChart(this.props); + } + + componentWillReceiveProps(nextProps) { + if (this.chart) { + this.chart.destroy(); + this.initChart(nextProps); + } + } + + componentWillUnmount() { + if (this.chart) { + this.chart.destroy(); + } + } + + initChart(props) { + var el = ReactDOM.findDOMNode(this.refs.canvas); + var ctx = el.getContext('2d'); + this.chart = new Chart(ctx).Doughnut(props.data, props.options || {}); //eslint-disable-line new-cap + } + + render() { + let content; + if (this.props.data == null) { + content = ( + <FormattedMessage + id='admin.analytics.loading' + defaultMessage='Loading...' + /> + ); + } else { + content = ( + <canvas + ref='canvas' + width={this.props.width} + height={this.props.height} + /> + ); + } + + return ( + <div className='col-sm-6'> + <div className='total-count'> + <div className='title'> + {this.props.title} + </div> + <div className='content'> + {content} + </div> + </div> + </div> + ); + } +} + +DoughnutChart.propTypes = { + title: React.PropTypes.string, + width: React.PropTypes.string, + height: React.PropTypes.string, + data: React.PropTypes.array, + options: React.PropTypes.object +}; diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx index ce3c8cd12..17f25a04c 100644 --- a/web/react/components/admin_console/email_settings.jsx +++ b/web/react/components/admin_console/email_settings.jsx @@ -112,6 +112,8 @@ class EmailSettings extends React.Component { buildConfig() { var config = this.props.config; config.EmailSettings.EnableSignUpWithEmail = ReactDOM.findDOMNode(this.refs.allowSignUpWithEmail).checked; + config.EmailSettings.EnableSignInWithEmail = ReactDOM.findDOMNode(this.refs.allowSignInWithEmail).checked; + config.EmailSettings.EnableSignInWithUsername = ReactDOM.findDOMNode(this.refs.allowSignInWithUsername).checked; config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked; config.EmailSettings.SendPushNotifications = ReactDOM.findDOMNode(this.refs.sendPushNotifications).checked; config.EmailSettings.RequireEmailVerification = ReactDOM.findDOMNode(this.refs.requireEmailVerification).checked; @@ -320,6 +322,88 @@ class EmailSettings extends React.Component { <div className='form-group'> <label className='control-label col-sm-4' + htmlFor='allowSignInWithEmail' + > + <FormattedMessage + id='admin.email.allowEmailSignInTitle' + defaultMessage='Allow Sign In With Email: ' + /> + </label> + <div className='col-sm-8'> + <label className='radio-inline'> + <input + type='radio' + name='allowSignInWithEmail' + value='true' + ref='allowSignInWithEmail' + defaultChecked={this.props.config.EmailSettings.EnableSignInWithEmail} + onChange={this.handleChange.bind(this, 'allowSignInWithEmail_true')} + /> + {'true'} + </label> + <label className='radio-inline'> + <input + type='radio' + name='allowSignInWithEmail' + value='false' + defaultChecked={!this.props.config.EmailSettings.EnableSignInWithEmail} + onChange={this.handleChange.bind(this, 'allowSignInWithEmail_false')} + /> + {'false'} + </label> + <p className='help-text'> + <FormattedMessage + id='admin.email.allowEmailSignInDescription' + defaultMessage='When true, Mattermost allows users to sign in using their email and password.' + /> + </p> + </div> + </div> + + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='allowSignInWithUsername' + > + <FormattedMessage + id='admin.email.allowUsernameSignInTitle' + defaultMessage='Allow Sign In With Username: ' + /> + </label> + <div className='col-sm-8'> + <label className='radio-inline'> + <input + type='radio' + name='allowSignInWithUsername' + value='true' + ref='allowSignInWithUsername' + defaultChecked={this.props.config.EmailSettings.EnableSignInWithUsername} + onChange={this.handleChange.bind(this, 'allowSignInWithUsername_true')} + /> + {'true'} + </label> + <label className='radio-inline'> + <input + type='radio' + name='allowSignInWithUsername' + value='false' + defaultChecked={!this.props.config.EmailSettings.EnableSignInWithUsername} + onChange={this.handleChange.bind(this, 'allowSignInWithUsername_false')} + /> + {'false'} + </label> + <p className='help-text'> + <FormattedMessage + id='admin.email.allowUsernameSignInDescription' + defaultMessage='When true, Mattermost allows users to sign in using their username and password. This setting is typically only used when email verification is disabled.' + /> + </p> + </div> + </div> + + <div className='form-group'> + <label + className='control-label col-sm-4' htmlFor='sendEmailNotifications' > <FormattedMessage diff --git a/web/react/components/admin_console/statistic_count.jsx b/web/react/components/admin_console/statistic_count.jsx new file mode 100644 index 000000000..57af0ed1b --- /dev/null +++ b/web/react/components/admin_console/statistic_count.jsx @@ -0,0 +1,37 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'mm-intl'; + +export default class StatisticCount extends React.Component { + constructor(props) { + super(props); + } + + render() { + let loading = ( + <FormattedMessage + id='admin.analytics.loading' + defaultMessage='Loading...' + /> + ); + + return ( + <div className='col-sm-3'> + <div className='total-count'> + <div className='title'> + {this.props.title} + <i className={'fa ' + this.props.icon}/> + </div> + <div className='content'>{this.props.count == null ? loading : this.props.count}</div> + </div> + </div> + ); + } +} + +StatisticCount.propTypes = { + title: React.PropTypes.string.isRequired, + icon: React.PropTypes.string.isRequired, + count: React.PropTypes.number +}; diff --git a/web/react/components/admin_console/system_analytics.jsx b/web/react/components/admin_console/system_analytics.jsx index 2dd833fb2..f983db177 100644 --- a/web/react/components/admin_console/system_analytics.jsx +++ b/web/react/components/admin_console/system_analytics.jsx @@ -140,6 +140,34 @@ class SystemAnalytics extends React.Component { this.setState({serverError: err.message}); } ); + + if (global.window.mm_license.IsLicensed === 'true') { + Client.getSystemAnalytics( + 'extra_counts', + (data) => { + for (var index in data) { + if (data[index].name === 'file_post_count') { + this.setState({file_post_count: data[index].value}); + } + + if (data[index].name === 'hashtag_post_count') { + this.setState({hashtag_post_count: data[index].value}); + } + + if (data[index].name === 'incoming_webhook_count') { + this.setState({incoming_webhook_count: data[index].value}); + } + + if (data[index].name === 'outgoing_webhook_count') { + this.setState({outgoing_webhook_count: data[index].value}); + } + } + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } } componentWillReceiveProps() { @@ -160,10 +188,16 @@ class SystemAnalytics extends React.Component { return ( <div> <Analytics + intl={this.props.intl} title={this.props.intl.formatMessage(labels.title)} channelOpenCount={this.state.channel_open_count} channelPrivateCount={this.state.channel_private_count} postCount={this.state.post_count} + showAdvanced={global.window.mm_license.IsLicensed === 'true'} + filePostCount={this.state.file_post_count} + hashtagPostCount={this.state.hashtag_post_count} + incomingWebhookCount={this.state.incoming_webhook_count} + outgoingWebhookCount={this.state.outgoing_webhook_count} postCountsDay={this.state.post_counts_day} userCountsWithPostsDay={this.state.user_counts_with_posts_day} uniqueUserCount={this.state.unique_user_count} @@ -179,4 +213,4 @@ SystemAnalytics.propTypes = { team: React.PropTypes.object }; -export default injectIntl(SystemAnalytics);
\ No newline at end of file +export default injectIntl(SystemAnalytics); diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx index ee59b0e66..808d8046d 100644 --- a/web/react/components/admin_console/team_analytics.jsx +++ b/web/react/components/admin_console/team_analytics.jsx @@ -227,6 +227,7 @@ class TeamAnalytics extends React.Component { return ( <div> <Analytics + intl={this.props.intl} title={this.props.team.name} users={this.state.users} channelOpenCount={this.state.channel_open_count} @@ -249,4 +250,4 @@ TeamAnalytics.propTypes = { team: React.PropTypes.object }; -export default injectIntl(TeamAnalytics);
\ No newline at end of file +export default injectIntl(TeamAnalytics); |