From 12896bd23eeba79884245c1c29fdc568cf21a7fa Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Mon, 14 Mar 2016 08:50:46 -0400 Subject: Converting to Webpack. Stage 1. --- webapp/components/about_build_modal.jsx | 137 +++ webapp/components/access_history_modal.jsx | 113 +++ webapp/components/activity_log_modal.jsx | 299 +++++++ .../components/admin_console/admin_controller.jsx | 218 +++++ .../admin_console/admin_navbar_dropdown.jsx | 114 +++ webapp/components/admin_console/admin_sidebar.jsx | 487 ++++++++++ .../admin_console/admin_sidebar_header.jsx | 70 ++ webapp/components/admin_console/audits.jsx | 97 ++ webapp/components/admin_console/email_settings.jsx | 961 ++++++++++++++++++++ .../components/admin_console/gitlab_settings.jsx | 383 ++++++++ webapp/components/admin_console/image_settings.jsx | 692 +++++++++++++++ webapp/components/admin_console/ldap_settings.jsx | 588 ++++++++++++ .../admin_console/legal_and_support_settings.jsx | 294 ++++++ .../components/admin_console/license_settings.jsx | 295 ++++++ webapp/components/admin_console/log_settings.jsx | 418 +++++++++ webapp/components/admin_console/logs.jsx | 102 +++ .../components/admin_console/privacy_settings.jsx | 215 +++++ webapp/components/admin_console/rate_settings.jsx | 371 ++++++++ .../admin_console/reset_password_modal.jsx | 162 ++++ .../components/admin_console/select_team_modal.jsx | 115 +++ .../components/admin_console/service_settings.jsx | 984 +++++++++++++++++++++ webapp/components/admin_console/sql_settings.jsx | 390 ++++++++ webapp/components/admin_console/team_settings.jsx | 420 +++++++++ webapp/components/admin_console/team_users.jsx | 188 ++++ webapp/components/admin_console/user_item.jsx | 423 +++++++++ webapp/components/analytics/doughnut_chart.jsx | 81 ++ webapp/components/analytics/line_chart.jsx | 94 ++ webapp/components/analytics/statistic_count.jsx | 35 + webapp/components/analytics/system_analytics.jsx | 348 ++++++++ webapp/components/analytics/table_chart.jsx | 61 ++ webapp/components/analytics/team_analytics.jsx | 237 +++++ webapp/components/audio_video_preview.jsx | 120 +++ webapp/components/audit_table.jsx | 626 +++++++++++++ webapp/components/authorize.jsx | 119 +++ webapp/components/center_panel.jsx | 141 +++ webapp/components/change_url_modal.jsx | 222 +++++ webapp/components/channel_header.jsx | 520 +++++++++++ webapp/components/channel_info_modal.jsx | 104 +++ webapp/components/channel_invite_modal.jsx | 217 +++++ webapp/components/channel_members_modal.jsx | 225 +++++ webapp/components/channel_notifications_modal.jsx | 417 +++++++++ webapp/components/channel_view.jsx | 20 + webapp/components/claim/claim_account.jsx | 116 +++ webapp/components/claim/email_to_sso.jsx | 154 ++++ webapp/components/claim/sso_to_email.jsx | 168 ++++ webapp/components/confirm_modal.jsx | 69 ++ webapp/components/create_comment.jsx | 448 ++++++++++ webapp/components/create_post.jsx | 510 +++++++++++ webapp/components/delete_channel_modal.jsx | 111 +++ webapp/components/delete_post_modal.jsx | 178 ++++ webapp/components/do_verify_email.jsx | 84 ++ webapp/components/edit_channel_header_modal.jsx | 176 ++++ webapp/components/edit_channel_purpose_modal.jsx | 188 ++++ webapp/components/edit_post_modal.jsx | 224 +++++ webapp/components/error_bar.jsx | 85 ++ webapp/components/file_attachment.jsx | 228 +++++ webapp/components/file_attachment_list.jsx | 64 ++ webapp/components/file_info_preview.jsx | 57 ++ webapp/components/file_preview.jsx | 121 +++ webapp/components/file_upload.jsx | 340 +++++++ webapp/components/file_upload_overlay.jsx | 50 ++ webapp/components/filtered_user_list.jsx | 136 +++ webapp/components/get_link_modal.jsx | 139 +++ webapp/components/get_post_link_modal.jsx | 78 ++ webapp/components/get_team_invite_link_modal.jsx | 78 ++ webapp/components/invite_member_modal.jsx | 516 +++++++++++ webapp/components/loading_screen.jsx | 40 + webapp/components/logged_in.jsx | 227 +++++ webapp/components/login.jsx | 304 +++++++ webapp/components/login_email.jsx | 167 ++++ webapp/components/login_ldap.jsx | 144 +++ webapp/components/login_username.jsx | 183 ++++ webapp/components/member_list_team.jsx | 62 ++ webapp/components/message_wrapper.jsx | 28 + webapp/components/more_channels.jsx | 233 +++++ webapp/components/more_direct_channels.jsx | 156 ++++ webapp/components/msg_typing.jsx | 137 +++ webapp/components/navbar.jsx | 569 ++++++++++++ webapp/components/navbar_dropdown.jsx | 281 ++++++ webapp/components/needs_team.jsx | 22 + webapp/components/new_channel_flow.jsx | 250 ++++++ webapp/components/new_channel_modal.jsx | 288 ++++++ webapp/components/not_logged_in.jsx | 73 ++ webapp/components/notify_counts.jsx | 54 ++ webapp/components/password_reset_form.jsx | 132 +++ webapp/components/password_reset_send_link.jsx | 153 ++++ webapp/components/popover_list_members.jsx | 175 ++++ webapp/components/post.jsx | 254 ++++++ webapp/components/post_attachment.jsx | 315 +++++++ webapp/components/post_attachment_list.jsx | 30 + webapp/components/post_attachment_oembed.jsx | 107 +++ webapp/components/post_body.jsx | 224 +++++ webapp/components/post_body_additional_content.jsx | 149 ++++ webapp/components/post_deleted_modal.jsx | 91 ++ webapp/components/post_focus_view.jsx | 135 +++ webapp/components/post_header.jsx | 80 ++ webapp/components/post_image.jsx | 83 ++ webapp/components/post_info.jsx | 252 ++++++ webapp/components/posts_view.jsx | 608 +++++++++++++ webapp/components/posts_view_container.jsx | 225 +++++ webapp/components/providers.json | 376 ++++++++ webapp/components/register_app_modal.jsx | 411 +++++++++ webapp/components/removed_from_channel_modal.jsx | 136 +++ webapp/components/rename_channel_modal.jsx | 324 +++++++ webapp/components/rhs_comment.jsx | 311 +++++++ webapp/components/rhs_header_post.jsx | 93 ++ webapp/components/rhs_root_post.jsx | 290 ++++++ webapp/components/rhs_thread.jsx | 225 +++++ webapp/components/root.jsx | 98 ++ webapp/components/search_bar.jsx | 210 +++++ webapp/components/search_results.jsx | 193 ++++ webapp/components/search_results_header.jsx | 77 ++ webapp/components/search_results_item.jsx | 146 +++ webapp/components/setting_item_max.jsx | 97 ++ webapp/components/setting_item_min.jsx | 47 + webapp/components/setting_picture.jsx | 162 ++++ webapp/components/setting_upload.jsx | 127 +++ webapp/components/settings_sidebar.jsx | 67 ++ webapp/components/should_verify_email.jsx | 113 +++ webapp/components/sidebar.jsx | 717 +++++++++++++++ webapp/components/sidebar_header.jsx | 143 +++ webapp/components/sidebar_right.jsx | 145 +++ webapp/components/sidebar_right_menu.jsx | 218 +++++ webapp/components/signup_team.jsx | 209 +++++ .../components/signup_team_complete.jsx | 81 ++ .../components/team_signup_display_name_page.jsx | 141 +++ .../components/team_signup_email_item.jsx | 89 ++ .../components/team_signup_finished.jsx | 17 + .../components/team_signup_password_page.jsx | 221 +++++ .../components/team_signup_send_invites_page.jsx | 215 +++++ .../components/team_signup_url_page.jsx | 211 +++++ .../components/team_signup_username_page.jsx | 169 ++++ .../components/team_signup_welcome_page.jsx | 239 +++++ webapp/components/signup_team_confirm.jsx | 48 + webapp/components/signup_user_complete.jsx | 496 +++++++++++ .../components/suggestion/at_mention_provider.jsx | 120 +++ webapp/components/suggestion/command_provider.jsx | 45 + webapp/components/suggestion/emoticon_provider.jsx | 93 ++ .../suggestion/search_channel_provider.jsx | 71 ++ .../suggestion/search_suggestion_list.jsx | 95 ++ .../components/suggestion/search_user_provider.jsx | 64 ++ webapp/components/suggestion/suggestion_box.jsx | 167 ++++ webapp/components/suggestion/suggestion_list.jsx | 129 +++ webapp/components/team_export_tab.jsx | 126 +++ webapp/components/team_general_tab.jsx | 658 ++++++++++++++ webapp/components/team_import_tab.jsx | 170 ++++ webapp/components/team_members_dropdown.jsx | 334 +++++++ webapp/components/team_members_modal.jsx | 89 ++ webapp/components/team_settings.jsx | 86 ++ webapp/components/team_settings_modal.jsx | 127 +++ webapp/components/team_signup_choose_auth.jsx | 140 +++ webapp/components/team_signup_with_email.jsx | 124 +++ webapp/components/team_signup_with_ldap.jsx | 238 +++++ webapp/components/team_signup_with_sso.jsx | 181 ++++ webapp/components/textbox.jsx | 267 ++++++ webapp/components/time_since.jsx | 67 ++ webapp/components/toggle_modal_button.jsx | 75 ++ .../components/tutorial/tutorial_intro_screens.jsx | 241 +++++ webapp/components/tutorial/tutorial_tip.jsx | 160 ++++ webapp/components/unread_channel_indicator.jsx | 34 + webapp/components/user_list.jsx | 51 ++ webapp/components/user_list_row.jsx | 65 ++ webapp/components/user_profile.jsx | 122 +++ .../user_settings/custom_theme_chooser.jsx | 394 +++++++++ .../user_settings/import_theme_modal.jsx | 218 +++++ .../user_settings/manage_command_hooks.jsx | 681 ++++++++++++++ .../user_settings/manage_incoming_hooks.jsx | 225 +++++ .../components/user_settings/manage_languages.jsx | 124 +++ .../user_settings/manage_outgoing_hooks.jsx | 397 +++++++++ .../user_settings/premade_theme_chooser.jsx | 61 ++ webapp/components/user_settings/user_settings.jsx | 160 ++++ .../user_settings/user_settings_advanced.jsx | 345 ++++++++ .../user_settings/user_settings_developer.jsx | 138 +++ .../user_settings/user_settings_display.jsx | 494 +++++++++++ .../user_settings/user_settings_general.jsx | 817 +++++++++++++++++ .../user_settings/user_settings_integrations.jsx | 210 +++++ .../user_settings/user_settings_modal.jsx | 341 +++++++ .../user_settings/user_settings_notifications.jsx | 834 +++++++++++++++++ .../user_settings/user_settings_security.jsx | 474 ++++++++++ .../user_settings/user_settings_theme.jsx | 302 +++++++ webapp/components/view_image.jsx | 426 +++++++++ webapp/components/view_image_popover_bar.jsx | 83 ++ webapp/components/youtube_video.jsx | 179 ++++ 183 files changed, 40576 insertions(+) create mode 100644 webapp/components/about_build_modal.jsx create mode 100644 webapp/components/access_history_modal.jsx create mode 100644 webapp/components/activity_log_modal.jsx create mode 100644 webapp/components/admin_console/admin_controller.jsx create mode 100644 webapp/components/admin_console/admin_navbar_dropdown.jsx create mode 100644 webapp/components/admin_console/admin_sidebar.jsx create mode 100644 webapp/components/admin_console/admin_sidebar_header.jsx create mode 100644 webapp/components/admin_console/audits.jsx create mode 100644 webapp/components/admin_console/email_settings.jsx create mode 100644 webapp/components/admin_console/gitlab_settings.jsx create mode 100644 webapp/components/admin_console/image_settings.jsx create mode 100644 webapp/components/admin_console/ldap_settings.jsx create mode 100644 webapp/components/admin_console/legal_and_support_settings.jsx create mode 100644 webapp/components/admin_console/license_settings.jsx create mode 100644 webapp/components/admin_console/log_settings.jsx create mode 100644 webapp/components/admin_console/logs.jsx create mode 100644 webapp/components/admin_console/privacy_settings.jsx create mode 100644 webapp/components/admin_console/rate_settings.jsx create mode 100644 webapp/components/admin_console/reset_password_modal.jsx create mode 100644 webapp/components/admin_console/select_team_modal.jsx create mode 100644 webapp/components/admin_console/service_settings.jsx create mode 100644 webapp/components/admin_console/sql_settings.jsx create mode 100644 webapp/components/admin_console/team_settings.jsx create mode 100644 webapp/components/admin_console/team_users.jsx create mode 100644 webapp/components/admin_console/user_item.jsx create mode 100644 webapp/components/analytics/doughnut_chart.jsx create mode 100644 webapp/components/analytics/line_chart.jsx create mode 100644 webapp/components/analytics/statistic_count.jsx create mode 100644 webapp/components/analytics/system_analytics.jsx create mode 100644 webapp/components/analytics/table_chart.jsx create mode 100644 webapp/components/analytics/team_analytics.jsx create mode 100644 webapp/components/audio_video_preview.jsx create mode 100644 webapp/components/audit_table.jsx create mode 100644 webapp/components/authorize.jsx create mode 100644 webapp/components/center_panel.jsx create mode 100644 webapp/components/change_url_modal.jsx create mode 100644 webapp/components/channel_header.jsx create mode 100644 webapp/components/channel_info_modal.jsx create mode 100644 webapp/components/channel_invite_modal.jsx create mode 100644 webapp/components/channel_members_modal.jsx create mode 100644 webapp/components/channel_notifications_modal.jsx create mode 100644 webapp/components/channel_view.jsx create mode 100644 webapp/components/claim/claim_account.jsx create mode 100644 webapp/components/claim/email_to_sso.jsx create mode 100644 webapp/components/claim/sso_to_email.jsx create mode 100644 webapp/components/confirm_modal.jsx create mode 100644 webapp/components/create_comment.jsx create mode 100644 webapp/components/create_post.jsx create mode 100644 webapp/components/delete_channel_modal.jsx create mode 100644 webapp/components/delete_post_modal.jsx create mode 100644 webapp/components/do_verify_email.jsx create mode 100644 webapp/components/edit_channel_header_modal.jsx create mode 100644 webapp/components/edit_channel_purpose_modal.jsx create mode 100644 webapp/components/edit_post_modal.jsx create mode 100644 webapp/components/error_bar.jsx create mode 100644 webapp/components/file_attachment.jsx create mode 100644 webapp/components/file_attachment_list.jsx create mode 100644 webapp/components/file_info_preview.jsx create mode 100644 webapp/components/file_preview.jsx create mode 100644 webapp/components/file_upload.jsx create mode 100644 webapp/components/file_upload_overlay.jsx create mode 100644 webapp/components/filtered_user_list.jsx create mode 100644 webapp/components/get_link_modal.jsx create mode 100644 webapp/components/get_post_link_modal.jsx create mode 100644 webapp/components/get_team_invite_link_modal.jsx create mode 100644 webapp/components/invite_member_modal.jsx create mode 100644 webapp/components/loading_screen.jsx create mode 100644 webapp/components/logged_in.jsx create mode 100644 webapp/components/login.jsx create mode 100644 webapp/components/login_email.jsx create mode 100644 webapp/components/login_ldap.jsx create mode 100644 webapp/components/login_username.jsx create mode 100644 webapp/components/member_list_team.jsx create mode 100644 webapp/components/message_wrapper.jsx create mode 100644 webapp/components/more_channels.jsx create mode 100644 webapp/components/more_direct_channels.jsx create mode 100644 webapp/components/msg_typing.jsx create mode 100644 webapp/components/navbar.jsx create mode 100644 webapp/components/navbar_dropdown.jsx create mode 100644 webapp/components/needs_team.jsx create mode 100644 webapp/components/new_channel_flow.jsx create mode 100644 webapp/components/new_channel_modal.jsx create mode 100644 webapp/components/not_logged_in.jsx create mode 100644 webapp/components/notify_counts.jsx create mode 100644 webapp/components/password_reset_form.jsx create mode 100644 webapp/components/password_reset_send_link.jsx create mode 100644 webapp/components/popover_list_members.jsx create mode 100644 webapp/components/post.jsx create mode 100644 webapp/components/post_attachment.jsx create mode 100644 webapp/components/post_attachment_list.jsx create mode 100644 webapp/components/post_attachment_oembed.jsx create mode 100644 webapp/components/post_body.jsx create mode 100644 webapp/components/post_body_additional_content.jsx create mode 100644 webapp/components/post_deleted_modal.jsx create mode 100644 webapp/components/post_focus_view.jsx create mode 100644 webapp/components/post_header.jsx create mode 100644 webapp/components/post_image.jsx create mode 100644 webapp/components/post_info.jsx create mode 100644 webapp/components/posts_view.jsx create mode 100644 webapp/components/posts_view_container.jsx create mode 100644 webapp/components/providers.json create mode 100644 webapp/components/register_app_modal.jsx create mode 100644 webapp/components/removed_from_channel_modal.jsx create mode 100644 webapp/components/rename_channel_modal.jsx create mode 100644 webapp/components/rhs_comment.jsx create mode 100644 webapp/components/rhs_header_post.jsx create mode 100644 webapp/components/rhs_root_post.jsx create mode 100644 webapp/components/rhs_thread.jsx create mode 100644 webapp/components/root.jsx create mode 100644 webapp/components/search_bar.jsx create mode 100644 webapp/components/search_results.jsx create mode 100644 webapp/components/search_results_header.jsx create mode 100644 webapp/components/search_results_item.jsx create mode 100644 webapp/components/setting_item_max.jsx create mode 100644 webapp/components/setting_item_min.jsx create mode 100644 webapp/components/setting_picture.jsx create mode 100644 webapp/components/setting_upload.jsx create mode 100644 webapp/components/settings_sidebar.jsx create mode 100644 webapp/components/should_verify_email.jsx create mode 100644 webapp/components/sidebar.jsx create mode 100644 webapp/components/sidebar_header.jsx create mode 100644 webapp/components/sidebar_right.jsx create mode 100644 webapp/components/sidebar_right_menu.jsx create mode 100644 webapp/components/signup_team.jsx create mode 100644 webapp/components/signup_team_complete/components/signup_team_complete.jsx create mode 100644 webapp/components/signup_team_complete/components/team_signup_display_name_page.jsx create mode 100644 webapp/components/signup_team_complete/components/team_signup_email_item.jsx create mode 100644 webapp/components/signup_team_complete/components/team_signup_finished.jsx create mode 100644 webapp/components/signup_team_complete/components/team_signup_password_page.jsx create mode 100644 webapp/components/signup_team_complete/components/team_signup_send_invites_page.jsx create mode 100644 webapp/components/signup_team_complete/components/team_signup_url_page.jsx create mode 100644 webapp/components/signup_team_complete/components/team_signup_username_page.jsx create mode 100644 webapp/components/signup_team_complete/components/team_signup_welcome_page.jsx create mode 100644 webapp/components/signup_team_confirm.jsx create mode 100644 webapp/components/signup_user_complete.jsx create mode 100644 webapp/components/suggestion/at_mention_provider.jsx create mode 100644 webapp/components/suggestion/command_provider.jsx create mode 100644 webapp/components/suggestion/emoticon_provider.jsx create mode 100644 webapp/components/suggestion/search_channel_provider.jsx create mode 100644 webapp/components/suggestion/search_suggestion_list.jsx create mode 100644 webapp/components/suggestion/search_user_provider.jsx create mode 100644 webapp/components/suggestion/suggestion_box.jsx create mode 100644 webapp/components/suggestion/suggestion_list.jsx create mode 100644 webapp/components/team_export_tab.jsx create mode 100644 webapp/components/team_general_tab.jsx create mode 100644 webapp/components/team_import_tab.jsx create mode 100644 webapp/components/team_members_dropdown.jsx create mode 100644 webapp/components/team_members_modal.jsx create mode 100644 webapp/components/team_settings.jsx create mode 100644 webapp/components/team_settings_modal.jsx create mode 100644 webapp/components/team_signup_choose_auth.jsx create mode 100644 webapp/components/team_signup_with_email.jsx create mode 100644 webapp/components/team_signup_with_ldap.jsx create mode 100644 webapp/components/team_signup_with_sso.jsx create mode 100644 webapp/components/textbox.jsx create mode 100644 webapp/components/time_since.jsx create mode 100644 webapp/components/toggle_modal_button.jsx create mode 100644 webapp/components/tutorial/tutorial_intro_screens.jsx create mode 100644 webapp/components/tutorial/tutorial_tip.jsx create mode 100644 webapp/components/unread_channel_indicator.jsx create mode 100644 webapp/components/user_list.jsx create mode 100644 webapp/components/user_list_row.jsx create mode 100644 webapp/components/user_profile.jsx create mode 100644 webapp/components/user_settings/custom_theme_chooser.jsx create mode 100644 webapp/components/user_settings/import_theme_modal.jsx create mode 100644 webapp/components/user_settings/manage_command_hooks.jsx create mode 100644 webapp/components/user_settings/manage_incoming_hooks.jsx create mode 100644 webapp/components/user_settings/manage_languages.jsx create mode 100644 webapp/components/user_settings/manage_outgoing_hooks.jsx create mode 100644 webapp/components/user_settings/premade_theme_chooser.jsx create mode 100644 webapp/components/user_settings/user_settings.jsx create mode 100644 webapp/components/user_settings/user_settings_advanced.jsx create mode 100644 webapp/components/user_settings/user_settings_developer.jsx create mode 100644 webapp/components/user_settings/user_settings_display.jsx create mode 100644 webapp/components/user_settings/user_settings_general.jsx create mode 100644 webapp/components/user_settings/user_settings_integrations.jsx create mode 100644 webapp/components/user_settings/user_settings_modal.jsx create mode 100644 webapp/components/user_settings/user_settings_notifications.jsx create mode 100644 webapp/components/user_settings/user_settings_security.jsx create mode 100644 webapp/components/user_settings/user_settings_theme.jsx create mode 100644 webapp/components/view_image.jsx create mode 100644 webapp/components/view_image_popover_bar.jsx create mode 100644 webapp/components/youtube_video.jsx (limited to 'webapp/components') diff --git a/webapp/components/about_build_modal.jsx b/webapp/components/about_build_modal.jsx new file mode 100644 index 000000000..e2fefc44e --- /dev/null +++ b/webapp/components/about_build_modal.jsx @@ -0,0 +1,137 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {Modal} from 'react-bootstrap'; + +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; + +export default class AboutBuildModal extends React.Component { + constructor(props) { + super(props); + this.doHide = this.doHide.bind(this); + } + + doHide() { + this.props.onModalDismissed(); + } + + render() { + const config = global.window.mm_config; + const license = global.window.mm_license; + + let title = ( + + ); + + let licensee; + if (config.BuildEnterpriseReady === 'true') { + title = ( + + ); + if (license.IsLicensed === 'true') { + title = ( + + ); + licensee = ( +
+
+ +
+
{license.Company}
+
+ ); + } + } + + return ( + + + + + + + +

{'Mattermost'} {title}

+ {licensee} +
+
+ +
+
{config.Version}
+
+
+
+ +
+
{config.BuildNumber}
+
+
+
+ +
+
{config.BuildDate}
+
+
+
+ +
+
{config.BuildHash}
+
+
+ + + +
+ ); + } +} + +AboutBuildModal.defaultProps = { + show: false +}; + +AboutBuildModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onModalDismissed: React.PropTypes.func.isRequired +}; diff --git a/webapp/components/access_history_modal.jsx b/webapp/components/access_history_modal.jsx new file mode 100644 index 000000000..94a10c97f --- /dev/null +++ b/webapp/components/access_history_modal.jsx @@ -0,0 +1,113 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import {Modal} from 'react-bootstrap'; +import LoadingScreen from './loading_screen.jsx'; +import AuditTable from './audit_table.jsx'; + +import UserStore from 'stores/user_store.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {intlShape, injectIntl, FormattedMessage} from 'react-intl'; + +import React from 'react'; + +class AccessHistoryModal extends React.Component { + constructor(props) { + super(props); + + this.onAuditChange = this.onAuditChange.bind(this); + this.onShow = this.onShow.bind(this); + this.onHide = this.onHide.bind(this); + + const state = this.getStateFromStoresForAudits(); + state.moreInfo = []; + + this.state = state; + } + getStateFromStoresForAudits() { + return { + audits: UserStore.getAudits() + }; + } + onShow() { + AsyncClient.getAudits(); + + if ($(window).width() > 768) { + $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200); + } else { + $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150); + } + } + onHide() { + this.setState({moreInfo: []}); + this.props.onHide(); + } + componentDidMount() { + UserStore.addAuditsChangeListener(this.onAuditChange); + + if (this.props.show) { + this.onShow(); + } + } + componentDidUpdate(prevProps) { + if (this.props.show && !prevProps.show) { + this.onShow(); + } + } + componentWillUnmount() { + UserStore.removeAuditsChangeListener(this.onAuditChange); + } + onAuditChange() { + var newState = this.getStateFromStoresForAudits(); + if (!Utils.areObjectsEqual(newState.audits, this.state.audits)) { + this.setState(newState); + } + } + render() { + var content; + if (this.state.audits.loading) { + content = (); + } else { + content = ( + + ); + } + + return ( + + + + + + + + {content} + + + ); + } +} + +AccessHistoryModal.propTypes = { + intl: intlShape.isRequired, + show: React.PropTypes.bool.isRequired, + onHide: React.PropTypes.func.isRequired +}; + +export default injectIntl(AccessHistoryModal); diff --git a/webapp/components/activity_log_modal.jsx b/webapp/components/activity_log_modal.jsx new file mode 100644 index 000000000..9a4ff3ef2 --- /dev/null +++ b/webapp/components/activity_log_modal.jsx @@ -0,0 +1,299 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import UserStore from 'stores/user_store.jsx'; +import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import {Modal} from 'react-bootstrap'; +import LoadingScreen from './loading_screen.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage, FormattedTime, FormattedDate} from 'react-intl'; + +import React from 'react'; + +export default class ActivityLogModal extends React.Component { + constructor(props) { + super(props); + + this.submitRevoke = this.submitRevoke.bind(this); + this.onListenerChange = this.onListenerChange.bind(this); + this.handleMoreInfo = this.handleMoreInfo.bind(this); + this.onHide = this.onHide.bind(this); + this.onShow = this.onShow.bind(this); + + let state = this.getStateFromStores(); + state.moreInfo = []; + + this.state = state; + } + getStateFromStores() { + return { + sessions: UserStore.getSessions(), + serverError: null, + clientError: null + }; + } + submitRevoke(altId, e) { + e.preventDefault(); + var modalContent = $(e.target).closest('.modal-content'); + modalContent.addClass('animation--highlight'); + setTimeout(() => { + modalContent.removeClass('animation--highlight'); + }, 1500); + Client.revokeSession(altId, + function handleRevokeSuccess() { + AsyncClient.getSessions(); + }, + function handleRevokeError(err) { + let state = this.getStateFromStores(); + state.serverError = err; + this.setState(state); + }.bind(this) + ); + } + onShow() { + AsyncClient.getSessions(); + + if ($(window).width() > 768) { + $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200); + } else { + $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150); + } + } + onHide() { + this.setState({moreInfo: []}); + this.props.onHide(); + } + componentDidMount() { + UserStore.addSessionsChangeListener(this.onListenerChange); + + if (this.props.show) { + this.onShow(); + } + } + componentDidUpdate(prevProps) { + if (this.props.show && !prevProps.show) { + this.onShow(); + } + } + componentWillUnmount() { + UserStore.removeSessionsChangeListener(this.onListenerChange); + } + onListenerChange() { + const newState = this.getStateFromStores(); + if (!Utils.areObjectsEqual(newState.sessions, this.state.sessions)) { + this.setState(newState); + } + } + handleMoreInfo(index) { + let newMoreInfo = this.state.moreInfo; + newMoreInfo[index] = true; + this.setState({moreInfo: newMoreInfo}); + } + render() { + let activityList = []; + + for (let i = 0; i < this.state.sessions.length; i++) { + const currentSession = this.state.sessions[i]; + const lastAccessTime = new Date(currentSession.last_activity_at); + const firstAccessTime = new Date(currentSession.create_at); + let devicePlatform = currentSession.props.platform; + let devicePicture = ''; + + if (currentSession.props.platform === 'Windows') { + devicePicture = 'fa fa-windows'; + } else if (currentSession.device_id && currentSession.device_id.indexOf('apple:') === 0) { + devicePicture = 'fa fa-apple'; + devicePlatform = ( + + ); + } else if (currentSession.device_id && currentSession.device_id.indexOf('android:') === 0) { + devicePlatform = ( + + ); + devicePicture = 'fa fa-android'; + } else if (currentSession.props.platform === 'Macintosh' || + currentSession.props.platform === 'iPhone') { + devicePicture = 'fa fa-apple'; + } else if (currentSession.props.platform === 'Linux') { + if (currentSession.props.os.indexOf('Android') >= 0) { + devicePlatform = ( + + ); + devicePicture = 'fa fa-android'; + } else { + devicePicture = 'fa fa-linux'; + } + } + + let moreInfo; + if (this.state.moreInfo[i]) { + moreInfo = ( +
+
+ + ), + time: ( + + ) + }} + /> +
+
+ +
+
+ +
+
+ +
+
+ ); + } else { + moreInfo = ( + + + + ); + } + + activityList[i] = ( +
+
+
{devicePlatform}
+
+
+ + ), + time: ( + + ) + }} + /> +
+ {moreInfo} +
+
+
+ +
+
+ ); + } + + let content; + if (this.state.sessions.loading) { + content = ; + } else { + content =
{activityList}
; + } + + return ( + + + + + + + +

+ +

+ {content} +
+
+ ); + } +} + +ActivityLogModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onHide: React.PropTypes.func.isRequired +}; diff --git a/webapp/components/admin_console/admin_controller.jsx b/webapp/components/admin_console/admin_controller.jsx new file mode 100644 index 000000000..e4a4e28fc --- /dev/null +++ b/webapp/components/admin_console/admin_controller.jsx @@ -0,0 +1,218 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import AdminSidebar from './admin_sidebar.jsx'; +import AdminStore from 'stores/admin_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import LoadingScreen from '../loading_screen.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'; +import GitLabSettingsTab from './gitlab_settings.jsx'; +import SqlSettingsTab from './sql_settings.jsx'; +import TeamSettingsTab from './team_settings.jsx'; +import ServiceSettingsTab from './service_settings.jsx'; +import LegalAndSupportSettingsTab from './legal_and_support_settings.jsx'; +import TeamUsersTab from './team_users.jsx'; +import TeamAnalyticsTab from '../analytics/team_analytics.jsx'; +import LdapSettingsTab from './ldap_settings.jsx'; +import LicenseSettingsTab from './license_settings.jsx'; +import SystemAnalyticsTab from '../analytics/system_analytics.jsx'; + +import React from 'react'; + +export default class AdminController extends React.Component { + constructor(props) { + super(props); + + this.selectTab = this.selectTab.bind(this); + this.removeSelectedTeam = this.removeSelectedTeam.bind(this); + this.addSelectedTeam = this.addSelectedTeam.bind(this); + this.onConfigListenerChange = this.onConfigListenerChange.bind(this); + this.onAllTeamsListenerChange = this.onAllTeamsListenerChange.bind(this); + + var selectedTeams = AdminStore.getSelectedTeams(); + if (selectedTeams == null) { + selectedTeams = {}; + selectedTeams[TeamStore.getCurrentId()] = 'true'; + AdminStore.saveSelectedTeams(selectedTeams); + } + + this.state = { + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams, + selected: props.tab || 'system_analytics', + selectedTeam: props.teamId || null + }; + } + + componentDidMount() { + AdminStore.addConfigChangeListener(this.onConfigListenerChange); + AsyncClient.getConfig(); + + AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange); + AsyncClient.getAllTeams(); + + $('[data-toggle="tooltip"]').tooltip(); + $('[data-toggle="popover"]').popover(); + } + + componentWillUnmount() { + AdminStore.removeConfigChangeListener(this.onConfigListenerChange); + AdminStore.removeAllTeamsChangeListener(this.onAllTeamsListenerChange); + } + + onConfigListenerChange() { + this.setState({ + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams: AdminStore.getSelectedTeams(), + selected: this.state.selected, + selectedTeam: this.state.selectedTeam + }); + } + + onAllTeamsListenerChange() { + this.setState({ + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams: AdminStore.getSelectedTeams(), + selected: this.state.selected, + selectedTeam: this.state.selectedTeam + + }); + } + + selectTab(tab, teamId) { + this.setState({ + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams: AdminStore.getSelectedTeams(), + selected: tab, + selectedTeam: teamId + }); + } + + removeSelectedTeam(teamId) { + var selectedTeams = AdminStore.getSelectedTeams(); + Reflect.deleteProperty(selectedTeams, teamId); + AdminStore.saveSelectedTeams(selectedTeams); + + this.setState({ + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams: AdminStore.getSelectedTeams(), + selected: this.state.selected, + selectedTeam: this.state.selectedTeam + }); + } + + addSelectedTeam(teamId) { + var selectedTeams = AdminStore.getSelectedTeams(); + selectedTeams[teamId] = 'true'; + AdminStore.saveSelectedTeams(selectedTeams); + + this.setState({ + config: AdminStore.getConfig(), + teams: AdminStore.getAllTeams(), + selectedTeams: AdminStore.getSelectedTeams(), + selected: this.state.selected, + selectedTeam: this.state.selectedTeam + }); + } + + render() { + var tab = ; + + if (this.state.config != null) { + if (this.state.selected === 'email_settings') { + tab = ; + } else if (this.state.selected === 'log_settings') { + tab = ; + } else if (this.state.selected === 'logs') { + tab = ; + } else if (this.state.selected === 'audits') { + tab = ; + } else if (this.state.selected === 'image_settings') { + tab = ; + } else if (this.state.selected === 'privacy_settings') { + tab = ; + } else if (this.state.selected === 'rate_settings') { + tab = ; + } else if (this.state.selected === 'gitlab_settings') { + tab = ; + } else if (this.state.selected === 'sql_settings') { + tab = ; + } else if (this.state.selected === 'team_settings') { + tab = ; + } else if (this.state.selected === 'service_settings') { + tab = ; + } else if (this.state.selected === 'legal_and_support_settings') { + tab = ; + } else if (this.state.selected === 'ldap_settings') { + tab = ; + } else if (this.state.selected === 'license') { + tab = ; + } else if (this.state.selected === 'team_users') { + if (this.state.teams) { + tab = ; + } + } else if (this.state.selected === 'team_analytics') { + if (this.state.teams) { + tab = ; + } + } else if (this.state.selected === 'system_analytics') { + tab = ; + } + } + + return ( +
+ + ); + } +} + +AdminController.defaultProps = { +}; + +AdminController.propTypes = { + tab: React.PropTypes.string, + teamId: React.PropTypes.string +}; diff --git a/webapp/components/admin_console/admin_navbar_dropdown.jsx b/webapp/components/admin_console/admin_navbar_dropdown.jsx new file mode 100644 index 000000000..56b78448a --- /dev/null +++ b/webapp/components/admin_console/admin_navbar_dropdown.jsx @@ -0,0 +1,114 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import * as Utils from 'utils/utils.jsx'; +import TeamStore from 'stores/team_store.jsx'; + +import Constants from 'utils/constants.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import {Link} from 'react-router'; + +function getStateFromStores() { + return {currentTeam: TeamStore.getCurrent()}; +} + +import React from 'react'; + +export default class AdminNavbarDropdown extends React.Component { + constructor(props) { + super(props); + this.blockToggle = false; + + this.state = getStateFromStores(); + } + + componentDidMount() { + $(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => { + this.blockToggle = true; + setTimeout(() => { + this.blockToggle = false; + }, 100); + }); + } + + componentWillUnmount() { + $(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown'); + } + + render() { + return ( + + ); + } +} diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx new file mode 100644 index 000000000..27d4a4112 --- /dev/null +++ b/webapp/components/admin_console/admin_sidebar.jsx @@ -0,0 +1,487 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AdminSidebarHeader from './admin_sidebar_header.jsx'; +import SelectTeamModal from './select_team_modal.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import {Tooltip, OverlayTrigger} from 'react-bootstrap'; + +import React from 'react'; + +export default class AdminSidebar extends React.Component { + constructor(props) { + super(props); + + this.isSelected = this.isSelected.bind(this); + this.handleClick = this.handleClick.bind(this); + this.removeTeam = this.removeTeam.bind(this); + + this.showTeamSelect = this.showTeamSelect.bind(this); + this.teamSelectedModal = this.teamSelectedModal.bind(this); + this.teamSelectedModalDismissed = this.teamSelectedModalDismissed.bind(this); + + this.state = { + showSelectModal: false + }; + } + + handleClick(name, teamId, e) { + e.preventDefault(); + this.props.selectTab(name, teamId); + } + + isSelected(name, teamId) { + if (this.props.selected === name) { + if (name === 'team_users' || name === 'team_analytics') { + if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) { + return 'active'; + } + } else { + return 'active'; + } + } + + return ''; + } + + removeTeam(teamId, e) { + e.preventDefault(); + e.stopPropagation(); + Reflect.deleteProperty(this.props.selectedTeams, teamId); + this.props.removeSelectedTeam(teamId); + + if (this.props.selected === 'team_users') { + if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) { + this.props.selectTab('service_settings', null); + } + } + } + + showTeamSelect(e) { + e.preventDefault(); + this.setState({showSelectModal: true}); + } + + teamSelectedModal(teamId) { + this.setState({showSelectModal: false}); + this.props.addSelectedTeam(teamId); + this.forceUpdate(); + } + + teamSelectedModalDismissed() { + this.setState({showSelectModal: false}); + } + + render() { + var count = '*'; + var teams = ( + + ); + const removeTooltip = ( + + + + ); + const addTeamTooltip = ( + + + + ); + + if (this.props.teams != null) { + count = '' + Object.keys(this.props.teams).length; + + teams = []; + for (var key in this.props.selectedTeams) { + if (this.props.selectedTeams.hasOwnProperty(key)) { + var team = this.props.teams[key]; + + if (team != null) { + teams.push( + + ); + } + } + } + } + + let ldapSettings; + let licenseSettings; + if (global.window.mm_config.BuildEnterpriseReady === 'true') { + if (global.window.mm_license.IsLicensed === 'true') { + ldapSettings = ( +
  • + + + +
  • + ); + } + + licenseSettings = ( +
  • + + + +
  • + ); + } + + let audits; + if (global.window.mm_license.IsLicensed === 'true') { + audits = ( +
  • + + + +
  • + ); + } + + return ( +
    +
    + +
    + +
    +
    + + +
    + ); + } +} + +AdminSidebar.propTypes = { + teams: React.PropTypes.object, + selectedTeams: React.PropTypes.object, + removeSelectedTeam: React.PropTypes.func, + addSelectedTeam: React.PropTypes.func, + selected: React.PropTypes.string, + selectedTeam: React.PropTypes.string, + selectTab: React.PropTypes.func +}; diff --git a/webapp/components/admin_console/admin_sidebar_header.jsx b/webapp/components/admin_console/admin_sidebar_header.jsx new file mode 100644 index 000000000..2e6252075 --- /dev/null +++ b/webapp/components/admin_console/admin_sidebar_header.jsx @@ -0,0 +1,70 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import AdminNavbarDropdown from './admin_navbar_dropdown.jsx'; +import UserStore from 'stores/user_store.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; + +export default class SidebarHeader extends React.Component { + constructor(props) { + super(props); + + this.toggleDropdown = this.toggleDropdown.bind(this); + + this.state = {}; + } + + toggleDropdown(e) { + e.preventDefault(); + + if (this.refs.dropdown.blockToggle) { + this.refs.dropdown.blockToggle = false; + return; + } + + $('.team__header').find('.dropdown-toggle').dropdown('toggle'); + } + + render() { + var me = UserStore.getCurrentUser(); + var profilePicture = null; + + if (!me) { + return null; + } + + if (me.last_picture_update) { + profilePicture = ( + + + {profilePicture} +
    +
    {'@' + me.username}
    +
    + +
    +
    +
    + +
    + ); + } +} diff --git a/webapp/components/admin_console/audits.jsx b/webapp/components/admin_console/audits.jsx new file mode 100644 index 000000000..28503d783 --- /dev/null +++ b/webapp/components/admin_console/audits.jsx @@ -0,0 +1,97 @@ +// 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 'react-intl'; + +import React from 'react'; + +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
    ; + } + + if (this.state.audits === null) { + content = ; + } else { + content = ( +
    + +
    + ); + } + + return ( +
    +

    + +

    + +
    + {content} +
    +
    + ); + } +} diff --git a/webapp/components/admin_console/email_settings.jsx b/webapp/components/admin_console/email_settings.jsx new file mode 100644 index 000000000..1decdae91 --- /dev/null +++ b/webapp/components/admin_console/email_settings.jsx @@ -0,0 +1,961 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import crypto from 'crypto'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl'; + +var holders = defineMessages({ + notificationDisplayExample: { + id: 'admin.email.notificationDisplayExample', + defaultMessage: 'Ex: "Mattermost Notification", "System", "No-Reply"' + }, + notificationEmailExample: { + id: 'admin.email.notificationEmailExample', + defaultMessage: 'Ex: "mattermost@yourcompany.com", "admin@yourcompany.com"' + }, + smtpUsernameExample: { + id: 'admin.email.smtpUsernameExample', + defaultMessage: 'Ex: "admin@yourcompany.com", "AKIADTOVBGERKLCBV"' + }, + smtpPasswordExample: { + id: 'admin.email.smtpPasswordExample', + defaultMessage: 'Ex: "yourpassword", "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"' + }, + smtpServerExample: { + id: 'admin.email.smtpServerExample', + defaultMessage: 'Ex: "smtp.yourcompany.com", "email-smtp.us-east-1.amazonaws.com"' + }, + smtpPortExample: { + id: 'admin.email.smtpPortExample', + defaultMessage: 'Ex: "25", "465"' + }, + connectionSecurityNone: { + id: 'admin.email.connectionSecurityNone', + defaultMessage: 'None' + }, + connectionSecurityTls: { + id: 'admin.email.connectionSecurityTls', + defaultMessage: 'TLS (Recommended)' + }, + connectionSecurityStart: { + id: 'admin.email.connectionSecurityStart', + defaultMessage: 'STARTTLS' + }, + inviteSaltExample: { + id: 'admin.email.inviteSaltExample', + defaultMessage: 'Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"' + }, + passwordSaltExample: { + id: 'admin.email.passwordSaltExample', + defaultMessage: 'Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"' + }, + pushServerEx: { + id: 'admin.email.pushServerEx', + defaultMessage: 'E.g.: "http://push-test.mattermost.com"' + }, + testing: { + id: 'admin.email.testing', + defaultMessage: 'Testing...' + }, + saving: { + id: 'admin.email.saving', + defaultMessage: 'Saving Config...' + } +}); + +import React from 'react'; + +class EmailSettings extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleTestConnection = this.handleTestConnection.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.buildConfig = this.buildConfig.bind(this); + this.handleGenerateInvite = this.handleGenerateInvite.bind(this); + this.handleGenerateReset = this.handleGenerateReset.bind(this); + + this.state = { + sendEmailNotifications: this.props.config.EmailSettings.SendEmailNotifications, + sendPushNotifications: this.props.config.EmailSettings.SendPushNotifications, + saveNeeded: false, + serverError: null, + emailSuccess: null, + emailFail: null + }; + } + + handleChange(action) { + var s = {saveNeeded: true, serverError: this.state.serverError}; + + if (action === 'sendEmailNotifications_true') { + s.sendEmailNotifications = true; + } + + if (action === 'sendEmailNotifications_false') { + s.sendEmailNotifications = false; + } + + if (action === 'sendPushNotifications_true') { + s.sendPushNotifications = true; + } + + if (action === 'sendPushNotifications_false') { + s.sendPushNotifications = false; + } + + this.setState(s); + } + + 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; + config.EmailSettings.FeedbackName = ReactDOM.findDOMNode(this.refs.feedbackName).value.trim(); + config.EmailSettings.FeedbackEmail = ReactDOM.findDOMNode(this.refs.feedbackEmail).value.trim(); + config.EmailSettings.SMTPServer = ReactDOM.findDOMNode(this.refs.SMTPServer).value.trim(); + config.EmailSettings.PushNotificationServer = ReactDOM.findDOMNode(this.refs.PushNotificationServer).value.trim(); + config.EmailSettings.SMTPPort = ReactDOM.findDOMNode(this.refs.SMTPPort).value.trim(); + config.EmailSettings.SMTPUsername = ReactDOM.findDOMNode(this.refs.SMTPUsername).value.trim(); + config.EmailSettings.SMTPPassword = ReactDOM.findDOMNode(this.refs.SMTPPassword).value.trim(); + config.EmailSettings.ConnectionSecurity = ReactDOM.findDOMNode(this.refs.ConnectionSecurity).value.trim(); + + config.EmailSettings.InviteSalt = ReactDOM.findDOMNode(this.refs.InviteSalt).value.trim(); + if (config.EmailSettings.InviteSalt === '') { + config.EmailSettings.InviteSalt = crypto.randomBytes(256).toString('base64').substring(0, 32); + ReactDOM.findDOMNode(this.refs.InviteSalt).value = config.EmailSettings.InviteSalt; + } + + config.EmailSettings.PasswordResetSalt = ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value.trim(); + if (config.EmailSettings.PasswordResetSalt === '') { + config.EmailSettings.PasswordResetSalt = crypto.randomBytes(256).toString('base64').substring(0, 32); + ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value = config.EmailSettings.PasswordResetSalt; + } + + return config; + } + + handleGenerateInvite(e) { + e.preventDefault(); + ReactDOM.findDOMNode(this.refs.InviteSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32); + var s = {saveNeeded: true, serverError: this.state.serverError}; + this.setState(s); + } + + handleGenerateReset(e) { + e.preventDefault(); + ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32); + var s = {saveNeeded: true, serverError: this.state.serverError}; + this.setState(s); + } + + handleTestConnection(e) { + e.preventDefault(); + $('#connection-button').button('loading'); + + var config = this.buildConfig(); + + Client.testEmail( + config, + () => { + this.setState({ + sendEmailNotifications: config.EmailSettings.SendEmailNotifications, + serverError: null, + saveNeeded: true, + emailSuccess: true, + emailFail: null + }); + $('#connection-button').button('reset'); + }, + (err) => { + this.setState({ + sendEmailNotifications: config.EmailSettings.SendEmailNotifications, + serverError: null, + saveNeeded: true, + emailSuccess: null, + emailFail: err.message + ' - ' + err.detailed_error + }); + $('#connection-button').button('reset'); + } + ); + } + + handleSubmit(e) { + e.preventDefault(); + $('#save-button').button('loading'); + + var config = this.buildConfig(); + + Client.saveConfig( + config, + () => { + AsyncClient.getConfig(); + this.setState({ + sendEmailNotifications: config.EmailSettings.SendEmailNotifications, + serverError: null, + saveNeeded: false, + emailSuccess: null, + emailFail: null + }); + $('#save-button').button('reset'); + }, + (err) => { + this.setState({ + sendEmailNotifications: config.EmailSettings.SendEmailNotifications, + serverError: err.message, + saveNeeded: true, + emailSuccess: null, + emailFail: null + }); + $('#save-button').button('reset'); + } + ); + } + + render() { + const {formatMessage} = this.props.intl; + var serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + var emailSuccess = ''; + if (this.state.emailSuccess) { + emailSuccess = ( +
    + + +
    + ); + } + + var emailFail = ''; + if (this.state.emailFail) { + emailSuccess = ( +
    + + +
    + ); + } + + return ( +
    +

    + +

    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +
    + + + + + + +
    + + + +
    {'TLS'} + +
    {'STARTTLS'} + +
    +
    +
    + + {emailSuccess} + {emailFail} +
    +
    +
    + +
    + +
    + +

    + +

    +
    + +
    +
    +
    + +
    + +
    + +

    + +

    +
    + +
    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +EmailSettings.propTypes = { + intl: intlShape.isRequired, + config: React.PropTypes.object +}; + +export default injectIntl(EmailSettings); diff --git a/webapp/components/admin_console/gitlab_settings.jsx b/webapp/components/admin_console/gitlab_settings.jsx new file mode 100644 index 000000000..7fdedde13 --- /dev/null +++ b/webapp/components/admin_console/gitlab_settings.jsx @@ -0,0 +1,383 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl'; + +const holders = defineMessages({ + clientIdExample: { + id: 'admin.gitlab.clientIdExample', + defaultMessage: 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"' + }, + clientSecretExample: { + id: 'admin.gitlab.clientSecretExample', + defaultMessage: 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"' + }, + authExample: { + id: 'admin.gitlab.authExample', + defaultMessage: 'Ex ""' + }, + tokenExample: { + id: 'admin.gitlab.tokenExample', + defaultMessage: 'Ex ""' + }, + userExample: { + id: 'admin.gitlab.userExample', + defaultMessage: 'Ex ""' + }, + saving: { + id: 'admin.gitlab.saving', + defaultMessage: 'Saving Config...' + } +}); + +import React from 'react'; + +class GitLabSettings extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + Enable: this.props.config.GitLabSettings.Enable, + saveNeeded: false, + serverError: null + }; + } + + handleChange(action) { + var s = {saveNeeded: true, serverError: this.state.serverError}; + + if (action === 'EnableTrue') { + s.Enable = true; + } + + if (action === 'EnableFalse') { + s.Enable = false; + } + + this.setState(s); + } + + handleSubmit(e) { + e.preventDefault(); + $('#save-button').button('loading'); + + var config = this.props.config; + config.GitLabSettings.Enable = ReactDOM.findDOMNode(this.refs.Enable).checked; + config.GitLabSettings.Secret = ReactDOM.findDOMNode(this.refs.Secret).value.trim(); + config.GitLabSettings.Id = ReactDOM.findDOMNode(this.refs.Id).value.trim(); + config.GitLabSettings.AuthEndpoint = ReactDOM.findDOMNode(this.refs.AuthEndpoint).value.trim(); + config.GitLabSettings.TokenEndpoint = ReactDOM.findDOMNode(this.refs.TokenEndpoint).value.trim(); + config.GitLabSettings.UserApiEndpoint = ReactDOM.findDOMNode(this.refs.UserApiEndpoint).value.trim(); + + Client.saveConfig( + config, + () => { + AsyncClient.getConfig(); + this.setState({ + serverError: null, + saveNeeded: false + }); + $('#save-button').button('reset'); + }, + (err) => { + this.setState({ + serverError: err.message, + saveNeeded: true + }); + $('#save-button').button('reset'); + } + ); + } + + render() { + const {formatMessage} = this.props.intl; + var serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + return ( +
    + +

    + +

    +
    + +
    + +
    + + +

    + +
    +

    +
    + +
    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +//config.GitLabSettings.Scope = ReactDOM.findDOMNode(this.refs.Scope).value.trim(); +//
    +// +//
    +// +//

    {'This field is not yet used by GitLab OAuth. Other OAuth providers may use this field to specify the scope of account data from OAuth provider that is sent to Mattermost.'}

    +//
    +//
    + +GitLabSettings.propTypes = { + intl: intlShape.isRequired, + config: React.PropTypes.object +}; + +export default injectIntl(GitLabSettings); diff --git a/webapp/components/admin_console/image_settings.jsx b/webapp/components/admin_console/image_settings.jsx new file mode 100644 index 000000000..576ff18fd --- /dev/null +++ b/webapp/components/admin_console/image_settings.jsx @@ -0,0 +1,692 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import crypto from 'crypto'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; + +const holders = defineMessages({ + storeLocal: { + id: 'admin.image.storeLocal', + defaultMessage: 'Local File System' + }, + storeAmazonS3: { + id: 'admin.image.storeAmazonS3', + defaultMessage: 'Amazon S3' + }, + localExample: { + id: 'admin.image.localExample', + defaultMessage: 'Ex "./data/"' + }, + amazonS3IdExample: { + id: 'admin.image.amazonS3IdExample', + defaultMessage: 'Ex "AKIADTOVBGERKLCBV"' + }, + amazonS3SecretExample: { + id: 'admin.image.amazonS3SecretExample', + defaultMessage: 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"' + }, + amazonS3BucketExample: { + id: 'admin.image.amazonS3BucketExample', + defaultMessage: 'Ex "mattermost-media"' + }, + amazonS3RegionExample: { + id: 'admin.image.amazonS3RegionExample', + defaultMessage: 'Ex "us-east-1"' + }, + thumbWidthExample: { + id: 'admin.image.thumbWidthExample', + defaultMessage: 'Ex "120"' + }, + thumbHeightExample: { + id: 'admin.image.thumbHeightExample', + defaultMessage: 'Ex "100"' + }, + previewWidthExample: { + id: 'admin.image.previewWidthExample', + defaultMessage: 'Ex "1024"' + }, + previewHeightExample: { + id: 'admin.image.previewHeightExample', + defaultMessage: 'Ex "0"' + }, + profileWidthExample: { + id: 'admin.image.profileWidthExample', + defaultMessage: 'Ex "1024"' + }, + profileHeightExample: { + id: 'admin.image.profileHeightExample', + defaultMessage: 'Ex "0"' + }, + publicLinkExample: { + id: 'admin.image.publicLinkExample', + defaultMessage: 'Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"' + }, + saving: { + id: 'admin.image.saving', + defaultMessage: 'Saving Config...' + } +}); + +import React from 'react'; + +class FileSettings extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleGenerate = this.handleGenerate.bind(this); + + this.state = { + saveNeeded: false, + serverError: null, + DriverName: this.props.config.FileSettings.DriverName + }; + } + + handleChange(action) { + var s = {saveNeeded: true, serverError: this.state.serverError}; + + if (action === 'DriverName') { + s.DriverName = ReactDOM.findDOMNode(this.refs.DriverName).value; + } + + this.setState(s); + } + + handleGenerate(e) { + e.preventDefault(); + ReactDOM.findDOMNode(this.refs.PublicLinkSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32); + var s = {saveNeeded: true, serverError: this.state.serverError}; + this.setState(s); + } + + handleSubmit(e) { + e.preventDefault(); + $('#save-button').button('loading'); + + var config = this.props.config; + config.FileSettings.DriverName = ReactDOM.findDOMNode(this.refs.DriverName).value; + config.FileSettings.Directory = ReactDOM.findDOMNode(this.refs.Directory).value; + config.FileSettings.AmazonS3AccessKeyId = ReactDOM.findDOMNode(this.refs.AmazonS3AccessKeyId).value; + config.FileSettings.AmazonS3SecretAccessKey = ReactDOM.findDOMNode(this.refs.AmazonS3SecretAccessKey).value; + config.FileSettings.AmazonS3Bucket = ReactDOM.findDOMNode(this.refs.AmazonS3Bucket).value; + config.FileSettings.AmazonS3Region = ReactDOM.findDOMNode(this.refs.AmazonS3Region).value; + config.FileSettings.EnablePublicLink = ReactDOM.findDOMNode(this.refs.EnablePublicLink).checked; + + config.FileSettings.PublicLinkSalt = ReactDOM.findDOMNode(this.refs.PublicLinkSalt).value.trim(); + + if (config.FileSettings.PublicLinkSalt === '') { + config.FileSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 32); + ReactDOM.findDOMNode(this.refs.PublicLinkSalt).value = config.FileSettings.PublicLinkSalt; + } + + var thumbnailWidth = 120; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailWidth).value, 10))) { + thumbnailWidth = parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailWidth).value, 10); + } + config.FileSettings.ThumbnailWidth = thumbnailWidth; + ReactDOM.findDOMNode(this.refs.ThumbnailWidth).value = thumbnailWidth; + + var thumbnailHeight = 100; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailHeight).value, 10))) { + thumbnailHeight = parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailHeight).value, 10); + } + config.FileSettings.ThumbnailHeight = thumbnailHeight; + ReactDOM.findDOMNode(this.refs.ThumbnailHeight).value = thumbnailHeight; + + var previewWidth = 1024; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.PreviewWidth).value, 10))) { + previewWidth = parseInt(ReactDOM.findDOMNode(this.refs.PreviewWidth).value, 10); + } + config.FileSettings.PreviewWidth = previewWidth; + ReactDOM.findDOMNode(this.refs.PreviewWidth).value = previewWidth; + + var previewHeight = 0; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.PreviewHeight).value, 10))) { + previewHeight = parseInt(ReactDOM.findDOMNode(this.refs.PreviewHeight).value, 10); + } + config.FileSettings.PreviewHeight = previewHeight; + ReactDOM.findDOMNode(this.refs.PreviewHeight).value = previewHeight; + + var profileWidth = 128; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ProfileWidth).value, 10))) { + profileWidth = parseInt(ReactDOM.findDOMNode(this.refs.ProfileWidth).value, 10); + } + config.FileSettings.ProfileWidth = profileWidth; + ReactDOM.findDOMNode(this.refs.ProfileWidth).value = profileWidth; + + var profileHeight = 128; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ProfileHeight).value, 10))) { + profileHeight = parseInt(ReactDOM.findDOMNode(this.refs.ProfileHeight).value, 10); + } + config.FileSettings.ProfileHeight = profileHeight; + ReactDOM.findDOMNode(this.refs.ProfileHeight).value = profileHeight; + + Client.saveConfig( + config, + () => { + AsyncClient.getConfig(); + this.setState({ + serverError: null, + saveNeeded: false + }); + $('#save-button').button('reset'); + }, + (err) => { + this.setState({ + serverError: err.message, + saveNeeded: true + }); + $('#save-button').button('reset'); + } + ); + } + + render() { + const {formatMessage} = this.props.intl; + var serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + var enableFile = false; + var enableS3 = false; + + if (this.state.DriverName === 'local') { + enableFile = true; + } + + if (this.state.DriverName === 'amazons3') { + enableS3 = true; + } + + return ( +
    +

    + +

    +
    + +
    + +
    + +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    + +
    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +FileSettings.propTypes = { + intl: intlShape.isRequired, + config: React.PropTypes.object +}; + +export default injectIntl(FileSettings); \ No newline at end of file diff --git a/webapp/components/admin_console/ldap_settings.jsx b/webapp/components/admin_console/ldap_settings.jsx new file mode 100644 index 000000000..7996a3aab --- /dev/null +++ b/webapp/components/admin_console/ldap_settings.jsx @@ -0,0 +1,588 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; + +import {injectIntl, intlShape, defineMessages, 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 { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleEnable = this.handleEnable.bind(this); + this.handleDisable = this.handleDisable.bind(this); + + this.state = { + saveNeeded: false, + serverError: null, + enable: this.props.config.LdapSettings.Enable + }; + } + handleChange() { + this.setState({saveNeeded: true}); + } + handleEnable() { + this.setState({saveNeeded: true, enable: true}); + } + handleDisable() { + this.setState({saveNeeded: true, enable: false}); + } + handleSubmit(e) { + e.preventDefault(); + $('#save-button').button('loading'); + + const config = this.props.config; + config.LdapSettings.Enable = this.refs.Enable.checked; + config.LdapSettings.LdapServer = this.refs.LdapServer.value.trim(); + + let LdapPort = DEFAULT_LDAP_PORT; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.LdapPort).value, 10))) { + LdapPort = parseInt(ReactDOM.findDOMNode(this.refs.LdapPort).value, 10); + } + config.LdapSettings.LdapPort = LdapPort; + + config.LdapSettings.BaseDN = this.refs.BaseDN.value.trim(); + config.LdapSettings.BindUsername = this.refs.BindUsername.value.trim(); + config.LdapSettings.BindPassword = this.refs.BindPassword.value.trim(); + config.LdapSettings.FirstNameAttribute = this.refs.FirstNameAttribute.value.trim(); + config.LdapSettings.LastNameAttribute = this.refs.LastNameAttribute.value.trim(); + config.LdapSettings.EmailAttribute = this.refs.EmailAttribute.value.trim(); + config.LdapSettings.UsernameAttribute = this.refs.UsernameAttribute.value.trim(); + config.LdapSettings.IdAttribute = this.refs.IdAttribute.value.trim(); + + let QueryTimeout = DEFAULT_QUERY_TIMEOUT; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.QueryTimeout).value, 10))) { + QueryTimeout = parseInt(ReactDOM.findDOMNode(this.refs.QueryTimeout).value, 10); + } + config.LdapSettings.QueryTimeout = QueryTimeout; + + Client.saveConfig( + config, + () => { + AsyncClient.getConfig(); + this.setState({ + serverError: null, + saveNeeded: false + }); + $('#save-button').button('reset'); + }, + (err) => { + this.setState({ + serverError: err.message, + saveNeeded: true + }); + $('#save-button').button('reset'); + } + ); + } + render() { + const {formatMessage} = this.props.intl; + let serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + let saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP === 'true'; + + let bannerContent; + if (licenseEnabled) { + bannerContent = ( +
    +
    +

    + +

    +

    + +

    +
    +
    + ); + } else { + bannerContent = ( +
    +
    + +
    +
    + ); + } + + return ( +
    + {bannerContent} +

    + +

    +
    +
    + +
    + + +

    + +

    +
    +
    +
    + +
    + +

    + +

    +
    +
    +
    + +
    + +

    + +

    +
    +
    +
    + +
    + +

    + +

    +
    +
    +
    + +
    + +

    + +

    +
    +
    +
    + +
    + +

    + +

    +
    +
    +
    + +
    + +

    + +

    +
    +
    +
    + +
    + +

    + +

    +
    +
    +
    + +
    + +

    + +

    +
    +
    +
    + +
    + +

    + +

    +
    +
    +
    + +
    + +

    + +

    +
    +
    +
    + +
    + +

    + +

    +
    +
    +
    +
    + {serverError} + +
    +
    +
    +
    + ); + } +} +LdapSettings.defaultProps = { +}; + +LdapSettings.propTypes = { + intl: intlShape.isRequired, + config: React.PropTypes.object +}; + +export default injectIntl(LdapSettings); diff --git a/webapp/components/admin_console/legal_and_support_settings.jsx b/webapp/components/admin_console/legal_and_support_settings.jsx new file mode 100644 index 000000000..4997a1385 --- /dev/null +++ b/webapp/components/admin_console/legal_and_support_settings.jsx @@ -0,0 +1,294 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; + +var holders = defineMessages({ + saving: { + id: 'admin.support.saving', + defaultMessage: 'Saving Config...' + } +}); + +import React from 'react'; + +class LegalAndSupportSettings extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + saveNeeded: false, + serverError: null + }; + } + + handleChange() { + var s = {saveNeeded: true, serverError: this.state.serverError}; + this.setState(s); + } + + handleSubmit(e) { + e.preventDefault(); + $('#save-button').button('loading'); + + var config = this.props.config; + + config.SupportSettings.TermsOfServiceLink = ReactDOM.findDOMNode(this.refs.TermsOfServiceLink).value.trim(); + config.SupportSettings.PrivacyPolicyLink = ReactDOM.findDOMNode(this.refs.PrivacyPolicyLink).value.trim(); + config.SupportSettings.AboutLink = ReactDOM.findDOMNode(this.refs.AboutLink).value.trim(); + config.SupportSettings.HelpLink = ReactDOM.findDOMNode(this.refs.HelpLink).value.trim(); + config.SupportSettings.ReportAProblemLink = ReactDOM.findDOMNode(this.refs.ReportAProblemLink).value.trim(); + config.SupportSettings.SupportEmail = ReactDOM.findDOMNode(this.refs.SupportEmail).value.trim(); + + Client.saveConfig( + config, + () => { + AsyncClient.getConfig(); + this.setState({ + serverError: null, + saveNeeded: false + }); + $('#save-button').button('reset'); + }, + (err) => { + this.setState({ + serverError: err.message, + saveNeeded: true + }); + $('#save-button').button('reset'); + } + ); + } + + render() { + var serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + return ( +
    + +

    + +

    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +LegalAndSupportSettings.propTypes = { + intl: intlShape.isRequired, + config: React.PropTypes.object +}; + +export default injectIntl(LegalAndSupportSettings); \ No newline at end of file diff --git a/webapp/components/admin_console/license_settings.jsx b/webapp/components/admin_console/license_settings.jsx new file mode 100644 index 000000000..5aa0dba7e --- /dev/null +++ b/webapp/components/admin_console/license_settings.jsx @@ -0,0 +1,295 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import * as Utils from 'utils/utils.jsx'; +import * as Client from 'utils/client.jsx'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl'; + +const holders = defineMessages({ + removing: { + id: 'admin.license.removing', + defaultMessage: 'Removing License...' + }, + uploading: { + id: 'admin.license.uploading', + defaultMessage: 'Uploading License...' + } +}); + +import React from 'react'; + +class LicenseSettings extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleRemove = this.handleRemove.bind(this); + + this.state = { + fileSelected: false, + fileName: null, + serverError: null + }; + } + + handleChange() { + const element = $(ReactDOM.findDOMNode(this.refs.fileInput)); + if (element.prop('files').length > 0) { + this.setState({fileSelected: true, fileName: element.prop('files')[0].name}); + } + } + + handleSubmit(e) { + e.preventDefault(); + + const element = $(ReactDOM.findDOMNode(this.refs.fileInput)); + if (element.prop('files').length === 0) { + return; + } + const file = element.prop('files')[0]; + + $('#upload-button').button('loading'); + + const formData = new FormData(); + formData.append('license', file, file.name); + + Client.uploadLicenseFile(formData, + () => { + Utils.clearFileInput(element[0]); + $('#upload-button').button('reset'); + this.setState({fileSelected: false, fileName: null, serverError: null}); + window.location.reload(true); + }, + (error) => { + Utils.clearFileInput(element[0]); + $('#upload-button').button('reset'); + this.setState({fileSelected: false, fileName: null, serverError: error.message}); + } + ); + } + + handleRemove(e) { + e.preventDefault(); + + $('#remove-button').button('loading'); + + Client.removeLicenseFile( + () => { + $('#remove-button').button('reset'); + this.setState({fileSelected: false, fileName: null, serverError: null}); + window.location.reload(true); + }, + (error) => { + $('#remove-button').button('reset'); + this.setState({fileSelected: false, fileName: null, serverError: error.message}); + } + ); + } + + render() { + var serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + var btnClass = 'btn'; + if (this.state.fileSelected) { + btnClass = 'btn btn-primary'; + } + + let edition; + let licenseType; + let licenseKey; + + if (global.window.mm_license.IsLicensed === 'true') { + edition = ( + + ); + licenseType = ( + + ); + + licenseKey = ( +
    + +
    +
    +

    + +

    +
    + ); + } else { + edition = ( + + ); + + licenseType = ( + + ); + + let fileName; + if (this.state.fileName) { + fileName = this.state.fileName; + } else { + fileName = ( + + ); + } + + licenseKey = ( +
    +
    + + +
    + +
    + {fileName} +
    +
    + {serverError} +

    + +

    +
    + ); + } + + return ( +
    +

    + +

    +
    +
    + +
    + {edition} +
    +
    +
    + +
    + {licenseType} +
    +
    +
    + + {licenseKey} +
    +
    +
    + ); + } +} + +LicenseSettings.propTypes = { + intl: intlShape.isRequired, + config: React.PropTypes.object +}; + +export default injectIntl(LicenseSettings); diff --git a/webapp/components/admin_console/log_settings.jsx b/webapp/components/admin_console/log_settings.jsx new file mode 100644 index 000000000..5aa3ca1e0 --- /dev/null +++ b/webapp/components/admin_console/log_settings.jsx @@ -0,0 +1,418 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; + +const holders = defineMessages({ + locationPlaceholder: { + id: 'admin.log.locationPlaceholder', + defaultMessage: 'Enter your file location' + }, + formatPlaceholder: { + id: 'admin.log.formatPlaceholder', + defaultMessage: 'Enter your file format' + }, + saving: { + id: 'admin.log.saving', + defaultMessage: 'Saving Config...' + } +}); + +import React from 'react'; + +class LogSettings extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + consoleEnable: this.props.config.LogSettings.EnableConsole, + fileEnable: this.props.config.LogSettings.EnableFile, + saveNeeded: false, + serverError: null + }; + } + + handleChange(action) { + var s = {saveNeeded: true, serverError: this.state.serverError}; + + if (action === 'console_true') { + s.consoleEnable = true; + } + + if (action === 'console_false') { + s.consoleEnable = false; + } + + if (action === 'file_true') { + s.fileEnable = true; + } + + if (action === 'file_false') { + s.fileEnable = false; + } + + this.setState(s); + } + + handleSubmit(e) { + e.preventDefault(); + $('#save-button').button('loading'); + + var config = this.props.config; + config.LogSettings.EnableConsole = ReactDOM.findDOMNode(this.refs.consoleEnable).checked; + config.LogSettings.ConsoleLevel = ReactDOM.findDOMNode(this.refs.consoleLevel).value; + config.LogSettings.EnableFile = ReactDOM.findDOMNode(this.refs.fileEnable).checked; + config.LogSettings.FileLevel = ReactDOM.findDOMNode(this.refs.fileLevel).value; + config.LogSettings.FileLocation = ReactDOM.findDOMNode(this.refs.fileLocation).value.trim(); + config.LogSettings.FileFormat = ReactDOM.findDOMNode(this.refs.fileFormat).value.trim(); + + Client.saveConfig( + config, + () => { + AsyncClient.getConfig(); + this.setState({ + consoleEnable: config.LogSettings.EnableConsole, + fileEnable: config.LogSettings.EnableFile, + serverError: null, + saveNeeded: false + }); + $('#save-button').button('reset'); + }, + (err) => { + this.setState({ + consoleEnable: config.LogSettings.EnableConsole, + fileEnable: config.LogSettings.EnableFile, + serverError: err.message, + saveNeeded: true + }); + $('#save-button').button('reset'); + } + ); + } + + render() { + const {formatMessage} = this.props.intl; + var serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + return ( +
    +

    + +

    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +
    + +
    + + + + + + + + + +
    {'%T'} + +
    {'%D'} + +
    {'%d'} + +
    {'%L'} + +
    {'%S'} + +
    {'%M'} + +
    +
    +
    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +LogSettings.propTypes = { + intl: intlShape.isRequired, + config: React.PropTypes.object +}; + +export default injectIntl(LogSettings); \ No newline at end of file diff --git a/webapp/components/admin_console/logs.jsx b/webapp/components/admin_console/logs.jsx new file mode 100644 index 000000000..f2c6d92c3 --- /dev/null +++ b/webapp/components/admin_console/logs.jsx @@ -0,0 +1,102 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AdminStore from 'stores/admin_store.jsx'; +import LoadingScreen from '../loading_screen.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; + +export default class Logs extends React.Component { + constructor(props) { + super(props); + + this.onLogListenerChange = this.onLogListenerChange.bind(this); + this.reload = this.reload.bind(this); + + this.state = { + logs: AdminStore.getLogs() + }; + } + + componentDidMount() { + AdminStore.addLogChangeListener(this.onLogListenerChange); + AsyncClient.getLogs(); + } + + componentWillUnmount() { + AdminStore.removeLogChangeListener(this.onLogListenerChange); + } + + onLogListenerChange() { + this.setState({ + logs: AdminStore.getLogs() + }); + } + + reload() { + AdminStore.saveLogs(null); + this.setState({ + logs: null + }); + + AsyncClient.getLogs(); + } + + render() { + var content = null; + + if (this.state.logs === null) { + content = ; + } else { + content = []; + + for (var i = 0; i < this.state.logs.length; i++) { + var style = { + whiteSpace: 'nowrap', + fontFamily: 'monospace' + }; + + if (this.state.logs[i].indexOf('[EROR]') > 0) { + style.color = 'red'; + } + + content.push(
    ); + content.push( + + {this.state.logs[i]} + + ); + } + } + + return ( +
    +

    + +

    + +
    + {content} +
    +
    + ); + } +} \ No newline at end of file diff --git a/webapp/components/admin_console/privacy_settings.jsx b/webapp/components/admin_console/privacy_settings.jsx new file mode 100644 index 000000000..a312dddca --- /dev/null +++ b/webapp/components/admin_console/privacy_settings.jsx @@ -0,0 +1,215 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; + +const holders = defineMessages({ + saving: { + id: 'admin.privacy.saving', + defaultMessage: 'Saving Config...' + } +}); + +import React from 'react'; + +class PrivacySettings extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + saveNeeded: false, + serverError: null + }; + } + + handleChange() { + var s = {saveNeeded: true, serverError: this.state.serverError}; + + this.setState(s); + } + + handleSubmit(e) { + e.preventDefault(); + $('#save-button').button('loading'); + + var config = this.props.config; + config.PrivacySettings.ShowEmailAddress = ReactDOM.findDOMNode(this.refs.ShowEmailAddress).checked; + config.PrivacySettings.ShowFullName = ReactDOM.findDOMNode(this.refs.ShowFullName).checked; + + Client.saveConfig( + config, + () => { + AsyncClient.getConfig(); + this.setState({ + serverError: null, + saveNeeded: false + }); + $('#save-button').button('reset'); + }, + (err) => { + this.setState({ + serverError: err.message, + saveNeeded: true + }); + $('#save-button').button('reset'); + } + ); + } + + render() { + var serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + return ( +
    +

    + +

    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +PrivacySettings.propTypes = { + intl: intlShape.isRequired, + config: React.PropTypes.object +}; + +export default injectIntl(PrivacySettings); \ No newline at end of file diff --git a/webapp/components/admin_console/rate_settings.jsx b/webapp/components/admin_console/rate_settings.jsx new file mode 100644 index 000000000..f3fb1742c --- /dev/null +++ b/webapp/components/admin_console/rate_settings.jsx @@ -0,0 +1,371 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; + +const holders = defineMessages({ + queriesExample: { + id: 'admin.rate.queriesExample', + defaultMessage: 'Ex "10"' + }, + memoryExample: { + id: 'admin.rate.memoryExample', + defaultMessage: 'Ex "10000"' + }, + httpHeaderExample: { + id: 'admin.rate.httpHeaderExample', + defaultMessage: 'Ex "X-Real-IP", "X-Forwarded-For"' + }, + saving: { + id: 'admin.rate.saving', + defaultMessage: 'Saving Config...' + } +}); + +import React from 'react'; + +class RateSettings extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + EnableRateLimiter: this.props.config.RateLimitSettings.EnableRateLimiter, + VaryByRemoteAddr: this.props.config.RateLimitSettings.VaryByRemoteAddr, + saveNeeded: false, + serverError: null + }; + } + + handleChange(action) { + var s = {saveNeeded: true, serverError: this.state.serverError}; + + if (action === 'EnableRateLimiterTrue') { + s.EnableRateLimiter = true; + } + + if (action === 'EnableRateLimiterFalse') { + s.EnableRateLimiter = false; + } + + if (action === 'VaryByRemoteAddrTrue') { + s.VaryByRemoteAddr = true; + } + + if (action === 'VaryByRemoteAddrFalse') { + s.VaryByRemoteAddr = false; + } + + this.setState(s); + } + + handleSubmit(e) { + e.preventDefault(); + $('#save-button').button('loading'); + + var config = this.props.config; + config.RateLimitSettings.EnableRateLimiter = ReactDOM.findDOMNode(this.refs.EnableRateLimiter).checked; + config.RateLimitSettings.VaryByRemoteAddr = ReactDOM.findDOMNode(this.refs.VaryByRemoteAddr).checked; + config.RateLimitSettings.VaryByHeader = ReactDOM.findDOMNode(this.refs.VaryByHeader).value.trim(); + + var PerSec = 10; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.PerSec).value, 10))) { + PerSec = parseInt(ReactDOM.findDOMNode(this.refs.PerSec).value, 10); + } + config.RateLimitSettings.PerSec = PerSec; + ReactDOM.findDOMNode(this.refs.PerSec).value = PerSec; + + var MemoryStoreSize = 10000; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MemoryStoreSize).value, 10))) { + MemoryStoreSize = parseInt(ReactDOM.findDOMNode(this.refs.MemoryStoreSize).value, 10); + } + config.RateLimitSettings.MemoryStoreSize = MemoryStoreSize; + ReactDOM.findDOMNode(this.refs.MemoryStoreSize).value = MemoryStoreSize; + + Client.saveConfig( + config, + () => { + AsyncClient.getConfig(); + this.setState({ + serverError: null, + saveNeeded: false + }); + $('#save-button').button('reset'); + }, + (err) => { + this.setState({ + serverError: err.message, + saveNeeded: true + }); + $('#save-button').button('reset'); + } + ); + } + + render() { + const {formatMessage} = this.props.intl; + var serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + return ( +
    + +
    +
    +

    + +

    +

    + +

    +
    +
    + +

    + +

    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +RateSettings.propTypes = { + intl: intlShape.isRequired, + config: React.PropTypes.object +}; + +export default injectIntl(RateSettings); \ No newline at end of file diff --git a/webapp/components/admin_console/reset_password_modal.jsx b/webapp/components/admin_console/reset_password_modal.jsx new file mode 100644 index 000000000..f80c740e3 --- /dev/null +++ b/webapp/components/admin_console/reset_password_modal.jsx @@ -0,0 +1,162 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ReactDOM from 'react-dom'; +import * as Client from 'utils/client.jsx'; +import Constants from 'utils/constants.jsx'; +import {Modal} from 'react-bootstrap'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; + +var holders = defineMessages({ + submit: { + id: 'admin.reset_password.submit', + defaultMessage: 'Please enter at least {chars} characters.' + } +}); + +import React from 'react'; + +class ResetPasswordModal extends React.Component { + constructor(props) { + super(props); + + this.doSubmit = this.doSubmit.bind(this); + this.doCancel = this.doCancel.bind(this); + + this.state = { + serverError: null + }; + } + + doSubmit(e) { + e.preventDefault(); + var password = ReactDOM.findDOMNode(this.refs.password).value; + + if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) { + this.setState({serverError: this.props.intl.formatMessage(holders.submit, {chars: Constants.MIN_PASSWORD_LENGTH})}); + return; + } + + this.setState({serverError: null}); + + var data = {}; + data.new_password = password; + data.name = this.props.team.name; + data.user_id = this.props.user.id; + + Client.resetPassword(data, + () => { + this.props.onModalSubmit(ReactDOM.findDOMNode(this.refs.password).value); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + doCancel() { + this.setState({serverError: null}); + this.props.onModalDismissed(); + } + + render() { + if (this.props.user == null) { + return
    ; + } + + let urlClass = 'input-group input-group--limit'; + let serverError = null; + + if (this.state.serverError) { + urlClass += ' has-error'; + serverError =

    {this.state.serverError}

    ; + } + + return ( + + + + + + +
    + +
    +
    +
    + + + + +
    + {serverError} +
    +
    +
    + + + + +
    +
    + ); + } +} + +ResetPasswordModal.defaultProps = { + show: false +}; + +ResetPasswordModal.propTypes = { + intl: intlShape.isRequired, + user: React.PropTypes.object, + team: React.PropTypes.object, + show: React.PropTypes.bool.isRequired, + onModalSubmit: React.PropTypes.func, + onModalDismissed: React.PropTypes.func +}; + +export default injectIntl(ResetPasswordModal); \ No newline at end of file diff --git a/webapp/components/admin_console/select_team_modal.jsx b/webapp/components/admin_console/select_team_modal.jsx new file mode 100644 index 000000000..ed6d33056 --- /dev/null +++ b/webapp/components/admin_console/select_team_modal.jsx @@ -0,0 +1,115 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ReactDOM from 'react-dom'; +import {FormattedMessage} from 'react-intl'; + +import {Modal} from 'react-bootstrap'; + +import React from 'react'; + +export default class SelectTeamModal extends React.Component { + constructor(props) { + super(props); + + this.doSubmit = this.doSubmit.bind(this); + this.doCancel = this.doCancel.bind(this); + } + + doSubmit(e) { + e.preventDefault(); + this.props.onModalSubmit(ReactDOM.findDOMNode(this.refs.team).value); + } + doCancel() { + this.props.onModalDismissed(); + } + render() { + if (this.props.teams == null) { + return
    ; + } + + var options = []; + + for (var key in this.props.teams) { + if (this.props.teams.hasOwnProperty(key)) { + var team = this.props.teams[key]; + options.push( + + ); + } + } + + return ( + + + + + + +
    + +
    +
    + +
    +
    +
    + + + + +
    +
    + ); + } +} + +SelectTeamModal.defaultProps = { + show: false +}; + +SelectTeamModal.propTypes = { + teams: React.PropTypes.object, + show: React.PropTypes.bool.isRequired, + onModalSubmit: React.PropTypes.func, + onModalDismissed: React.PropTypes.func +}; \ No newline at end of file diff --git a/webapp/components/admin_console/service_settings.jsx b/webapp/components/admin_console/service_settings.jsx new file mode 100644 index 000000000..881d22d76 --- /dev/null +++ b/webapp/components/admin_console/service_settings.jsx @@ -0,0 +1,984 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl'; + +const DefaultSessionLength = 30; +const DefaultMaximumLoginAttempts = 10; +const DefaultSessionCacheInMinutes = 10; + +var holders = defineMessages({ + listenExample: { + id: 'admin.service.listenExample', + defaultMessage: 'Ex ":8065"' + }, + attemptExample: { + id: 'admin.service.attemptExample', + defaultMessage: 'Ex "10"' + }, + segmentExample: { + id: 'admin.service.segmentExample', + defaultMessage: 'Ex "g3fgGOXJAQ43QV7rAh6iwQCkV4cA1Gs"' + }, + googleExample: { + id: 'admin.service.googleExample', + defaultMessage: 'Ex "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV"' + }, + sessionDaysEx: { + id: 'admin.service.sessionDaysEx', + defaultMessage: 'Ex "30"' + }, + corsExample: { + id: 'admin.service.corsEx', + defaultMessage: 'http://example.com' + }, + saving: { + id: 'admin.service.saving', + defaultMessage: 'Saving Config...' + } +}); + +import React from 'react'; + +class ServiceSettings extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + saveNeeded: false, + serverError: null + }; + } + + handleChange() { + var s = {saveNeeded: true, serverError: this.state.serverError}; + this.setState(s); + } + + handleSubmit(e) { + e.preventDefault(); + $('#save-button').button('loading'); + + var config = this.props.config; + config.ServiceSettings.ListenAddress = ReactDOM.findDOMNode(this.refs.ListenAddress).value.trim(); + if (config.ServiceSettings.ListenAddress === '') { + config.ServiceSettings.ListenAddress = ':8065'; + ReactDOM.findDOMNode(this.refs.ListenAddress).value = config.ServiceSettings.ListenAddress; + } + + config.ServiceSettings.SegmentDeveloperKey = ReactDOM.findDOMNode(this.refs.SegmentDeveloperKey).value.trim(); + config.ServiceSettings.GoogleDeveloperKey = ReactDOM.findDOMNode(this.refs.GoogleDeveloperKey).value.trim(); + config.ServiceSettings.EnableIncomingWebhooks = ReactDOM.findDOMNode(this.refs.EnableIncomingWebhooks).checked; + config.ServiceSettings.EnableOutgoingWebhooks = ReactDOM.findDOMNode(this.refs.EnableOutgoingWebhooks).checked; + config.ServiceSettings.EnablePostUsernameOverride = ReactDOM.findDOMNode(this.refs.EnablePostUsernameOverride).checked; + config.ServiceSettings.EnablePostIconOverride = ReactDOM.findDOMNode(this.refs.EnablePostIconOverride).checked; + config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked; + config.ServiceSettings.EnableDeveloper = ReactDOM.findDOMNode(this.refs.EnableDeveloper).checked; + config.ServiceSettings.EnableSecurityFixAlert = ReactDOM.findDOMNode(this.refs.EnableSecurityFixAlert).checked; + config.ServiceSettings.EnableInsecureOutgoingConnections = ReactDOM.findDOMNode(this.refs.EnableInsecureOutgoingConnections).checked; + config.ServiceSettings.EnableCommands = ReactDOM.findDOMNode(this.refs.EnableCommands).checked; + config.ServiceSettings.EnableOnlyAdminIntegrations = ReactDOM.findDOMNode(this.refs.EnableOnlyAdminIntegrations).checked; + + //config.ServiceSettings.EnableOAuthServiceProvider = ReactDOM.findDOMNode(this.refs.EnableOAuthServiceProvider).checked; + + var MaximumLoginAttempts = DefaultMaximumLoginAttempts; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaximumLoginAttempts).value, 10))) { + MaximumLoginAttempts = parseInt(ReactDOM.findDOMNode(this.refs.MaximumLoginAttempts).value, 10); + } + if (MaximumLoginAttempts < 1) { + MaximumLoginAttempts = 1; + } + config.ServiceSettings.MaximumLoginAttempts = MaximumLoginAttempts; + ReactDOM.findDOMNode(this.refs.MaximumLoginAttempts).value = MaximumLoginAttempts; + + var SessionLengthWebInDays = DefaultSessionLength; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthWebInDays).value, 10))) { + SessionLengthWebInDays = parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthWebInDays).value, 10); + } + if (SessionLengthWebInDays < 1) { + SessionLengthWebInDays = 1; + } + config.ServiceSettings.SessionLengthWebInDays = SessionLengthWebInDays; + ReactDOM.findDOMNode(this.refs.SessionLengthWebInDays).value = SessionLengthWebInDays; + + var SessionLengthMobileInDays = DefaultSessionLength; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthMobileInDays).value, 10))) { + SessionLengthMobileInDays = parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthMobileInDays).value, 10); + } + if (SessionLengthMobileInDays < 1) { + SessionLengthMobileInDays = 1; + } + config.ServiceSettings.SessionLengthMobileInDays = SessionLengthMobileInDays; + ReactDOM.findDOMNode(this.refs.SessionLengthMobileInDays).value = SessionLengthMobileInDays; + + var SessionLengthSSOInDays = DefaultSessionLength; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthSSOInDays).value, 10))) { + SessionLengthSSOInDays = parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthSSOInDays).value, 10); + } + if (SessionLengthSSOInDays < 1) { + SessionLengthSSOInDays = 1; + } + config.ServiceSettings.SessionLengthSSOInDays = SessionLengthSSOInDays; + ReactDOM.findDOMNode(this.refs.SessionLengthSSOInDays).value = SessionLengthSSOInDays; + + var SessionCacheInMinutes = DefaultSessionCacheInMinutes; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionCacheInMinutes).value, 10))) { + SessionCacheInMinutes = parseInt(ReactDOM.findDOMNode(this.refs.SessionCacheInMinutes).value, 10); + } + if (SessionCacheInMinutes < -1) { + SessionCacheInMinutes = -1; + } + config.ServiceSettings.SessionCacheInMinutes = SessionCacheInMinutes; + ReactDOM.findDOMNode(this.refs.SessionCacheInMinutes).value = SessionCacheInMinutes; + + config.ServiceSettings.AllowCorsFrom = ReactDOM.findDOMNode(this.refs.AllowCorsFrom).value.trim(); + + Client.saveConfig( + config, + () => { + AsyncClient.getConfig(); + this.setState({ + serverError: null, + saveNeeded: false + }); + $('#save-button').button('reset'); + }, + (err) => { + this.setState({ + serverError: err.message, + saveNeeded: true + }); + $('#save-button').button('reset'); + } + ); + } + + render() { + const {formatMessage} = this.props.intl; + var serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + return ( +
    + +

    + +

    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +//
    +// +//
    +// +// +//

    {'When enabled Mattermost will act as an OAuth2 Provider. Changing this will require a server restart before taking effect.'}

    +//
    +//
    + +ServiceSettings.propTypes = { + intl: intlShape.isRequired, + config: React.PropTypes.object +}; + +export default injectIntl(ServiceSettings); diff --git a/webapp/components/admin_console/sql_settings.jsx b/webapp/components/admin_console/sql_settings.jsx new file mode 100644 index 000000000..33bb2cece --- /dev/null +++ b/webapp/components/admin_console/sql_settings.jsx @@ -0,0 +1,390 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import crypto from 'crypto'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; + +const holders = defineMessages({ + warning: { + id: 'admin.sql.warning', + defaultMessage: 'Warning: re-generating this salt may cause some columns in the database to return empty results.' + }, + maxConnectionsExample: { + id: 'admin.sql.maxConnectionsExample', + defaultMessage: 'Ex "10"' + }, + maxOpenExample: { + id: 'admin.sql.maxOpenExample', + defaultMessage: 'Ex "10"' + }, + keyExample: { + id: 'admin.sql.keyExample', + defaultMessage: 'Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"' + }, + saving: { + id: 'admin.sql.saving', + defaultMessage: 'Saving Config...' + } +}); + +import React from 'react'; + +class SqlSettings extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleGenerate = this.handleGenerate.bind(this); + + this.state = { + saveNeeded: false, + serverError: null + }; + } + + handleChange() { + var s = {saveNeeded: true, serverError: this.state.serverError}; + this.setState(s); + } + + handleSubmit(e) { + e.preventDefault(); + $('#save-button').button('loading'); + + var config = this.props.config; + config.SqlSettings.Trace = ReactDOM.findDOMNode(this.refs.Trace).checked; + config.SqlSettings.AtRestEncryptKey = ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value.trim(); + + if (config.SqlSettings.AtRestEncryptKey === '') { + config.SqlSettings.AtRestEncryptKey = crypto.randomBytes(256).toString('base64').substring(0, 32); + ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value = config.SqlSettings.AtRestEncryptKey; + } + + var MaxOpenConns = 10; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxOpenConns).value, 10))) { + MaxOpenConns = parseInt(ReactDOM.findDOMNode(this.refs.MaxOpenConns).value, 10); + } + config.SqlSettings.MaxOpenConns = MaxOpenConns; + ReactDOM.findDOMNode(this.refs.MaxOpenConns).value = MaxOpenConns; + + var MaxIdleConns = 10; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxIdleConns).value, 10))) { + MaxIdleConns = parseInt(ReactDOM.findDOMNode(this.refs.MaxIdleConns).value, 10); + } + config.SqlSettings.MaxIdleConns = MaxIdleConns; + ReactDOM.findDOMNode(this.refs.MaxIdleConns).value = MaxIdleConns; + + Client.saveConfig( + config, + () => { + AsyncClient.getConfig(); + this.setState({ + serverError: null, + saveNeeded: false + }); + $('#save-button').button('reset'); + }, + (err) => { + this.setState({ + serverError: err.message, + saveNeeded: true + }); + $('#save-button').button('reset'); + } + ); + } + + handleGenerate(e) { + e.preventDefault(); + + var cfm = global.window.confirm(this.props.intl.formatMessage(holders.warning)); + if (cfm === false) { + return; + } + + ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 32); + var s = {saveNeeded: true, serverError: this.state.serverError}; + this.setState(s); + } + + render() { + const {formatMessage} = this.props.intl; + var serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + var dataSource = '**********' + this.props.config.SqlSettings.DataSource.substring(this.props.config.SqlSettings.DataSource.indexOf('@')); + + var dataSourceReplicas = ''; + this.props.config.SqlSettings.DataSourceReplicas.forEach((replica) => { + dataSourceReplicas += '[**********' + replica.substring(replica.indexOf('@')) + '] '; + }); + + if (this.props.config.SqlSettings.DataSourceReplicas.length === 0) { + dataSourceReplicas = 'none'; + } + + return ( +
    + +
    +
    +

    + +

    +

    + +

    +
    +
    + +

    + +

    +
    + +
    + +
    +

    {this.props.config.SqlSettings.DriverName}

    +
    +
    + +
    + +
    +

    {dataSource}

    +
    +
    + +
    + +
    +

    {dataSourceReplicas}

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    + +
    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +SqlSettings.propTypes = { + intl: intlShape.isRequired, + config: React.PropTypes.object +}; + +export default injectIntl(SqlSettings); \ No newline at end of file diff --git a/webapp/components/admin_console/team_settings.jsx b/webapp/components/admin_console/team_settings.jsx new file mode 100644 index 000000000..654f0085d --- /dev/null +++ b/webapp/components/admin_console/team_settings.jsx @@ -0,0 +1,420 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; + +import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; + +const holders = defineMessages({ + siteNameExample: { + id: 'admin.team.siteNameExample', + defaultMessage: 'Ex "Mattermost"' + }, + maxUsersExample: { + id: 'admin.team.maxUsersExample', + defaultMessage: 'Ex "25"' + }, + restrictExample: { + id: 'admin.team.restrictExample', + defaultMessage: 'Ex "corp.mattermost.com, mattermost.org"' + }, + saving: { + id: 'admin.team.saving', + defaultMessage: 'Saving Config...' + } +}); + +import React from 'react'; + +class TeamSettings extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + saveNeeded: false, + serverError: null + }; + } + + handleChange() { + var s = {saveNeeded: true, serverError: this.state.serverError}; + this.setState(s); + } + + handleSubmit(e) { + e.preventDefault(); + $('#save-button').button('loading'); + + var config = this.props.config; + config.TeamSettings.SiteName = ReactDOM.findDOMNode(this.refs.SiteName).value.trim(); + config.TeamSettings.RestrictCreationToDomains = ReactDOM.findDOMNode(this.refs.RestrictCreationToDomains).value.trim(); + config.TeamSettings.EnableTeamCreation = ReactDOM.findDOMNode(this.refs.EnableTeamCreation).checked; + config.TeamSettings.EnableUserCreation = ReactDOM.findDOMNode(this.refs.EnableUserCreation).checked; + config.TeamSettings.RestrictTeamNames = ReactDOM.findDOMNode(this.refs.RestrictTeamNames).checked; + config.TeamSettings.EnableTeamListing = ReactDOM.findDOMNode(this.refs.EnableTeamListing).checked; + + var MaxUsersPerTeam = 50; + if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) { + MaxUsersPerTeam = parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10); + } + config.TeamSettings.MaxUsersPerTeam = MaxUsersPerTeam; + ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value = MaxUsersPerTeam; + + Client.saveConfig( + config, + () => { + AsyncClient.getConfig(); + this.setState({ + serverError: null, + saveNeeded: false + }); + $('#save-button').button('reset'); + }, + (err) => { + this.setState({ + serverError: err.message, + saveNeeded: true + }); + $('#save-button').button('reset'); + } + ); + } + + render() { + const {formatMessage} = this.props.intl; + var serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + return ( +
    + +

    + +

    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    + +
    + + +

    + +

    +
    +
    + +
    +
    + {serverError} + +
    +
    + +
    +
    + ); + } +} + +TeamSettings.propTypes = { + intl: intlShape.isRequired, + config: React.PropTypes.object +}; + +export default injectIntl(TeamSettings); \ No newline at end of file diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx new file mode 100644 index 000000000..1bf3f785b --- /dev/null +++ b/webapp/components/admin_console/team_users.jsx @@ -0,0 +1,188 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Client from 'utils/client.jsx'; +import LoadingScreen from '../loading_screen.jsx'; +import UserItem from './user_item.jsx'; +import ResetPasswordModal from './reset_password_modal.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; + +export default class UserList extends React.Component { + constructor(props) { + super(props); + + this.getTeamProfiles = this.getTeamProfiles.bind(this); + this.getCurrentTeamProfiles = this.getCurrentTeamProfiles.bind(this); + this.doPasswordReset = this.doPasswordReset.bind(this); + this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this); + this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this); + + this.state = { + teamId: props.team.id, + users: null, + serverError: null, + showPasswordModal: false, + user: null + }; + } + + componentDidMount() { + this.getCurrentTeamProfiles(); + } + + getCurrentTeamProfiles() { + this.getTeamProfiles(this.props.team.id); + } + + getTeamProfiles(teamId) { + Client.getProfilesForTeam( + teamId, + (users) => { + var memberList = []; + for (var id in users) { + if (users.hasOwnProperty(id)) { + memberList.push(users[id]); + } + } + + memberList.sort((a, b) => { + if (a.username < b.username) { + return -1; + } + + if (a.username > b.username) { + return 1; + } + + return 0; + }); + + this.setState({ + teamId: this.state.teamId, + users: memberList, + serverError: this.state.serverError, + showPasswordModal: this.state.showPasswordModal, + user: this.state.user + }); + }, + (err) => { + this.setState({ + teamId: this.state.teamId, + users: null, + serverError: err.message, + showPasswordModal: this.state.showPasswordModal, + user: this.state.user + }); + } + ); + } + + doPasswordReset(user) { + this.setState({ + teamId: this.state.teamId, + users: this.state.users, + serverError: this.state.serverError, + showPasswordModal: true, + user + }); + } + + doPasswordResetDismiss() { + this.setState({ + teamId: this.state.teamId, + users: this.state.users, + serverError: this.state.serverError, + showPasswordModal: false, + user: null + }); + } + + doPasswordResetSubmit() { + this.setState({ + teamId: this.state.teamId, + users: this.state.users, + serverError: this.state.serverError, + showPasswordModal: false, + user: null + }); + } + + componentWillReceiveProps(newProps) { + this.getTeamProfiles(newProps.team.id); + } + + render() { + var serverError = ''; + if (this.state.serverError) { + serverError =
    ; + } + + if (this.state.users == null) { + return ( +
    +

    + +

    + {serverError} + +
    + ); + } + + var memberList = this.state.users.map((user) => { + return ( + ); + }); + + return ( +
    +

    + +

    + {serverError} +
    + + + {memberList} + +
    +
    + +
    + ); + } +} + +UserList.propTypes = { + team: React.PropTypes.object +}; diff --git a/webapp/components/admin_console/user_item.jsx b/webapp/components/admin_console/user_item.jsx new file mode 100644 index 000000000..c6498eafc --- /dev/null +++ b/webapp/components/admin_console/user_item.jsx @@ -0,0 +1,423 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Client from 'utils/client.jsx'; +import * as Utils from 'utils/utils.jsx'; +import UserStore from 'stores/user_store.jsx'; +import ConfirmModal from '../confirm_modal.jsx'; +import TeamStore from 'stores/team_store.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; + +export default class UserItem extends React.Component { + constructor(props) { + super(props); + + this.handleMakeMember = this.handleMakeMember.bind(this); + this.handleMakeActive = this.handleMakeActive.bind(this); + this.handleMakeNotActive = this.handleMakeNotActive.bind(this); + this.handleMakeAdmin = this.handleMakeAdmin.bind(this); + this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this); + this.handleResetPassword = this.handleResetPassword.bind(this); + this.handleDemote = this.handleDemote.bind(this); + this.handleDemoteSubmit = this.handleDemoteSubmit.bind(this); + this.handleDemoteCancel = this.handleDemoteCancel.bind(this); + + this.state = { + serverError: null, + showDemoteModal: false, + user: null, + role: null + }; + } + + handleMakeMember(e) { + e.preventDefault(); + const me = UserStore.getCurrentUser(); + if (this.props.user.id === me.id) { + this.handleDemote(this.props.user, ''); + } else { + const data = { + user_id: this.props.user.id, + new_roles: '' + }; + + Client.updateRoles(data, + () => { + this.props.refreshProfiles(); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + } + + handleMakeActive(e) { + e.preventDefault(); + Client.updateActive(this.props.user.id, true, + () => { + this.props.refreshProfiles(); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + handleMakeNotActive(e) { + e.preventDefault(); + Client.updateActive(this.props.user.id, false, + () => { + this.props.refreshProfiles(); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + handleMakeAdmin(e) { + e.preventDefault(); + const me = UserStore.getCurrentUser(); + if (this.props.user.id === me.id) { + this.handleDemote(this.props.user, 'admin'); + } else { + const data = { + user_id: this.props.user.id, + new_roles: 'admin' + }; + + Client.updateRoles(data, + () => { + this.props.refreshProfiles(); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + } + + handleMakeSystemAdmin(e) { + e.preventDefault(); + const data = { + user_id: this.props.user.id, + new_roles: 'system_admin' + }; + + Client.updateRoles(data, + () => { + this.props.refreshProfiles(); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + handleResetPassword(e) { + e.preventDefault(); + this.props.doPasswordReset(this.props.user); + } + + handleDemote(user, role) { + this.setState({ + serverError: this.state.serverError, + showDemoteModal: true, + user, + role + }); + } + + handleDemoteCancel() { + this.setState({ + serverError: null, + showDemoteModal: false, + user: null, + role: null + }); + } + + handleDemoteSubmit() { + const data = { + user_id: this.props.user.id, + new_roles: this.state.role + }; + + Client.updateRoles(data, + () => { + this.setState({ + serverError: null, + showDemoteModal: false, + user: null, + role: null + }); + + const teamUrl = TeamStore.getCurrentTeamUrl(); + if (teamUrl) { + window.location.href = teamUrl; + } else { + window.location.href = '/'; + } + }, + (err) => { + this.setState({ + serverError: err.message + }); + } + ); + } + + render() { + let serverError = null; + if (this.state.serverError) { + serverError = ( +
    + +
    + ); + } + + const user = this.props.user; + let currentRoles = ( + + ); + if (user.roles.length > 0) { + if (Utils.isSystemAdmin(user.roles)) { + currentRoles = ( + + ); + } else if (Utils.isAdmin(user.roles)) { + currentRoles = ( + + ); + } else { + currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1); + } + } + + const email = user.email; + let showMakeMember = user.roles === 'admin' || user.roles === 'system_admin'; + let showMakeAdmin = user.roles === '' || user.roles === 'system_admin'; + let showMakeSystemAdmin = user.roles === '' || user.roles === 'admin'; + let showMakeActive = false; + let showMakeNotActive = user.roles !== 'system_admin'; + + if (user.delete_at > 0) { + currentRoles = ( + + ); + showMakeMember = false; + showMakeAdmin = false; + showMakeSystemAdmin = false; + showMakeActive = true; + showMakeNotActive = false; + } + + let makeSystemAdmin = null; + if (showMakeSystemAdmin) { + makeSystemAdmin = ( +
  • + + + +
  • + ); + } + + let makeAdmin = null; + if (showMakeAdmin) { + makeAdmin = ( +
  • + + + +
  • + ); + } + + let makeMember = null; + if (showMakeMember) { + makeMember = ( +
  • + + + +
  • + ); + } + + let makeActive = null; + if (showMakeActive) { + makeActive = ( +
  • + + + +
  • + ); + } + + let makeNotActive = null; + if (showMakeNotActive) { + makeNotActive = ( +
  • + + + +
  • + ); + } + const me = UserStore.getCurrentUser(); + let makeDemoteModal = null; + if (this.props.user.id === me.id) { + const title = ( + + ); + + const message = ( +
    + +
    +
    + + {serverError} +
    + ); + + const confirmButton = ( + + ); + + makeDemoteModal = ( + + ); + } + + return ( + + + + {Utils.getDisplayName(user)} + {email} +
    + + {currentRoles} + + +
      + {makeAdmin} + {makeMember} + {makeActive} + {makeNotActive} + {makeSystemAdmin} +
    • + + + +
    • +
    +
    + {makeDemoteModal} + {serverError} + + + ); + } +} + +UserItem.propTypes = { + user: React.PropTypes.object.isRequired, + refreshProfiles: React.PropTypes.func.isRequired, + doPasswordReset: React.PropTypes.func.isRequired +}; diff --git a/webapp/components/analytics/doughnut_chart.jsx b/webapp/components/analytics/doughnut_chart.jsx new file mode 100644 index 000000000..169ac3105 --- /dev/null +++ b/webapp/components/analytics/doughnut_chart.jsx @@ -0,0 +1,81 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ReactDOM from 'react-dom'; +import {FormattedMessage} from 'react-intl'; +import Chart from 'chart.js'; + +import React from 'react'; + +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 = ( + + ); + } else { + content = ( + + ); + } + + return ( +
    +
    +
    + {this.props.title} +
    +
    + {content} +
    +
    +
    + ); + } +} + +DoughnutChart.propTypes = { + title: React.PropTypes.node, + width: React.PropTypes.string, + height: React.PropTypes.string, + data: React.PropTypes.array, + options: React.PropTypes.object +}; diff --git a/webapp/components/analytics/line_chart.jsx b/webapp/components/analytics/line_chart.jsx new file mode 100644 index 000000000..6a3b8c0f0 --- /dev/null +++ b/webapp/components/analytics/line_chart.jsx @@ -0,0 +1,94 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ReactDOM from 'react-dom'; +import {FormattedMessage} from 'react-intl'; +import Chart from 'chart.js'; + +import React from 'react'; + +export default class LineChart extends React.Component { + constructor(props) { + super(props); + + this.initChart = this.initChart.bind(this); + this.chart = null; + } + + componentDidMount() { + this.initChart(); + } + + componentDidUpdate() { + if (this.chart) { + this.chart.destroy(); + } + this.initChart(); + } + + componentWillUnmount() { + if (this.chart) { + this.chart.destroy(); + } + } + + initChart() { + if (!this.refs.canvas) { + return; + } + var el = ReactDOM.findDOMNode(this.refs.canvas); + var ctx = el.getContext('2d'); + this.chart = new Chart(ctx).Line(this.props.data, this.props.options || {}); //eslint-disable-line new-cap + } + + render() { + let content; + if (this.props.data == null) { + content = ( + + ); + } else if (this.props.data.labels.length === 0) { + content = ( +
    + +
    + ); + } else { + content = ( + + ); + } + + return ( +
    +
    +
    + {this.props.title} +
    +
    + {content} +
    +
    +
    + ); + } +} + +LineChart.propTypes = { + title: React.PropTypes.node.isRequired, + width: React.PropTypes.string.isRequired, + height: React.PropTypes.string.isRequired, + data: React.PropTypes.object, + options: React.PropTypes.object +}; + diff --git a/webapp/components/analytics/statistic_count.jsx b/webapp/components/analytics/statistic_count.jsx new file mode 100644 index 000000000..cbb8935dd --- /dev/null +++ b/webapp/components/analytics/statistic_count.jsx @@ -0,0 +1,35 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; + +export default class StatisticCount extends React.Component { + render() { + let loading = ( + + ); + + return ( +
    +
    +
    + {this.props.title} + +
    +
    {this.props.count == null ? loading : this.props.count}
    +
    +
    + ); + } +} + +StatisticCount.propTypes = { + title: React.PropTypes.node.isRequired, + icon: React.PropTypes.string.isRequired, + count: React.PropTypes.number +}; diff --git a/webapp/components/analytics/system_analytics.jsx b/webapp/components/analytics/system_analytics.jsx new file mode 100644 index 000000000..77f5efaa6 --- /dev/null +++ b/webapp/components/analytics/system_analytics.jsx @@ -0,0 +1,348 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import LineChart from './line_chart.jsx'; +import DoughnutChart from './doughnut_chart.jsx'; +import StatisticCount from './statistic_count.jsx'; + +import AnalyticsStore from 'stores/analytics_store.jsx'; + +import * as Utils from 'utils/utils.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import Constants from 'utils/constants.jsx'; +const StatTypes = Constants.StatTypes; + +import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; + +const holders = defineMessages({ + analyticsPublicChannels: { + id: 'analytics.system.publicChannels', + defaultMessage: 'Public Channels' + }, + analyticsPrivateGroups: { + id: 'analytics.system.privateGroups', + defaultMessage: 'Private Groups' + }, + analyticsFilePosts: { + id: 'analytics.system.totalFilePosts', + defaultMessage: 'Posts with Files' + }, + analyticsHashtagPosts: { + id: 'analytics.system.totalHashtagPosts', + defaultMessage: 'Posts with Hashtags' + }, + analyticsTextPosts: { + id: 'analytics.system.textPosts', + defaultMessage: 'Posts with Text-only' + } +}); + +import React from 'react'; + +class SystemAnalytics extends React.Component { + constructor(props) { + super(props); + + this.onChange = this.onChange.bind(this); + + this.state = {stats: AnalyticsStore.getAllSystem()}; + } + + componentDidMount() { + AnalyticsStore.addChangeListener(this.onChange); + + AsyncClient.getStandardAnalytics(); + AsyncClient.getPostsPerDayAnalytics(); + AsyncClient.getUsersPerDayAnalytics(); + + if (global.window.mm_license.IsLicensed === 'true') { + AsyncClient.getAdvancedAnalytics(); + } + } + + componentWillUnmount() { + AnalyticsStore.removeChangeListener(this.onChange); + } + + shouldComponentUpdate(nextProps, nextState) { + if (!Utils.areObjectsEqual(nextState.stats, this.state.stats)) { + return true; + } + + return false; + } + + onChange() { + this.setState({stats: AnalyticsStore.getAllSystem()}); + } + + render() { + const stats = this.state.stats; + + let advancedCounts; + let advancedGraphs; + if (global.window.mm_license.IsLicensed === 'true') { + advancedCounts = ( +
    + + } + icon='fa-signal' + count={stats[StatTypes.TOTAL_SESSIONS]} + /> + + } + icon='fa-terminal' + count={stats[StatTypes.TOTAL_COMMANDS]} + /> + + } + icon='fa-arrow-down' + count={stats[StatTypes.TOTAL_IHOOKS]} + /> + + } + icon='fa-arrow-up' + count={stats[StatTypes.TOTAL_OHOOKS]} + /> +
    + ); + + const channelTypeData = formatChannelDoughtnutData(stats[StatTypes.TOTAL_PUBLIC_CHANNELS], stats[StatTypes.TOTAL_PRIVATE_GROUPS], this.props.intl); + const postTypeData = formatPostDoughtnutData(stats[StatTypes.TOTAL_FILE_POSTS], stats[StatTypes.TOTAL_HASHTAG_POSTS], stats[StatTypes.TOTAL_POSTS], this.props.intl); + + advancedGraphs = ( +
    + + } + data={channelTypeData} + width='300' + height='225' + /> + + } + data={postTypeData} + width='300' + height='225' + /> +
    + ); + } + + const postCountsDay = formatPostsPerDayData(stats[StatTypes.POST_PER_DAY]); + const userCountsWithPostsDay = formatUsersWithPostsPerDayData(stats[StatTypes.USERS_WITH_POSTS_PER_DAY]); + + return ( +
    +

    + +

    +
    + + } + icon='fa-user' + count={stats[StatTypes.TOTAL_USERS]} + /> + + } + icon='fa-users' + count={stats[StatTypes.TOTAL_TEAMS]} + /> + + } + icon='fa-comment' + count={stats[StatTypes.TOTAL_POSTS]} + /> + + } + icon='fa-globe' + count={stats[StatTypes.TOTAL_PUBLIC_CHANNELS] + stats[StatTypes.TOTAL_PRIVATE_GROUPS]} + /> +
    + {advancedCounts} + {advancedGraphs} +
    + + } + data={postCountsDay} + width='740' + height='225' + /> +
    +
    + + } + data={userCountsWithPostsDay} + width='740' + height='225' + /> +
    +
    + ); + } +} + +SystemAnalytics.propTypes = { + intl: intlShape.isRequired, + team: React.PropTypes.object +}; + +export default injectIntl(SystemAnalytics); + +export function formatChannelDoughtnutData(totalPublic, totalPrivate, intl) { + const {formatMessage} = intl; + const channelTypeData = [ + { + value: totalPublic, + color: '#46BFBD', + highlight: '#5AD3D1', + label: formatMessage(holders.analyticsPublicChannels) + }, + { + value: totalPrivate, + color: '#FDB45C', + highlight: '#FFC870', + label: formatMessage(holders.analyticsPrivateGroups) + } + ]; + + return channelTypeData; +} + +export function formatPostDoughtnutData(filePosts, hashtagPosts, totalPosts, intl) { + const {formatMessage} = intl; + const postTypeData = [ + { + value: filePosts, + color: '#46BFBD', + highlight: '#5AD3D1', + label: formatMessage(holders.analyticsFilePosts) + }, + { + value: hashtagPosts, + color: '#F7464A', + highlight: '#FF5A5E', + label: formatMessage(holders.analyticsHashtagPosts) + }, + { + value: totalPosts - filePosts - hashtagPosts, + color: '#FDB45C', + highlight: '#FFC870', + label: formatMessage(holders.analyticsTextPosts) + } + ]; + + return postTypeData; +} + +export function formatPostsPerDayData(data) { + var chartData = { + labels: [], + datasets: [{ + fillColor: 'rgba(151,187,205,0.2)', + strokeColor: 'rgba(151,187,205,1)', + pointColor: 'rgba(151,187,205,1)', + pointStrokeColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStroke: 'rgba(151,187,205,1)', + data: [] + }] + }; + + for (var index in data) { + if (data[index]) { + var row = data[index]; + chartData.labels.push(row.name); + chartData.datasets[0].data.push(row.value); + } + } + + return chartData; +} + +export function formatUsersWithPostsPerDayData(data) { + var chartData = { + labels: [], + datasets: [{ + fillColor: 'rgba(151,187,205,0.2)', + strokeColor: 'rgba(151,187,205,1)', + pointColor: 'rgba(151,187,205,1)', + pointStrokeColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStroke: 'rgba(151,187,205,1)', + data: [] + }] + }; + + for (var index in data) { + if (data[index]) { + var row = data[index]; + chartData.labels.push(row.name); + chartData.datasets[0].data.push(row.value); + } + } + + return chartData; +} diff --git a/webapp/components/analytics/table_chart.jsx b/webapp/components/analytics/table_chart.jsx new file mode 100644 index 000000000..18ed54f96 --- /dev/null +++ b/webapp/components/analytics/table_chart.jsx @@ -0,0 +1,61 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Constants from 'utils/constants.jsx'; + +import {Tooltip, OverlayTrigger} from 'react-bootstrap'; + +import React from 'react'; + +export default class TableChart extends React.Component { + render() { + return ( +
    +
    +
    + {this.props.title} +
    +
    + + + { + this.props.data.map((item) => { + const tooltip = ( + + {item.tip} + + ); + + return ( + + + + + ); + }) + } + +
    + + + + + {item.value} +
    +
    +
    +
    + ); + } +} + +TableChart.propTypes = { + title: React.PropTypes.node, + data: React.PropTypes.array +}; diff --git a/webapp/components/analytics/team_analytics.jsx b/webapp/components/analytics/team_analytics.jsx new file mode 100644 index 000000000..efc965f24 --- /dev/null +++ b/webapp/components/analytics/team_analytics.jsx @@ -0,0 +1,237 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import LineChart from './line_chart.jsx'; +import StatisticCount from './statistic_count.jsx'; +import TableChart from './table_chart.jsx'; + +import AnalyticsStore from 'stores/analytics_store.jsx'; + +import * as Utils from 'utils/utils.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import Constants from 'utils/constants.jsx'; +const StatTypes = Constants.StatTypes; + +import {formatPostsPerDayData, formatUsersWithPostsPerDayData} from './system_analytics.jsx'; +import {injectIntl, intlShape, FormattedMessage, FormattedDate} from 'react-intl'; + +import React from 'react'; + +class TeamAnalytics extends React.Component { + constructor(props) { + super(props); + + this.onChange = this.onChange.bind(this); + + this.state = {stats: AnalyticsStore.getAllTeam(this.props.team.id)}; + } + + componentDidMount() { + AnalyticsStore.addChangeListener(this.onChange); + + this.getData(this.props.team.id); + } + + getData(id) { + AsyncClient.getStandardAnalytics(id); + AsyncClient.getPostsPerDayAnalytics(id); + AsyncClient.getUsersPerDayAnalytics(id); + AsyncClient.getRecentAndNewUsersAnalytics(id); + } + + componentWillUnmount() { + AnalyticsStore.removeChangeListener(this.onChange); + } + + componentWillReceiveProps(nextProps) { + this.getData(nextProps.team.id); + this.setState({stats: AnalyticsStore.getAllTeam(nextProps.team.id)}); + } + + shouldComponentUpdate(nextProps, nextState) { + if (!Utils.areObjectsEqual(nextState.stats, this.state.stats)) { + return true; + } + + if (!Utils.areObjectsEqual(nextProps.team, this.props.team)) { + return true; + } + + return false; + } + + onChange() { + this.setState({stats: AnalyticsStore.getAllTeam(this.props.team.id)}); + } + + render() { + const stats = this.state.stats; + const postCountsDay = formatPostsPerDayData(stats[StatTypes.POST_PER_DAY]); + const userCountsWithPostsDay = formatUsersWithPostsPerDayData(stats[StatTypes.USERS_WITH_POSTS_PER_DAY]); + const recentActiveUsers = formatRecentUsersData(stats[StatTypes.RECENTLY_ACTIVE_USERS]); + const newlyCreatedUsers = formatNewUsersData(stats[StatTypes.NEWLY_CREATED_USERS]); + + return ( +
    +

    + +

    +
    + + } + icon='fa-user' + count={stats[StatTypes.TOTAL_USERS]} + /> + + } + icon='fa-users' + count={stats[StatTypes.TOTAL_PUBLIC_CHANNELS]} + /> + + } + icon='fa-globe' + count={stats[StatTypes.TOTAL_PRIVATE_GROUPS]} + /> + + } + icon='fa-comment' + count={stats[StatTypes.TOTAL_POSTS]} + /> +
    +
    + + } + data={postCountsDay} + width='740' + height='225' + /> +
    +
    + + } + data={userCountsWithPostsDay} + width='740' + height='225' + /> +
    +
    + + } + data={recentActiveUsers} + /> + + } + data={newlyCreatedUsers} + /> +
    +
    + ); + } +} + +TeamAnalytics.propTypes = { + intl: intlShape.isRequired, + team: React.PropTypes.object.isRequired +}; + +export default injectIntl(TeamAnalytics); + +export function formatRecentUsersData(data) { + if (data == null) { + return []; + } + + const formattedData = data.map((user) => { + const item = {}; + item.name = user.username; + item.value = ( + + ); + item.tip = user.email; + + return item; + }); + + return formattedData; +} + +export function formatNewUsersData(data) { + if (data == null) { + return []; + } + + const formattedData = data.map((user) => { + const item = {}; + item.name = user.username; + item.value = ( + + ); + item.tip = user.email; + + return item; + }); + + return formattedData; +} diff --git a/webapp/components/audio_video_preview.jsx b/webapp/components/audio_video_preview.jsx new file mode 100644 index 000000000..dd2e910b3 --- /dev/null +++ b/webapp/components/audio_video_preview.jsx @@ -0,0 +1,120 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import Constants from 'utils/constants.jsx'; +import FileInfoPreview from './file_info_preview.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import React from 'react'; + +export default class AudioVideoPreview extends React.Component { + constructor(props) { + super(props); + + this.handleFileInfoChanged = this.handleFileInfoChanged.bind(this); + this.handleLoadError = this.handleLoadError.bind(this); + + this.stop = this.stop.bind(this); + + this.state = { + canPlay: true + }; + } + + componentWillMount() { + this.handleFileInfoChanged(this.props.fileInfo); + } + + componentDidMount() { + if (this.refs.source) { + $(ReactDOM.findDOMNode(this.refs.source)).one('error', this.handleLoadError); + } + } + + componentWillReceiveProps(nextProps) { + if (this.props.fileUrl !== nextProps.fileUrl) { + this.handleFileInfoChanged(nextProps.fileInfo); + } + } + + handleFileInfoChanged(fileInfo) { + let video = ReactDOM.findDOMNode(this.refs.video); + if (!video) { + video = document.createElement('video'); + } + + const canPlayType = video.canPlayType(fileInfo.mime_type); + + this.setState({ + canPlay: canPlayType === 'probably' || canPlayType === 'maybe' + }); + } + + componentDidUpdate() { + if (this.refs.source) { + $(ReactDOM.findDOMNode(this.refs.source)).one('error', this.handleLoadError); + } + } + + handleLoadError() { + this.setState({ + canPlay: false + }); + } + + stop() { + if (this.refs.video) { + const video = ReactDOM.findDOMNode(this.refs.video); + video.pause(); + video.currentTime = 0; + } + } + + render() { + if (!this.state.canPlay) { + return ( + + ); + } + + let width = Constants.WEB_VIDEO_WIDTH; + let height = Constants.WEB_VIDEO_HEIGHT; + if (Utils.isMobile()) { + width = Constants.MOBILE_VIDEO_WIDTH; + height = Constants.MOBILE_VIDEO_HEIGHT; + } + + // add a key to the video to prevent React from using an old video source while a new one is loading + return ( + + ); + } +} + +AudioVideoPreview.propTypes = { + filename: React.PropTypes.string.isRequired, + fileUrl: React.PropTypes.string.isRequired, + fileInfo: React.PropTypes.object.isRequired, + maxHeight: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]).isRequired, + formatMessage: React.PropTypes.func.isRequired +}; diff --git a/webapp/components/audit_table.jsx b/webapp/components/audit_table.jsx new file mode 100644 index 000000000..73dcfccc3 --- /dev/null +++ b/webapp/components/audit_table.jsx @@ -0,0 +1,626 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import UserStore from 'stores/user_store.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate, FormattedTime} from 'react-intl'; + +const holders = defineMessages({ + sessionRevoked: { + id: 'audit_table.sessionRevoked', + defaultMessage: 'The session with id {sessionId} was revoked' + }, + channelCreated: { + id: 'audit_table.channelCreated', + defaultMessage: 'Created the {channelName} channel/group' + }, + establishedDM: { + id: 'audit_table.establishedDM', + defaultMessage: 'Established a direct message channel with {username}' + }, + nameUpdated: { + id: 'audit_table.nameUpdated', + defaultMessage: 'Updated the {channelName} channel/group name' + }, + headerUpdated: { + id: 'audit_table.headerUpdated', + defaultMessage: 'Updated the {channelName} channel/group header' + }, + channelDeleted: { + id: 'audit_table.channelDeleted', + defaultMessage: 'Deleted the channel/group with the URL {url}' + }, + userAdded: { + id: 'audit_table.userAdded', + defaultMessage: 'Added {username} to the {channelName} channel/group' + }, + userRemoved: { + id: 'audit_table.userRemoved', + defaultMessage: 'Removed {username} to the {channelName} channel/group' + }, + attemptedRegisterApp: { + id: 'audit_table.attemptedRegisterApp', + defaultMessage: 'Attempted to register a new OAuth Application with ID {id}' + }, + attemptedAllowOAuthAccess: { + id: 'audit_table.attemptedAllowOAuthAccess', + defaultMessage: 'Attempted to allow a new OAuth service access' + }, + successfullOAuthAccess: { + id: 'audit_table.successfullOAuthAccess', + defaultMessage: 'Successfully gave a new OAuth service access' + }, + failedOAuthAccess: { + id: 'audit_table.failedOAuthAccess', + defaultMessage: 'Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback' + }, + attemptedOAuthToken: { + id: 'audit_table.attemptedOAuthToken', + defaultMessage: 'Attempted to get an OAuth access token' + }, + successfullOAuthToken: { + id: 'audit_table.successfullOAuthToken', + defaultMessage: 'Successfully added a new OAuth service' + }, + oauthTokenFailed: { + id: 'audit_table.oauthTokenFailed', + defaultMessage: 'Failed to get an OAuth access token - {token}' + }, + attemptedLogin: { + id: 'audit_table.attemptedLogin', + defaultMessage: 'Attempted to login' + }, + successfullLogin: { + id: 'audit_table.successfullLogin', + defaultMessage: 'Successfully logged in' + }, + failedLogin: { + id: 'audit_table.failedLogin', + defaultMessage: 'FAILED login attempt' + }, + updatePicture: { + id: 'audit_table.updatePicture', + defaultMessage: 'Updated your profile picture' + }, + updateGeneral: { + id: 'audit_table.updateGeneral', + defaultMessage: 'Updated the general settings of your account' + }, + attemptedPassword: { + id: 'audit_table.attemptedPassword', + defaultMessage: 'Attempted to change password' + }, + successfullPassword: { + id: 'audit_table.successfullPassword', + defaultMessage: 'Successfully changed password' + }, + failedPassword: { + id: 'audit_table.failedPassword', + defaultMessage: 'Failed to change password - tried to update user password who was logged in through oauth' + }, + updatedRol: { + id: 'audit_table.updatedRol', + defaultMessage: 'Updated user role(s) to ' + }, + member: { + id: 'audit_table.member', + defaultMessage: 'member' + }, + accountActive: { + id: 'audit_table.accountActive', + defaultMessage: 'Account made active' + }, + accountInactive: { + id: 'audit_table.accountInactive', + defaultMessage: 'Account made inactive' + }, + by: { + id: 'audit_table.by', + defaultMessage: ' by {username}' + }, + byAdmin: { + id: 'audit_table.byAdmin', + defaultMessage: ' by an admin' + }, + sentEmail: { + id: 'audit_table.sentEmail', + defaultMessage: 'Sent an email to {email} to reset your password' + }, + attemptedReset: { + id: 'audit_table.attemptedReset', + defaultMessage: 'Attempted to reset password' + }, + successfullReset: { + id: 'audit_table.successfullReset', + defaultMessage: 'Successfully reset password' + }, + updateGlobalNotifications: { + id: 'audit_table.updateGlobalNotifications', + defaultMessage: 'Updated your global notification settings' + }, + attemptedWebhookCreate: { + id: 'audit_table.attemptedWebhookCreate', + defaultMessage: 'Attempted to create a webhook' + }, + succcessfullWebhookCreate: { + id: 'audit_table.successfullWebhookCreate', + defaultMessage: 'Successfully created a webhook' + }, + failedWebhookCreate: { + id: 'audit_table.failedWebhookCreate', + defaultMessage: 'Failed to create a webhook - bad channel permissions' + }, + attemptedWebhookDelete: { + id: 'audit_table.attemptedWebhookDelete', + defaultMessage: 'Attempted to delete a webhook' + }, + successfullWebhookDelete: { + id: 'audit_table.successfullWebhookDelete', + defaultMessage: 'Successfully deleted a webhook' + }, + failedWebhookDelete: { + id: 'audit_table.failedWebhookDelete', + defaultMessage: 'Failed to delete a webhook - inappropriate conditions' + }, + logout: { + id: 'audit_table.logout', + defaultMessage: 'Logged out of your account' + }, + verified: { + id: 'audit_table.verified', + defaultMessage: 'Sucessfully verified your email address' + }, + revokedAll: { + id: 'audit_table.revokedAll', + defaultMessage: 'Revoked all current sessions for the team' + }, + loginAttempt: { + id: 'audit_table.loginAttempt', + defaultMessage: ' (Login attempt)' + }, + loginFailure: { + id: 'audit_table.loginFailure', + defaultMessage: ' (Login failure)' + }, + attemptedLicenseAdd: { + id: 'audit_table.attemptedLicenseAdd', + defaultMessage: 'Attempted to add new license' + }, + successfullLicenseAdd: { + id: 'audit_table.successfullLicenseAdd', + defaultMessage: 'Successfully added new license' + }, + failedExpiredLicenseAdd: { + id: 'audit_table.failedExpiredLicenseAdd', + defaultMessage: 'Failed to add a new license as it has either expired or not yet been started' + }, + failedInvalidLicenseAdd: { + id: 'audit_table.failedInvalidLicenseAdd', + defaultMessage: 'Failed to add an invalid license' + }, + licenseRemoved: { + id: 'audit_table.licenseRemoved', + defaultMessage: 'Successfully removed a license' + } +}); + +import React from 'react'; + +class AuditTable extends React.Component { + render() { + var accessList = []; + + const {formatMessage} = this.props.intl; + for (var i = 0; i < this.props.audits.length; i++) { + const audit = this.props.audits[i]; + const auditInfo = formatAuditInfo(audit, formatMessage); + + let uContent; + if (this.props.showUserId) { + uContent = {auditInfo.userId}; + } + + let iContent; + if (this.props.showIp) { + iContent = {auditInfo.ip}; + } + + let sContent; + if (this.props.showSession) { + sContent = {auditInfo.sessionId}; + } + + let descStyle = {}; + if (auditInfo.desc.toLowerCase().indexOf('fail') !== -1) { + descStyle.color = 'red'; + } + + accessList[i] = ( + + {auditInfo.timestamp} + {uContent} + {auditInfo.desc} + {iContent} + {sContent} + + ); + } + + let userIdContent; + if (this.props.showUserId) { + userIdContent = ( + + + + ); + } + + let ipContent; + if (this.props.showIp) { + ipContent = ( + + + + ); + } + + let sessionContent; + if (this.props.showSession) { + sessionContent = ( + + + + ); + } + + return ( + + + + + {userIdContent} + + {ipContent} + {sessionContent} + + + + {accessList} + +
    + + + +
    + ); + } +} + +AuditTable.propTypes = { + intl: intlShape.isRequired, + audits: React.PropTypes.array.isRequired, + showUserId: React.PropTypes.bool, + showIp: React.PropTypes.bool, + showSession: React.PropTypes.bool +}; + +export default injectIntl(AuditTable); + +export function formatAuditInfo(audit, formatMessage) { + const actionURL = audit.action.replace(/\/api\/v[1-9]/, ''); + let auditDesc = ''; + + if (actionURL.indexOf('/channels') === 0) { + const channelInfo = audit.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 (actionURL) { + case '/channels/create': + auditDesc = formatMessage(holders.channelCreated, {channelName}); + break; + case '/channels/create_direct': + auditDesc = formatMessage(holders.establishedDM, {username: Utils.getDirectTeammate(channelObj.id).username}); + break; + case '/channels/update': + auditDesc = formatMessage(holders.nameUpdated, {channelName}); + break; + case '/channels/update_desc': // support the old path + case '/channels/update_header': + auditDesc = formatMessage(holders.headerUpdated, {channelName}); + 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(actionURL)) { + auditDesc = formatMessage(holders.channelDeleted, {url: channelURL}); + } else if (/\/channels\/[A-Za-z0-9]+\/add/.test(actionURL)) { + auditDesc = formatMessage(holders.userAdded, {username, channelName}); + } else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(actionURL)) { + auditDesc = formatMessage(holders.userRemoved, {username, channelName}); + } + + break; + } + } + } else if (actionURL.indexOf('/oauth') === 0) { + const oauthInfo = audit.extra_info.split(' '); + + switch (actionURL) { + case '/oauth/register': { + const clientIdField = oauthInfo[0].split('='); + + if (clientIdField[0] === 'client_id') { + auditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]}); + } + + break; + } + case '/oauth/allow': + if (oauthInfo[0] === 'attempt') { + auditDesc = formatMessage(holders.attemptedAllowOAuthAccess); + } else if (oauthInfo[0] === 'success') { + auditDesc = formatMessage(holders.successfullOAuthAccess); + } else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') { + auditDesc = formatMessage(holders.failedOAuthAccess); + } + + break; + case '/oauth/access_token': + if (oauthInfo[0] === 'attempt') { + auditDesc = formatMessage(holders.attemptedOAuthToken); + } else if (oauthInfo[0] === 'success') { + auditDesc = formatMessage(holders.successfullOAuthToken); + } else { + const oauthTokenFailure = oauthInfo[0].split('-'); + + if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) { + auditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()}); + } + } + + break; + default: + break; + } + } else if (actionURL.indexOf('/users') === 0) { + const userInfo = audit.extra_info.split(' '); + + switch (actionURL) { + case '/users/login': + if (userInfo[0] === 'attempt') { + auditDesc = formatMessage(holders.attemptedLogin); + } else if (userInfo[0] === 'success') { + auditDesc = formatMessage(holders.successfullLogin); + } else if (userInfo[0]) { + auditDesc = formatMessage(holders.failedLogin); + } + + break; + case '/users/revoke_session': + auditDesc = formatMessage(holders.sessionRevoked, {sessionId: userInfo[0].split('=')[1]}); + break; + case '/users/newimage': + auditDesc = formatMessage(holders.updatePicture); + break; + case '/users/update': + auditDesc = formatMessage(holders.updateGeneral); + break; + case '/users/newpassword': + if (userInfo[0] === 'attempted') { + auditDesc = formatMessage(holders.attemptedPassword); + } else if (userInfo[0] === 'completed') { + auditDesc = formatMessage(holders.successfullPassword); + } else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') { + auditDesc = formatMessage(holders.failedPassword); + } + + break; + case '/users/update_roles': { + const userRoles = userInfo[0].split('=')[1]; + + auditDesc = formatMessage(holders.updatedRol); + if (userRoles.trim()) { + auditDesc += userRoles; + } else { + auditDesc += formatMessage(holders.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') { + auditDesc = formatMessage(holders.accountActive); + } else if (updateField === 'false') { + auditDesc = formatMessage(holders.accountInactive); + } + + const actingUserInfo = userInfo[1].split('='); + if (actingUserInfo[0] === 'session_user') { + const actingUser = UserStore.getProfile(actingUserInfo[1]); + const user = UserStore.getCurrentUser(); + if (user && actingUser && (Utils.isAdmin(user.roles) || Utils.isSystemAdmin(user.roles))) { + auditDesc += formatMessage(holders.by, {username: actingUser.username}); + } else if (user && actingUser) { + auditDesc += formatMessage(holders.byAdmin); + } + } + } else if (updateType === 'session_id') { + auditDesc = formatMessage(holders.sessionRevoked, {sessionId: updateField}); + } + + break; + } + case '/users/send_password_reset': + auditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]}); + break; + case '/users/reset_password': + if (userInfo[0] === 'attempt') { + auditDesc = formatMessage(holders.attemptedReset); + } else if (userInfo[0] === 'success') { + auditDesc = formatMessage(holders.successfullReset); + } + + break; + case '/users/update_notify': + auditDesc = formatMessage(holders.updateGlobalNotifications); + break; + default: + break; + } + } else if (actionURL.indexOf('/hooks') === 0) { + const webhookInfo = audit.extra_info; + + switch (actionURL) { + case '/hooks/incoming/create': + if (webhookInfo === 'attempt') { + auditDesc = formatMessage(holders.attemptedWebhookCreate); + } else if (webhookInfo === 'success') { + auditDesc = formatMessage(holders.succcessfullWebhookCreate); + } else if (webhookInfo === 'fail - bad channel permissions') { + auditDesc = formatMessage(holders.failedWebhookCreate); + } + + break; + case '/hooks/incoming/delete': + if (webhookInfo === 'attempt') { + auditDesc = formatMessage(holders.attemptedWebhookDelete); + } else if (webhookInfo === 'success') { + auditDesc = formatMessage(holders.successfullWebhookDelete); + } else if (webhookInfo === 'fail - inappropriate conditions') { + auditDesc = formatMessage(holders.failedWebhookDelete); + } + + break; + default: + break; + } + } else if (actionURL.indexOf('/license') === 0) { + const licenseInfo = audit.extra_info; + + switch (actionURL) { + case '/license/add': + if (licenseInfo === 'attempt') { + auditDesc = formatMessage(holders.attemptedLicenseAdd); + } else if (licenseInfo === 'success') { + auditDesc = formatMessage(holders.successfullLicenseAdd); + } else if (licenseInfo === 'failed - expired or non-started license') { + auditDesc = formatMessage(holders.failedExpiredLicenseAdd); + } else if (licenseInfo === 'failed - invalid license') { + auditDesc = formatMessage(holders.failedInvalidLicenseAdd); + } + + break; + case '/license/remove': + auditDesc = formatMessage(holders.licenseRemoved); + break; + default: + break; + } + } else { + switch (actionURL) { + case '/logout': + auditDesc = formatMessage(holders.logout); + break; + case '/verify_email': + auditDesc = formatMessage(holders.verified); + break; + default: + break; + } + } + + /* If all else fails... */ + if (!auditDesc) { + /* Currently not called anywhere */ + if (audit.extra_info.indexOf('revoked_all=') >= 0) { + auditDesc = formatMessage(holders.revokedAll); + } else { + let actionDesc = ''; + if (actionURL && actionURL.lastIndexOf('/') !== -1) { + actionDesc = actionURL.substring(actionURL.lastIndexOf('/') + 1).replace('_', ' '); + actionDesc = Utils.toTitleCase(actionDesc); + } + + let extraInfoDesc = ''; + if (audit.extra_info) { + extraInfoDesc = audit.extra_info; + + if (extraInfoDesc.indexOf('=') !== -1) { + extraInfoDesc = extraInfoDesc.substring(extraInfoDesc.indexOf('=') + 1); + } + } + auditDesc = actionDesc + ' ' + extraInfoDesc; + } + } + + const date = new Date(audit.create_at); + const auditInfo = {}; + auditInfo.timestamp = ( +
    + + {' - '} + +
    + ); + auditInfo.userId = audit.user_id; + auditInfo.desc = auditDesc; + auditInfo.ip = audit.ip_address; + auditInfo.sessionId = audit.session_id; + + return auditInfo; +} diff --git a/webapp/components/authorize.jsx b/webapp/components/authorize.jsx new file mode 100644 index 000000000..01b37f439 --- /dev/null +++ b/webapp/components/authorize.jsx @@ -0,0 +1,119 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Client from 'utils/client.jsx'; + +import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; + +import React from 'react'; + +import icon50 from 'images/icon50x50.png'; + +export default class Authorize extends React.Component { + constructor(props) { + super(props); + + this.handleAllow = this.handleAllow.bind(this); + this.handleDeny = this.handleDeny.bind(this); + + this.state = {}; + } + handleAllow() { + const responseType = this.props.responseType; + const clientId = this.props.clientId; + const redirectUri = this.props.redirectUri; + const state = this.props.state; + const scope = this.props.scope; + + Client.allowOAuth2(responseType, clientId, redirectUri, state, scope, + (data) => { + if (data.redirect) { + window.location.replace(data.redirect); + } + }, + () => { + //Do nothing on error + } + ); + } + handleDeny() { + window.location.replace(this.props.redirectUri + '?error=access_denied'); + } + render() { + return ( +
    +
    +
    +
    + +
    +
    + +
    +
    +

    + +

    +

    + +

    +
    + + +
    +
    +
    + ); + } +} + +Authorize.propTypes = { + appName: React.PropTypes.string, + teamName: React.PropTypes.string, + responseType: React.PropTypes.string, + clientId: React.PropTypes.string, + redirectUri: React.PropTypes.string, + state: React.PropTypes.string, + scope: React.PropTypes.string +}; diff --git a/webapp/components/center_panel.jsx b/webapp/components/center_panel.jsx new file mode 100644 index 000000000..17e5e43d9 --- /dev/null +++ b/webapp/components/center_panel.jsx @@ -0,0 +1,141 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import TutorialIntroScreens from './tutorial/tutorial_intro_screens.jsx'; +import CreatePost from './create_post.jsx'; +import PostsViewContainer from './posts_view_container.jsx'; +import PostFocusView from './post_focus_view.jsx'; +import ChannelHeader from './channel_header.jsx'; +import Navbar from './navbar.jsx'; +import FileUploadOverlay from './file_upload_overlay.jsx'; + +import PreferenceStore from 'stores/preference_store.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; +import UserStore from 'stores/user_store.jsx'; + +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import Constants from 'utils/constants.jsx'; +const TutorialSteps = Constants.TutorialSteps; +const Preferences = Constants.Preferences; + +import React from 'react'; + +export default class CenterPanel extends React.Component { + constructor(props) { + super(props); + + this.getStateFromStores = this.getStateFromStores.bind(this); + this.validState = this.validState.bind(this); + this.onStoresChange = this.onStoresChange.bind(this); + + this.state = this.getStateFromStores(); + } + getStateFromStores() { + const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); + return { + showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS, + showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS, + user: UserStore.getCurrentUser(), + channel: ChannelStore.getCurrent(), + profiles: JSON.parse(JSON.stringify(UserStore.getProfiles())) + }; + } + validState() { + return this.state.user && this.state.channel && this.state.profiles; + } + onStoresChange() { + this.setState(this.getStateFromStores()); + } + componentDidMount() { + PreferenceStore.addChangeListener(this.onStoresChange); + ChannelStore.addChangeListener(this.onStoresChange); + UserStore.addChangeListener(this.onStoresChange); + } + componentWillUnmount() { + PreferenceStore.removeChangeListener(this.onStoresChange); + ChannelStore.removeChangeListener(this.onStoresChange); + UserStore.removeChangeListener(this.onStoresChange); + } + render() { + if (!this.validState()) { + return null; + } + const channel = this.state.channel; + var handleClick = null; + let postsContainer; + let createPost; + if (this.state.showTutorialScreens) { + postsContainer = ; + createPost = null; + } else if (this.state.showPostFocus) { + postsContainer = ; + + handleClick = function clickHandler(e) { + e.preventDefault(); + Utils.switchChannel(channel); + }; + + createPost = ( + + ); + } else { + postsContainer = ; + createPost = ( +
    + +
    + ); + } + + return ( +
    +
    + +
    +
    + +
    +
    + +
    + {postsContainer} + {createPost} +
    +
    +
    + ); + } +} + +CenterPanel.defaultProps = { +}; + +CenterPanel.propTypes = { +}; diff --git a/webapp/components/change_url_modal.jsx b/webapp/components/change_url_modal.jsx new file mode 100644 index 000000000..daba16827 --- /dev/null +++ b/webapp/components/change_url_modal.jsx @@ -0,0 +1,222 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ReactDOM from 'react-dom'; +import {Modal} from 'react-bootstrap'; +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; + +export default class ChangeUrlModal extends React.Component { + constructor(props) { + super(props); + + this.onURLChanged = this.onURLChanged.bind(this); + this.doSubmit = this.doSubmit.bind(this); + this.doCancel = this.doCancel.bind(this); + + this.state = { + currentURL: props.currentURL, + urlError: '', + userEdit: false + }; + } + componentWillReceiveProps(nextProps) { + // This check prevents the url being deleted when we re-render + // because of user status check + if (!this.state.userEdit) { + this.setState({ + currentURL: nextProps.currentURL + }); + } + } + componentDidUpdate(prevProps) { + if (this.props.show === true && prevProps.show === false) { + ReactDOM.findDOMNode(this.refs.urlinput).select(); + } + } + onURLChanged(e) { + const url = e.target.value.trim(); + this.setState({currentURL: url.replace(/[^A-Za-z0-9-_]/g, '').toLowerCase(), userEdit: true}); + } + getURLError(url) { + let error = []; //eslint-disable-line prefer-const + if (url.length < 2) { + error.push( + + +
    +
    + ); + } + if (url.charAt(0) === '-' || url.charAt(0) === '_') { + error.push( + + +
    +
    + ); + } + if (url.length > 1 && (url.charAt(url.length - 1) === '-' || url.charAt(url.length - 1) === '_')) { + error.push( + + +
    +
    ); + } + if (url.indexOf('__') > -1) { + error.push( + + +
    +
    ); + } + + // In case of error we don't detect + if (error.length === 0) { + error.push( + + +
    +
    ); + } + return error; + } + doSubmit(e) { + e.preventDefault(); + + const url = ReactDOM.findDOMNode(this.refs.urlinput).value; + const cleanedURL = Utils.cleanUpUrlable(url); + if (cleanedURL !== url || url.length < 2 || url.indexOf('__') > -1) { + this.setState({urlError: this.getURLError(url)}); + return; + } + this.setState({urlError: '', userEdit: false}); + this.props.onModalSubmit(url); + } + doCancel() { + this.setState({urlError: '', userEdit: false}); + this.props.onModalDismissed(); + } + render() { + let urlClass = 'input-group input-group--limit'; + let urlError = null; + let serverError = null; + + if (this.state.urlError) { + urlClass += ' has-error'; + urlError = (

    {this.state.urlError}

    ); + } + + if (this.props.serverError) { + serverError =

    {this.props.serverError}

    ; + } + + const fullTeamUrl = Utils.getTeamURLFromAddressBar(); + const teamURL = Utils.getShortenedTeamURL(); + + return ( + + + {this.props.title} + +
    + +
    {this.props.description}
    +
    + +
    +
    + + {teamURL} + + +
    + {urlError} + {serverError} +
    +
    +
    + + + + +
    +
    + ); + } +} + +ChangeUrlModal.defaultProps = { + show: false, + title: 'Change URL', + desciption: '', + urlLabel: 'URL', + submitButtonText: 'Save', + currentURL: '', + serverError: '' +}; + +ChangeUrlModal.propTypes = { + show: React.PropTypes.bool.isRequired, + title: React.PropTypes.string, + description: React.PropTypes.string, + urlLabel: React.PropTypes.string, + submitButtonText: React.PropTypes.string, + currentURL: React.PropTypes.string, + serverError: React.PropTypes.string, + onModalSubmit: React.PropTypes.func.isRequired, + onModalDismissed: React.PropTypes.func.isRequired +}; diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx new file mode 100644 index 000000000..7cd713942 --- /dev/null +++ b/webapp/components/channel_header.jsx @@ -0,0 +1,520 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import 'bootstrap'; +import NavbarSearchBox from './search_bar.jsx'; +import MessageWrapper from './message_wrapper.jsx'; +import PopoverListMembers from './popover_list_members.jsx'; +import EditChannelHeaderModal from './edit_channel_header_modal.jsx'; +import EditChannelPurposeModal from './edit_channel_purpose_modal.jsx'; +import ChannelInfoModal from './channel_info_modal.jsx'; +import ChannelInviteModal from './channel_invite_modal.jsx'; +import ChannelMembersModal from './channel_members_modal.jsx'; +import ChannelNotificationsModal from './channel_notifications_modal.jsx'; +import DeleteChannelModal from './delete_channel_modal.jsx'; +import RenameChannelModal from './rename_channel_modal.jsx'; +import ToggleModalButton from './toggle_modal_button.jsx'; + +import ChannelStore from 'stores/channel_store.jsx'; +import UserStore from 'stores/user_store.jsx'; +import SearchStore from 'stores/search_store.jsx'; +import PreferenceStore from 'stores/preference_store.jsx'; + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import * as Utils from 'utils/utils.jsx'; +import * as TextFormatting from 'utils/text_formatting.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import * as Client from 'utils/client.jsx'; +import Constants from 'utils/constants.jsx'; + +import {FormattedMessage} from 'react-intl'; + +const ActionTypes = Constants.ActionTypes; + +import {Tooltip, OverlayTrigger, Popover} from 'react-bootstrap'; + +import React from 'react'; + +export default class ChannelHeader extends React.Component { + constructor(props) { + super(props); + + this.onListenerChange = this.onListenerChange.bind(this); + this.handleLeave = this.handleLeave.bind(this); + this.searchMentions = this.searchMentions.bind(this); + this.showRenameChannelModal = this.showRenameChannelModal.bind(this); + this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this); + + const state = this.getStateFromStores(); + state.showEditChannelPurposeModal = false; + state.showMembersModal = false; + state.showRenameChannelModal = false; + this.state = state; + } + getStateFromStores() { + const extraInfo = ChannelStore.getCurrentExtraInfo(); + + return { + channel: ChannelStore.getCurrent(), + memberChannel: ChannelStore.getCurrentMember(), + users: extraInfo.members, + userCount: extraInfo.member_count, + searchVisible: SearchStore.getSearchResults() !== null, + currentUser: UserStore.getCurrentUser() + }; + } + validState() { + if (!this.state.channel || + !this.state.memberChannel || + !this.state.users || + !this.state.userCount || + !this.state.currentUser) { + return false; + } + return true; + } + componentDidMount() { + ChannelStore.addChangeListener(this.onListenerChange); + ChannelStore.addExtraInfoChangeListener(this.onListenerChange); + SearchStore.addSearchChangeListener(this.onListenerChange); + PreferenceStore.addChangeListener(this.onListenerChange); + UserStore.addChangeListener(this.onListenerChange); + } + componentWillUnmount() { + ChannelStore.removeChangeListener(this.onListenerChange); + ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); + SearchStore.removeSearchChangeListener(this.onListenerChange); + PreferenceStore.removeChangeListener(this.onListenerChange); + UserStore.removeChangeListener(this.onListenerChange); + } + onListenerChange() { + const newState = this.getStateFromStores(); + if (!Utils.areObjectsEqual(newState, this.state)) { + this.setState(newState); + } + $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}}); + } + handleLeave() { + Client.leaveChannel(this.state.channel.id, + () => { + AppDispatcher.handleViewAction({ + type: ActionTypes.LEAVE_CHANNEL, + id: this.state.channel.id + }); + + const townsquare = ChannelStore.getByName('town-square'); + Utils.switchChannel(townsquare); + }, + (err) => { + AsyncClient.dispatchError(err, 'handleLeave'); + } + ); + } + searchMentions(e) { + e.preventDefault(); + + const user = this.state.currentUser; + + let terms = ''; + if (user.notify_props && user.notify_props.mention_keys) { + const termKeys = UserStore.getMentionKeys(user.id); + + if (user.notify_props.all === 'true' && termKeys.indexOf('@all') !== -1) { + termKeys.splice(termKeys.indexOf('@all'), 1); + } + + if (user.notify_props.channel === 'true' && termKeys.indexOf('@channel') !== -1) { + termKeys.splice(termKeys.indexOf('@channel'), 1); + } + terms = termKeys.join(' '); + } + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_SEARCH_TERM, + term: terms, + do_search: true, + is_mention_search: true + }); + } + showRenameChannelModal(e) { + e.preventDefault(); + + this.setState({ + showRenameChannelModal: true + }); + } + hideRenameChannelModal() { + this.setState({ + showRenameChannelModal: false + }); + } + render() { + if (!this.validState()) { + return null; + } + + const channel = this.state.channel; + const recentMentionsTooltip = ( + + + + ); + const popoverContent = ( + this.refs.headerOverlay.show()} + onMouseOut={() => this.refs.headerOverlay.hide()} + > + + + ); + let channelTitle = channel.display_name; + const currentId = this.state.currentUser.id; + const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.state.currentUser.roles); + const isDirect = (this.state.channel.type === 'D'); + + if (isDirect) { + if (this.state.users.length > 1) { + let contact; + if (this.state.users[0].id === currentId) { + contact = this.state.users[1]; + } else { + contact = this.state.users[0]; + } + channelTitle = Utils.displayUsername(contact.id); + } + } + + let channelTerm = ( + + ); + if (channel.type === Constants.PRIVATE_CHANNEL) { + channelTerm = ( + + ); + } + + let popoverListMembers; + if (!isDirect) { + popoverListMembers = ( + + ); + } + + const dropdownContents = []; + if (isDirect) { + dropdownContents.push( +
  • + + + +
  • + ); + } else { + dropdownContents.push( +
  • + + + +
  • + ); + + if (!ChannelStore.isDefault(channel)) { + dropdownContents.push( +
  • + + + +
  • + ); + + if (isAdmin) { + dropdownContents.push( +
  • + this.setState({showMembersModal: true})} + > + + +
  • + ); + } + } + + dropdownContents.push( +
  • + + + +
  • + ); + dropdownContents.push( +
  • + this.setState({showEditChannelPurposeModal: true})} + > + + +
  • + ); + dropdownContents.push( +
  • + + + +
  • + ); + + if (isAdmin) { + dropdownContents.push( +
  • + + + +
  • + ); + + if (!ChannelStore.isDefault(channel)) { + dropdownContents.push( +
  • + + + +
  • + ); + } + } + + if (!ChannelStore.isDefault(channel)) { + dropdownContents.push( +
  • + + + +
  • + ); + } + } + + return ( +
    + + + + + + + + + +
    +
    +
    + + {channelTitle} + + +
      + {dropdownContents} +
    +
    + +
    + +
    +
    + {popoverListMembers} + +
    + + + {'@'} + + +
    +
    + this.setState({showEditChannelPurposeModal: false})} + channel={channel} + /> + this.setState({showMembersModal: false})} + channel={channel} + /> + +
    + ); + } +} + +ChannelHeader.propTypes = { +}; diff --git a/webapp/components/channel_info_modal.jsx b/webapp/components/channel_info_modal.jsx new file mode 100644 index 000000000..444f3db8d --- /dev/null +++ b/webapp/components/channel_info_modal.jsx @@ -0,0 +1,104 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from 'utils/utils.jsx'; + +import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; + +import {Modal} from 'react-bootstrap'; + +const holders = defineMessages({ + notFound: { + id: 'channel_info.notFound', + defaultMessage: 'No Channel Found' + } +}); + +import React from 'react'; + +class ChannelInfoModal extends React.Component { + render() { + const {formatMessage} = this.props.intl; + let channel = this.props.channel; + if (!channel) { + channel = { + display_name: formatMessage(holders.notFound), + name: formatMessage(holders.notFound), + purpose: formatMessage(holders.notFound), + id: formatMessage(holders.notFound) + }; + } + + const channelURL = Utils.getShortenedTeamURL() + channel.name; + + return ( + + + {channel.display_name} + + +
    +
    + +
    +
    {channel.display_name}
    +
    +
    +
    + +
    +
    {channelURL}
    +
    +
    +
    + +
    +
    {channel.id}
    +
    +
    +
    + +
    +
    {channel.purpose}
    +
    +
    + + + +
    + ); + } +} + +ChannelInfoModal.propTypes = { + intl: intlShape.isRequired, + show: React.PropTypes.bool.isRequired, + onHide: React.PropTypes.func.isRequired, + channel: React.PropTypes.object.isRequired +}; + +export default injectIntl(ChannelInfoModal); \ No newline at end of file diff --git a/webapp/components/channel_invite_modal.jsx b/webapp/components/channel_invite_modal.jsx new file mode 100644 index 000000000..dfb0d4187 --- /dev/null +++ b/webapp/components/channel_invite_modal.jsx @@ -0,0 +1,217 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import FilteredUserList from './filtered_user_list.jsx'; +import LoadingScreen from './loading_screen.jsx'; + +import ChannelStore from 'stores/channel_store.jsx'; +import UserStore from 'stores/user_store.jsx'; + +import * as Utils from 'utils/utils.jsx'; +import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import {Modal} from 'react-bootstrap'; + +import React from 'react'; + +export default class ChannelInviteModal extends React.Component { + constructor(props) { + super(props); + + this.onListenerChange = this.onListenerChange.bind(this); + this.handleInvite = this.handleInvite.bind(this); + this.getStateFromStores = this.getStateFromStores.bind(this); + this.createInviteButton = this.createInviteButton.bind(this); + + this.state = this.getStateFromStores(); + } + shouldComponentUpdate(nextProps, nextState) { + if (!this.props.show && !nextProps.show) { + return false; + } + + if (!Utils.areObjectsEqual(this.props, nextProps)) { + return true; + } + + if (!Utils.areObjectsEqual(this.state, nextState)) { + return true; + } + + return false; + } + getStateFromStores() { + const users = UserStore.getActiveOnlyProfiles(); + + if ($.isEmptyObject(users)) { + return { + loading: true + }; + } + + // make sure we have all members of this channel before rendering + const extraInfo = ChannelStore.getCurrentExtraInfo(); + if (extraInfo.member_count !== extraInfo.members.length) { + AsyncClient.getChannelExtraInfo(this.props.channel.id, -1); + + return { + loading: true + }; + } + + const currentUser = UserStore.getCurrentUser(); + if (!currentUser) { + return { + loading: true + }; + } + + const currentMember = ChannelStore.getCurrentMember(); + if (!currentMember) { + return { + loading: true + }; + } + + const memberIds = extraInfo.members.map((user) => user.id); + + var nonmembers = []; + for (var id in users) { + if (memberIds.indexOf(id) === -1) { + nonmembers.push(users[id]); + } + } + + nonmembers.sort((a, b) => { + return a.username.localeCompare(b.username); + }); + + return { + nonmembers, + loading: false, + currentUser, + currentMember + }; + } + componentWillReceiveProps(nextProps) { + if (!this.props.show && nextProps.show) { + ChannelStore.addExtraInfoChangeListener(this.onListenerChange); + ChannelStore.addChangeListener(this.onListenerChange); + UserStore.addChangeListener(this.onListenerChange); + this.onListenerChange(); + } else if (this.props.show && !nextProps.show) { + ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); + ChannelStore.removeChangeListener(this.onListenerChange); + UserStore.removeChangeListener(this.onListenerChange); + } + } + componentWillUnmount() { + ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); + ChannelStore.removeChangeListener(this.onListenerChange); + UserStore.removeChangeListener(this.onListenerChange); + } + onListenerChange() { + var newState = this.getStateFromStores(); + if (!Utils.areObjectsEqual(this.state, newState)) { + this.setState(newState); + } + } + handleInvite(user) { + const data = { + user_id: user.id + }; + + Client.addChannelMember( + this.props.channel.id, + data, + () => { + this.setState({inviteError: null}); + AsyncClient.getChannelExtraInfo(); + }, + (err) => { + this.setState({inviteError: err.message}); + } + ); + } + createInviteButton({user}) { + return ( + + + + + ); + } + render() { + var inviteError = null; + if (this.state.inviteError) { + inviteError = (); + } + + var content; + if (this.state.loading) { + content = (); + } else { + let maxHeight = 1000; + if (Utils.windowHeight() <= 1200) { + maxHeight = Utils.windowHeight() - 300; + } + content = ( + + ); + } + + return ( + + + + + {this.props.channel.display_name} + + + + {inviteError} + {content} + + + + + + ); + } +} + +ChannelInviteModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onHide: React.PropTypes.func.isRequired, + channel: React.PropTypes.object.isRequired +}; diff --git a/webapp/components/channel_members_modal.jsx b/webapp/components/channel_members_modal.jsx new file mode 100644 index 000000000..67be2ef50 --- /dev/null +++ b/webapp/components/channel_members_modal.jsx @@ -0,0 +1,225 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import FilteredUserList from './filtered_user_list.jsx'; +import LoadingScreen from './loading_screen.jsx'; +import ChannelInviteModal from './channel_invite_modal.jsx'; + +import UserStore from 'stores/user_store.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import * as Client from 'utils/client.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import {Modal} from 'react-bootstrap'; + +import React from 'react'; + +export default class ChannelMembersModal extends React.Component { + constructor(props) { + super(props); + + this.getStateFromStores = this.getStateFromStores.bind(this); + this.onChange = this.onChange.bind(this); + this.handleRemove = this.handleRemove.bind(this); + + this.createRemoveMemberButton = this.createRemoveMemberButton.bind(this); + + // the rest of the state gets populated when the modal is shown + this.state = { + showInviteModal: false + }; + } + shouldComponentUpdate(nextProps, nextState) { + if (!Utils.areObjectsEqual(this.props, nextProps)) { + return true; + } + + if (!Utils.areObjectsEqual(this.state, nextState)) { + return true; + } + + return false; + } + getStateFromStores() { + const extraInfo = ChannelStore.getCurrentExtraInfo(); + const profiles = UserStore.getActiveOnlyProfiles(); + + if (extraInfo.member_count !== extraInfo.members.length) { + AsyncClient.getChannelExtraInfo(this.props.channel.id, -1); + + return { + loading: true + }; + } + + const memberList = extraInfo.members.map((member) => { + return profiles[member.id]; + }); + + function compareByUsername(a, b) { + if (a.username < b.username) { + return -1; + } else if (a.username > b.username) { + return 1; + } + + return 0; + } + + memberList.sort(compareByUsername); + + return { + memberList, + loading: false + }; + } + componentWillReceiveProps(nextProps) { + if (!this.props.show && nextProps.show) { + ChannelStore.addExtraInfoChangeListener(this.onChange); + ChannelStore.addChangeListener(this.onChange); + + this.onChange(); + } else if (this.props.show && !nextProps.show) { + ChannelStore.removeExtraInfoChangeListener(this.onChange); + ChannelStore.removeChangeListener(this.onChange); + } + } + onChange() { + const newState = this.getStateFromStores(); + if (!Utils.areObjectsEqual(this.state, newState)) { + this.setState(newState); + } + } + handleRemove(user) { + const userId = user.id; + + const data = {}; + data.user_id = userId; + + Client.removeChannelMember( + ChannelStore.getCurrentId(), + data, + () => { + const memberList = this.state.memberList.slice(); + for (let i = 0; i < memberList.length; i++) { + if (userId === memberList[i].id) { + memberList.splice(i, 1); + break; + } + } + + this.setState({memberList}); + AsyncClient.getChannelExtraInfo(); + }, + (err) => { + this.setState({inviteError: err.message}); + } + ); + } + createRemoveMemberButton({user}) { + if (user.id === UserStore.getCurrentId()) { + return null; + } + + return ( + + ); + } + render() { + let content; + if (this.state.loading) { + content = (); + } else { + let maxHeight = 1000; + if (Utils.windowHeight() <= 1200) { + maxHeight = Utils.windowHeight() - 300; + } + + content = ( + + ); + } + + return ( +
    + + + + {this.props.channel.display_name} + + + { + this.setState({showInviteModal: true}); + this.props.onModalDismissed(); + }} + > + + + + + {content} + + + + + + this.setState({showInviteModal: false})} + channel={this.props.channel} + /> +
    + ); + } +} + +ChannelMembersModal.defaultProps = { + show: false +}; + +ChannelMembersModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onModalDismissed: React.PropTypes.func.isRequired, + channel: React.PropTypes.object.isRequired +}; diff --git a/webapp/components/channel_notifications_modal.jsx b/webapp/components/channel_notifications_modal.jsx new file mode 100644 index 000000000..cc1162b77 --- /dev/null +++ b/webapp/components/channel_notifications_modal.jsx @@ -0,0 +1,417 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {Modal} from 'react-bootstrap'; +import SettingItemMin from './setting_item_min.jsx'; +import SettingItemMax from './setting_item_max.jsx'; + +import * as Client from 'utils/client.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; + +export default class ChannelNotificationsModal extends React.Component { + constructor(props) { + super(props); + + this.updateSection = this.updateSection.bind(this); + + this.handleSubmitNotifyLevel = this.handleSubmitNotifyLevel.bind(this); + this.handleUpdateNotifyLevel = this.handleUpdateNotifyLevel.bind(this); + this.createNotifyLevelSection = this.createNotifyLevelSection.bind(this); + + this.handleSubmitMarkUnreadLevel = this.handleSubmitMarkUnreadLevel.bind(this); + this.handleUpdateMarkUnreadLevel = this.handleUpdateMarkUnreadLevel.bind(this); + this.createMarkUnreadLevelSection = this.createMarkUnreadLevelSection.bind(this); + + this.state = { + activeSection: '', + notifyLevel: '', + unreadLevel: '' + }; + } + updateSection(section) { + this.setState({activeSection: section}); + } + componentWillReceiveProps(nextProps) { + if (!this.props.show && nextProps.show) { + this.setState({ + notifyLevel: nextProps.channelMember.notify_props.desktop, + unreadLevel: nextProps.channelMember.notify_props.mark_unread + }); + } + } + handleSubmitNotifyLevel() { + var channelId = this.props.channel.id; + var notifyLevel = this.state.notifyLevel; + + if (this.props.channelMember.notify_props.desktop === notifyLevel) { + this.updateSection(''); + return; + } + + var data = {}; + data.channel_id = channelId; + data.user_id = this.props.currentUser.id; + data.desktop = notifyLevel; + + //TODO: This should be moved to event_helpers + Client.updateNotifyProps(data, + () => { + // YUCK + var member = ChannelStore.getMember(channelId); + member.notify_props.desktop = notifyLevel; + ChannelStore.setChannelMember(member); + this.updateSection(''); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + handleUpdateNotifyLevel(notifyLevel) { + this.setState({notifyLevel}); + } + createNotifyLevelSection(serverError) { + // Get glabal user setting for notifications + const globalNotifyLevel = this.props.currentUser.notify_props.desktop; + let globalNotifyLevelName; + if (globalNotifyLevel === 'all') { + globalNotifyLevelName = ( + + ); + } else if (globalNotifyLevel === 'mention') { + globalNotifyLevelName = ( + + ); + } else { + globalNotifyLevelName = ( + + ); + } + + const sendDesktop = ( + + ); + + const notificationLevel = this.state.notifyLevel; + + if (this.state.activeSection === 'desktop') { + const notifyActive = [false, false, false, false]; + if (notificationLevel === 'default') { + notifyActive[0] = true; + } else if (notificationLevel === 'all') { + notifyActive[1] = true; + } else if (notificationLevel === 'mention') { + notifyActive[2] = true; + } else { + notifyActive[3] = true; + } + + var inputs = []; + + inputs.push( +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    + ); + + const handleUpdateSection = function updateSection(e) { + this.updateSection(''); + this.onListenerChange(); + e.preventDefault(); + }.bind(this); + + const extraInfo = ( + + + + ); + + return ( + + ); + } + + var describe; + if (notificationLevel === 'default') { + describe = ( + + ); + } else if (notificationLevel === 'mention') { + describe = (); + } else if (notificationLevel === 'all') { + describe = (); + } else { + describe = (); + } + + return ( + { + this.updateSection('desktop'); + }} + /> + ); + } + + handleSubmitMarkUnreadLevel() { + const channelId = this.props.channel.id; + const markUnreadLevel = this.state.unreadLevel; + + if (this.props.channelMember.notify_props.mark_unread === markUnreadLevel) { + this.updateSection(''); + return; + } + + const data = { + channel_id: channelId, + user_id: this.props.currentUser.id, + mark_unread: markUnreadLevel + }; + + //TODO: This should be fixed, moved to event_helpers + Client.updateNotifyProps(data, + () => { + // Yuck... + var member = ChannelStore.getMember(channelId); + member.notify_props.mark_unread = markUnreadLevel; + ChannelStore.setChannelMember(member); + this.updateSection(''); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + handleUpdateMarkUnreadLevel(unreadLevel) { + this.setState({unreadLevel}); + } + + createMarkUnreadLevelSection(serverError) { + let content; + + const markUnread = ( + + ); + if (this.state.activeSection === 'markUnreadLevel') { + const inputs = [( +
    +
    + +
    +
    +
    + +
    +
    +
    + )]; + + const handleUpdateSection = function handleUpdateSection(e) { + this.updateSection(''); + this.onListenerChange(); + e.preventDefault(); + }.bind(this); + + const extraInfo = ( + + + + ); + + content = ( + + ); + } else { + let describe; + + if (!this.state.unreadLevel || this.state.unreadLevel === 'all') { + describe = ( + + ); + } else { + describe = (); + } + + const handleUpdateSection = function handleUpdateSection(e) { + this.updateSection('markUnreadLevel'); + e.preventDefault(); + }.bind(this); + + content = ( + + ); + } + + return content; + } + + render() { + var serverError = null; + if (this.state.serverError) { + serverError =
    ; + } + + return ( + + + + + {this.props.channel.display_name} + + + +
    +
    +
    +
    +
    + {this.createNotifyLevelSection(serverError)} +
    + {this.createMarkUnreadLevelSection(serverError)} +
    +
    +
    +
    + {serverError} + + + ); + } +} + +ChannelNotificationsModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onHide: React.PropTypes.func.isRequired, + channel: React.PropTypes.object.isRequired, + channelMember: React.PropTypes.object.isRequired, + currentUser: React.PropTypes.object.isRequired +}; diff --git a/webapp/components/channel_view.jsx b/webapp/components/channel_view.jsx new file mode 100644 index 000000000..34e1666d0 --- /dev/null +++ b/webapp/components/channel_view.jsx @@ -0,0 +1,20 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import CenterPanel from 'components/center_panel.jsx'; + +import React from 'react'; + +export default class ChannelView extends React.Component { + render() { + return ( + + ); + } +} +ChannelView.defaultProps = { +}; + +ChannelView.propTypes = { + params: React.PropTypes.object +}; diff --git a/webapp/components/claim/claim_account.jsx b/webapp/components/claim/claim_account.jsx new file mode 100644 index 000000000..b6495e283 --- /dev/null +++ b/webapp/components/claim/claim_account.jsx @@ -0,0 +1,116 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import EmailToSSO from './email_to_sso.jsx'; +import SSOToEmail from './sso_to_email.jsx'; +import TeamStore from 'stores/team_store.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import React from 'react'; +import logoImage from 'images/logo.png'; + +export default class ClaimAccount extends React.Component { + constructor(props) { + super(props); + + this.onTeamChange = this.onTeamChange.bind(this); + this.updateStateFromStores = this.updateStateFromStores.bind(this); + + this.state = {}; + } + componentWillMount() { + this.setState({ + email: this.props.location.query.email, + newType: this.props.location.query.new_type, + oldType: this.props.location.query.old_type, + teamName: this.props.params.team, + teamDisplayName: '' + }); + this.updateStateFromStores(); + } + componentDidMount() { + TeamStore.addChangeListener(this.onTeamChange); + } + componentWillUnmount() { + TeamStore.removeChangeListener(this.onTeamChange); + } + updateStateFromStores() { + const team = TeamStore.getByName(this.state.teamName); + let displayName = ''; + if (team) { + displayName = team.displayName; + } + this.setState({ + teamDisplayName: displayName + }); + } + onTeamChange() { + this.updateStateFromStores(); + } + render() { + if (this.state.teamDisplayName === '') { + return (
    ); + } + let content; + if (this.state.email === '') { + content = ( +

    + +

    + ); + } else if (this.state.oldType === '' && this.state.newType !== '') { + content = ( + + ); + } else { + content = ( + + ); + } + + return ( +
    + +
    +
    + +
    + {content} +
    +
    +
    +
    + ); + } +} + +ClaimAccount.defaultProps = { +}; +ClaimAccount.propTypes = { + params: React.PropTypes.object.isRequired, + location: React.PropTypes.object.isRequired +}; diff --git a/webapp/components/claim/email_to_sso.jsx b/webapp/components/claim/email_to_sso.jsx new file mode 100644 index 000000000..d09449247 --- /dev/null +++ b/webapp/components/claim/email_to_sso.jsx @@ -0,0 +1,154 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ReactDOM from 'react-dom'; +import * as Utils from 'utils/utils.jsx'; +import * as Client from 'utils/client.jsx'; + +import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; + +const holders = defineMessages({ + pwdError: { + id: 'claim.email_to_sso.pwdError', + defaultMessage: 'Please enter your password.' + }, + pwd: { + id: 'claim.email_to_sso.pwd', + defaultMessage: 'Password' + } +}); + +import React from 'react'; + +class EmailToSSO extends React.Component { + constructor(props) { + super(props); + + this.submit = this.submit.bind(this); + + this.state = {}; + } + submit(e) { + e.preventDefault(); + var state = {}; + + var password = ReactDOM.findDOMNode(this.refs.password).value.trim(); + if (!password) { + state.error = this.props.intl.formatMessage(holders.pwdError); + this.setState(state); + return; + } + + state.error = null; + this.setState(state); + + var postData = {}; + postData.password = password; + postData.email = this.props.email; + postData.team_name = this.props.teamName; + postData.service = this.props.type; + + Client.switchToSSO(postData, + (data) => { + if (data.follow_link) { + window.location.href = data.follow_link; + } + }, + (error) => { + this.setState({error}); + } + ); + } + render() { + var error = null; + if (this.state.error) { + error =
    ; + } + + var formClass = 'form-group'; + if (error) { + formClass += ' has-error'; + } + + const uiType = Utils.toTitleCase(this.props.type) + ' SSO'; + + return ( +
    +

    + +

    +
    +

    + +

    +

    + +

    +

    + +

    +
    + +
    + {error} + +
    +
    + ); + } +} + +EmailToSSO.defaultProps = { +}; +EmailToSSO.propTypes = { + intl: intlShape.isRequired, + type: React.PropTypes.string.isRequired, + email: React.PropTypes.string.isRequired, + teamName: React.PropTypes.string.isRequired, + teamDisplayName: React.PropTypes.string.isRequired +}; + +export default injectIntl(EmailToSSO); diff --git a/webapp/components/claim/sso_to_email.jsx b/webapp/components/claim/sso_to_email.jsx new file mode 100644 index 000000000..a41e09969 --- /dev/null +++ b/webapp/components/claim/sso_to_email.jsx @@ -0,0 +1,168 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ReactDOM from 'react-dom'; +import * as Utils from 'utils/utils.jsx'; +import * as Client from 'utils/client.jsx'; + +import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; + +const holders = defineMessages({ + enterPwd: { + id: 'claim.sso_to_email.enterPwd', + defaultMessage: 'Please enter a password.' + }, + pwdNotMatch: { + id: 'claim.sso_to_email.pwdNotMatch', + defaultMessage: 'Password do not match.' + }, + newPwd: { + id: 'claim.sso_to_email.newPwd', + defaultMessage: 'New Password' + }, + confirm: { + id: 'claim.sso_to_email.confirm', + defaultMessage: 'Confirm Password' + } +}); + +import React from 'react'; + +class SSOToEmail extends React.Component { + constructor(props) { + super(props); + + this.submit = this.submit.bind(this); + + this.state = {}; + } + submit(e) { + const {formatMessage} = this.props.intl; + e.preventDefault(); + const state = {}; + + const password = ReactDOM.findDOMNode(this.refs.password).value.trim(); + if (!password) { + state.error = formatMessage(holders.enterPwd); + this.setState(state); + return; + } + + const confirmPassword = ReactDOM.findDOMNode(this.refs.passwordconfirm).value.trim(); + if (!confirmPassword || password !== confirmPassword) { + state.error = formatMessage(holders.pwdNotMatch); + this.setState(state); + return; + } + + state.error = null; + this.setState(state); + + var postData = {}; + postData.password = password; + postData.email = this.props.email; + postData.team_name = this.props.teamName; + + Client.switchToEmail(postData, + (data) => { + if (data.follow_link) { + window.location.href = data.follow_link; + } + }, + (error) => { + this.setState({error}); + } + ); + } + render() { + const {formatMessage} = this.props.intl; + var error = null; + if (this.state.error) { + error =
    ; + } + + var formClass = 'form-group'; + if (error) { + formClass += ' has-error'; + } + + const uiType = Utils.toTitleCase(this.props.currentType) + ' SSO'; + + return ( +
    +

    + +

    +
    +

    + +

    +

    + +

    +
    + +
    +
    + +
    + {error} + +
    +
    + ); + } +} + +SSOToEmail.defaultProps = { +}; +SSOToEmail.propTypes = { + intl: intlShape.isRequired, + currentType: React.PropTypes.string.isRequired, + email: React.PropTypes.string.isRequired, + teamName: React.PropTypes.string.isRequired, + teamDisplayName: React.PropTypes.string +}; + +export default injectIntl(SSOToEmail); diff --git a/webapp/components/confirm_modal.jsx b/webapp/components/confirm_modal.jsx new file mode 100644 index 000000000..ef91b5ec0 --- /dev/null +++ b/webapp/components/confirm_modal.jsx @@ -0,0 +1,69 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'react-intl'; +import {Modal} from 'react-bootstrap'; + +import React from 'react'; + +export default class ConfirmModal extends React.Component { + constructor(props) { + super(props); + + this.handleConfirm = this.handleConfirm.bind(this); + } + + handleConfirm() { + this.props.onConfirm(); + } + + render() { + return ( + + + {this.props.title} + + + {this.props.message} + + + + + + + ); + } +} + +ConfirmModal.defaultProps = { + title: '', + message: '', + confirmButton: '' +}; +ConfirmModal.propTypes = { + show: React.PropTypes.bool.isRequired, + title: React.PropTypes.node, + message: React.PropTypes.node, + confirmButton: React.PropTypes.node, + onConfirm: React.PropTypes.func.isRequired, + onCancel: React.PropTypes.func.isRequired +}; diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx new file mode 100644 index 000000000..0aeb70c57 --- /dev/null +++ b/webapp/components/create_comment.jsx @@ -0,0 +1,448 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import SocketStore from 'stores/socket_store.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; +import UserStore from 'stores/user_store.jsx'; +import PostDeletedModal from './post_deleted_modal.jsx'; +import PostStore from 'stores/post_store.jsx'; +import PreferenceStore from 'stores/preference_store.jsx'; +import Textbox from './textbox.jsx'; +import MsgTyping from './msg_typing.jsx'; +import FileUpload from './file_upload.jsx'; +import FilePreview from './file_preview.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import Constants from 'utils/constants.jsx'; + +import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; + +const ActionTypes = Constants.ActionTypes; +const KeyCodes = Constants.KeyCodes; + +const holders = defineMessages({ + commentLength: { + id: 'create_comment.commentLength', + defaultMessage: 'Comment length must be less than {max} characters.' + }, + comment: { + id: 'create_comment.comment', + defaultMessage: 'Add Comment' + }, + addComment: { + id: 'create_comment.addComment', + defaultMessage: 'Add a comment...' + }, + commentTitle: { + id: 'create_comment.commentTitle', + defaultMessage: 'Comment' + } +}); + +import React from 'react'; + +class CreateComment extends React.Component { + constructor(props) { + super(props); + + this.lastTime = 0; + + this.handleSubmit = this.handleSubmit.bind(this); + this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this); + this.handleUserInput = this.handleUserInput.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleUploadClick = this.handleUploadClick.bind(this); + this.handleUploadStart = this.handleUploadStart.bind(this); + this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this); + this.handleUploadError = this.handleUploadError.bind(this); + this.removePreview = this.removePreview.bind(this); + this.getFileCount = this.getFileCount.bind(this); + this.handleResize = this.handleResize.bind(this); + this.onPreferenceChange = this.onPreferenceChange.bind(this); + this.focusTextbox = this.focusTextbox.bind(this); + this.showPostDeletedModal = this.showPostDeletedModal.bind(this); + this.hidePostDeletedModal = this.hidePostDeletedModal.bind(this); + + PostStore.clearCommentDraftUploads(); + + const draft = PostStore.getCommentDraft(this.props.rootId); + this.state = { + messageText: draft.message, + uploadsInProgress: draft.uploadsInProgress, + previews: draft.previews, + submitting: false, + windowWidth: Utils.windowWidth(), + ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'), + showPostDeletedModal: false + }; + } + componentDidMount() { + PreferenceStore.addChangeListener(this.onPreferenceChange); + window.addEventListener('resize', this.handleResize); + + this.focusTextbox(); + } + componentWillUnmount() { + PreferenceStore.removeChangeListener(this.onPreferenceChange); + window.removeEventListener('resize', this.handleResize); + } + onPreferenceChange() { + this.setState({ + ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter') + }); + } + handleResize() { + this.setState({windowWidth: Utils.windowWidth()}); + } + componentDidUpdate(prevProps, prevState) { + if (prevState.uploadsInProgress < this.state.uploadsInProgress) { + $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight); + } + + if (prevProps.rootId !== this.props.rootId) { + this.focusTextbox(); + } + } + handleSubmit(e) { + e.preventDefault(); + + if (this.state.uploadsInProgress.length > 0) { + return; + } + + if (this.state.submitting) { + return; + } + + let post = {}; + post.filenames = []; + post.message = this.state.messageText; + + if (post.message.trim().length === 0 && this.state.previews.length === 0) { + return; + } + + if (post.message.length > Constants.CHARACTER_LIMIT) { + this.setState({postError: this.props.intl.formatMessage(holders.commentLength, {max: Constants.CHARACTER_LIMIT})}); + return; + } + + const userId = UserStore.getCurrentId(); + + post.channel_id = this.props.channelId; + post.root_id = this.props.rootId; + post.parent_id = this.props.rootId; + post.filenames = this.state.previews; + const time = Utils.getTimestamp(); + post.pending_post_id = `${userId}:${time}`; + post.user_id = userId; + post.create_at = time; + + PostStore.storePendingPost(post); + PostStore.storeCommentDraft(this.props.rootId, null); + + Client.createPost( + post, + ChannelStore.getCurrent(), + (data) => { + AsyncClient.getPosts(this.props.channelId); + + const channel = ChannelStore.get(this.props.channelId); + let member = ChannelStore.getMember(this.props.channelId); + member.msg_count = channel.total_msg_count; + member.last_viewed_at = Date.now(); + ChannelStore.setChannelMember(member); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POST, + post: data + }); + }, + (err) => { + if (err.id === 'api.post.create_post.root_id.app_error') { + this.showPostDeletedModal(); + + PostStore.removePendingPost(post.channel_id, post.pending_post_id); + } else { + post.state = Constants.POST_FAILED; + PostStore.updatePendingPost(post); + } + + this.setState({ + submitting: false + }); + } + ); + + this.setState({ + messageText: '', + submitting: false, + postError: null, + previews: [], + serverError: null + }); + } + commentMsgKeyPress(e) { + if (this.state.ctrlSend && e.ctrlKey || !this.state.ctrlSend) { + if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) { + e.preventDefault(); + ReactDOM.findDOMNode(this.refs.textbox).blur(); + this.handleSubmit(e); + } + } + + const t = Date.now(); + if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) { + SocketStore.sendMessage({channel_id: this.props.channelId, action: 'typing', props: {parent_id: this.props.rootId}}); + this.lastTime = t; + } + } + handleUserInput(messageText) { + let draft = PostStore.getCommentDraft(this.props.rootId); + draft.message = messageText; + PostStore.storeCommentDraft(this.props.rootId, draft); + + $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight); + this.setState({messageText: messageText}); + } + handleKeyDown(e) { + if (this.state.ctrlSend && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) { + this.commentMsgKeyPress(e); + return; + } + + if (e.keyCode === KeyCodes.UP && this.state.messageText === '') { + e.preventDefault(); + + const lastPost = PostStore.getCurrentUsersLatestPost(this.props.channelId, this.props.rootId); + if (!lastPost) { + return; + } + + AppDispatcher.handleViewAction({ + type: ActionTypes.RECEIVED_EDIT_POST, + refocusId: '#reply_textbox', + title: this.props.intl.formatMessage(holders.commentTitle), + message: lastPost.message, + postId: lastPost.id, + channelId: lastPost.channel_id, + comments: PostStore.getCommentCount(lastPost) + }); + } + } + handleUploadClick() { + this.focusTextbox(); + } + handleUploadStart(clientIds) { + let draft = PostStore.getCommentDraft(this.props.rootId); + + draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds); + PostStore.storeCommentDraft(this.props.rootId, draft); + + this.setState({uploadsInProgress: draft.uploadsInProgress}); + + // this is a bit redundant with the code that sets focus when the file input is clicked, + // but this also resets the focus after a drag and drop + this.focusTextbox(); + } + handleFileUploadComplete(filenames, clientIds) { + let draft = PostStore.getCommentDraft(this.props.rootId); + + // remove each finished file from uploads + for (let i = 0; i < clientIds.length; i++) { + const index = draft.uploadsInProgress.indexOf(clientIds[i]); + + if (index !== -1) { + draft.uploadsInProgress.splice(index, 1); + } + } + + draft.previews = draft.previews.concat(filenames); + PostStore.storeCommentDraft(this.props.rootId, draft); + + this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews}); + } + handleUploadError(err, clientId) { + if (clientId === -1) { + this.setState({serverError: err}); + } else { + let draft = PostStore.getCommentDraft(this.props.rootId); + + const index = draft.uploadsInProgress.indexOf(clientId); + if (index !== -1) { + draft.uploadsInProgress.splice(index, 1); + } + + PostStore.storeCommentDraft(this.props.rootId, draft); + + this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err}); + } + } + removePreview(id) { + let previews = this.state.previews; + let uploadsInProgress = this.state.uploadsInProgress; + + // id can either be the path of an uploaded file or the client id of an in progress upload + let index = previews.indexOf(id); + if (index === -1) { + index = uploadsInProgress.indexOf(id); + + if (index !== -1) { + uploadsInProgress.splice(index, 1); + this.refs.fileUpload.getWrappedInstance().cancelUpload(id); + } + } else { + previews.splice(index, 1); + } + + let draft = PostStore.getCommentDraft(this.props.rootId); + draft.previews = previews; + draft.uploadsInProgress = uploadsInProgress; + PostStore.storeCommentDraft(this.props.rootId, draft); + + this.setState({previews: previews, uploadsInProgress: uploadsInProgress}); + } + componentWillReceiveProps(newProps) { + if (newProps.rootId !== this.props.rootId) { + const draft = PostStore.getCommentDraft(newProps.rootId); + this.setState({messageText: draft.message, uploadsInProgress: draft.uploadsInProgress, previews: draft.previews}); + } + } + getFileCount() { + return this.state.previews.length + this.state.uploadsInProgress.length; + } + focusTextbox() { + if (!Utils.isMobile()) { + this.refs.textbox.focus(); + } + } + showPostDeletedModal() { + this.setState({ + showPostDeletedModal: true + }); + } + hidePostDeletedModal() { + this.setState({ + showPostDeletedModal: false + }); + } + render() { + let serverError = null; + if (this.state.serverError) { + serverError = ( +
    + +
    + ); + } + + let postError = null; + if (this.state.postError) { + postError = ; + } + + let preview = null; + if (this.state.previews.length > 0 || this.state.uploadsInProgress.length > 0) { + preview = ( + + ); + } + + let postFooterClassName = 'post-create-footer'; + if (postError) { + postFooterClassName += ' has-error'; + } + + let uploadsInProgressText = null; + if (this.state.uploadsInProgress.length > 0) { + uploadsInProgressText = ( + + {this.state.uploadsInProgress.length === 1 ? ( + + ) : ( + + )} + + ); + } + + const {formatMessage} = this.props.intl; + return ( +
    +
    +
    +
    + + +
    +
    + +
    + + {uploadsInProgressText} + {preview} + {postError} + {serverError} +
    +
    + + + ); + } +} + +CreateComment.propTypes = { + intl: intlShape.isRequired, + channelId: React.PropTypes.string.isRequired, + rootId: React.PropTypes.string.isRequired +}; + +export default injectIntl(CreateComment); diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx new file mode 100644 index 000000000..36bfbf22d --- /dev/null +++ b/webapp/components/create_post.jsx @@ -0,0 +1,510 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ReactDOM from 'react-dom'; +import MsgTyping from './msg_typing.jsx'; +import Textbox from './textbox.jsx'; +import FileUpload from './file_upload.jsx'; +import FilePreview from './file_preview.jsx'; +import PostDeletedModal from './post_deleted_modal.jsx'; +import TutorialTip from './tutorial/tutorial_tip.jsx'; + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import * as GlobalActions from 'action_creators/global_actions.jsx'; +import * as Client from 'utils/client.jsx'; +import * as AsyncClient from 'utils/async_client.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import ChannelStore from 'stores/channel_store.jsx'; +import PostStore from 'stores/post_store.jsx'; +import UserStore from 'stores/user_store.jsx'; +import PreferenceStore from 'stores/preference_store.jsx'; +import SocketStore from 'stores/socket_store.jsx'; + +import Constants from 'utils/constants.jsx'; + +import {intlShape, injectIntl, defineMessages, FormattedHTMLMessage} from 'react-intl'; + +const Preferences = Constants.Preferences; +const TutorialSteps = Constants.TutorialSteps; +const ActionTypes = Constants.ActionTypes; +const KeyCodes = Constants.KeyCodes; + +const holders = defineMessages({ + comment: { + id: 'create_post.comment', + defaultMessage: 'Comment' + }, + post: { + id: 'create_post.post', + defaultMessage: 'Post' + }, + write: { + id: 'create_post.write', + defaultMessage: 'Write a message...' + } +}); + +import React from 'react'; + +class CreatePost extends React.Component { + constructor(props) { + super(props); + + this.lastTime = 0; + + this.getCurrentDraft = this.getCurrentDraft.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.postMsgKeyPress = this.postMsgKeyPress.bind(this); + this.handleUserInput = this.handleUserInput.bind(this); + this.handleUploadClick = this.handleUploadClick.bind(this); + this.handleUploadStart = this.handleUploadStart.bind(this); + this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this); + this.handleUploadError = this.handleUploadError.bind(this); + this.removePreview = this.removePreview.bind(this); + this.onChange = this.onChange.bind(this); + this.onPreferenceChange = this.onPreferenceChange.bind(this); + this.getFileCount = this.getFileCount.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.sendMessage = this.sendMessage.bind(this); + this.focusTextbox = this.focusTextbox.bind(this); + this.showPostDeletedModal = this.showPostDeletedModal.bind(this); + this.hidePostDeletedModal = this.hidePostDeletedModal.bind(this); + + PostStore.clearDraftUploads(); + + const draft = this.getCurrentDraft(); + + this.state = { + channelId: ChannelStore.getCurrentId(), + messageText: draft.messageText, + uploadsInProgress: draft.uploadsInProgress, + previews: draft.previews, + submitting: false, + initialText: draft.messageText, + ctrlSend: false, + showTutorialTip: false, + showPostDeletedModal: false + }; + } + getCurrentDraft() { + const draft = PostStore.getCurrentDraft(); + const safeDraft = {previews: [], messageText: '', uploadsInProgress: []}; + + if (draft) { + if (draft.message) { + safeDraft.messageText = draft.message; + } + if (draft.previews) { + safeDraft.previews = draft.previews; + } + if (draft.uploadsInProgress) { + safeDraft.uploadsInProgress = draft.uploadsInProgress; + } + } + + return safeDraft; + } + handleSubmit(e) { + e.preventDefault(); + + if (this.state.uploadsInProgress.length > 0 || this.state.submitting) { + return; + } + + const post = {}; + post.filenames = []; + post.message = this.state.messageText; + + if (post.message.trim().length === 0 && this.state.previews.length === 0) { + return; + } + + if (post.message.length > Constants.CHARACTER_LIMIT) { + this.setState({postError: `Post length must be less than ${Constants.CHARACTER_LIMIT} characters.`}); + return; + } + + this.setState({submitting: true, serverError: null}); + + if (post.message.indexOf('/') === 0) { + Client.executeCommand( + this.state.channelId, + post.message, + false, + (data) => { + PostStore.storeDraft(this.state.channelId, null); + this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); + + if (data.goto_location && data.goto_location.length > 0) { + window.location.href = data.goto_location; + } + }, + (err) => { + if (err.sendMessage) { + this.sendMessage(post); + } else { + const state = {}; + state.serverError = err.message; + state.submitting = false; + this.setState(state); + } + } + ); + } else { + this.sendMessage(post); + } + } + sendMessage(post) { + post.channel_id = this.state.channelId; + post.filenames = this.state.previews; + + const time = Utils.getTimestamp(); + const userId = UserStore.getCurrentId(); + post.pending_post_id = `${userId}:${time}`; + post.user_id = userId; + post.create_at = time; + post.parent_id = this.state.parentId; + + const channel = ChannelStore.get(this.state.channelId); + + GlobalActions.emitUserPostedEvent(post); + this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); + + Client.createPost(post, channel, + (data) => { + AsyncClient.getPosts(); + + const member = ChannelStore.getMember(channel.id); + member.msg_count = channel.total_msg_count; + member.last_viewed_at = Date.now(); + ChannelStore.setChannelMember(member); + + GlobalActions.emitPostRecievedEvent(data); + }, + (err) => { + if (err.id === 'api.post.create_post.root_id.app_error') { + // this should never actually happen since you can't reply from this textbox + this.showPostDeletedModal(); + + PostStore.removePendingPost(post.pending_post_id); + } else { + post.state = Constants.POST_FAILED; + PostStore.updatePendingPost(post); + } + + this.setState({ + submitting: false + }); + } + ); + } + focusTextbox() { + if (!Utils.isMobile()) { + this.refs.textbox.focus(); + } + } + postMsgKeyPress(e) { + if (this.state.ctrlSend && e.ctrlKey || !this.state.ctrlSend) { + if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) { + e.preventDefault(); + ReactDOM.findDOMNode(this.refs.textbox).blur(); + this.handleSubmit(e); + } + } + + const t = Date.now(); + if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) { + SocketStore.sendMessage({channel_id: this.state.channelId, action: 'typing', props: {parent_id: ''}, state: {}}); + this.lastTime = t; + } + } + handleUserInput(messageText) { + this.setState({messageText}); + + const draft = PostStore.getCurrentDraft(); + draft.message = messageText; + PostStore.storeCurrentDraft(draft); + } + handleUploadClick() { + this.focusTextbox(); + } + handleUploadStart(clientIds, channelId) { + const draft = PostStore.getDraft(channelId); + + draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds); + PostStore.storeDraft(channelId, draft); + + this.setState({uploadsInProgress: draft.uploadsInProgress}); + + // this is a bit redundant with the code that sets focus when the file input is clicked, + // but this also resets the focus after a drag and drop + this.focusTextbox(); + } + handleFileUploadComplete(filenames, clientIds, channelId) { + const draft = PostStore.getDraft(channelId); + + // remove each finished file from uploads + for (let i = 0; i < clientIds.length; i++) { + const index = draft.uploadsInProgress.indexOf(clientIds[i]); + + if (index !== -1) { + draft.uploadsInProgress.splice(index, 1); + } + } + + draft.previews = draft.previews.concat(filenames); + PostStore.storeDraft(channelId, draft); + + this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews}); + } + handleUploadError(err, clientId) { + let message = err; + if (message && typeof message !== 'string') { + // err is an AppError from the server + message = err.message; + } + + if (clientId !== -1) { + const draft = PostStore.getDraft(this.state.channelId); + + const index = draft.uploadsInProgress.indexOf(clientId); + if (index !== -1) { + draft.uploadsInProgress.splice(index, 1); + } + + PostStore.storeDraft(this.state.channelId, draft); + + this.setState({uploadsInProgress: draft.uploadsInProgress}); + } + + this.setState({serverError: message}); + } + removePreview(id) { + const previews = Object.assign([], this.state.previews); + const uploadsInProgress = this.state.uploadsInProgress; + + // id can either be the path of an uploaded file or the client id of an in progress upload + let index = previews.indexOf(id); + if (index === -1) { + index = uploadsInProgress.indexOf(id); + + if (index !== -1) { + uploadsInProgress.splice(index, 1); + this.refs.fileUpload.getWrappedInstance().cancelUpload(id); + } + } else { + previews.splice(index, 1); + } + + const draft = PostStore.getCurrentDraft(); + draft.previews = previews; + draft.uploadsInProgress = uploadsInProgress; + PostStore.storeCurrentDraft(draft); + + this.setState({previews, uploadsInProgress}); + } + componentWillMount() { + const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); + + // wait to load these since they may have changed since the component was constructed (particularly in the case of skipping the tutorial) + this.setState({ + ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'), + showTutorialTip: tutorialStep === TutorialSteps.POST_POPOVER + }); + } + componentDidMount() { + ChannelStore.addChangeListener(this.onChange); + PreferenceStore.addChangeListener(this.onPreferenceChange); + + this.focusTextbox(); + } + componentDidUpdate(prevProps, prevState) { + if (prevState.channelId !== this.state.channelId) { + this.focusTextbox(); + } + } + componentWillUnmount() { + ChannelStore.removeChangeListener(this.onChange); + PreferenceStore.removeChangeListener(this.onPreferenceChange); + } + onChange() { + const channelId = ChannelStore.getCurrentId(); + if (this.state.channelId !== channelId) { + const draft = this.getCurrentDraft(); + + this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress}); + } + } + onPreferenceChange() { + const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); + this.setState({ + showTutorialTip: tutorialStep === TutorialSteps.POST_POPOVER, + ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter') + }); + } + getFileCount(channelId) { + if (channelId === this.state.channelId) { + return this.state.previews.length + this.state.uploadsInProgress.length; + } + + const draft = PostStore.getDraft(channelId); + return draft.previews.length + draft.uploadsInProgress.length; + } + handleKeyDown(e) { + if (this.state.ctrlSend && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) { + this.postMsgKeyPress(e); + return; + } + + if (e.keyCode === KeyCodes.UP && this.state.messageText === '') { + e.preventDefault(); + + const channelId = ChannelStore.getCurrentId(); + const lastPost = PostStore.getCurrentUsersLatestPost(channelId); + if (!lastPost) { + return; + } + const {formatMessage} = this.props.intl; + var type = (lastPost.root_id && lastPost.root_id.length > 0) ? formatMessage(holders.comment) : formatMessage(holders.post); + + AppDispatcher.handleViewAction({ + type: ActionTypes.RECEIVED_EDIT_POST, + refocusId: '#post_textbox', + title: type, + message: lastPost.message, + postId: lastPost.id, + channelId: lastPost.channel_id, + comments: PostStore.getCommentCount(lastPost) + }); + } + } + showPostDeletedModal() { + this.setState({ + showPostDeletedModal: true + }); + } + hidePostDeletedModal() { + this.setState({ + showPostDeletedModal: false + }); + } + createTutorialTip() { + const screens = []; + + screens.push( +
    + +
    + ); + + return ( + + ); + } + render() { + let serverError = null; + if (this.state.serverError) { + serverError = ( +
    + +
    + ); + } + + let postError = null; + if (this.state.postError) { + postError = ; + } + + let preview = null; + if (this.state.previews.length > 0 || this.state.uploadsInProgress.length > 0) { + preview = ( + + ); + } + + let postFooterClassName = 'post-create-footer'; + if (postError) { + postFooterClassName += ' has-error'; + } + + let tutorialTip = null; + if (this.state.showTutorialTip) { + tutorialTip = this.createTutorialTip(); + } + + return ( +
    +
    +
    +
    + + +
    + + + + {tutorialTip} +
    +
    + + {preview} + {postError} + {serverError} +
    +
    + + + ); + } +} + +CreatePost.propTypes = { + intl: intlShape.isRequired +}; + +export default injectIntl(CreatePost); diff --git a/webapp/components/delete_channel_modal.jsx b/webapp/components/delete_channel_modal.jsx new file mode 100644 index 000000000..244472a56 --- /dev/null +++ b/webapp/components/delete_channel_modal.jsx @@ -0,0 +1,111 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as AsyncClient from 'utils/async_client.jsx'; +import * as Client from 'utils/client.jsx'; +import {Modal} from 'react-bootstrap'; +import TeamStore from 'stores/team_store.jsx'; +import Constants from 'utils/constants.jsx'; + +import {FormattedMessage} from 'react-intl'; + +import {browserHistory} from 'react-router'; + +import React from 'react'; + +export default class DeleteChannelModal extends React.Component { + constructor(props) { + super(props); + + this.handleDelete = this.handleDelete.bind(this); + } + + handleDelete() { + if (this.props.channel.id.length !== 26) { + return; + } + + browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square'); + Client.deleteChannel( + this.props.channel.id, + () => { + AsyncClient.getChannels(true); + }, + (err) => { + AsyncClient.dispatchError(err, 'handleDelete'); + } + ); + } + + render() { + let channelTerm = ( + + ); + if (this.props.channel.type === Constants.PRIVATE_CHANNEL) { + channelTerm = ( + + ); + } + + return ( + + +

    + +

    +
    + + + + + + + +
    + ); + } +} + +DeleteChannelModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onHide: React.PropTypes.func.isRequired, + channel: React.PropTypes.object.isRequired +}; diff --git a/webapp/components/delete_post_modal.jsx b/webapp/components/delete_post_modal.jsx new file mode 100644 index 000000000..0dbdc2b43 --- /dev/null +++ b/webapp/components/delete_post_modal.jsx @@ -0,0 +1,178 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import $ from 'jquery'; +import ReactDOM from 'react-dom'; +import * as Client from 'utils/client.jsx'; +import PostStore from 'stores/post_store.jsx'; +import ModalStore from 'stores/modal_store.jsx'; +import {Modal} from 'react-bootstrap'; +import * as AsyncClient from 'utils/async_client.jsx'; +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import Constants from 'utils/constants.jsx'; + +import {FormattedMessage} from 'react-intl'; + +var ActionTypes = Constants.ActionTypes; + +import React from 'react'; + +export default class DeletePostModal extends React.Component { + constructor(props) { + super(props); + + this.handleDelete = this.handleDelete.bind(this); + this.handleToggle = this.handleToggle.bind(this); + this.handleHide = this.handleHide.bind(this); + + this.state = { + show: false, + post: null, + commentCount: 0, + error: '' + }; + } + + componentDidMount() { + ModalStore.addModalListener(ActionTypes.TOGGLE_DELETE_POST_MODAL, this.handleToggle); + } + + componentWillUnmount() { + ModalStore.removeModalListener(ActionTypes.TOGGLE_DELETE_POST_MODAL, this.handleToggle); + } + + componentDidUpdate(prevProps, prevState) { + if (this.state.show && !prevState.show) { + setTimeout(() => { + $(ReactDOM.findDOMNode(this.refs.deletePostBtn)).focus(); + }, 0); + } + } + + handleDelete() { + Client.deletePost( + this.state.post.channel_id, + this.state.post.id, + () => { + PostStore.deletePost(this.state.post); + AsyncClient.getPosts(this.state.post.channel_id); + + if (this.state.post.id === PostStore.getSelectedPostId()) { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POST_SELECTED, + postId: null + }); + } + }, + (err) => { + AsyncClient.dispatchError(err, 'deletePost'); + } + ); + + this.handleHide(); + } + + handleToggle(value, args) { + this.setState({ + show: value, + post: args.post, + commentCount: args.commentCount, + error: '' + }); + } + + handleHide() { + this.setState({show: false}); + } + + render() { + if (!this.state.post) { + return null; + } + + var error = null; + if (this.state.error) { + error =
    ; + } + + var commentWarning = ''; + if (this.state.commentCount > 0) { + commentWarning = ( + + ); + } + + const postTerm = this.state.post.root_id ? ( + + ) : ( + + ); + + return ( + + + + + + + + +
    +
    + {commentWarning} + {error} +
    + + + + +
    + ); + } +} diff --git a/webapp/components/do_verify_email.jsx b/webapp/components/do_verify_email.jsx new file mode 100644 index 000000000..a984d2e52 --- /dev/null +++ b/webapp/components/do_verify_email.jsx @@ -0,0 +1,84 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'react-intl'; +import * as Client from 'utils/client.jsx'; +import LoadingScreen from './loading_screen.jsx'; + +import {browserHistory} from 'react-router'; + +import React from 'react'; + +export default class DoVerifyEmail extends React.Component { + constructor(props) { + super(props); + + this.state = { + verifyStatus: 'pending', + serverError: '' + }; + } + componentWillMount() { + const uid = this.props.location.query.uid; + const hid = this.props.location.query.hid; + const teamName = this.props.location.query.teamname; + const email = this.props.location.query.email; + + Client.verifyEmail( + () => { + browserHistory.push('/' + teamName + '/login?extra=verified&email=' + email); + }, + (err) => { + this.setState({verifyStatus: 'failure', serverError: err.message}); + }, + uid, + hid + ); + } + render() { + if (this.state.verifyStatus !== 'failure') { + return (); + } + + return ( +
    + +
    +
    +

    + +

    +
    +

    + +

    +

    + + {this.state.serverError} +

    +
    +
    +
    +
    + ); + } +} + +DoVerifyEmail.defaultProps = { +}; +DoVerifyEmail.propTypes = { + location: React.PropTypes.object.isRequired +}; diff --git a/webapp/components/edit_channel_header_modal.jsx b/webapp/components/edit_channel_header_modal.jsx new file mode 100644 index 000000000..35a5fb9dc --- /dev/null +++ b/webapp/components/edit_channel_header_modal.jsx @@ -0,0 +1,176 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import ReactDOM from 'react-dom'; +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import * as Client from 'utils/client.jsx'; +import Constants from 'utils/constants.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; + +import {Modal} from 'react-bootstrap'; + +const holders = defineMessages({ + error: { + id: 'edit_channel_header_modal.error', + defaultMessage: 'This channel header is too long, please enter a shorter one' + } +}); + +import React from 'react'; + +class EditChannelHeaderModal extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.onShow = this.onShow.bind(this); + this.onHide = this.onHide.bind(this); + + this.state = { + header: props.channel.header, + serverError: '' + }; + } + + componentDidMount() { + if (this.props.show) { + this.onShow(); + } + } + + componentWillReceiveProps(nextProps) { + if (this.props.channel.header !== nextProps.channel.header) { + this.setState({ + header: nextProps.channel.header + }); + } + } + + componentDidUpdate(prevProps) { + if (this.props.show && !prevProps.show) { + this.onShow(); + } + } + + handleChange(e) { + this.setState({ + header: e.target.value + }); + } + + handleSubmit() { + Client.updateChannelHeader( + this.props.channel.id, + this.state.header, + (channel) => { + this.setState({serverError: ''}); + this.onHide(); + + AppDispatcher.handleServerAction({ + type: Constants.ActionTypes.RECEIVED_CHANNEL, + channel + }); + }, + (err) => { + if (err.id === 'api.context.invalid_param.app_error') { + this.setState({serverError: this.props.intl.formatMessage(holders.error)}); + } else { + this.setState({serverError: err.message}); + } + } + ); + } + + onShow() { + const textarea = ReactDOM.findDOMNode(this.refs.textarea); + Utils.placeCaretAtEnd(textarea); + } + + onHide() { + this.setState({ + serverError: '', + header: this.props.channel.header + }); + + this.props.onHide(); + } + + render() { + var serverError = null; + if (this.state.serverError) { + serverError =

    ; + } + + return ( + + + + + + + +

    + +

    +