summaryrefslogtreecommitdiffstats
path: root/webapp/components/backstage
diff options
context:
space:
mode:
Diffstat (limited to 'webapp/components/backstage')
-rw-r--r--webapp/components/backstage/add_incoming_webhook.jsx188
-rw-r--r--webapp/components/backstage/add_integration.jsx78
-rw-r--r--webapp/components/backstage/add_integration_option.jsx39
-rw-r--r--webapp/components/backstage/add_outgoing_webhook.jsx254
-rw-r--r--webapp/components/backstage/backstage_category.jsx68
-rw-r--r--webapp/components/backstage/backstage_navbar.jsx61
-rw-r--r--webapp/components/backstage/backstage_section.jsx80
-rw-r--r--webapp/components/backstage/backstage_sidebar.jsx68
-rw-r--r--webapp/components/backstage/installed_incoming_webhook.jsx71
-rw-r--r--webapp/components/backstage/installed_integrations.jsx289
-rw-r--r--webapp/components/backstage/installed_outgoing_webhook.jsx91
11 files changed, 1287 insertions, 0 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>
+ );
+ }
+}