summaryrefslogtreecommitdiffstats
path: root/webapp/components/backstage
diff options
context:
space:
mode:
authorHarrison Healey <harrisonmhealey@gmail.com>2016-04-08 11:51:28 -0400
committerChristopher Speller <crspeller@gmail.com>2016-04-08 11:51:28 -0400
commit77ee1ce7fee698847e211dc15d4673300901aa48 (patch)
tree115391ae591f7e008cf357238be612e7482742fc /webapp/components/backstage
parent742d611ba4c08dbc4d30d3ef7a40a872186bd9eb (diff)
downloadchat-77ee1ce7fee698847e211dc15d4673300901aa48.tar.gz
chat-77ee1ce7fee698847e211dc15d4673300901aa48.tar.bz2
chat-77ee1ce7fee698847e211dc15d4673300901aa48.zip
PLT-2553 Updated backstage page navigation (#2661)
* Updated integrations list based on feedback * Reorganized Integrations pages * Repurposed AddIntegration page as a landing page for Integrations * Moved backstage breadcrumb header into its own component * Removed unnecessary prop * Fixed Save links on AddIntegration pages
Diffstat (limited to 'webapp/components/backstage')
-rw-r--r--webapp/components/backstage/add_command.jsx27
-rw-r--r--webapp/components/backstage/add_incoming_webhook.jsx29
-rw-r--r--webapp/components/backstage/add_outgoing_webhook.jsx29
-rw-r--r--webapp/components/backstage/backstage_header.jsx39
-rw-r--r--webapp/components/backstage/backstage_section.jsx1
-rw-r--r--webapp/components/backstage/backstage_sidebar.jsx51
-rw-r--r--webapp/components/backstage/installed_command.jsx64
-rw-r--r--webapp/components/backstage/installed_commands.jsx93
-rw-r--r--webapp/components/backstage/installed_incoming_webhook.jsx67
-rw-r--r--webapp/components/backstage/installed_incoming_webhooks.jsx85
-rw-r--r--webapp/components/backstage/installed_integrations.jsx335
-rw-r--r--webapp/components/backstage/installed_outgoing_webhook.jsx95
-rw-r--r--webapp/components/backstage/installed_outgoing_webhooks.jsx91
-rw-r--r--webapp/components/backstage/integration_option.jsx (renamed from webapp/components/backstage/add_integration_option.jsx)10
-rw-r--r--webapp/components/backstage/integrations.jsx (renamed from webapp/components/backstage/add_integration.jsx)38
15 files changed, 601 insertions, 453 deletions
diff --git a/webapp/components/backstage/add_command.jsx b/webapp/components/backstage/add_command.jsx
index b6f01b4d8..2eb7bdb21 100644
--- a/webapp/components/backstage/add_command.jsx
+++ b/webapp/components/backstage/add_command.jsx
@@ -7,6 +7,7 @@ import * as AsyncClient from 'utils/async_client.jsx';
import {browserHistory} from 'react-router';
import * as Utils from 'utils/utils.jsx';
+import BackstageHeader from './backstage_header.jsx';
import {FormattedMessage} from 'react-intl';
import FormError from 'components/form_error.jsx';
import {Link} from 'react-router';
@@ -105,7 +106,7 @@ export default class AddCommand extends React.Component {
AsyncClient.addCommand(
command,
() => {
- browserHistory.push('/settings/integrations/installed');
+ browserHistory.push('/settings/integrations/commands');
},
(err) => {
this.setState({
@@ -249,16 +250,18 @@ export default class AddCommand extends React.Component {
return (
<div className='backstage-content row'>
- <div className='add-command'>
- <div className='backstage-header'>
- <h1>
- <FormattedMessage
- id='add_command.header'
- defaultMessage='Add Slash Command'
- />
- </h1>
- </div>
- </div>
+ <BackstageHeader>
+ <Link to={'/settings/integrations/commands'}>
+ <FormattedMessage
+ id='installed_command.header'
+ defaultMessage='Slash Commands'
+ />
+ </Link>
+ <FormattedMessage
+ id='add_command.header'
+ defaultMessage='Add'
+ />
+ </BackstageHeader>
<div className='backstage-form'>
<form className='form-horizontal'>
<div className='form-group'>
@@ -479,7 +482,7 @@ export default class AddCommand extends React.Component {
<FormError errors={[this.state.serverError, this.state.clientError]}/>
<Link
className='btn btn-sm'
- to={'/settings/integrations/add'}
+ to={'/settings/integrations/commands'}
>
<FormattedMessage
id='add_command.cancel'
diff --git a/webapp/components/backstage/add_incoming_webhook.jsx b/webapp/components/backstage/add_incoming_webhook.jsx
index b0c16b9ff..f68a263be 100644
--- a/webapp/components/backstage/add_incoming_webhook.jsx
+++ b/webapp/components/backstage/add_incoming_webhook.jsx
@@ -6,6 +6,7 @@ import React from 'react';
import * as AsyncClient from 'utils/async_client.jsx';
import {browserHistory} from 'react-router';
+import BackstageHeader from './backstage_header.jsx';
import ChannelSelect from 'components/channel_select.jsx';
import {FormattedMessage} from 'react-intl';
import FormError from 'components/form_error.jsx';
@@ -68,7 +69,7 @@ export default class AddIncomingWebhook extends React.Component {
AsyncClient.addIncomingHook(
hook,
() => {
- browserHistory.push('/settings/integrations/installed');
+ browserHistory.push('/settings/integrations/incoming_webhooks');
},
(err) => {
this.setState({
@@ -99,17 +100,19 @@ export default class AddIncomingWebhook extends React.Component {
render() {
return (
- <div className='backstage-content row'>
- <div className='add-incoming-webhook'>
- <div className='backstage-header'>
- <h1>
- <FormattedMessage
- id='add_incoming_webhook.header'
- defaultMessage='Add Incoming Webhook'
- />
- </h1>
- </div>
- </div>
+ <div className='backstage-content'>
+ <BackstageHeader>
+ <Link to={'/settings/integrations/incoming_webhooks'}>
+ <FormattedMessage
+ id='installed_incoming_webhooks.header'
+ defaultMessage='Incoming Webhooks'
+ />
+ </Link>
+ <FormattedMessage
+ id='add_incoming_webhook.header'
+ defaultMessage='Add'
+ />
+ </BackstageHeader>
<div className='backstage-form'>
<form className='form-horizontal'>
<div className='form-group'>
@@ -176,7 +179,7 @@ export default class AddIncomingWebhook extends React.Component {
<FormError errors={[this.state.serverError, this.state.clientError]}/>
<Link
className='btn btn-sm'
- to={'/settings/integrations/add'}
+ to={'/settings/integrations/incoming_webhooks'}
>
<FormattedMessage
id='add_incoming_webhook.cancel'
diff --git a/webapp/components/backstage/add_outgoing_webhook.jsx b/webapp/components/backstage/add_outgoing_webhook.jsx
index 9d1f79e5d..acdd98ba8 100644
--- a/webapp/components/backstage/add_outgoing_webhook.jsx
+++ b/webapp/components/backstage/add_outgoing_webhook.jsx
@@ -6,6 +6,7 @@ import React from 'react';
import * as AsyncClient from 'utils/async_client.jsx';
import {browserHistory} from 'react-router';
+import BackstageHeader from './backstage_header.jsx';
import ChannelSelect from 'components/channel_select.jsx';
import {FormattedMessage} from 'react-intl';
import FormError from 'components/form_error.jsx';
@@ -88,7 +89,7 @@ export default class AddOutgoingWebhook extends React.Component {
AsyncClient.addOutgoingHook(
hook,
() => {
- browserHistory.push('/settings/integrations/installed');
+ browserHistory.push('/settings/integrations/outgoing_webhooks');
},
(err) => {
this.setState({
@@ -131,17 +132,19 @@ export default class AddOutgoingWebhook extends React.Component {
render() {
return (
- <div className='backstage-content row'>
- <div className='add-outgoing-webhook'>
- <div className='backstage-header'>
- <h1>
- <FormattedMessage
- id='add_outgoing_webhook.header'
- defaultMessage='Add Outgoing Webhook'
- />
- </h1>
- </div>
- </div>
+ <div className='backstage-content'>
+ <BackstageHeader>
+ <Link to={'/settings/integrations/outgoing_webhooks'}>
+ <FormattedMessage
+ id='installed_outgoing_webhooks.header'
+ defaultMessage='Outgoing Webhooks'
+ />
+ </Link>
+ <FormattedMessage
+ id='add_outgoing_webhook.header'
+ defaultMessage='Add'
+ />
+ </BackstageHeader>
<div className='backstage-form'>
<form className='form-horizontal'>
<div className='form-group'>
@@ -250,7 +253,7 @@ export default class AddOutgoingWebhook extends React.Component {
<FormError errors={[this.state.serverError, this.state.clientError]}/>
<Link
className='btn btn-sm'
- to={'/settings/integrations/add'}
+ to={'/settings/integrations/outgoing_webhooks'}
>
<FormattedMessage
id='add_outgoing_webhook.cancel'
diff --git a/webapp/components/backstage/backstage_header.jsx b/webapp/components/backstage/backstage_header.jsx
new file mode 100644
index 000000000..95b35d7a9
--- /dev/null
+++ b/webapp/components/backstage/backstage_header.jsx
@@ -0,0 +1,39 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+export default class BackstageHeader extends React.Component {
+ static get propTypes() {
+ return {
+ children: React.PropTypes.node
+ };
+ }
+
+ render() {
+ const children = [];
+
+ React.Children.forEach(this.props.children, (child, index) => {
+ if (index !== 0) {
+ children.push(
+ <span
+ key={'divider' + index}
+ className='backstage-header__divider'
+ >
+ {'>'}
+ </span>
+ );
+ }
+
+ children.push(child);
+ });
+
+ return (
+ <div className='backstage-header'>
+ <h1>
+ {children}
+ </h1>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/backstage/backstage_section.jsx b/webapp/components/backstage/backstage_section.jsx
index d6ce2b258..120e956b0 100644
--- a/webapp/components/backstage/backstage_section.jsx
+++ b/webapp/components/backstage/backstage_section.jsx
@@ -65,7 +65,6 @@ export default class BackstageSection extends React.Component {
<Link
className={`${className}-title`}
activeClassName={`${className}-title--active`}
- onlyActiveOnIndex={true}
onClick={this.handleClick}
to={link}
>
diff --git a/webapp/components/backstage/backstage_sidebar.jsx b/webapp/components/backstage/backstage_sidebar.jsx
index 172119b32..eb84709a3 100644
--- a/webapp/components/backstage/backstage_sidebar.jsx
+++ b/webapp/components/backstage/backstage_sidebar.jsx
@@ -24,51 +24,32 @@ export default class BackstageSidebar extends React.Component {
}
>
<BackstageSection
- name='installed'
+ name='incoming_webhooks'
title={(
<FormattedMessage
- id='backstage_sidebar.integrations.installed'
- defaultMessage='Installed Integrations'
+ id='backstage_sidebar.integrations.incoming_webhooks'
+ defaultMessage='Incoming Webhooks'
/>
)}
/>
<BackstageSection
- name='add'
+ name='outgoing_webhooks'
title={(
<FormattedMessage
- id='backstage_sidebar.integrations.add'
- defaultMessage='Add Integration'
+ id='backstage_sidebar.integrations.outgoing_webhooks'
+ defaultMessage='Outgoing Webhooks'
/>
)}
- >
- <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
- name='command'
- title={(
- <FormattedMessage
- id='backstage_sidebar.integrations.add.command'
- defaultMessage='Slash Command'
- />
- )}
- />
- </BackstageSection>
+ />
+ <BackstageSection
+ name='commands'
+ title={(
+ <FormattedMessage
+ id='backstage_sidebar.integrations.commands'
+ defaultMessage='Slash Commands'
+ />
+ )}
+ />
</BackstageCategory>
</ul>
</div>
diff --git a/webapp/components/backstage/installed_command.jsx b/webapp/components/backstage/installed_command.jsx
index 51adce160..8b56ed595 100644
--- a/webapp/components/backstage/installed_command.jsx
+++ b/webapp/components/backstage/installed_command.jsx
@@ -12,7 +12,8 @@ export default class InstalledCommand extends React.Component {
return {
command: React.PropTypes.object.isRequired,
onRegenToken: React.PropTypes.func.isRequired,
- onDelete: React.PropTypes.func.isRequired
+ onDelete: React.PropTypes.func.isRequired,
+ filter: React.PropTypes.string
};
}
@@ -21,6 +22,8 @@ export default class InstalledCommand extends React.Component {
this.handleRegenToken = this.handleRegenToken.bind(this);
this.handleDelete = this.handleDelete.bind(this);
+
+ this.matchesFilter = this.matchesFilter.bind(this);
}
handleRegenToken(e) {
@@ -35,26 +38,67 @@ export default class InstalledCommand extends React.Component {
this.props.onDelete(this.props.command);
}
+ matchesFilter(command, filter) {
+ if (!filter) {
+ return true;
+ }
+
+ return command.display_name.toLowerCase().indexOf(filter) !== -1 ||
+ command.description.toLowerCase().indexOf(filter) !== -1 ||
+ command.trigger.toLowerCase().indexOf(filter) !== -1;
+ }
+
render() {
const command = this.props.command;
+ if (!this.matchesFilter(command, this.props.filter)) {
+ return null;
+ }
+
+ let name;
+ if (command.display_name) {
+ name = command.display_name;
+ } else {
+ name = (
+ <FormattedMessage
+ id='installed_integraions.unnamed_command'
+ defaultMessage='Unnamed Slash Command'
+ />
+ );
+ }
+
+ let description = null;
+ if (command.description) {
+ description = (
+ <div className='item-details__row'>
+ <span className='item-details__description'>
+ {command.description}
+ </span>
+ </div>
+ );
+ }
+
return (
<div className='backstage-list__item'>
<div className='item-details'>
<div className='item-details__row'>
<span className='item-details__name'>
- {command.display_name}
+ {name}
</span>
- <span className='item-details__type'>
- <FormattedMessage
- id='installed_integrations.commandType'
- defaultMessage='(Slash Command)'
- />
+ <span className='item-details__trigger'>
+ {'- /' + command.trigger}
</span>
</div>
+ {description}
<div className='item-details__row'>
- <span className='item-details__description'>
- {command.description}
+ <span className='item-details__token'>
+ <FormattedMessage
+ id='installed_integrations.token'
+ defaultMessage='Token: {token}'
+ values={{
+ token: command.token
+ }}
+ />
</span>
</div>
<div className='item-details__row'>
@@ -63,7 +107,7 @@ export default class InstalledCommand extends React.Component {
id='installed_integrations.creation'
defaultMessage='Created by {creator} on {createAt, date, full}'
values={{
- creator: Utils.displayUsername(command.creator_Id),
+ creator: Utils.displayUsername(command.creator_id),
createAt: command.create_at
}}
/>
diff --git a/webapp/components/backstage/installed_commands.jsx b/webapp/components/backstage/installed_commands.jsx
new file mode 100644
index 000000000..ead2f9850
--- /dev/null
+++ b/webapp/components/backstage/installed_commands.jsx
@@ -0,0 +1,93 @@
+// 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 IntegrationStore from 'stores/integration_store.jsx';
+
+import {FormattedMessage} from 'react-intl';
+import InstalledCommand from './installed_command.jsx';
+import InstalledIntegrations from './installed_integrations.jsx';
+
+export default class InstalledCommands extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleIntegrationChange = this.handleIntegrationChange.bind(this);
+
+ this.regenCommandToken = this.regenCommandToken.bind(this);
+ this.deleteCommand = this.deleteCommand.bind(this);
+
+ this.state = {
+ commands: []
+ };
+ }
+
+ componentWillMount() {
+ IntegrationStore.addChangeListener(this.handleIntegrationChange);
+
+ if (window.mm_config.EnableCommands === 'true') {
+ if (IntegrationStore.hasReceivedCommands()) {
+ this.setState({
+ commands: IntegrationStore.getCommands()
+ });
+ } else {
+ AsyncClient.listTeamCommands();
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ IntegrationStore.removeChangeListener(this.handleIntegrationChange);
+ }
+
+ handleIntegrationChange() {
+ const commands = IntegrationStore.getCommands();
+
+ this.setState({
+ commands
+ });
+ }
+
+ regenCommandToken(command) {
+ AsyncClient.regenCommandToken(command.id);
+ }
+
+ deleteCommand(command) {
+ AsyncClient.deleteCommand(command.id);
+ }
+
+ render() {
+ const commands = this.state.commands.map((command) => {
+ return (
+ <InstalledCommand
+ key={command.id}
+ command={command}
+ onRegenToken={this.regenCommandToken}
+ onDelete={this.deleteCommand}
+ />
+ );
+ });
+
+ return (
+ <InstalledIntegrations
+ header={
+ <FormattedMessage
+ id='installed_integrations.commands'
+ defaultMessage='Installed Commands'
+ />
+ }
+ addText={
+ <FormattedMessage
+ id='installed_integrations.add_command'
+ defaultMessage='Add Command'
+ />
+ }
+ addLink='/settings/integrations/commands/add'
+ >
+ {commands}
+ </InstalledIntegrations>
+ );
+ }
+}
diff --git a/webapp/components/backstage/installed_incoming_webhook.jsx b/webapp/components/backstage/installed_incoming_webhook.jsx
index cd9a6d761..58d318310 100644
--- a/webapp/components/backstage/installed_incoming_webhook.jsx
+++ b/webapp/components/backstage/installed_incoming_webhook.jsx
@@ -12,7 +12,8 @@ export default class InstalledIncomingWebhook extends React.Component {
static get propTypes() {
return {
incomingWebhook: React.PropTypes.object.isRequired,
- onDelete: React.PropTypes.func.isRequired
+ onDelete: React.PropTypes.func.isRequired,
+ filter: React.PropTypes.string
};
}
@@ -28,31 +29,67 @@ export default class InstalledIncomingWebhook extends React.Component {
this.props.onDelete(this.props.incomingWebhook);
}
+ matchesFilter(incomingWebhook, channel, filter) {
+ if (!filter) {
+ return true;
+ }
+
+ if (incomingWebhook.display_name.toLowerCase().indexOf(filter) !== -1 ||
+ incomingWebhook.description.toLowerCase().indexOf(filter) !== -1) {
+ return true;
+ }
+
+ if (incomingWebhook.channel_id) {
+ if (channel && channel.name.toLowerCase().indexOf(filter) !== -1) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
render() {
const incomingWebhook = this.props.incomingWebhook;
-
const channel = ChannelStore.get(incomingWebhook.channel_id);
- const channelName = channel ? channel.display_name : 'cannot find channel';
+
+ if (!this.matchesFilter(incomingWebhook, channel, this.props.filter)) {
+ return null;
+ }
+
+ let displayName;
+ if (incomingWebhook.display_name) {
+ displayName = incomingWebhook.display_name;
+ } else if (channel) {
+ displayName = channel.display_name;
+ } else {
+ displayName = (
+ <FormattedMessage
+ id='installed_incoming_webhooks.unknown_channel'
+ defaultMessage='A Private Webhook'
+ />
+ );
+ }
+
+ let description = null;
+ if (incomingWebhook.description) {
+ description = (
+ <div className='item-details__row'>
+ <span className='item-details__description'>
+ {incomingWebhook.description}
+ </span>
+ </div>
+ );
+ }
return (
<div className='backstage-list__item'>
<div className='item-details'>
<div className='item-details__row'>
<span className='item-details__name'>
- {incomingWebhook.display_name || channelName}
- </span>
- <span className='item-details__type'>
- <FormattedMessage
- id='installed_integrations.incomingWebhookType'
- defaultMessage='(Incoming Webhook)'
- />
- </span>
- </div>
- <div className='item-details__row'>
- <span className='item-details__description'>
- {incomingWebhook.description}
+ {displayName}
</span>
</div>
+ {description}
<div className='tem-details__row'>
<span className='item-details__creation'>
<FormattedMessage
diff --git a/webapp/components/backstage/installed_incoming_webhooks.jsx b/webapp/components/backstage/installed_incoming_webhooks.jsx
new file mode 100644
index 000000000..de7154afe
--- /dev/null
+++ b/webapp/components/backstage/installed_incoming_webhooks.jsx
@@ -0,0 +1,85 @@
+// 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 IntegrationStore from 'stores/integration_store.jsx';
+
+import {FormattedMessage} from 'react-intl';
+import InstalledIncomingWebhook from './installed_incoming_webhook.jsx';
+import InstalledIntegrations from './installed_integrations.jsx';
+
+export default class InstalledIncomingWebhooks extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleIntegrationChange = this.handleIntegrationChange.bind(this);
+
+ this.deleteIncomingWebhook = this.deleteIncomingWebhook.bind(this);
+
+ this.state = {
+ incomingWebhooks: []
+ };
+ }
+
+ componentWillMount() {
+ IntegrationStore.addChangeListener(this.handleIntegrationChange);
+
+ if (window.mm_config.EnableIncomingWebhooks === 'true') {
+ if (IntegrationStore.hasReceivedIncomingWebhooks()) {
+ this.setState({
+ incomingWebhooks: IntegrationStore.getIncomingWebhooks()
+ });
+ } else {
+ AsyncClient.listIncomingHooks();
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ IntegrationStore.removeChangeListener(this.handleIntegrationChange);
+ }
+
+ handleIntegrationChange() {
+ this.setState({
+ incomingWebhooks: IntegrationStore.getIncomingWebhooks()
+ });
+ }
+
+ deleteIncomingWebhook(incomingWebhook) {
+ AsyncClient.deleteIncomingHook(incomingWebhook.id);
+ }
+
+ render() {
+ const incomingWebhooks = this.state.incomingWebhooks.map((incomingWebhook) => {
+ return (
+ <InstalledIncomingWebhook
+ key={incomingWebhook.id}
+ incomingWebhook={incomingWebhook}
+ onDelete={this.deleteIncomingWebhook}
+ />
+ );
+ });
+
+ return (
+ <InstalledIntegrations
+ header={
+ <FormattedMessage
+ id='installed_incoming_webhooks.header'
+ defaultMessage='Installed Incoming Webhooks'
+ />
+ }
+ addText={
+ <FormattedMessage
+ id='installed_incoming_webhooks.add'
+ defaultMessage='Add Incoming Webhook'
+ />
+ }
+ addLink='/settings/integrations/incoming_webhooks/add'
+ >
+ {incomingWebhooks}
+ </InstalledIntegrations>
+ );
+ }
+}
diff --git a/webapp/components/backstage/installed_integrations.jsx b/webapp/components/backstage/installed_integrations.jsx
index e353b7f29..baf74447f 100644
--- a/webapp/components/backstage/installed_integrations.jsx
+++ b/webapp/components/backstage/installed_integrations.jsx
@@ -3,366 +3,65 @@
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 InstalledCommand from './installed_command.jsx';
import {Link} from 'react-router';
export default class InstalledIntegrations extends React.Component {
+ static get propTypes() {
+ return {
+ children: React.PropTypes.node,
+ header: React.PropTypes.node.isRequired,
+ addLink: React.PropTypes.string.isRequired,
+ addText: React.PropTypes.node.isRequired
+ };
+ }
+
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.regenCommandToken = this.regenCommandToken.bind(this);
- this.deleteCommand = this.deleteCommand.bind(this);
this.state = {
- incomingWebhooks: [],
- outgoingWebhooks: [],
- commands: [],
- 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();
- }
- }
-
- if (window.mm_config.EnableCommands === 'true') {
- if (IntegrationStore.hasReceivedCommands()) {
- this.setState({
- commands: IntegrationStore.getCommands()
- });
- } else {
- AsyncClient.listTeamCommands();
- }
- }
- }
-
- componentWillUnmount() {
- IntegrationStore.removeChangeListener(this.handleIntegrationChange);
- }
-
- handleIntegrationChange() {
- const incomingWebhooks = IntegrationStore.getIncomingWebhooks();
- const outgoingWebhooks = IntegrationStore.getOutgoingWebhooks();
- const commands = IntegrationStore.getCommands();
-
- this.setState({
- incomingWebhooks,
- outgoingWebhooks,
- commands
- });
-
- // reset the type filter if we were viewing a category that is now empty
- if ((this.state.typeFilter === 'incomingWebhooks' && incomingWebhooks.length === 0) ||
- (this.state.typeFilter === 'outgoingWebhooks' && outgoingWebhooks.length === 0) ||
- (this.state.typeFilter === 'commands' && commands.length === 0)) {
- this.setState({
- typeFilter: ''
- });
- }
- }
-
- 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);
- }
-
- regenCommandToken(command) {
- AsyncClient.regenCommandToken(command.id);
- }
-
- deleteCommand(command) {
- AsyncClient.deleteCommand(command.id);
- }
-
- renderTypeFilters(incomingWebhooks, outgoingWebhooks, commands) {
- const fields = [];
-
- if (incomingWebhooks.length > 0 || outgoingWebhooks.length > 0 || commands.length > 0) {
- let filterClassName = 'filter-sort';
- if (this.state.typeFilter === '') {
- filterClassName += ' filter-sort--active';
- }
-
- 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 = 'filter-sort';
- if (this.state.typeFilter === 'incomingWebhooks') {
- filterClassName += ' filter-sort--active';
- }
-
- 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 = 'filter-sort';
- if (this.state.typeFilter === 'outgoingWebhooks') {
- filterClassName += ' filter-sort--active';
- }
-
- 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>
- );
- }
-
- if (commands.length > 0) {
- fields.push(
- <span
- key='commandsDivider'
- className='divider'
- >
- {'|'}
- </span>
- );
-
- let filterClassName = 'filter-sort';
- if (this.state.typeFilter === 'commands') {
- filterClassName += ' filter-sort--active';
- }
-
- fields.push(
- <a
- key='commandsFilter'
- className={filterClassName}
- href='#'
- onClick={(e) => this.updateTypeFilter(e, 'commands')}
- >
- <FormattedMessage
- id='installed_integrations.commandsFilter'
- defaultMessage='Slash Commands ({count})'
- values={{
- count: commands.length
- }}
- />
- </a>
- );
- }
-
- return (
- <div className='backstage-filters__sort'>
- {fields}
- </div>
- );
- }
-
render() {
- const incomingWebhooks = this.state.incomingWebhooks;
- const outgoingWebhooks = this.state.outgoingWebhooks;
- const commands = this.state.commands;
-
- // TODO description, name, creator filtering
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}
- onDelete={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}
- />
- );
- }
- }
-
- if (!this.state.typeFilter || this.state.typeFilter === 'commands') {
- for (const command of commands) {
- if (filter) {
- const channel = ChannelStore.get(command.channel_id);
-
- if (!channel || channel.name.toLowerCase().indexOf(filter) === -1) {
- continue;
- }
- }
-
- integrations.push(
- <InstalledCommand
- key={command.id}
- command={command}
- onRegenToken={this.regenCommandToken}
- onDelete={this.deleteCommand}
- />
- );
- }
- }
+ const children = React.Children.map(this.props.children, (child) => {
+ return React.cloneElement(child, {filter});
+ });
return (
- <div className='backstage-content row'>
+ <div className='backstage-content'>
<div className='installed-integrations'>
<div className='backstage-header'>
<h1>
- <FormattedMessage
- id='installed_integrations.header'
- defaultMessage='Installed Integrations'
- />
+ {this.props.header}
</h1>
<Link
className='add-integrations-link'
- to={'/settings/integrations/add'}
+ to={this.props.addLink}
>
<button
type='button'
className='btn btn-primary'
>
<span>
- <FormattedMessage
- id='installed_integrations.add'
- defaultMessage='Add Integration'
- />
+ {this.props.addText}
</span>
</button>
</Link>
</div>
<div className='backstage-filters'>
- {this.renderTypeFilters(incomingWebhooks, outgoingWebhooks, commands)}
<div className='backstage-filter__search'>
<i className='fa fa-search'></i>
<input
@@ -376,7 +75,7 @@ export default class InstalledIntegrations extends React.Component {
</div>
</div>
<div className='backstage-list'>
- {integrations}
+ {children}
</div>
</div>
</div>
diff --git a/webapp/components/backstage/installed_outgoing_webhook.jsx b/webapp/components/backstage/installed_outgoing_webhook.jsx
index 530474dc3..b8704ccef 100644
--- a/webapp/components/backstage/installed_outgoing_webhook.jsx
+++ b/webapp/components/backstage/installed_outgoing_webhook.jsx
@@ -13,7 +13,8 @@ export default class InstalledOutgoingWebhook extends React.Component {
return {
outgoingWebhook: React.PropTypes.object.isRequired,
onRegenToken: React.PropTypes.func.isRequired,
- onDelete: React.PropTypes.func.isRequired
+ onDelete: React.PropTypes.func.isRequired,
+ filter: React.PropTypes.string
};
}
@@ -36,29 +37,82 @@ export default class InstalledOutgoingWebhook extends React.Component {
this.props.onDelete(this.props.outgoingWebhook);
}
+ matchesFilter(outgoingWebhook, channel, filter) {
+ if (!filter) {
+ return true;
+ }
+
+ if (outgoingWebhook.display_name.toLowerCase().indexOf(filter) !== -1 ||
+ outgoingWebhook.description.toLowerCase().indexOf(filter) !== -1) {
+ return true;
+ }
+
+ for (const trigger of outgoingWebhook.trigger_words) {
+ if (trigger.toLowerCase().indexOf(filter) !== -1) {
+ return true;
+ }
+ }
+
+ if (channel) {
+ if (channel && channel.name.toLowerCase().indexOf(filter) !== -1) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
render() {
const outgoingWebhook = this.props.outgoingWebhook;
-
const channel = ChannelStore.get(outgoingWebhook.channel_id);
- const channelName = channel ? channel.display_name : 'cannot find channel';
+
+ if (!this.matchesFilter(outgoingWebhook, channel, this.props.filter)) {
+ return null;
+ }
+
+ let displayName;
+ if (outgoingWebhook.display_name) {
+ displayName = outgoingWebhook.display_name;
+ } else if (channel) {
+ displayName = channel.display_name;
+ } else {
+ displayName = (
+ <FormattedMessage
+ id='installed_outgoing_webhooks.unknown_channel'
+ defaultMessage='A Private Webhook'
+ />
+ );
+ }
+
+ let description = null;
+ if (outgoingWebhook.description) {
+ description = (
+ <div className='item-details__row'>
+ <span className='item-details__description'>
+ {outgoingWebhook.description}
+ </span>
+ </div>
+ );
+ }
return (
<div className='backstage-list__item'>
<div className='item-details'>
<div className='item-details__row'>
<span className='item-details__name'>
- {outgoingWebhook.display_name || channelName}
- </span>
- <span className='item-details__type'>
- <FormattedMessage
- id='installed_integrations.outgoingWebhookType'
- defaultMessage='(Outgoing Webhook)'
- />
+ {displayName}
</span>
</div>
+ {description}
<div className='item-details__row'>
- <span className='item-details__description'>
- {outgoingWebhook.description}
+ <span className='item-details__token'>
+ <FormattedMessage
+ id='installed_integrations.token'
+ defaultMessage='Token: {token}'
+ values={{
+ token: outgoingWebhook.token
+ }}
+ />
</span>
</div>
<div className='item-details__row'>
@@ -98,4 +152,21 @@ export default class InstalledOutgoingWebhook extends React.Component {
</div>
);
}
+
+ static matches(outgoingWebhook, filter) {
+ if (outgoingWebhook.display_name.toLowerCase().indexOf(filter) !== -1 ||
+ outgoingWebhook.description.toLowerCase().indexOf(filter) !== -1) {
+ return true;
+ }
+
+ if (outgoingWebhook.channel_id) {
+ const channel = ChannelStore.get(outgoingWebhook.channel_id);
+
+ if (channel && channel.name.toLowerCase().indexOf(filter) !== -1) {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/webapp/components/backstage/installed_outgoing_webhooks.jsx b/webapp/components/backstage/installed_outgoing_webhooks.jsx
new file mode 100644
index 000000000..15d927a41
--- /dev/null
+++ b/webapp/components/backstage/installed_outgoing_webhooks.jsx
@@ -0,0 +1,91 @@
+// 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 IntegrationStore from 'stores/integration_store.jsx';
+
+import {FormattedMessage} from 'react-intl';
+import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx';
+import InstalledIntegrations from './installed_integrations.jsx';
+
+export default class InstalledOutgoingWebhooks extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleIntegrationChange = this.handleIntegrationChange.bind(this);
+
+ this.regenOutgoingWebhookToken = this.regenOutgoingWebhookToken.bind(this);
+ this.deleteOutgoingWebhook = this.deleteOutgoingWebhook.bind(this);
+
+ this.state = {
+ outgoingWebhooks: []
+ };
+ }
+
+ componentWillMount() {
+ IntegrationStore.addChangeListener(this.handleIntegrationChange);
+
+ 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({
+ outgoingWebhooks: IntegrationStore.getOutgoingWebhooks()
+ });
+ }
+
+ regenOutgoingWebhookToken(outgoingWebhook) {
+ AsyncClient.regenOutgoingHookToken(outgoingWebhook.id);
+ }
+
+ deleteOutgoingWebhook(outgoingWebhook) {
+ AsyncClient.deleteOutgoingHook(outgoingWebhook.id);
+ }
+
+ render() {
+ const outgoingWebhooks = this.state.outgoingWebhooks.map((outgoingWebhook) => {
+ return (
+ <InstalledOutgoingWebhook
+ key={outgoingWebhook.id}
+ outgoingWebhook={outgoingWebhook}
+ onRegenToken={this.regenOutgoingWebhookToken}
+ onDelete={this.deleteOutgoingWebhook}
+ />
+ );
+ });
+
+ return (
+ <InstalledIntegrations
+ header={
+ <FormattedMessage
+ id='installed_outgoing_webhooks.header'
+ defaultMessage='Installed Outgoing Webhooks'
+ />
+ }
+ addText={
+ <FormattedMessage
+ id='installed_outgoing_webhooks.add'
+ defaultMessage='Add Outgoing Webhook'
+ />
+ }
+ addLink='/settings/integrations/outgoing_webhooks/add'
+ >
+ {outgoingWebhooks}
+ </InstalledIntegrations>
+ );
+ }
+}
diff --git a/webapp/components/backstage/add_integration_option.jsx b/webapp/components/backstage/integration_option.jsx
index b17ebb185..dd7cc0c4c 100644
--- a/webapp/components/backstage/add_integration_option.jsx
+++ b/webapp/components/backstage/integration_option.jsx
@@ -5,7 +5,7 @@ import React from 'react';
import {Link} from 'react-router';
-export default class AddIntegrationOption extends React.Component {
+export default class IntegrationOption extends React.Component {
static get propTypes() {
return {
image: React.PropTypes.string.isRequired,
@@ -21,16 +21,16 @@ export default class AddIntegrationOption extends React.Component {
return (
<Link
to={link}
- className='add-integration'
+ className='integration-option'
>
<img
- className='add-integration__image'
+ className='integration-option__image'
src={image}
/>
- <div className='add-integration__title'>
+ <div className='integration-option__title'>
{title}
</div>
- <div className='add-integration__description'>
+ <div className='integration-option__description'>
{description}
</div>
</Link>
diff --git a/webapp/components/backstage/add_integration.jsx b/webapp/components/backstage/integrations.jsx
index 0ab36e101..71232ea45 100644
--- a/webapp/components/backstage/add_integration.jsx
+++ b/webapp/components/backstage/integrations.jsx
@@ -4,76 +4,76 @@
import React from 'react';
import {FormattedMessage} from 'react-intl';
-import AddIntegrationOption from './add_integration_option.jsx';
+import IntegrationOption from './integration_option.jsx';
import WebhookIcon from 'images/webhook_icon.jpg';
-export default class AddIntegration extends React.Component {
+export default class Integrations extends React.Component {
render() {
const options = [];
if (window.mm_config.EnableIncomingWebhooks === 'true') {
options.push(
- <AddIntegrationOption
+ <IntegrationOption
key='incomingWebhook'
image={WebhookIcon}
title={
<FormattedMessage
- id='add_integration.incomingWebhook.title'
+ id='integrations.incomingWebhook.title'
defaultMessage='Incoming Webhook'
/>
}
description={
<FormattedMessage
- id='add_integration.incomingWebhook.description'
- defaultMessage='Create webhook URLs for use in external integrations.'
+ id='integrations.incomingWebhook.description'
+ defaultMessage='Incoming webhooks allow external integrations to send messages'
/>
}
- link={'/settings/integrations/add/incoming_webhook'}
+ link={'/settings/integrations/incoming_webhooks'}
/>
);
}
if (window.mm_config.EnableOutgoingWebhooks === 'true') {
options.push(
- <AddIntegrationOption
+ <IntegrationOption
key='outgoingWebhook'
image={WebhookIcon}
title={
<FormattedMessage
- id='add_integration.outgoingWebhook.title'
+ id='integrations.outgoingWebhook.title'
defaultMessage='Outgoing Webhook'
/>
}
description={
<FormattedMessage
- id='add_integration.outgoingWebhook.description'
- defaultMessage='Create webhooks to send new message events to an external integration.'
+ id='integrations.outgoingWebhook.description'
+ defaultMessage='Outgoing webhooks allow external integrations to receive and respond to messages'
/>
}
- link={'/settings/integrations/add/outgoing_webhook'}
+ link={'/settings/integrations/outgoing_webhooks'}
/>
);
}
if (window.mm_config.EnableCommands === 'true') {
options.push(
- <AddIntegrationOption
+ <IntegrationOption
key='command'
image={WebhookIcon}
title={
<FormattedMessage
- id='add_integration.command.title'
+ id='integrations.command.title'
defaultMessage='Slash Command'
/>
}
description={
<FormattedMessage
- id='add_integration.command.description'
- defaultMessage='Create slash commands to send events to external integrations and receive a response.'
+ id='integrations.command.description'
+ defaultMessage='Slash commands send events to an external integration'
/>
}
- link={'/settings/integrations/add/command'}
+ link={'/settings/integrations/commands'}
/>
);
}
@@ -83,8 +83,8 @@ export default class AddIntegration extends React.Component {
<div className='backstage-header'>
<h1>
<FormattedMessage
- id='add_integration.header'
- defaultMessage='Add Integration'
+ id='integrations.header'
+ defaultMessage='Integrations'
/>
</h1>
</div>