From 77ee1ce7fee698847e211dc15d4673300901aa48 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Fri, 8 Apr 2016 11:51:28 -0400 Subject: 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 --- webapp/components/backstage/add_command.jsx | 27 +- .../components/backstage/add_incoming_webhook.jsx | 29 +- webapp/components/backstage/add_integration.jsx | 98 ------ .../backstage/add_integration_option.jsx | 39 --- .../components/backstage/add_outgoing_webhook.jsx | 29 +- webapp/components/backstage/backstage_header.jsx | 39 +++ webapp/components/backstage/backstage_section.jsx | 1 - webapp/components/backstage/backstage_sidebar.jsx | 51 +--- webapp/components/backstage/installed_command.jsx | 64 +++- webapp/components/backstage/installed_commands.jsx | 93 ++++++ .../backstage/installed_incoming_webhook.jsx | 67 ++++- .../backstage/installed_incoming_webhooks.jsx | 85 ++++++ .../backstage/installed_integrations.jsx | 335 ++------------------- .../backstage/installed_outgoing_webhook.jsx | 95 +++++- .../backstage/installed_outgoing_webhooks.jsx | 91 ++++++ webapp/components/backstage/integration_option.jsx | 39 +++ webapp/components/backstage/integrations.jsx | 98 ++++++ webapp/i18n/en.json | 44 ++- webapp/root.jsx | 40 ++- webapp/sass/routes/_backstage.scss | 11 +- 20 files changed, 769 insertions(+), 606 deletions(-) delete mode 100644 webapp/components/backstage/add_integration.jsx delete mode 100644 webapp/components/backstage/add_integration_option.jsx create mode 100644 webapp/components/backstage/backstage_header.jsx create mode 100644 webapp/components/backstage/installed_commands.jsx create mode 100644 webapp/components/backstage/installed_incoming_webhooks.jsx create mode 100644 webapp/components/backstage/installed_outgoing_webhooks.jsx create mode 100644 webapp/components/backstage/integration_option.jsx create mode 100644 webapp/components/backstage/integrations.jsx 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 (
-
-
-

- -

-
-
+ + + + + +
@@ -479,7 +482,7 @@ export default class AddCommand extends React.Component { { - 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 ( -
-
-
-

- -

-
-
+
+ + + + + +
@@ -176,7 +179,7 @@ export default class AddIncomingWebhook extends React.Component { - } - description={ - - } - link={'/settings/integrations/add/incoming_webhook'} - /> - ); - } - - if (window.mm_config.EnableOutgoingWebhooks === 'true') { - options.push( - - } - description={ - - } - link={'/settings/integrations/add/outgoing_webhook'} - /> - ); - } - - if (window.mm_config.EnableCommands === 'true') { - options.push( - - } - description={ - - } - link={'/settings/integrations/add/command'} - /> - ); - } - - return ( -
-
-

- -

-
-
- {options} -
-
- ); - } -} - diff --git a/webapp/components/backstage/add_integration_option.jsx b/webapp/components/backstage/add_integration_option.jsx deleted file mode 100644 index b17ebb185..000000000 --- a/webapp/components/backstage/add_integration_option.jsx +++ /dev/null @@ -1,39 +0,0 @@ -// 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 ( - - -
- {title} -
-
- {description} -
- - ); - } -} 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 ( -
-
-
-

- -

-
-
+
+ + + + + +
@@ -250,7 +253,7 @@ export default class AddOutgoingWebhook extends React.Component { { + if (index !== 0) { + children.push( + + {'>'} + + ); + } + + children.push(child); + }); + + return ( +
+

+ {children} +

+
+ ); + } +} 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 { 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 { } > )} /> )} - > - - )} - /> - - )} - /> - - )} - /> - + /> + + )} + />
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 = ( + + ); + } + + let description = null; + if (command.description) { + description = ( +
+ + {command.description} + +
+ ); + } + return (
- {command.display_name} + {name} - - + + {'- /' + command.trigger}
+ {description}
- - {command.description} + +
@@ -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 ( + + ); + }); + + return ( + + } + addText={ + + } + addLink='/settings/integrations/commands/add' + > + {commands} + + ); + } +} 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 = ( + + ); + } + + let description = null; + if (incomingWebhook.description) { + description = ( +
+ + {incomingWebhook.description} + +
+ ); + } return (
- {incomingWebhook.display_name || channelName} - - - - -
-
- - {incomingWebhook.description} + {displayName}
+ {description}
{ + return ( + + ); + }); + + return ( + + } + addText={ + + } + addLink='/settings/integrations/incoming_webhooks/add' + > + {incomingWebhooks} + + ); + } +} 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( - this.updateTypeFilter(e, '')} - > - - - ); - } - - if (incomingWebhooks.length > 0) { - fields.push( - - {'|'} - - ); - - let filterClassName = 'filter-sort'; - if (this.state.typeFilter === 'incomingWebhooks') { - filterClassName += ' filter-sort--active'; - } - - fields.push( - this.updateTypeFilter(e, 'incomingWebhooks')} - > - - - ); - } - - if (outgoingWebhooks.length > 0) { - fields.push( - - {'|'} - - ); - - let filterClassName = 'filter-sort'; - if (this.state.typeFilter === 'outgoingWebhooks') { - filterClassName += ' filter-sort--active'; - } - - fields.push( - this.updateTypeFilter(e, 'outgoingWebhooks')} - > - - - ); - } - - if (commands.length > 0) { - fields.push( - - {'|'} - - ); - - let filterClassName = 'filter-sort'; - if (this.state.typeFilter === 'commands') { - filterClassName += ' filter-sort--active'; - } - - fields.push( - this.updateTypeFilter(e, 'commands')} - > - - - ); - } - - return ( -
- {fields} -
- ); - } - 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( - - ); - } - } - - 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( - - ); - } - } - - 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( - - ); - } - } + const children = React.Children.map(this.props.children, (child) => { + return React.cloneElement(child, {filter}); + }); return ( -
+

- + {this.props.header}

- {this.renderTypeFilters(incomingWebhooks, outgoingWebhooks, commands)}
- {integrations} + {children}
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 = ( + + ); + } + + let description = null; + if (outgoingWebhook.description) { + description = ( +
+ + {outgoingWebhook.description} + +
+ ); + } return (
- {outgoingWebhook.display_name || channelName} - - - + {displayName}
+ {description}
- - {outgoingWebhook.description} + +
@@ -98,4 +152,21 @@ export default class InstalledOutgoingWebhook extends React.Component {
); } + + 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 ( + + ); + }); + + return ( + + } + addText={ + + } + addLink='/settings/integrations/outgoing_webhooks/add' + > + {outgoingWebhooks} + + ); + } +} diff --git a/webapp/components/backstage/integration_option.jsx b/webapp/components/backstage/integration_option.jsx new file mode 100644 index 000000000..dd7cc0c4c --- /dev/null +++ b/webapp/components/backstage/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 IntegrationOption 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 ( + + +
+ {title} +
+
+ {description} +
+ + ); + } +} diff --git a/webapp/components/backstage/integrations.jsx b/webapp/components/backstage/integrations.jsx new file mode 100644 index 000000000..71232ea45 --- /dev/null +++ b/webapp/components/backstage/integrations.jsx @@ -0,0 +1,98 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import {FormattedMessage} from 'react-intl'; +import IntegrationOption from './integration_option.jsx'; + +import WebhookIcon from 'images/webhook_icon.jpg'; + +export default class Integrations extends React.Component { + render() { + const options = []; + + if (window.mm_config.EnableIncomingWebhooks === 'true') { + options.push( + + } + description={ + + } + link={'/settings/integrations/incoming_webhooks'} + /> + ); + } + + if (window.mm_config.EnableOutgoingWebhooks === 'true') { + options.push( + + } + description={ + + } + link={'/settings/integrations/outgoing_webhooks'} + /> + ); + } + + if (window.mm_config.EnableCommands === 'true') { + options.push( + + } + description={ + + } + link={'/settings/integrations/commands'} + /> + ); + } + + return ( +
+
+

+ +

+
+
+ {options} +
+
+ ); + } +} + diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 12671284a..fd8f44c36 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -37,7 +37,7 @@ "add_command.autocompleteHint.placeholder": "Example: [Patient Name]", "add_command.description": "Description", "add_command.displayName": "Display Name", - "add_command.header": "Add Slash Command", + "add_command.header": "Add", "add_command.iconUrl": "Response Icon", "add_command.iconUrl.help": "Choose a profile picture override for the post responses to this slash command. Enter the URL of a .png or .jpg file at least 128 pixels by 128 pixels.", "add_command.iconUrl.placeholder": "https://www.example.com/myicon.png", @@ -61,22 +61,15 @@ "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.header": "Add", "add_incoming_webhook.name": "Name", "add_incoming_webhook.save": "Save", - "add_integration.command.description": "Create slash commands to send events to external integrations and receive a response.", - "add_integration.command.title": "Slash Command", - "add_integration.header": "Add Integration", - "add_integration.incomingWebhook.description": "Create webhook URLs for use in external integrations.", - "add_integration.incomingWebhook.title": "Incoming Webhook", - "add_integration.outgoingWebhook.description": "Create webhooks to send new message events to an external integration.", - "add_integration.outgoingWebhook.title": "Outgoing Webhook", "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.header": "Add", "add_outgoing_webhook.name": "Name", "add_outgoing_webhook.save": "Save", "add_outgoing_webhook.triggerWOrds": "Trigger Words (One Per Line)", @@ -624,11 +617,9 @@ "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.add": "Add Integration", - "backstage_sidebar.integrations.add.command": "Slash Command", - "backstage_sidebar.integrations.add.incomingWebhook": "Incoming Webhook", - "backstage_sidebar.integrations.add.outgoingWebhook": "Outgoing Webhook", - "backstage_sidebar.integrations.installed": "Installed Integrations", + "backstage_sidebar.integrations.incoming_webhooks": "Incoming Webhooks", + "backstage_sidebar.integrations.outgoing_webhooks": "Outgoing Webhooks", + "backstage_sidebar.integrations.commands": "Commands", "center_panel.recent": "Click here to jump to recent messages. ", "chanel_header.addMembers": "Add Members", "change_url.close": "Close", @@ -850,19 +841,24 @@ "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 ({count})", - "installed_integrations.commandType": "(Slash Command)", - "installed_integrations.commandsFilter": "Slash Commands ({count})", + "installed_commands.add": "Add Slash Command", + "installed_commands.header": "Slash Commands", + "installed_incoming_webhooks.add": "Add Incoming Webhook", + "installed_incoming_webhooks.header": "Incoming Webhooks", "installed_integrations.creation": "Created by {creator} on {createAt, date, full}", "installed_integrations.delete": "Delete", - "installed_integrations.header": "Installed Integrations", - "installed_integrations.incomingWebhookType": "(Incoming Webhook)", - "installed_integrations.incomingWebhooksFilter": "Incoming Webhooks ({count})", - "installed_integrations.outgoingWebhookType": "(Outgoing Webhook)", - "installed_integrations.outgoingWebhooksFilter": "Outgoing Webhooks ({count})", "installed_integrations.regenToken": "Regen Token", "installed_integrations.search": "Search Integrations", + "installed_integrations.token": "Token: {token}", + "installed_outgoing_webhooks.add": "Add Outgoing Webhook", + "installed_outgoing_webhooks.header": "Outgoing Webhooks", + "integrations.command.description": "Slash commands send events to external integrations", + "integrations.command.title": "Slash Command", + "integrations.header": "Integrations", + "integrations.incomingWebhook.description": "Incoming webhooks allow external integrations to send messages", + "integrations.incomingWebhook.title": "Incoming Webhook", + "integrations.outgoingWebhook.description": "Outgoing webhooks allow external integrations to receive and respond to messages", + "integrations.outgoingWebhook.title": "Outgoing Webhook", "intro_messages.DM": "This is the start of your direct message history with {teammate}.
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}", diff --git a/webapp/root.jsx b/webapp/root.jsx index a76f7cf7e..9268643f3 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -38,8 +38,10 @@ 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 Integrations from 'components/backstage/integrations.jsx'; +import InstalledIncomingWebhooks from 'components/backstage/installed_incoming_webhooks.jsx'; +import InstalledOutgoingWebhooks from 'components/backstage/installed_outgoing_webhooks.jsx'; +import InstalledCommands from 'components/backstage/installed_commands.jsx'; import AddIncomingWebhook from 'components/backstage/add_incoming_webhook.jsx'; import AddOutgoingWebhook from 'components/backstage/add_outgoing_webhook.jsx'; import AddCommand from 'components/backstage/add_command.jsx'; @@ -253,41 +255,57 @@ function renderRootComponent() { onEnter={onLoggedOut} /> - - - + + + + + + +