diff options
Diffstat (limited to 'webapp/components')
21 files changed, 1449 insertions, 728 deletions
diff --git a/webapp/components/backstage/add_incoming_webhook.jsx b/webapp/components/backstage/add_incoming_webhook.jsx new file mode 100644 index 000000000..fa7531fc6 --- /dev/null +++ b/webapp/components/backstage/add_incoming_webhook.jsx @@ -0,0 +1,188 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import {browserHistory} from 'react-router'; + +import ChannelSelect from 'components/channel_select.jsx'; +import {FormattedMessage} from 'react-intl'; +import FormError from 'components/form_error.jsx'; +import {Link} from 'react-router'; +import SpinnerButton from 'components/spinner_button.jsx'; + +export default class AddIncomingWebhook extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.updateName = this.updateName.bind(this); + this.updateDescription = this.updateDescription.bind(this); + this.updateChannelId = this.updateChannelId.bind(this); + + this.state = { + name: '', + description: '', + channelId: '', + saving: false, + serverError: '', + clientError: null + }; + } + + handleSubmit(e) { + e.preventDefault(); + + if (this.state.saving) { + return; + } + + this.setState({ + saving: true, + serverError: '', + clientError: '' + }); + + if (!this.state.channelId) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_incoming_webhook.channelRequired' + defaultMessage='A valid channel is required' + /> + ) + }); + + return; + } + + const hook = { + channel_id: this.state.channelId + }; + + AsyncClient.addIncomingHook( + hook, + () => { + browserHistory.push('/settings/integrations/installed'); + }, + (err) => { + this.setState({ + serverError: err.message + }); + } + ); + } + + updateName(e) { + this.setState({ + name: e.target.value + }); + } + + updateDescription(e) { + this.setState({ + description: e.target.value + }); + } + + updateChannelId(e) { + this.setState({ + channelId: e.target.value + }); + } + + render() { + return ( + <div className='backstage row'> + <div className='add-incoming-webhook'> + <div className='backstage__header'> + <h1 className='text'> + <FormattedMessage + id='add_incoming_webhook.header' + defaultMessage='Add Incoming Webhook' + /> + </h1> + </div> + </div> + <form className='add-incoming-webhook__body'> + <div className='add-integration__row'> + <label + className='add-integration__label' + htmlFor='name' + > + <FormattedMessage + id='add_incoming_webhook.name' + defaultMessage='Name' + /> + </label> + <input + id='name' + type='text' + value={this.state.name} + onChange={this.updateName} + /> + </div> + <div className='add-integration__row'> + <label + className='add-integration__label' + htmlFor='description' + > + <FormattedMessage + id='add_incoming_webhook.description' + defaultMessage='Description' + /> + </label> + <input + id='description' + type='text' + value={this.state.description} + onChange={this.updateDescription} + /> + </div> + <div className='add-integration__row'> + <label + className='add-integration__label' + htmlFor='channelId' + > + <FormattedMessage + id='add_incoming_webhook.channel' + defaultMessage='Channel' + /> + </label> + <ChannelSelect + id='channelId' + value={this.state.channelId} + onChange={this.updateChannelId} + /> + </div> + <div className='add-integration__submit-row'> + <Link + className='btn btn-sm' + to={'/settings/integrations/add'} + > + <FormattedMessage + id='add_incoming_webhook.cancel' + defaultMessage='Cancel' + /> + </Link> + <SpinnerButton + className='btn btn-primary' + type='submit' + spinning={this.state.saving} + onClick={this.handleSubmit} + > + <FormattedMessage + id='add_incoming_webhook.save' + defaultMessage='Save' + /> + </SpinnerButton> + </div> + <FormError errors={[this.state.serverError, this.state.clientError]}/> + </form> + </div> + ); + } +} diff --git a/webapp/components/backstage/add_integration.jsx b/webapp/components/backstage/add_integration.jsx new file mode 100644 index 000000000..cebc1e8b0 --- /dev/null +++ b/webapp/components/backstage/add_integration.jsx @@ -0,0 +1,78 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import {FormattedMessage} from 'react-intl'; +import AddIntegrationOption from './add_integration_option.jsx'; + +import WebhookIcon from 'images/webhook_icon.jpg'; + +export default class AddIntegration extends React.Component { + render() { + const options = []; + + if (window.mm_config.EnableIncomingWebhooks === 'true') { + options.push( + <AddIntegrationOption + key='incomingWebhook' + image={WebhookIcon} + title={ + <FormattedMessage + id='add_integration.incomingWebhook.title' + defaultMessage='Incoming Webhook' + /> + } + description={ + <FormattedMessage + id='add_integration.incomingWebhook.description' + defaultMessage='Create webhook URLs for use in external integrations.' + /> + } + link={'/settings/integrations/add/incoming_webhook'} + /> + ); + } + + if (window.mm_config.EnableOutgoingWebhooks === 'true') { + options.push( + <AddIntegrationOption + key='outgoingWebhook' + image={WebhookIcon} + title={ + <FormattedMessage + id='add_integration.outgoingWebhook.title' + defaultMessage='Outgoing Webhook' + /> + } + description={ + <FormattedMessage + id='add_integration.outgoingWebhook.description' + defaultMessage='Create webhooks to send new message events to an external integration.' + /> + } + link={'/settings/integrations/add/outgoing_webhook'} + /> + ); + } + + return ( + <div className='backstage row'> + <div className='add-integration'> + <div className='backstage__header'> + <h1 className='text'> + <FormattedMessage + id='add_integration.header' + defaultMessage='Add Integration' + /> + </h1> + </div> + </div> + <div className='add-integration__options'> + {options} + </div> + </div> + ); + } +} + diff --git a/webapp/components/backstage/add_integration_option.jsx b/webapp/components/backstage/add_integration_option.jsx new file mode 100644 index 000000000..3c3caf2f4 --- /dev/null +++ b/webapp/components/backstage/add_integration_option.jsx @@ -0,0 +1,39 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import {Link} from 'react-router'; + +export default class AddIntegrationOption extends React.Component { + static get propTypes() { + return { + image: React.PropTypes.string.isRequired, + title: React.PropTypes.node.isRequired, + description: React.PropTypes.node.isRequired, + link: React.PropTypes.string.isRequired + }; + } + + render() { + const {image, title, description, link} = this.props; + + return ( + <Link + to={link} + className='add-integration-option' + > + <img + className='add-integration-option__image' + src={image} + /> + <div className='add-integration-option__title'> + {title} + </div> + <div className='add-integration-option__description'> + {description} + </div> + </Link> + ); + } +} diff --git a/webapp/components/backstage/add_outgoing_webhook.jsx b/webapp/components/backstage/add_outgoing_webhook.jsx new file mode 100644 index 000000000..3ae2f8606 --- /dev/null +++ b/webapp/components/backstage/add_outgoing_webhook.jsx @@ -0,0 +1,254 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import {browserHistory} from 'react-router'; + +import ChannelSelect from 'components/channel_select.jsx'; +import {FormattedMessage} from 'react-intl'; +import FormError from 'components/form_error.jsx'; +import {Link} from 'react-router'; +import SpinnerButton from 'components/spinner_button.jsx'; + +export default class AddOutgoingWebhook extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.updateName = this.updateName.bind(this); + this.updateDescription = this.updateDescription.bind(this); + this.updateChannelId = this.updateChannelId.bind(this); + this.updateTriggerWords = this.updateTriggerWords.bind(this); + this.updateCallbackUrls = this.updateCallbackUrls.bind(this); + + this.state = { + name: '', + description: '', + channelId: '', + triggerWords: '', + callbackUrls: '', + saving: false, + serverError: '', + clientError: null + }; + } + + handleSubmit(e) { + e.preventDefault(); + + if (this.state.saving) { + return; + } + + this.setState({ + saving: true, + serverError: '', + clientError: '' + }); + + if (!this.state.channelId && !this.state.triggerWords) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_outgoing_webhook.triggerWordsOrChannelRequired' + defaultMessage='A valid channel or a list of trigger words is required' + /> + ) + }); + + return; + } + + if (!this.state.callbackUrls) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_outgoing_webhook.callbackUrlsRequired' + defaultMessage='One or more callback URLs are required' + /> + ) + }); + + return; + } + + const hook = { + channel_id: this.state.channelId, + trigger_words: this.state.triggerWords.split('\n').map((word) => word.trim()), + callback_urls: this.state.callbackUrls.split('\n').map((url) => url.trim()) + }; + + AsyncClient.addOutgoingHook( + hook, + () => { + browserHistory.push('/settings/integrations/installed'); + }, + (err) => { + this.setState({ + serverError: err.message + }); + } + ); + } + + updateName(e) { + this.setState({ + name: e.target.value + }); + } + + updateDescription(e) { + this.setState({ + description: e.target.value + }); + } + + updateChannelId(e) { + this.setState({ + channelId: e.target.value + }); + } + + updateTriggerWords(e) { + this.setState({ + triggerWords: e.target.value + }); + } + + updateCallbackUrls(e) { + this.setState({ + callbackUrls: e.target.value + }); + } + + render() { + return ( + <div className='backstage row'> + <div className='add-outgoing-webhook'> + <div className='backstage__header'> + <h1 className='text'> + <FormattedMessage + id='add_outgoing_webhook.header' + defaultMessage='Add Outgoing Webhook' + /> + </h1> + </div> + </div> + <form className='add-outgoing-webhook__body'> + <div className='add-integration__row'> + <label + className='add-integration__label' + htmlFor='name' + > + <FormattedMessage + id='add_outgoing_webhook.name' + defaultMessage='Name' + /> + </label> + <input + id='name' + type='text' + value={this.state.name} + onChange={this.updateName} + /> + </div> + <div className='add-integration__row'> + <label + className='add-integration__label' + htmlFor='description' + > + <FormattedMessage + id='add_outgoing_webhook.description' + defaultMessage='Description' + /> + </label> + <input + id='description' + type='text' + value={this.state.description} + onChange={this.updateDescription} + /> + </div> + <div className='add-integration__row'> + <label + className='add-integration__label' + htmlFor='channelId' + > + <FormattedMessage + id='add_outgoing_webhook.channel' + defaultMessage='Channel' + /> + </label> + <ChannelSelect + id='channelId' + value={this.state.channelId} + onChange={this.updateChannelId} + /> + </div> + <div className='add-integration__row'> + <label + className='add-integration__label' + htmlFor='triggerWords' + > + <FormattedMessage + id='add_outgoing_webhook.triggerWords' + defaultMessage='Trigger Words (One Per Line)' + /> + </label> + <textarea + id='triggerWords' + rows='3' + value={this.state.triggerWords} + onChange={this.updateTriggerWords} + /> + </div> + <div className='add-integration__row'> + <label + className='add-integration__label' + htmlFor='callbackUrls' + > + <FormattedMessage + id='add_outgoing_webhook.callbackUrls' + defaultMessage='Callback URLs (One Per Line)' + /> + </label> + <textarea + id='callbackUrls' + rows='3' + value={this.state.callbackUrls} + onChange={this.updateCallbackUrls} + /> + </div> + <div className='add-integration__submit-row'> + <Link + className='btn btn-sm' + to={'/settings/integrations/add'} + > + <FormattedMessage + id='add_outgoing_webhook.cancel' + defaultMessage='Cancel' + /> + </Link> + <SpinnerButton + className='btn btn-primary' + type='submit' + spinning={this.state.saving} + onClick={this.handleSubmit} + > + <FormattedMessage + id='add_outgoing_webhook.save' + defaultMessage='Save' + /> + </SpinnerButton> + </div> + <FormError errors={[this.state.serverError, this.state.clientError]}/> + </form> + </div> + ); + } +} diff --git a/webapp/components/backstage/backstage_category.jsx b/webapp/components/backstage/backstage_category.jsx new file mode 100644 index 000000000..e8b0b57ae --- /dev/null +++ b/webapp/components/backstage/backstage_category.jsx @@ -0,0 +1,68 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import {Link} from 'react-router'; + +export default class BackstageCategory extends React.Component { + static get propTypes() { + return { + name: React.PropTypes.string.isRequired, + title: React.PropTypes.node.isRequired, + icon: React.PropTypes.string.isRequired, + parentLink: React.PropTypes.string, + children: React.PropTypes.arrayOf(React.PropTypes.element) + }; + } + + static get defaultProps() { + return { + parentLink: '', + children: [] + }; + } + + static get contextTypes() { + return { + router: React.PropTypes.object.isRequired + }; + } + + render() { + const {name, title, icon, parentLink, children} = this.props; + + const link = parentLink + '/' + name; + + let clonedChildren = null; + if (children.length > 0 && this.context.router.isActive(link)) { + clonedChildren = ( + <ul className='sections'> + { + React.Children.map(children, (child) => { + return React.cloneElement(child, { + parentLink: link + }); + }) + } + </ul> + ); + } + + return ( + <li className='backstage__sidebar__category'> + <Link + to={link} + className='category-title' + activeClassName='category-title--active' + > + <i className={'fa ' + icon}/> + <span className='category-title__text'> + {title} + </span> + </Link> + {clonedChildren} + </li> + ); + } +} diff --git a/webapp/components/backstage/backstage_navbar.jsx b/webapp/components/backstage/backstage_navbar.jsx new file mode 100644 index 000000000..555165791 --- /dev/null +++ b/webapp/components/backstage/backstage_navbar.jsx @@ -0,0 +1,61 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import TeamStore from 'stores/team_store.jsx'; + +import {FormattedMessage} from 'react-intl'; +import {Link} from 'react-router'; + +export default class BackstageNavbar extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + + this.state = { + team: TeamStore.getCurrent() + }; + } + + componentDidMount() { + TeamStore.addChangeListener(this.handleChange); + } + + componentWillUnmount() { + TeamStore.removeChangeListener(this.handleChange); + } + + handleChange() { + this.setState({ + team: TeamStore.getCurrent() + }); + } + + render() { + if (!this.state.team) { + return null; + } + + return ( + <div className='backstage__navbar row'> + <Link + className='backstage__navbar__back' + to={`/${this.state.team.display_name}/channels/town-square`} + > + <i className='fa fa-angle-left'/> + <span> + <FormattedMessage + id='backstage_navbar.backToMattermost' + defaultMessage='Back to {siteName}' + values={{ + siteName: global.window.mm_config.SiteName + }} + /> + </span> + </Link> + </div> + ); + } +} diff --git a/webapp/components/backstage/backstage_section.jsx b/webapp/components/backstage/backstage_section.jsx new file mode 100644 index 000000000..d6ce2b258 --- /dev/null +++ b/webapp/components/backstage/backstage_section.jsx @@ -0,0 +1,80 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import {Link} from 'react-router'; + +export default class BackstageSection extends React.Component { + static get propTypes() { + return { + name: React.PropTypes.string.isRequired, + title: React.PropTypes.node.isRequired, + parentLink: React.PropTypes.string, + subsection: React.PropTypes.bool, + children: React.PropTypes.arrayOf(React.PropTypes.element) + }; + } + + static get defaultProps() { + return { + parentLink: '', + subsection: false, + children: [] + }; + } + + static get contextTypes() { + return { + router: React.PropTypes.object.isRequired + }; + } + + getLink() { + return this.props.parentLink + '/' + this.props.name; + } + + render() { + const {title, subsection, children} = this.props; + + const link = this.getLink(); + + let clonedChildren = null; + if (children.length > 0) { + clonedChildren = ( + <ul className='subsections'> + { + React.Children.map(children, (child) => { + return React.cloneElement(child, { + parentLink: link, + subsection: true + }); + }) + } + </ul> + ); + } + + let className = 'section'; + if (subsection) { + className = 'subsection'; + } + + return ( + <li className={className}> + <Link + className={`${className}-title`} + activeClassName={`${className}-title--active`} + onlyActiveOnIndex={true} + onClick={this.handleClick} + to={link} + > + <span className={`${className}-title__text`}> + {title} + </span> + </Link> + {clonedChildren} + </li> + ); + } +} diff --git a/webapp/components/backstage/backstage_sidebar.jsx b/webapp/components/backstage/backstage_sidebar.jsx new file mode 100644 index 000000000..63a0df5cb --- /dev/null +++ b/webapp/components/backstage/backstage_sidebar.jsx @@ -0,0 +1,68 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import BackstageCategory from './backstage_category.jsx'; +import BackstageSection from './backstage_section.jsx'; +import {FormattedMessage} from 'react-intl'; + +export default class BackstageSidebar extends React.Component { + render() { + return ( + <div className='backstage__sidebar'> + <ul> + <BackstageCategory + name='integrations' + parentLink={'/settings'} + icon='fa-link' + title={ + <FormattedMessage + id='backstage_sidebar.integrations' + defaultMessage='Integrations' + /> + } + > + <BackstageSection + name='installed' + title={( + <FormattedMessage + id='backstage_sidebar.integrations.installed' + defaultMessage='Installed Integrations' + /> + )} + /> + <BackstageSection + name='add' + title={( + <FormattedMessage + id='backstage_sidebar.integrations.add' + defaultMessage='Add Integration' + /> + )} + > + <BackstageSection + name='incoming_webhook' + title={( + <FormattedMessage + id='backstage_sidebar.integrations.add.incomingWebhook' + defaultMessage='Incoming Webhook' + /> + )} + /> + <BackstageSection + name='outgoing_webhook' + title={( + <FormattedMessage + id='backstage_sidebar.integrations.add.outgoingWebhook' + defaultMessage='Outgoing Webhook' + /> + )} + /> + </BackstageSection> + </BackstageCategory> + </ul> + </div> + ); + } +} diff --git a/webapp/components/backstage/installed_incoming_webhook.jsx b/webapp/components/backstage/installed_incoming_webhook.jsx new file mode 100644 index 000000000..4ca421a02 --- /dev/null +++ b/webapp/components/backstage/installed_incoming_webhook.jsx @@ -0,0 +1,71 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import ChannelStore from 'stores/channel_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; + +export default class InstalledIncomingWebhook extends React.Component { + static get propTypes() { + return { + incomingWebhook: React.PropTypes.object.isRequired, + onDeleteClick: React.PropTypes.func.isRequired + }; + } + + constructor(props) { + super(props); + + this.handleDeleteClick = this.handleDeleteClick.bind(this); + } + + handleDeleteClick(e) { + e.preventDefault(); + + this.props.onDeleteClick(this.props.incomingWebhook); + } + + render() { + const incomingWebhook = this.props.incomingWebhook; + + const channel = ChannelStore.get(incomingWebhook.channel_id); + const channelName = channel ? channel.display_name : 'cannot find channel'; + + return ( + <div className='installed-integrations__item installed-integrations__incoming-webhook'> + <div className='details'> + <div className='details-row'> + <span className='name'> + {channelName} + </span> + <span className='type'> + <FormattedMessage + id='installed_integrations.incomingWebhookType' + defaultMessage='(Incoming Webhook)' + /> + </span> + </div> + <div className='details-row'> + <span className='description'> + {Utils.getWindowLocationOrigin() + '/hooks/' + incomingWebhook.id} + </span> + </div> + </div> + <div className='actions'> + <a + href='#' + onClick={this.handleDeleteClick} + > + <FormattedMessage + id='installed_integrations.delete' + defaultMessage='Delete' + /> + </a> + </div> + </div> + ); + } +} diff --git a/webapp/components/backstage/installed_integrations.jsx b/webapp/components/backstage/installed_integrations.jsx new file mode 100644 index 000000000..ff0b6e4ec --- /dev/null +++ b/webapp/components/backstage/installed_integrations.jsx @@ -0,0 +1,289 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; +import IntegrationStore from 'stores/integration_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; +import InstalledIncomingWebhook from './installed_incoming_webhook.jsx'; +import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx'; +import {Link} from 'react-router'; + +export default class InstalledIntegrations extends React.Component { + constructor(props) { + super(props); + + this.handleIntegrationChange = this.handleIntegrationChange.bind(this); + this.updateFilter = this.updateFilter.bind(this); + this.updateTypeFilter = this.updateTypeFilter.bind(this); + + this.deleteIncomingWebhook = this.deleteIncomingWebhook.bind(this); + this.regenOutgoingWebhookToken = this.regenOutgoingWebhookToken.bind(this); + this.deleteOutgoingWebhook = this.deleteOutgoingWebhook.bind(this); + + this.state = { + incomingWebhooks: [], + outgoingWebhooks: [], + typeFilter: '', + filter: '' + }; + } + + componentWillMount() { + IntegrationStore.addChangeListener(this.handleIntegrationChange); + + if (window.mm_config.EnableIncomingWebhooks === 'true') { + if (IntegrationStore.hasReceivedIncomingWebhooks()) { + this.setState({ + incomingWebhooks: IntegrationStore.getIncomingWebhooks() + }); + } else { + AsyncClient.listIncomingHooks(); + } + } + + if (window.mm_config.EnableOutgoingWebhooks === 'true') { + if (IntegrationStore.hasReceivedOutgoingWebhooks()) { + this.setState({ + outgoingWebhooks: IntegrationStore.getOutgoingWebhooks() + }); + } else { + AsyncClient.listOutgoingHooks(); + } + } + } + + componentWillUnmount() { + IntegrationStore.removeChangeListener(this.handleIntegrationChange); + } + + handleIntegrationChange() { + this.setState({ + incomingWebhooks: IntegrationStore.getIncomingWebhooks(), + outgoingWebhooks: IntegrationStore.getOutgoingWebhooks() + }); + } + + updateTypeFilter(e, typeFilter) { + e.preventDefault(); + + this.setState({ + typeFilter + }); + } + + updateFilter(e) { + this.setState({ + filter: e.target.value + }); + } + + deleteIncomingWebhook(incomingWebhook) { + AsyncClient.deleteIncomingHook(incomingWebhook.id); + } + + regenOutgoingWebhookToken(outgoingWebhook) { + AsyncClient.regenOutgoingHookToken(outgoingWebhook.id); + } + + deleteOutgoingWebhook(outgoingWebhook) { + AsyncClient.deleteOutgoingHook(outgoingWebhook.id); + } + + renderTypeFilters(incomingWebhooks, outgoingWebhooks) { + const fields = []; + + if (incomingWebhooks.length > 0 || outgoingWebhooks.length > 0) { + let filterClassName = 'type-filter'; + if (this.state.typeFilter === '') { + filterClassName += ' type-filter--selected'; + } + + fields.push( + <a + key='allFilter' + className={filterClassName} + href='#' + onClick={(e) => this.updateTypeFilter(e, '')} + > + <FormattedMessage + id='installed_integrations.allFilter' + defaultMessage='All ({count})' + values={{ + count: incomingWebhooks.length + outgoingWebhooks.length + }} + /> + </a> + ); + } + + if (incomingWebhooks.length > 0) { + fields.push( + <span + key='incomingWebhooksDivider' + className='divider' + > + {'|'} + </span> + ); + + let filterClassName = 'type-filter'; + if (this.state.typeFilter === 'incomingWebhooks') { + filterClassName += ' type-filter--selected'; + } + + fields.push( + <a + key='incomingWebhooksFilter' + className={filterClassName} + href='#' + onClick={(e) => this.updateTypeFilter(e, 'incomingWebhooks')} + > + <FormattedMessage + id='installed_integrations.incomingWebhooksFilter' + defaultMessage='Incoming Webhooks ({count})' + values={{ + count: incomingWebhooks.length + }} + /> + </a> + ); + } + + if (outgoingWebhooks.length > 0) { + fields.push( + <span + key='outgoingWebhooksDivider' + className='divider' + > + {'|'} + </span> + ); + + let filterClassName = 'type-filter'; + if (this.state.typeFilter === 'outgoingWebhooks') { + filterClassName += ' type-filter--selected'; + } + + fields.push( + <a + key='outgoingWebhooksFilter' + className={filterClassName} + href='#' + onClick={(e) => this.updateTypeFilter(e, 'outgoingWebhooks')} + > + <FormattedMessage + id='installed_integrations.outgoingWebhooksFilter' + defaultMessage='Outgoing Webhooks ({count})' + values={{ + count: outgoingWebhooks.length + }} + /> + </a> + ); + } + + return ( + <div className='type-filters'> + {fields} + </div> + ); + } + + render() { + const incomingWebhooks = this.state.incomingWebhooks; + const outgoingWebhooks = this.state.outgoingWebhooks; + + const filter = this.state.filter.toLowerCase(); + + const integrations = []; + if (!this.state.typeFilter || this.state.typeFilter === 'incomingWebhooks') { + for (const incomingWebhook of incomingWebhooks) { + if (filter) { + const channel = ChannelStore.get(incomingWebhook.channel_id); + + if (!channel || channel.name.toLowerCase().indexOf(filter) === -1) { + continue; + } + } + + integrations.push( + <InstalledIncomingWebhook + key={incomingWebhook.id} + incomingWebhook={incomingWebhook} + onDeleteClick={this.deleteIncomingWebhook} + /> + ); + } + } + + if (!this.state.typeFilter || this.state.typeFilter === 'outgoingWebhooks') { + for (const outgoingWebhook of outgoingWebhooks) { + if (filter) { + const channel = ChannelStore.get(outgoingWebhook.channel_id); + + if (!channel || channel.name.toLowerCase().indexOf(filter) === -1) { + continue; + } + } + + integrations.push( + <InstalledOutgoingWebhook + key={outgoingWebhook.id} + outgoingWebhook={outgoingWebhook} + onRegenToken={this.regenOutgoingWebhookToken} + onDelete={this.deleteOutgoingWebhook} + /> + ); + } + } + + return ( + <div className='backstage row'> + <div className='installed-integrations'> + <div className='backstage__header'> + <h1 className='text'> + <FormattedMessage + id='installed_integrations.header' + defaultMessage='Installed Integrations' + /> + </h1> + <Link + className='add-integrations-link' + to={'/settings/integrations/add'} + > + <button + type='button' + className='btn btn-primary' + > + <span> + <FormattedMessage + id='installed_integrations.add' + defaultMessage='Add Integration' + /> + </span> + </button> + </Link> + </div> + <div className='installed-integrations__filters'> + {this.renderTypeFilters(this.state.incomingWebhooks, this.state.outgoingWebhooks)} + <input + type='search' + placeholder={Utils.localizeMessage('installed_integrations.search', 'Search Integrations')} + value={this.state.filter} + onChange={this.updateFilter} + style={{flexGrow: 0, flexShrink: 0}} + /> + </div> + <div className='installed-integrations__list'> + {integrations} + </div> + </div> + </div> + ); + } +} diff --git a/webapp/components/backstage/installed_outgoing_webhook.jsx b/webapp/components/backstage/installed_outgoing_webhook.jsx new file mode 100644 index 000000000..12e1a5c81 --- /dev/null +++ b/webapp/components/backstage/installed_outgoing_webhook.jsx @@ -0,0 +1,91 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import ChannelStore from 'stores/channel_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +import {FormattedMessage} from 'react-intl'; + +export default class InstalledOutgoingWebhook extends React.Component { + static get propTypes() { + return { + outgoingWebhook: React.PropTypes.object.isRequired, + onRegenToken: React.PropTypes.func.isRequired, + onDelete: React.PropTypes.func.isRequired + }; + } + + constructor(props) { + super(props); + + this.handleRegenToken = this.handleRegenToken.bind(this); + this.handleDelete = this.handleDelete.bind(this); + } + + handleRegenToken(e) { + e.preventDefault(); + + this.props.onRegenToken(this.props.outgoingWebhook); + } + + handleDelete(e) { + e.preventDefault(); + + this.props.onDelete(this.props.outgoingWebhook); + } + + render() { + const outgoingWebhook = this.props.outgoingWebhook; + + const channel = ChannelStore.get(outgoingWebhook.channel_id); + const channelName = channel ? channel.display_name : 'cannot find channel'; + + return ( + <div className='installed-integrations__item installed-integrations__outgoing-webhook'> + <div className='details'> + <div className='details-row'> + <span className='name'> + {channelName} + </span> + <span className='type'> + <FormattedMessage + id='installed_integrations.outgoingWebhookType' + defaultMessage='(Outgoing Webhook)' + /> + </span> + </div> + <div className='details-row'> + <span className='description'> + {Utils.getWindowLocationOrigin() + '/hooks/' + outgoingWebhook.id} + {' - '} + {outgoingWebhook.token} + </span> + </div> + </div> + <div className='actions'> + <a + href='#' + onClick={this.handleRegenToken} + > + <FormattedMessage + id='installed_integrations.regenToken' + defaultMessage='Regen Token' + /> + </a> + {' - '} + <a + href='#' + onClick={this.handleDelete} + > + <FormattedMessage + id='installed_integrations.delete' + defaultMessage='Delete' + /> + </a> + </div> + </div> + ); + } +} diff --git a/webapp/components/channel_invite_button.jsx b/webapp/components/channel_invite_button.jsx index e4af9f9ce..1fcd461ea 100644 --- a/webapp/components/channel_invite_button.jsx +++ b/webapp/components/channel_invite_button.jsx @@ -65,6 +65,7 @@ export default class ChannelInviteButton extends React.Component { render() { return ( <SpinnerButton + className='btn btn-sm btn-primary' onClick={this.handleClick} spinning={this.state.addingUser} > diff --git a/webapp/components/channel_select.jsx b/webapp/components/channel_select.jsx new file mode 100644 index 000000000..8622d1f57 --- /dev/null +++ b/webapp/components/channel_select.jsx @@ -0,0 +1,79 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import Constants from 'utils/constants.jsx'; +import ChannelStore from 'stores/channel_store.jsx'; +import * as Utils from 'utils/utils.jsx'; + +export default class ChannelSelect extends React.Component { + static get propTypes() { + return { + onChange: React.PropTypes.func, + value: React.PropTypes.string + }; + } + + constructor(props) { + super(props); + + this.handleChannelChange = this.handleChannelChange.bind(this); + + this.state = { + channels: [] + }; + } + + componentWillMount() { + this.setState({ + channels: ChannelStore.getAll() + }); + + ChannelStore.addChangeListener(this.handleChannelChange); + } + + componentWillUnmount() { + ChannelStore.removeChangeListener(this.handleChannelChange); + } + + handleChannelChange() { + this.setState({ + channels: ChannelStore.getAll() + }); + } + + render() { + const options = [ + <option + key='' + value='' + > + {Utils.localizeMessage('channel_select.placeholder', '--- Select a channel ---')} + </option> + ]; + + this.state.channels.forEach((channel) => { + if (channel.type !== Constants.DM_CHANNEL) { + options.push( + <option + key={channel.id} + value={channel.id} + > + {channel.display_name} + </option> + ); + } + }); + + return ( + <select + className='form-control' + value={this.props.value} + onChange={this.props.onChange} + > + {options} + </select> + ); + } +} diff --git a/webapp/components/form_error.jsx b/webapp/components/form_error.jsx new file mode 100644 index 000000000..b7d1de16a --- /dev/null +++ b/webapp/components/form_error.jsx @@ -0,0 +1,50 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +export default class FormError extends React.Component { + static get propTypes() { + // accepts either a single error or an array of errors + return { + error: React.PropTypes.node, + errors: React.PropTypes.arrayOf(React.PropTypes.node) + }; + } + + static get defaultProps() { + return { + error: null, + errors: [] + }; + } + + render() { + if (!this.props.error && this.props.errors.length === 0) { + return null; + } + + // look for the first truthy error to display + let message = this.props.error; + + if (!message) { + for (const error of this.props.errors) { + if (error) { + message = error; + } + } + } + + if (!message) { + return null; + } + + return ( + <div className='form-group has-error'> + <label className='control-label'> + {message} + </label> + </div> + ); + } +} diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx index 53db501bf..fd09aac9e 100644 --- a/webapp/components/logged_in.jsx +++ b/webapp/components/logged_in.jsx @@ -200,6 +200,9 @@ export default class LoggedIn extends React.Component { content = this.props.children; } else { content.push( + this.props.navbar + ); + content.push( this.props.sidebar ); content.push( @@ -247,8 +250,9 @@ LoggedIn.defaultProps = { }; LoggedIn.propTypes = { - children: React.PropTypes.object, - sidebar: React.PropTypes.object, - center: React.PropTypes.object, + children: React.PropTypes.arrayOf(React.PropTypes.element), + navbar: React.PropTypes.element, + sidebar: React.PropTypes.element, + center: React.PropTypes.element, params: React.PropTypes.object }; diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx index feab8c9db..29d64517e 100644 --- a/webapp/components/more_direct_channels.jsx +++ b/webapp/components/more_direct_channels.jsx @@ -86,6 +86,7 @@ export default class MoreDirectChannels extends React.Component { createJoinDirectChannelButton({user}) { return ( <SpinnerButton + className='btn btm-sm btn-primary' spinning={this.state.loadingDMChannel === user.id} onClick={this.handleShowDirectChannel.bind(this, user)} > diff --git a/webapp/components/navbar_dropdown.jsx b/webapp/components/navbar_dropdown.jsx index 7e42a71ea..da1ae237e 100644 --- a/webapp/components/navbar_dropdown.jsx +++ b/webapp/components/navbar_dropdown.jsx @@ -59,6 +59,7 @@ export default class NavbarDropdown extends React.Component { var isAdmin = false; var isSystemAdmin = false; var teamSettings = null; + let integrationsLink = null; if (currentUser != null) { isAdmin = Utils.isAdmin(currentUser.roles); @@ -125,6 +126,21 @@ export default class NavbarDropdown extends React.Component { ); } + if (window.mm_config.EnableIncomingWebhooks === 'true' || window.mm_config.EnableOutgoingWebhooks === 'true') { + if (isAdmin || window.EnableAdminOnlyIntegrations !== 'true') { + integrationsLink = ( + <li> + <Link to={'/settings/integrations'}> + <FormattedMessage + id='navbar_dropdown.integrations' + defaultMessage='Integrations' + /> + </Link> + </li> + ); + } + } + if (isSystemAdmin) { sysAdminLink = ( <li> @@ -238,6 +254,7 @@ export default class NavbarDropdown extends React.Component { </li> {adminDivider} {teamSettings} + {integrationsLink} {manageLink} {sysAdminLink} {teams} diff --git a/webapp/components/spinner_button.jsx b/webapp/components/spinner_button.jsx index fcc9af8cd..becf395c5 100644 --- a/webapp/components/spinner_button.jsx +++ b/webapp/components/spinner_button.jsx @@ -14,20 +14,10 @@ export default class SpinnerButton extends React.Component { }; } - constructor(props) { - super(props); - - this.handleClick = this.handleClick.bind(this); - } - - handleClick(e) { - if (this.props.onClick) { - this.props.onClick(e); - } - } - render() { - if (this.props.spinning) { + const {spinning, children, ...props} = this.props; // eslint-disable-line no-use-before-define + + if (spinning) { return ( <img className='spinner-button__gif' @@ -38,10 +28,10 @@ export default class SpinnerButton extends React.Component { return ( <button - onClick={this.handleClick} - className='btn btn-sm btn-primary' + className='btn btn-primary' + {...props} > - {this.props.children} + {children} </button> ); } diff --git a/webapp/components/user_settings/manage_incoming_hooks.jsx b/webapp/components/user_settings/manage_incoming_hooks.jsx deleted file mode 100644 index b61b331ce..000000000 --- a/webapp/components/user_settings/manage_incoming_hooks.jsx +++ /dev/null @@ -1,225 +0,0 @@ -// 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 Constants from 'utils/constants.jsx'; -import ChannelStore from 'stores/channel_store.jsx'; -import LoadingScreen from '../loading_screen.jsx'; - -import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; - -import React from 'react'; - -export default class ManageIncomingHooks extends React.Component { - constructor() { - super(); - - this.getHooks = this.getHooks.bind(this); - this.addNewHook = this.addNewHook.bind(this); - this.updateChannelId = this.updateChannelId.bind(this); - - this.state = {hooks: [], channelId: ChannelStore.getByName(Constants.DEFAULT_CHANNEL).id, getHooksComplete: false}; - } - componentDidMount() { - this.getHooks(); - } - addNewHook() { - const hook = {}; - hook.channel_id = this.state.channelId; - - Client.addIncomingHook( - hook, - (data) => { - let hooks = this.state.hooks; - if (!hooks) { - hooks = []; - } - hooks.push(data); - this.setState({hooks}); - }, - (err) => { - this.setState({serverError: err}); - } - ); - } - removeHook(id) { - const data = {}; - data.id = id; - - Client.deleteIncomingHook( - data, - () => { - const hooks = this.state.hooks; - let index = -1; - for (let i = 0; i < hooks.length; i++) { - if (hooks[i].id === id) { - index = i; - break; - } - } - - if (index !== -1) { - hooks.splice(index, 1); - } - - this.setState({hooks}); - }, - (err) => { - this.setState({serverError: err}); - } - ); - } - getHooks() { - Client.listIncomingHooks( - (data) => { - const state = this.state; - - if (data) { - state.hooks = data; - } - - state.getHooksComplete = true; - this.setState(state); - }, - (err) => { - this.setState({serverError: err}); - } - ); - } - updateChannelId(e) { - this.setState({channelId: e.target.value}); - } - render() { - let serverError; - if (this.state.serverError) { - serverError = <label className='has-error'>{this.state.serverError}</label>; - } - - const channels = ChannelStore.getAll(); - const options = []; - channels.forEach((channel) => { - if (channel.type !== Constants.DM_CHANNEL) { - options.push( - <option - key={'incoming-hook' + channel.id} - value={channel.id} - > - {channel.display_name} - </option> - ); - } - }); - - let disableButton = ''; - if (this.state.channelId === '') { - disableButton = ' disable'; - } - - const hooks = []; - this.state.hooks.forEach((hook) => { - const c = ChannelStore.get(hook.channel_id); - if (c) { - hooks.push( - <div - key={hook.id} - className='webhook__item' - > - <div className='padding-top x2 webhook__url'> - <strong>{'URL: '}</strong> - <span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span> - </div> - <div className='padding-top'> - <strong> - <FormattedMessage - id='user.settings.hooks_in.channel' - defaultMessage='Channel: ' - /> - </strong>{c.display_name} - </div> - <a - className={'webhook__remove'} - href='#' - onClick={this.removeHook.bind(this, hook.id)} - > - <span aria-hidden='true'>{'×'}</span> - </a> - <div className='padding-top x2 divider-light'></div> - </div> - ); - } - }); - - let displayHooks; - if (!this.state.getHooksComplete) { - displayHooks = <LoadingScreen/>; - } else if (hooks.length > 0) { - displayHooks = hooks; - } else { - displayHooks = ( - <div className='padding-top x2'> - <FormattedMessage - id='user.settings.hooks_in.none' - defaultMessage='None' - /> - </div> - ); - } - - const existingHooks = ( - <div className='webhooks__container'> - <label className='control-label padding-top x2'> - <FormattedMessage - id='user.settings.hooks_in.existing' - defaultMessage='Existing incoming webhooks' - /> - </label> - <div className='padding-top divider-light'></div> - <div className='webhooks__list'> - {displayHooks} - </div> - </div> - ); - - return ( - <div key='addIncomingHook'> - <FormattedHTMLMessage - id='user.settings.hooks_in.description' - defaultMessage='Create webhook URLs for use in external integrations. Please see <a href="http://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">incoming webhooks documentation</a> to learn more. View all incoming webhooks configured on this team below.' - /> - <div><label className='control-label padding-top x2'> - <FormattedMessage - id='user.settings.hooks_in.addTitle' - defaultMessage='Add a new incoming webhook' - /> - </label></div> - <div className='row padding-top'> - <div className='col-sm-10 padding-bottom'> - <select - ref='channelName' - className='form-control' - value={this.state.channelId} - onChange={this.updateChannelId} - > - {options} - </select> - {serverError} - </div> - <div className='col-sm-2 col-xs-4 no-padding--left padding-bottom'> - <a - className={'btn form-control no-padding btn-sm btn-primary' + disableButton} - href='#' - onClick={this.addNewHook} - > - <FormattedMessage - id='user.settings.hooks_in.add' - defaultMessage='Add' - /> - </a> - </div> - </div> - {existingHooks} - </div> - ); - } -} diff --git a/webapp/components/user_settings/manage_outgoing_hooks.jsx b/webapp/components/user_settings/manage_outgoing_hooks.jsx deleted file mode 100644 index 8adec09ce..000000000 --- a/webapp/components/user_settings/manage_outgoing_hooks.jsx +++ /dev/null @@ -1,397 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import LoadingScreen from '../loading_screen.jsx'; - -import ChannelStore from 'stores/channel_store.jsx'; - -import * as Client from 'utils/client.jsx'; -import Constants from 'utils/constants.jsx'; - -import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl'; - -const holders = defineMessages({ - optional: { - id: 'user.settings.hooks_out.optional', - defaultMessage: 'Optional if channel selected' - }, - callbackHolder: { - id: 'user.settings.hooks_out.callbackHolder', - defaultMessage: 'Each URL must start with http:// or https://' - }, - select: { - id: 'user.settings.hooks_out.select', - defaultMessage: '--- Select a channel ---' - } -}); - -import React from 'react'; - -class ManageOutgoingHooks extends React.Component { - constructor() { - super(); - - this.getHooks = this.getHooks.bind(this); - this.addNewHook = this.addNewHook.bind(this); - this.updateChannelId = this.updateChannelId.bind(this); - this.updateTriggerWords = this.updateTriggerWords.bind(this); - this.updateCallbackURLs = this.updateCallbackURLs.bind(this); - - this.state = {hooks: [], channelId: '', triggerWords: '', callbackURLs: '', getHooksComplete: false}; - } - componentDidMount() { - this.getHooks(); - } - addNewHook(e) { - e.preventDefault(); - - if ((this.state.channelId === '' && this.state.triggerWords === '') || - this.state.callbackURLs === '') { - return; - } - - const hook = {}; - hook.channel_id = this.state.channelId; - if (this.state.triggerWords.length !== 0) { - hook.trigger_words = this.state.triggerWords.trim().split(','); - } - hook.callback_urls = this.state.callbackURLs.split('\n').map((url) => url.trim()); - - Client.addOutgoingHook( - hook, - (data) => { - let hooks = Object.assign([], this.state.hooks); - if (!hooks) { - hooks = []; - } - hooks.push(data); - this.setState({hooks, addError: null, channelId: '', triggerWords: '', callbackURLs: ''}); - }, - (err) => { - this.setState({addError: err.message}); - } - ); - } - removeHook(id) { - const data = {}; - data.id = id; - - Client.deleteOutgoingHook( - data, - () => { - const hooks = this.state.hooks; - let index = -1; - for (let i = 0; i < hooks.length; i++) { - if (hooks[i].id === id) { - index = i; - break; - } - } - - if (index !== -1) { - hooks.splice(index, 1); - } - - this.setState({hooks}); - }, - (err) => { - this.setState({editError: err.message}); - } - ); - } - regenToken(id) { - const regenData = {}; - regenData.id = id; - - Client.regenOutgoingHookToken( - regenData, - (data) => { - const hooks = Object.assign([], this.state.hooks); - for (let i = 0; i < hooks.length; i++) { - if (hooks[i].id === id) { - hooks[i] = data; - break; - } - } - - this.setState({hooks, editError: null}); - }, - (err) => { - this.setState({editError: err.message}); - } - ); - } - getHooks() { - Client.listOutgoingHooks( - (data) => { - if (data) { - this.setState({hooks: data, getHooksComplete: true, editError: null}); - } - }, - (err) => { - this.setState({editError: err.message}); - } - ); - } - updateChannelId(e) { - this.setState({channelId: e.target.value}); - } - updateTriggerWords(e) { - this.setState({triggerWords: e.target.value}); - } - updateCallbackURLs(e) { - this.setState({callbackURLs: e.target.value}); - } - render() { - let addError; - if (this.state.addError) { - addError = <label className='has-error'>{this.state.addError}</label>; - } - let editError; - if (this.state.editError) { - addError = <label className='has-error'>{this.state.editError}</label>; - } - - const channels = ChannelStore.getAll(); - const options = []; - options.push( - <option - key='select-channel' - value='' - > - {this.props.intl.formatMessage(holders.select)} - </option> - ); - - channels.forEach((channel) => { - if (channel.type === Constants.OPEN_CHANNEL) { - options.push( - <option - key={'outgoing-hook' + channel.id} - value={channel.id} - > - {channel.display_name} - </option> - ); - } - }); - - const hooks = []; - this.state.hooks.forEach((hook) => { - const c = ChannelStore.get(hook.channel_id); - - if (!c && hook.channel_id && hook.channel_id.length !== 0) { - return; - } - - let channelDiv; - if (c) { - channelDiv = ( - <div className='padding-top'> - <strong> - <FormattedMessage - id='user.settings.hooks_out.channel' - defaultMessage='Channel: ' - /> - </strong>{c.display_name} - </div> - ); - } - - let triggerDiv; - if (hook.trigger_words && hook.trigger_words.length !== 0) { - triggerDiv = ( - <div className='padding-top'> - <strong> - <FormattedMessage - id='user.settings.hooks_out.trigger' - defaultMessage='Trigger Words: ' - /> - </strong>{hook.trigger_words.join(', ')} - </div> - ); - } - - hooks.push( - <div - key={hook.id} - className='webhook__item' - > - <div className='padding-top x2 webhook__url'> - <strong>{'URLs: '}</strong><span className='word-break--all'>{hook.callback_urls.join(', ')}</span> - </div> - {channelDiv} - {triggerDiv} - <div className='padding-top'> - <strong>{'Token: '}</strong>{hook.token} - </div> - <div className='padding-top'> - <a - className='text-danger' - href='#' - onClick={this.regenToken.bind(this, hook.id)} - > - <FormattedMessage - id='user.settings.hooks_out.regen' - defaultMessage='Regen Token' - /> - </a> - <a - className='webhook__remove' - href='#' - onClick={this.removeHook.bind(this, hook.id)} - > - <span aria-hidden='true'>{'×'}</span> - </a> - </div> - <div className='padding-top x2 divider-light'></div> - </div> - ); - }); - - let displayHooks; - if (!this.state.getHooksComplete) { - displayHooks = <LoadingScreen/>; - } else if (hooks.length > 0) { - displayHooks = hooks; - } else { - displayHooks = ( - <div className='padding-top x2'> - <FormattedMessage - id='user.settings.hooks_out.none' - defaultMessage='None' - /> - </div> - ); - } - - const existingHooks = ( - <div className='webhooks__container'> - <label className='control-label padding-top x2'> - <FormattedMessage - id='user.settings.hooks_out.existing' - defaultMessage='Existing outgoing webhooks' - /> - </label> - <div className='padding-top divider-light'></div> - <div className='webhooks__list'> - {displayHooks} - </div> - </div> - ); - - const disableButton = (this.state.channelId === '' && this.state.triggerWords === '') || this.state.callbackURLs === ''; - - return ( - <div key='addOutgoingHook'> - <FormattedHTMLMessage - id='user.settings.hooks_out.addDescription' - defaultMessage='Create webhooks to send new message events to an external integration. Please see <a href="http://docs.mattermost.com/developer/webhooks-outgoing.html" target="_blank">outgoing webhooks documentation</a> to learn more. View all outgoing webhooks configured on this team below.' - /> - <div><label className='control-label padding-top x2'> - <FormattedMessage - id='user.settings.hooks_out.addTitle' - defaultMessage='Add a new outgoing webhook' - /> - </label></div> - <div className='padding-top divider-light'></div> - <div className='padding-top'> - <div> - <label className='control-label'> - <FormattedMessage - id='user.settings.hooks_out.channel' - defaultMessage='Channel: ' - /> - </label> - <div className='padding-top'> - <select - ref='channelName' - className='form-control' - value={this.state.channelId} - onChange={this.updateChannelId} - > - {options} - </select> - </div> - <div className='padding-top'> - <FormattedMessage - id='user.settings.hooks_out.only' - defaultMessage='Only public channels can be used' - /> - </div> - </div> - <div className='padding-top x2'> - <label className='control-label'> - <FormattedMessage - id='user.settings.hooks_out.trigger' - defaultMessage='Trigger Words: ' - /> - </label> - <div className='padding-top'> - <input - ref='triggerWords' - className='form-control' - value={this.state.triggerWords} - onChange={this.updateTriggerWords} - placeholder={this.props.intl.formatMessage(holders.optional)} - /> - </div> - <div className='padding-top'> - <FormattedMessage - id='user.settings.hooks_out.comma' - defaultMessage='Comma separated words to trigger on' - /> - </div> - </div> - <div className='padding-top x2'> - <label className='control-label'> - <FormattedMessage - id='user.settings.hooks_out.callback' - defaultMessage='Callback URLs: ' - /> - </label> - <div className='padding-top'> - <textarea - ref='callbackURLs' - className='form-control no-resize' - value={this.state.callbackURLs} - resize={false} - rows={3} - onChange={this.updateCallbackURLs} - placeholder={this.props.intl.formatMessage(holders.callbackHolder)} - /> - </div> - <div className='padding-top'> - <FormattedMessage - id='user.settings.hooks_out.callbackDesc' - defaultMessage='New line separated URLs that will receive the HTTP POST event' - /> - </div> - {addError} - </div> - <div className='padding-top padding-bottom'> - <a - className={'btn btn-sm btn-primary'} - href='#' - disabled={disableButton} - onClick={this.addNewHook} - > - <FormattedMessage - id='user.settings.hooks_out.add' - defaultMessage='Add' - /> - </a> - </div> - </div> - {existingHooks} - {editError} - </div> - ); - } -} - -ManageOutgoingHooks.propTypes = { - intl: intlShape.isRequired -}; - -export default injectIntl(ManageOutgoingHooks); diff --git a/webapp/components/user_settings/user_settings_integrations.jsx b/webapp/components/user_settings/user_settings_integrations.jsx index 9061e34df..37081b863 100644 --- a/webapp/components/user_settings/user_settings_integrations.jsx +++ b/webapp/components/user_settings/user_settings_integrations.jsx @@ -4,29 +4,11 @@ import $ from 'jquery'; import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; -import ManageIncomingHooks from './manage_incoming_hooks.jsx'; -import ManageOutgoingHooks from './manage_outgoing_hooks.jsx'; import ManageCommandHooks from './manage_command_hooks.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; const holders = defineMessages({ - inName: { - id: 'user.settings.integrations.incomingWebhooks', - defaultMessage: 'Incoming Webhooks' - }, - inDesc: { - id: 'user.settings.integrations.incomingWebhooksDescription', - defaultMessage: 'Manage your incoming webhooks' - }, - outName: { - id: 'user.settings.integrations.outWebhooks', - defaultMessage: 'Outgoing Webhooks' - }, - outDesc: { - id: 'user.settings.integrations.outWebhooksDescription', - defaultMessage: 'Manage your outgoing webhooks' - }, cmdName: { id: 'user.settings.integrations.commands', defaultMessage: 'Slash Commands' @@ -52,74 +34,10 @@ class UserSettingsIntegrationsTab extends React.Component { this.props.updateSection(section); } render() { - let incomingHooksSection; - let outgoingHooksSection; let commandHooksSection; var inputs = []; const {formatMessage} = this.props.intl; - if (global.window.mm_config.EnableIncomingWebhooks === 'true') { - if (this.props.activeSection === 'incoming-hooks') { - inputs.push( - <ManageIncomingHooks key='incoming-hook-ui'/> - ); - - incomingHooksSection = ( - <SettingItemMax - title={formatMessage(holders.inName)} - width='medium' - inputs={inputs} - updateSection={(e) => { - this.updateSection(''); - e.preventDefault(); - }} - /> - ); - } else { - incomingHooksSection = ( - <SettingItemMin - title={formatMessage(holders.inName)} - width='medium' - describe={formatMessage(holders.inDesc)} - updateSection={() => { - this.updateSection('incoming-hooks'); - }} - /> - ); - } - } - - if (global.window.mm_config.EnableOutgoingWebhooks === 'true') { - if (this.props.activeSection === 'outgoing-hooks') { - inputs.push( - <ManageOutgoingHooks key='outgoing-hook-ui'/> - ); - - outgoingHooksSection = ( - <SettingItemMax - title={formatMessage(holders.outName)} - width='medium' - inputs={inputs} - updateSection={(e) => { - this.updateSection(''); - e.preventDefault(); - }} - /> - ); - } else { - outgoingHooksSection = ( - <SettingItemMin - title={formatMessage(holders.outName)} - width='medium' - describe={formatMessage(holders.outDesc)} - updateSection={() => { - this.updateSection('outgoing-hooks'); - }} - /> - ); - } - } - if (global.window.mm_config.EnableCommands === 'true') { if (this.props.activeSection === 'command-hooks') { inputs.push( @@ -187,10 +105,6 @@ class UserSettingsIntegrationsTab extends React.Component { /> </h3> <div className='divider-dark first'/> - {incomingHooksSection} - <div className='divider-light'/> - {outgoingHooksSection} - <div className='divider-dark'/> {commandHooksSection} <div className='divider-dark'/> </div> @@ -209,4 +123,4 @@ UserSettingsIntegrationsTab.propTypes = { collapseModal: React.PropTypes.func.isRequired }; -export default injectIntl(UserSettingsIntegrationsTab);
\ No newline at end of file +export default injectIntl(UserSettingsIntegrationsTab); |