diff options
author | Christopher Speller <crspeller@gmail.com> | 2016-03-30 10:05:26 -0400 |
---|---|---|
committer | Christopher Speller <crspeller@gmail.com> | 2016-03-30 10:05:26 -0400 |
commit | 2ab4581d5e658b22c4a957ec57bb3530f92ad66b (patch) | |
tree | 4202ebdcbc92905873a15d90a6fb68464bb1629f | |
parent | fcc80818a8afb6f1e2f9974916f02d5fdeb72ec8 (diff) | |
parent | 6a101292c74d33e542e47f8e54fff5a5389bf2ef (diff) | |
download | chat-2ab4581d5e658b22c4a957ec57bb3530f92ad66b.tar.gz chat-2ab4581d5e658b22c4a957ec57bb3530f92ad66b.tar.bz2 chat-2ab4581d5e658b22c4a957ec57bb3530f92ad66b.zip |
Merge pull request #2561 from hmhealey/plt1736
PLT-1736 Initial Backstage Work
33 files changed, 2026 insertions, 836 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); diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 11dfdf1ed..40e486434 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -22,6 +22,28 @@ "activity_log_modal.android": "Android", "activity_log_modal.androidNativeApp": "Android Native App", "activity_log_modal.iphoneNativeApp": "iPhone Native App", + "add_incoming_webhook.cancel": "Cancel", + "add_incoming_webhook.channel": "Channel", + "add_incoming_webhook.channelRequired": "A valid channel is required", + "add_incoming_webhook.description": "Description", + "add_incoming_webhook.header": "Add Incoming Webhook", + "add_incoming_webhook.name": "Name", + "add_incoming_webhook.save": "Save", + "add_integration.header": "Add Integration", + "add_integration.incomingWebhook.title": "Incoming Webhook", + "add_integration.incomingWebhook.description": "Create webhook URLs for use in external integrations.", + "add_integration.outgoingWebhook.title": "Outgoing Webhook", + "add_integration.outgoingWebhook.description": "Create webhooks to send new message events to an external integration.", + "add_outgoing_webhook.callbackUrls": "Callback URLs (One Per Line)", + "add_outgoing_webhook.callbackUrlsRequired": "One or more callback URLs are required", + "add_outgoing_webhook.cancel": "Cancel", + "add_outgoing_webhook.channel": "Channel", + "add_outgoing_webhook.description": "Description", + "add_outgoing_webhook.header": "Add Outgoing Webhook", + "add_outgoing_webhook.name": "Name", + "add_outgoing_webhook.save": "Save", + "add_outgoing_webhook.triggerWOrds": "Trigger Words (One Per Line)", + "add_outgoing_webhook.triggerWordsOrChannelRequired": "A valid channel or a list of trigger words is required", "admin.audits.reload": "Reload", "admin.audits.title": "User Activity", "admin.compliance.directoryDescription": "Directory to which compliance reports are written. If blank, will be set to ./data/.", @@ -550,6 +572,12 @@ "authorize.app": "The app <strong>{appName}</strong> would like the ability to access and modify your basic information.", "authorize.deny": "Deny", "authorize.title": "An application would like to connect to your {teamName} account", + "backstage_navbar.backToMattermost": "Back to {siteName}", + "backstage_sidebar.integrations": "Integrations", + "backstage_sidebar.integrations.installed": "Installed Integrations", + "backstage_sidebar.integrations.add": "Add Integration", + "backstage_sidebar.integrations.add.incomingWebhook": "Incoming Webhook", + "backstage_sidebar.integrations.add.outgoingWebhook": "Outgoing Webhook", "center_panel.recent": "Click here to jump to recent messages. ", "chanel_header.addMembers": "Add Members", "change_url.close": "Close", @@ -626,6 +654,7 @@ "channel_notifications.preferences": "Notification Preferences for ", "channel_notifications.sendDesktop": "Send desktop notifications", "channel_notifications.unreadInfo": "The channel name is bolded in the sidebar when there are unread messages. Selecting \"Only for mentions\" will bold the channel only when you are mentioned.", + "channel_select.placeholder": "--- Select a channel ---", "choose_auth_page.emailCreate": "Create new team with email address", "choose_auth_page.find": "Find my teams", "choose_auth_page.gitlabCreate": "Create new team with GitLab Account", @@ -768,6 +797,16 @@ "get_team_invite_link_modal.help": "Send teammates the link below for them to sign-up to this team site. The Team Invite Link can be shared with multiple teammates as it does not change unless it's regenerated in Team Settings by a Team Admin.", "get_team_invite_link_modal.helpDisabled": "User creation has been disabled for your team. Please ask your team administrator for details.", "get_team_invite_link_modal.title": "Team Invite Link", + "installed_integrations.add": "Add Integration", + "installed_integrations.allFilter": "All", + "installed_integrations.delete": "Delete", + "installed_integrations.header": "Installed Integrations", + "installed_integrations.incomingWebhooksFilter": "Incoming Webhooks ({count})", + "installed_integrations.incomingWebhookType": "(Incoming Webhook)", + "installed_integrations.outgoingWebhooksFilter": "Outgoing Webhooks ({count})", + "installed_integrations.outgoingWebhookType": "(Outgoing Webhook)", + "installed_integrations.regenToken": "Regen Token", + "installed_integrations.search": "Search Integrations", "intro_messages.DM": "This is the start of your direct message history with {teammate}.<br />Direct messages and files shared here are not shown to people outside this area.", "intro_messages.anyMember": " Any member can join and read this channel.", "intro_messages.beginning": "Beginning of {name}", @@ -873,6 +912,7 @@ "navbar_dropdown.console": "System Console", "navbar_dropdown.create": "Create a New Team", "navbar_dropdown.help": "Help", + "navbar_dropdown.integrations": "Integrations", "navbar_dropdown.inviteMember": "Invite New Member", "navbar_dropdown.logout": "Logout", "navbar_dropdown.manageMembers": "Manage Members", @@ -1274,27 +1314,6 @@ "user.settings.general.usernameRestrictions": "Username must begin with a letter, and contain between {min} to {max} lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'.", "user.settings.general.validEmail": "Please enter a valid email address", "user.settings.general.validImage": "Only JPG or PNG images may be used for profile pictures", - "user.settings.hooks_in.add": "Add", - "user.settings.hooks_in.addTitle": "Add a new incoming webhook", - "user.settings.hooks_in.channel": "Channel: ", - "user.settings.hooks_in.description": "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.", - "user.settings.hooks_in.existing": "Existing incoming webhooks", - "user.settings.hooks_in.none": "None", - "user.settings.hooks_out.add": "Add", - "user.settings.hooks_out.addDescription": "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.", - "user.settings.hooks_out.addTitle": "Add a new outgoing webhook", - "user.settings.hooks_out.callback": "Callback URLs: ", - "user.settings.hooks_out.callbackDesc": "New line separated URLs that will receive the HTTP POST event", - "user.settings.hooks_out.callbackHolder": "Each URL must start with http:// or https://", - "user.settings.hooks_out.channel": "Channel: ", - "user.settings.hooks_out.comma": "Comma separated words to trigger on", - "user.settings.hooks_out.existing": "Existing outgoing webhooks", - "user.settings.hooks_out.none": "None", - "user.settings.hooks_out.only": "Only public channels can be used", - "user.settings.hooks_out.optional": "Optional if channel selected", - "user.settings.hooks_out.regen": "Regen Token", - "user.settings.hooks_out.select": "--- Select a channel ---", - "user.settings.hooks_out.trigger": "Trigger Words: ", "user.settings.import_theme.cancel": "Cancel", "user.settings.import_theme.importBody": "To import a theme, go to a Slack team and look for “Preferences -> Sidebar Theme”. Open the custom theme option, copy the theme color values and paste them here:", "user.settings.import_theme.importHeader": "Import Slack Theme", @@ -1302,10 +1321,6 @@ "user.settings.import_theme.submitError": "Invalid format, please try copying and pasting in again.", "user.settings.integrations.commands": "Slash Commands", "user.settings.integrations.commandsDescription": "Manage your slash commands", - "user.settings.integrations.incomingWebhooks": "Incoming Webhooks", - "user.settings.integrations.incomingWebhooksDescription": "Manage your incoming webhooks", - "user.settings.integrations.outWebhooks": "Outgoing Webhooks", - "user.settings.integrations.outWebhooksDescription": "Manage your outgoing webhooks", "user.settings.integrations.title": "Integration Settings", "user.settings.languages.change": "Change interface language", "user.settings.modal.advanced": "Advanced", diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json index c2b56e1cc..607e46483 100644 --- a/webapp/i18n/es.json +++ b/webapp/i18n/es.json @@ -1274,27 +1274,6 @@ "user.settings.general.usernameRestrictions": "El nombre de usuario debe empezar con una letra, y contener entre {min} a {max} caracteres en minúscula con números, lettras, y los símbolos '.', '-' y '_'.", "user.settings.general.validEmail": "Por favor ingresa una dirección de correo electrónico válida", "user.settings.general.validImage": "Sólo pueden ser utilizadas imágenes JPG o PNG en el perfil", - "user.settings.hooks_in.add": "Agregar", - "user.settings.hooks_in.addTitle": "Agregar un nuevo webhook de entrada", - "user.settings.hooks_in.channel": "Canal: ", - "user.settings.hooks_in.description": "Crea URLs para webhooks a utilizar con integraciones externas. Revisa la <a href=\"http://docs.mattermost.com/developer/webhooks-incoming.html\" target=\"_blank\">documentación de webhooks de entrada</a> para conocer más. Ver todos los webhooks de entrada configurados para este equipo en la parte de abajo.", - "user.settings.hooks_in.existing": "Webhooks de entrada existentes", - "user.settings.hooks_in.none": "Ninguno", - "user.settings.hooks_out.add": "Agregar", - "user.settings.hooks_out.addDescription": "Crea webhooks para enviar mensajes a ingraciones externas. Revisa la <a href=\"http://docs.mattermost.com/developer/webhooks-outgoing.html\" target=\"_blank\">documentación de webhooks de saldida</a> para conocer más. Ver todos los webhooks de salida configurados para este equipo en la parte de abajo.", - "user.settings.hooks_out.addTitle": "Agregar un nuevo webhook de salida", - "user.settings.hooks_out.callback": "Callback URLs:", - "user.settings.hooks_out.callbackDesc": "Separa por una nueva linea cada URL donde quieres recibir el evento de HTTP POST", - "user.settings.hooks_out.callbackHolder": "Cada URL debe comenzar con http:// o https://", - "user.settings.hooks_out.channel": "Canal: ", - "user.settings.hooks_out.comma": "Escribe las palabras de activación que ejecutan el evento separadas por coma", - "user.settings.hooks_out.existing": "Webhooks de salida existentes", - "user.settings.hooks_out.none": "Ninguno", - "user.settings.hooks_out.only": "Sólo se pueden utilizar Canales", - "user.settings.hooks_out.optional": "Opcional si se selecciona un canal", - "user.settings.hooks_out.regen": "Regenerar Token", - "user.settings.hooks_out.select": "--- Selecciona un canal ---", - "user.settings.hooks_out.trigger": "Palabras de activación: ", "user.settings.import_theme.cancel": "Cancelar", "user.settings.import_theme.importBody": "Para importar un tema, anda al equipo Slack y busca en [Preferences -> Sidebar Theme]. Abre las opciones del tema, copia los valores de color del tema y pégalo aquí:", "user.settings.import_theme.importHeader": "Importar Tema de Slack", @@ -1302,10 +1281,6 @@ "user.settings.import_theme.submitError": "Formato inválido, por favor intenta copiando y pegando nuevamente.", "user.settings.integrations.commands": "Comandos de Barra", "user.settings.integrations.commandsDescription": "Administra tus comandos de barra", - "user.settings.integrations.incomingWebhooks": "Webhooks de entrada", - "user.settings.integrations.incomingWebhooksDescription": "Administra tus webhooks de entrada", - "user.settings.integrations.outWebhooks": "Webhooks de salida", - "user.settings.integrations.outWebhooksDescription": "Administra tus webhooks de salida", "user.settings.integrations.title": "Configuraciones de Integración", "user.settings.languages.change": "Cambia el idioma con el que se muestra la intefaz de usuario", "user.settings.modal.advanced": "Avanzada", diff --git a/webapp/i18n/fr.json b/webapp/i18n/fr.json index 3270b8847..9d68c54ec 100644 --- a/webapp/i18n/fr.json +++ b/webapp/i18n/fr.json @@ -1274,27 +1274,6 @@ "user.settings.general.usernameRestrictions": "Les noms d'utilisateurs doivent commencer par une lettre et contenir entre {min} et {max} caractères composés de chiffres, lettres minuscules et des symboles '.', '-' et '_'", "user.settings.general.validEmail": "Veuillez entrer une adresse électronique valide", "user.settings.general.validImage": "Seules les images JPG ou PNG sont autorisées pour les photos de profil", - "user.settings.hooks_in.add": "Ajouter", - "user.settings.hooks_in.addTitle": "Ajouter un webhook entrant", - "user.settings.hooks_in.channel": "Canal\u00a0: ", - "user.settings.hooks_in.description": "Crééez des URLs de webhooks pour des intégrations externes. Veuillez consulter <a href=\"http://docs.mattermost.com/developer/webhooks-incoming.html\" target=\"_blank\">la documentation sur les webhooks entrants</a> pour en savoir plus. Examinez tous les webhooks entrants configurés pour cette équipe ci-dessous.", - "user.settings.hooks_in.existing": "Webhooks entrants", - "user.settings.hooks_in.none": "Aucun", - "user.settings.hooks_out.add": "Ajouter", - "user.settings.hooks_out.addDescription": "Crééez des webhooks pour envoyer les évènements de nouveaux messages vers des intégrations externes. Veuillez consulter <a href=\"http://docs.mattermost.com/developer/webhooks-outgoing.html\" target=\"_blank\">la documentation sur les webhooks sortants</a> pour en savoir plus. Examinez tous les webhooks sortants configurés pour cette équipe ci-dessous.", - "user.settings.hooks_out.addTitle": "Ajouter un webhook sortant", - "user.settings.hooks_out.callback": "URLs de callback :", - "user.settings.hooks_out.callbackDesc": "URLs séparés par un saut de ligne qui recevront l'événement HTTP POST", - "user.settings.hooks_out.callbackHolder": "Chaque URL doit commencer par http:// ou https://", - "user.settings.hooks_out.channel": "Canal\u00a0: ", - "user.settings.hooks_out.comma": "Liste de mots déclencheurs séparés par une virgule", - "user.settings.hooks_out.existing": "Webhooks sortants", - "user.settings.hooks_out.none": "Aucun", - "user.settings.hooks_out.only": "Seuls les canaux publics peuvent être utilisés", - "user.settings.hooks_out.optional": "Facultatif si un canal est sélectionné", - "user.settings.hooks_out.regen": "Réinitialiser le jeton", - "user.settings.hooks_out.select": "--- Choisissez un canal ---", - "user.settings.hooks_out.trigger": "Mots de déclenchement :", "user.settings.import_theme.cancel": "Annuler", "user.settings.import_theme.importBody": "Pour importer un thème, rendez-vous sur une Slack team et cliquez sur \"Preferences -> Sidebar Theme\". Ouvrez la fenêtre de personnalisation, copiez les couleurs du thèmes et collez-les ici :", "user.settings.import_theme.importHeader": "Importer un thème Slack", @@ -1302,10 +1281,6 @@ "user.settings.import_theme.submitError": "Format invalide, veuillez réessayer de copier-coller.", "user.settings.integrations.commands": "Commandes slash", "user.settings.integrations.commandsDescription": "Gérez vos commandes slash", - "user.settings.integrations.incomingWebhooks": "Webhooks entrants", - "user.settings.integrations.incomingWebhooksDescription": "Gérer les webhooks entrants", - "user.settings.integrations.outWebhooks": "Webhooks sortants", - "user.settings.integrations.outWebhooksDescription": "Gérer les webhooks sortants", "user.settings.integrations.title": "Paramètres d'intégration", "user.settings.languages.change": "Changer la langue de l'interface", "user.settings.modal.advanced": "Options avancées", @@ -1374,4 +1349,4 @@ "web.footer.terms": "Termes", "web.header.back": "Précédent", "web.root.singup_info": "Toute la communication de votre équipe à un endroit, accessible de partout" -}
\ No newline at end of file +} diff --git a/webapp/i18n/pt.json b/webapp/i18n/pt.json index b758dd0b2..c9ab089fb 100644 --- a/webapp/i18n/pt.json +++ b/webapp/i18n/pt.json @@ -1273,27 +1273,6 @@ "user.settings.general.usernameRestrictions": "O nome de usuário precisa começar com uma letra, e conter entre {min} e {max} caracteres minúsculos contendo números, letras, e os símbolos '.', '-' e '_'.", "user.settings.general.validEmail": "Por favor entre um endereço de e-mail válido", "user.settings.general.validImage": "Somente imagens em JPG ou PNG podem ser usadas como imagem do perfil", - "user.settings.hooks_in.add": "Adicionar", - "user.settings.hooks_in.addTitle": "Adicionar um novo webhook entrada", - "user.settings.hooks_in.channel": "Canal: ", - "user.settings.hooks_in.description": "Criar URLs webhook para usar em integrações externas. Por favor veja <a href=\"http://docs.mattermost.com/developer/webhooks-incoming.html\" target=\"_blank\">documentação webhook entrada</a> para saber mais. Ver todos os webhooks de entrada configurados nesta equipe abaixo.", - "user.settings.hooks_in.existing": "Webhooks de entrada existentes", - "user.settings.hooks_in.none": "Nenhum", - "user.settings.hooks_out.add": "Adicionar", - "user.settings.hooks_out.addDescription": "Criar webhooks para enviar novos mensagens de eventos para uma integração externa. Por favor veja <a href=\"http://docs.mattermost.com/developer/webhooks-outgoing.html\" target=\"_blank\">documentação webhook saída</a> para saber mais. Ver todos os webhooks de saída desta equipe configurados abaixo.", - "user.settings.hooks_out.addTitle": "Adicionar um novo webhook saída", - "user.settings.hooks_out.callback": "Callback URLs: ", - "user.settings.hooks_out.callbackDesc": "Nova linha separada de URLs que receberá o evento HTTP POST", - "user.settings.hooks_out.callbackHolder": "Cada URL deve começar com http:// ou https://", - "user.settings.hooks_out.channel": "Canal: ", - "user.settings.hooks_out.comma": "Palavras separadas por virgula para gatilho em", - "user.settings.hooks_out.existing": "Webhooks de saída existentes", - "user.settings.hooks_out.none": "Nenhum", - "user.settings.hooks_out.only": "Apenas canais públicos pode ser usado", - "user.settings.hooks_out.optional": "Opcional se o canal selecionado", - "user.settings.hooks_out.regen": "Re-Gerar Token", - "user.settings.hooks_out.select": "--- Selecione um canal ---", - "user.settings.hooks_out.trigger": "Palavras de Gatilho: ", "user.settings.import_theme.cancel": "Cancelar", "user.settings.import_theme.importBody": "Para importar um tema, vá para uma equipe no Slack e olhe para “Preferences -> Sidebar Theme”. Abra a opção de tema customizado, copie os valores das cores do tema e cole eles aqui:", "user.settings.import_theme.importHeader": "Importar Tema Slack", @@ -1301,10 +1280,6 @@ "user.settings.import_theme.submitError": "Formato inválido, por favor tente copiar e colar novamente.", "user.settings.integrations.commands": "Comandos Slash", "user.settings.integrations.commandsDescription": "Gerenciar seus comandos slash", - "user.settings.integrations.incomingWebhooks": "Webhooks Entrada", - "user.settings.integrations.incomingWebhooksDescription": "Gerencie seus webhooks entrada", - "user.settings.integrations.outWebhooks": "Webhooks Saída", - "user.settings.integrations.outWebhooksDescription": "Gerencie seus webhooks saída", "user.settings.integrations.title": "Configuração de Integração", "user.settings.languages.change": "Alterar o idioma da interface", "user.settings.modal.advanced": "Avançado", @@ -1373,4 +1348,4 @@ "web.footer.terms": "Termos", "web.header.back": "Voltar", "web.root.singup_info": "Toda comunicação em um só lugar, pesquisável e acessível em qualquer lugar" -}
\ No newline at end of file +} diff --git a/webapp/root.jsx b/webapp/root.jsx index ce59a95c9..fca368bdb 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -36,6 +36,12 @@ import ShouldVerifyEmail from 'components/should_verify_email.jsx'; import DoVerifyEmail from 'components/do_verify_email.jsx'; import AdminConsole from 'components/admin_console/admin_controller.jsx'; import TutorialView from 'components/tutorial/tutorial_view.jsx'; +import BackstageNavbar from 'components/backstage/backstage_navbar.jsx'; +import BackstageSidebar from 'components/backstage/backstage_sidebar.jsx'; +import InstalledIntegrations from 'components/backstage/installed_integrations.jsx'; +import AddIntegration from 'components/backstage/add_integration.jsx'; +import AddIncomingWebhook from 'components/backstage/add_incoming_webhook.jsx'; +import AddOutgoingWebhook from 'components/backstage/add_outgoing_webhook.jsx'; import SignupTeamComplete from 'components/signup_team_complete/components/signup_team_complete.jsx'; import WelcomePage from 'components/signup_team_complete/components/team_signup_welcome_page.jsx'; @@ -241,6 +247,42 @@ function renderRootComponent() { path=':team/logout' onEnter={onLoggedOut} /> + <Route path='settings/integrations'> + <IndexRedirect to='installed'/> + <Route + path='installed' + components={{ + navbar: BackstageNavbar, + sidebar: BackstageSidebar, + center: InstalledIntegrations + }} + /> + <Route path='add'> + <IndexRoute + components={{ + navbar: BackstageNavbar, + sidebar: BackstageSidebar, + center: AddIntegration + }} + /> + <Route + path='incoming_webhook' + components={{ + navbar: BackstageNavbar, + sidebar: BackstageSidebar, + center: AddIncomingWebhook + }} + /> + <Route + path='outgoing_webhook' + components={{ + navbar: BackstageNavbar, + sidebar: BackstageSidebar, + center: AddOutgoingWebhook + }} + /> + </Route> + </Route> <Route path='admin_console' component={AdminConsole} diff --git a/webapp/sass/routes/_backstage.scss b/webapp/sass/routes/_backstage.scss new file mode 100644 index 000000000..70bab99cb --- /dev/null +++ b/webapp/sass/routes/_backstage.scss @@ -0,0 +1,212 @@ +.backstage { + background-color: #f2f2f2; + height: 100%; + padding-left: 260px; + padding-top: 45px; +} + +.backstage__navbar { + background: white; + border-bottom: lightgray 1px solid; + margin: 0 -15px; + padding: 10px; + z-index: 10; +} + +.backstage__navbar__back { + color: black; + + .fa { + font-weight: bold; + margin-right: 5px; + } +} + +.backstage__sidebar { + position: absolute; + left: 0; + width: 260px; + height: 100%; + background-color: #f2f2f2; + padding-bottom: 20px; + padding-left: 20px; + padding-right: 20px; + padding-top: 45px; + z-index: 5; + + ul { + padding: 0px; + list-style: none; + } +} + +.backstage__sidebar__category { + border: lightgray 1px solid; + + .category-title { + color: gray; + display: block; + padding: 5px 10px; + position: relative; + } + + .category-title--active { + color: black; + } + + .category-title__text { + position: absolute; + left: 2em; + } + + .sections { + background: white; + border-top: lightgray 1px solid; + } + + .section-title { + display: block; + padding-left: 2em; + } + + .subsection { + } + + .subsection-title { + display: block; + padding-left: 3em; + } + + .section-title--active, .subsection-title--active { + background-color:#2388d6; + color: white; + } +} + +.backstage__sidebar__category + .backstage__sidebar__category { + border-top-width: 0px; +} + +.installed-integrations { + height: 100%; + max-width: 958px; +} + +.backstage__header { + margin-bottom: 20px; + width: 100%; + + .text { + display: inline; + } + + .add-integrations-link { + float: right; + } +} + +.installed-integrations__filters { + display: flex; + flex-direction: row; + margin-bottom: 8px; + width: 100%; + + .type-filters { + flex-grow: 1; + flex-shrink: 0; + + .type-filter { + &--selected { + color: black; + cursor: default; + font-weight: bold; + } + } + + .divider { + margin-left: 8px; + margin-right: 8px; + } + } + + .filter-box { + flex-grow: 0; + flex-shrink: 0; + } +} + +.installed-integrations__list { + background-color: white; + border: lightgray 1px solid; + padding-bottom: 30px; + padding-left: 30px; + padding-right: 30px; + padding-top: 10px; +} + +.installed-integrations__item { + border-bottom: lightgray 1px solid; + display: flex; + padding: 20px; + + .details { + flex-grow: 1; + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + + .details-row + .details-row { + margin-top: 15px; + } + + .name { + font-weight: bold; + margin-bottom: 1em; + } + + .type { + margin-left: 6px; + } + + .description { + color: gray; + margin-bottom: 1em; + } + } + + .actions { + flex-grow: 0; + flex-shrink: 0; + padding-left: 20px; + } +} + +.add-integration-option { + background-color: white; + border: lightgray 1px solid; + display: inline-block; + height: 210px; + margin-right: 30px; + padding: 20px; + text-align: center; + width: 250px; + + &:hover { + color: default; + text-decoration: none; + } + + &__image { + width: 80px; + height: 80px; + } + + &__title { + color: black; + margin-bottom: 10px; + } + + &__description { + color: gray; + } +} diff --git a/webapp/sass/routes/_module.scss b/webapp/sass/routes/_module.scss index 48c1af1d9..b76dd147f 100644 --- a/webapp/sass/routes/_module.scss +++ b/webapp/sass/routes/_module.scss @@ -2,6 +2,7 @@ @import 'access-history'; @import 'activity-log'; @import 'admin-console'; +@import 'backstage'; @import 'docs'; @import 'error-page'; @import 'loading'; diff --git a/webapp/stores/file_store.jsx b/webapp/stores/file_store.jsx index 2628685cc..6473e0474 100644 --- a/webapp/stores/file_store.jsx +++ b/webapp/stores/file_store.jsx @@ -13,11 +13,6 @@ class FileStore extends EventEmitter { constructor() { super(); - this.addChangeListener = this.addChangeListener.bind(this); - this.removeChangeListener = this.removeChangeListener.bind(this); - this.emitChange = this.emitChange.bind(this); - - this.handleEventPayload = this.handleEventPayload.bind(this); this.dispatchToken = AppDispatcher.register(this.handleEventPayload); this.fileInfo = new Map(); diff --git a/webapp/stores/integration_store.jsx b/webapp/stores/integration_store.jsx new file mode 100644 index 000000000..abd7e3558 --- /dev/null +++ b/webapp/stores/integration_store.jsx @@ -0,0 +1,134 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import Constants from 'utils/constants.jsx'; +import EventEmitter from 'events'; + +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'changed'; + +class IntegrationStore extends EventEmitter { + constructor() { + super(); + + this.dispatchToken = AppDispatcher.register(this.handleEventPayload.bind(this)); + + this.incomingWebhooks = []; + this.receivedIncomingWebhooks = false; + + this.outgoingWebhooks = []; + this.receivedOutgoingWebhooks = false; + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + emitChange() { + this.emit(CHANGE_EVENT); + } + + hasReceivedIncomingWebhooks() { + return this.receivedIncomingWebhooks; + } + + getIncomingWebhooks() { + return this.incomingWebhooks; + } + + setIncomingWebhooks(incomingWebhooks) { + this.incomingWebhooks = incomingWebhooks; + this.receivedIncomingWebhooks = true; + } + + addIncomingWebhook(incomingWebhook) { + this.incomingWebhooks.push(incomingWebhook); + } + + removeIncomingWebhook(id) { + for (let i = 0; i < this.incomingWebhooks.length; i++) { + if (this.incomingWebhooks[i].id === id) { + this.incomingWebhooks.splice(i, 1); + break; + } + } + } + + hasReceivedOutgoingWebhooks() { + return this.receivedIncomingWebhooks; + } + + getOutgoingWebhooks() { + return this.outgoingWebhooks; + } + + setOutgoingWebhooks(outgoingWebhooks) { + this.outgoingWebhooks = outgoingWebhooks; + this.receivedOutgoingWebhooks = true; + } + + addOutgoingWebhook(outgoingWebhook) { + this.outgoingWebhooks.push(outgoingWebhook); + } + + updateOutgoingWebhook(outgoingWebhook) { + for (let i = 0; i < this.outgoingWebhooks.length; i++) { + if (this.outgoingWebhooks[i].id === outgoingWebhook.id) { + this.outgoingWebhooks[i] = outgoingWebhook; + break; + } + } + } + + removeOutgoingWebhook(id) { + for (let i = 0; i < this.outgoingWebhooks.length; i++) { + if (this.outgoingWebhooks[i].id === id) { + this.outgoingWebhooks.splice(i, 1); + break; + } + } + } + + handleEventPayload(payload) { + const action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_INCOMING_WEBHOOKS: + this.setIncomingWebhooks(action.incomingWebhooks); + this.emitChange(); + break; + case ActionTypes.RECEIVED_INCOMING_WEBHOOK: + this.addIncomingWebhook(action.incomingWebhook); + this.emitChange(); + break; + case ActionTypes.REMOVED_INCOMING_WEBHOOK: + this.removeIncomingWebhook(action.id); + this.emitChange(); + break; + case ActionTypes.RECEIVED_OUTGOING_WEBHOOKS: + this.setOutgoingWebhooks(action.outgoingWebhooks); + this.emitChange(); + break; + case ActionTypes.RECEIVED_OUTGOING_WEBHOOK: + this.addOutgoingWebhook(action.outgoingWebhook); + this.emitChange(); + break; + case ActionTypes.UPDATED_OUTGOING_WEBHOOK: + this.updateOutgoingWebhook(action.outgoingWebhook); + this.emitChange(); + break; + case ActionTypes.REMOVED_OUTGOING_WEBHOOK: + this.removeOutgoingWebhook(action.id); + this.emitChange(); + break; + } + } +} + +export default new IntegrationStore(); diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index 6140fd9e0..cc19baa7e 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -1121,3 +1121,140 @@ export function getRecentAndNewUsersAnalytics(teamId) { } ); } + +export function listIncomingHooks() { + if (isCallInProgress('listIncomingHooks')) { + return; + } + + callTracker.listIncomingHooks = utils.getTimestamp(); + + client.listIncomingHooks( + (data) => { + callTracker.listIncomingHooks = 0; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_INCOMING_WEBHOOKS, + incomingWebhooks: data + }); + }, + (err) => { + callTracker.listIncomingHooks = 0; + dispatchError(err, 'getIncomingHooks'); + } + ); +} + +export function listOutgoingHooks() { + if (isCallInProgress('listOutgoingHooks')) { + return; + } + + callTracker.listOutgoingHooks = utils.getTimestamp(); + + client.listOutgoingHooks( + (data) => { + callTracker.listOutgoingHooks = 0; + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_OUTGOING_WEBHOOKS, + outgoingWebhooks: data + }); + }, + (err) => { + callTracker.listOutgoingHooks = 0; + dispatchError(err, 'getOutgoingHooks'); + } + ); +} + +export function addIncomingHook(hook, success, error) { + client.addIncomingHook( + hook, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_INCOMING_WEBHOOK, + incomingWebhook: data + }); + + if (success) { + success(); + } + }, + (err) => { + dispatchError(err, 'addIncomingHook'); + + if (error) { + error(err); + } + } + ); +} + +export function addOutgoingHook(hook, success, error) { + client.addOutgoingHook( + hook, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_OUTGOING_WEBHOOK, + outgoingWebhook: data + }); + + if (success) { + success(); + } + }, + (err) => { + dispatchError(err, 'addOutgoingHook'); + + if (error) { + error(err); + } + } + ); +} + +export function deleteIncomingHook(id) { + client.deleteIncomingHook( + {id}, + () => { + AppDispatcher.handleServerAction({ + type: ActionTypes.REMOVED_INCOMING_WEBHOOK, + id + }); + }, + (err) => { + dispatchError(err, 'deleteIncomingHook'); + } + ); +} + +export function deleteOutgoingHook(id) { + client.deleteOutgoingHook( + {id}, + () => { + AppDispatcher.handleServerAction({ + type: ActionTypes.REMOVED_OUTGOING_WEBHOOK, + id + }); + }, + (err) => { + dispatchError(err, 'deleteOutgoingHook'); + } + ); +} + +export function regenOutgoingHookToken(id) { + client.regenOutgoingHookToken( + {id}, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.UPDATED_OUTGOING_WEBHOOK, + outgoingWebhook: data + }); + }, + (err) => { + dispatchError(err, 'regenOutgoingHookToken'); + } + ); +} diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 642ff5fe3..f0e8c260e 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -70,6 +70,14 @@ export default { RECEIVED_FILE_INFO: null, RECEIVED_ANALYTICS: null, + RECEIVED_INCOMING_WEBHOOKS: null, + RECEIVED_INCOMING_WEBHOOK: null, + REMOVED_INCOMING_WEBHOOK: null, + RECEIVED_OUTGOING_WEBHOOKS: null, + RECEIVED_OUTGOING_WEBHOOK: null, + UPDATED_OUTGOING_WEBHOOK: null, + REMOVED_OUTGOING_WEBHOOK: null, + RECEIVED_MSG: null, RECEIVED_MY_TEAM: null, diff --git a/webapp/webpack.config.js b/webapp/webpack.config.js index a049898d6..478c5de81 100644 --- a/webapp/webpack.config.js +++ b/webapp/webpack.config.js @@ -56,7 +56,7 @@ var config = { loaders: ['style', 'css'] }, { - test: /\.(png|eot|tiff|svg|woff2|woff|ttf|gif|mp3)$/, + test: /\.(png|eot|tiff|svg|woff2|woff|ttf|gif|mp3|jpg)$/, loader: 'file', query: { name: 'files/[hash].[ext]' |