summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
authorHarrison Healey <harrisonmhealey@gmail.com>2016-03-17 10:30:49 -0400
committerHarrison Healey <harrisonmhealey@gmail.com>2016-03-29 15:18:26 -0400
commitc417fdc152e953982d9c9af2c04ca2c04ced41b3 (patch)
tree6bf1f8618474d3e60bbe844876de665407f80095 /webapp
parent9c36210edd7cae4026e3a2ee472cf2fa751a0f77 (diff)
downloadchat-c417fdc152e953982d9c9af2c04ca2c04ced41b3.tar.gz
chat-c417fdc152e953982d9c9af2c04ca2c04ced41b3.tar.bz2
chat-c417fdc152e953982d9c9af2c04ca2c04ced41b3.zip
Added initial backstage components and InstalledIntegrations page
Diffstat (limited to 'webapp')
-rw-r--r--webapp/components/backstage/backstage_category.jsx68
-rw-r--r--webapp/components/backstage/backstage_navbar.jsx62
-rw-r--r--webapp/components/backstage/backstage_section.jsx122
-rw-r--r--webapp/components/backstage/backstage_sidebar.jsx113
-rw-r--r--webapp/components/backstage/installed_integrations.jsx304
-rw-r--r--webapp/components/logged_in.jsx10
-rw-r--r--webapp/root.jsx48
-rw-r--r--webapp/sass/routes/_backstage.scss182
-rw-r--r--webapp/sass/routes/_module.scss1
-rw-r--r--webapp/stores/file_store.jsx5
-rw-r--r--webapp/stores/integration_store.jsx80
-rw-r--r--webapp/utils/async_client.jsx46
-rw-r--r--webapp/utils/constants.jsx2
-rw-r--r--webapp/utils/utils.jsx10
14 files changed, 1045 insertions, 8 deletions
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..8ba8669c5
--- /dev/null
+++ b/webapp/components/backstage/backstage_navbar.jsx
@@ -0,0 +1,62 @@
+// 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.back_to_mattermost'
+ defaultMessage='Back to {siteName}'
+ values={{
+ siteName: global.window.mm_config.SiteName
+ }}
+ />
+ </span>
+ </Link>
+ <span style={{float: 'right'}}>{'TODO: Switch Teams'}</span>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/backstage/backstage_section.jsx b/webapp/components/backstage/backstage_section.jsx
new file mode 100644
index 000000000..41ce766bd
--- /dev/null
+++ b/webapp/components/backstage/backstage_section.jsx
@@ -0,0 +1,122 @@
+// 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
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleClick = this.handleClick.bind(this);
+
+ this.state = {
+ expanded: true
+ };
+ }
+
+ getLink() {
+ return this.props.parentLink + '/' + this.props.name;
+ }
+
+ isActive() {
+ const link = this.getLink();
+
+ return this.context.router.isActive(link);
+ }
+
+ handleClick(e) {
+ if (this.isActive()) {
+ // we're already on this page so just toggle the link
+ e.preventDefault();
+
+ this.setState({
+ expanded: !this.state.expanded
+ });
+ }
+
+ // otherwise, just follow the link
+ }
+
+ render() {
+ const {title, subsection, children} = this.props;
+
+ const link = this.getLink();
+ const active = this.isActive();
+
+ // act like docs.mattermost.com and only expand if this link is active
+ const expanded = active && this.state.expanded;
+
+ let toggle = null;
+ if (children.length > 0) {
+ if (expanded) {
+ toggle = <i className='fa fa-minus-square-o'/>;
+ } else {
+ toggle = <i className='fa fa-plus-square-o'/>;
+ }
+ }
+
+ let clonedChildren = null;
+ if (children.length > 0 && expanded) {
+ 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`}
+ onClick={this.handleClick}
+ to={link}
+ >
+ {toggle}
+ <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..672005333
--- /dev/null
+++ b/webapp/components/backstage/backstage_sidebar.jsx
@@ -0,0 +1,113 @@
+// 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 BackstageCategory from './backstage_category.jsx';
+import BackstageSection from './backstage_section.jsx';
+import {FormattedMessage} from 'react-intl';
+
+export default class BackstageSidebar 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() {
+ const team = TeamStore.getCurrent();
+
+ if (!team) {
+ return null;
+ }
+
+ return (
+ <div className='backstage__sidebar'>
+ <ul>
+ <BackstageCategory
+ name='team_settings'
+ parentLink={`/${team.name}`}
+ icon='fa-users'
+ title={
+ <FormattedMessage
+ id='backstage.team_settings'
+ defaultMessage='Team Settings'
+ />
+ }
+ />
+ <BackstageCategory
+ name='integrations'
+ parentLink={`/${team.name}`}
+ icon='fa-link'
+ title={
+ <FormattedMessage
+ id='backstage.integrations'
+ defaultMessage='Integrations'
+ />
+ }
+ >
+ <BackstageSection
+ name='installed'
+ title={(
+ <FormattedMessage
+ id='backstage.integrations.installed'
+ defaultMessage='Installed Integrations'
+ />
+ )}
+ />
+ <BackstageSection
+ name='add'
+ title={(
+ <FormattedMessage
+ id='backstage.integrations.add'
+ defaultMessage='Add Integration'
+ />
+ )}
+ collapsible={true}
+ >
+ <BackstageSection
+ name='incoming_webhook'
+ title={(
+ <FormattedMessage
+ id='backstage.integrations.add.incomingWebhook'
+ defaultMessage='Incoming Webhook'
+ />
+ )}
+ />
+ <BackstageSection
+ name='outgoing_webhook'
+ title={(
+ <FormattedMessage
+ id='backstage.integrations.add.outgoingWebhook'
+ defaultMessage='Outgoing Webhook'
+ />
+ )}
+ />
+ </BackstageSection>
+ </BackstageCategory>
+ </ul>
+ </div>
+ );
+ }
+}
+
diff --git a/webapp/components/backstage/installed_integrations.jsx b/webapp/components/backstage/installed_integrations.jsx
new file mode 100644
index 000000000..cfb68c660
--- /dev/null
+++ b/webapp/components/backstage/installed_integrations.jsx
@@ -0,0 +1,304 @@
+// 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 {Link} from 'react-router';
+
+export default class InstalledIntegrations extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.setFilter = this.setFilter.bind(this);
+
+ this.state = {
+ incomingWebhooks: [],
+ outgoingWebhooks: [],
+ filter: ''
+ };
+ }
+
+ componentWillMount() {
+ IntegrationStore.addChangeListener(this.handleChange);
+
+ 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.handleChange);
+ }
+
+ handleChange() {
+ this.setState({
+ incomingWebhooks: IntegrationStore.getIncomingWebhooks(),
+ outgoingWebhooks: IntegrationStore.getOutgoingWebhooks()
+ });
+ }
+
+ setFilter(e, filter) {
+ e.preventDefault();
+
+ this.setState({
+ filter
+ });
+ }
+
+ renderTypeFilters(incomingWebhooks, outgoingWebhooks) {
+ const fields = [];
+
+ if (incomingWebhooks.length > 0 || outgoingWebhooks.length > 0) {
+ let filterClassName = 'type-filter';
+ if (this.state.filter === '') {
+ filterClassName += ' type-filter--selected';
+ }
+
+ fields.push(
+ <a
+ key='allFilter'
+ className={filterClassName}
+ href='#'
+ onClick={(e) => this.setFilter(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.filter === 'incomingWebhooks') {
+ filterClassName += ' type-filter--selected';
+ }
+
+ fields.push(
+ <a
+ key='incomingWebhooksFilter'
+ className={filterClassName}
+ href='#'
+ onClick={(e) => this.setFilter(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.filter === 'outgoingWebhooks') {
+ filterClassName += ' type-filter--selected';
+ }
+
+ fields.push(
+ <a
+ key='outgoingWebhooksFilter'
+ className={filterClassName}
+ href='#'
+ onClick={(e) => this.setFilter(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 integrations = [];
+ if (!this.state.filter || this.state.filter === 'incomingWebhooks') {
+ for (const incomingWebhook of incomingWebhooks) {
+ integrations.push(
+ <IncomingWebhook
+ key={incomingWebhook.id}
+ incomingWebhook={incomingWebhook}
+ />
+ );
+ }
+ }
+
+ if (!this.state.filter || this.state.filter === 'outgoingWebhooks') {
+ for (const outgoingWebhook of outgoingWebhooks) {
+ integrations.push(
+ <OutgoingWebhook
+ key={outgoingWebhook.id}
+ outgoingWebhook={outgoingWebhook}
+ />
+ );
+ }
+ }
+
+ return (
+ <div className='backstage row'>
+ <div className='installed-integrations'>
+ <div className='installed-integrations__header'>
+ <h1 className='text'>
+ <FormattedMessage
+ id='installed_integrations.header'
+ defaultMessage='Installed Integrations'
+ />
+ </h1>
+ <Link
+ className='add-integrations-link'
+ to={'/yourteamhere/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')}
+ style={{flexGrow: 0, flexShrink: 0}}
+ />
+ </div>
+ <div className='installed-integrations__list'>
+ {integrations}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+function IncomingWebhook({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>
+ );
+}
+
+IncomingWebhook.propTypes = {
+ incomingWebhook: React.PropTypes.object.isRequired
+};
+
+function OutgoingWebhook({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}
+ </span>
+ </div>
+ </div>
+ </div>
+ );
+}
+
+OutgoingWebhook.propTypes = {
+ outgoingWebhook: React.PropTypes.object.isRequired
+};
diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx
index 53db501bf..fd09aac9e 100644
--- a/webapp/components/logged_in.jsx
+++ b/webapp/components/logged_in.jsx
@@ -200,6 +200,9 @@ export default class LoggedIn extends React.Component {
content = this.props.children;
} else {
content.push(
+ this.props.navbar
+ );
+ content.push(
this.props.sidebar
);
content.push(
@@ -247,8 +250,9 @@ LoggedIn.defaultProps = {
};
LoggedIn.propTypes = {
- children: React.PropTypes.object,
- sidebar: React.PropTypes.object,
- center: React.PropTypes.object,
+ children: React.PropTypes.arrayOf(React.PropTypes.element),
+ navbar: React.PropTypes.element,
+ sidebar: React.PropTypes.element,
+ center: React.PropTypes.element,
params: React.PropTypes.object
};
diff --git a/webapp/root.jsx b/webapp/root.jsx
index ce59a95c9..c10099967 100644
--- a/webapp/root.jsx
+++ b/webapp/root.jsx
@@ -36,6 +36,9 @@ import ShouldVerifyEmail from 'components/should_verify_email.jsx';
import DoVerifyEmail from 'components/do_verify_email.jsx';
import AdminConsole from 'components/admin_console/admin_controller.jsx';
import TutorialView from 'components/tutorial/tutorial_view.jsx';
+import BackstageNavbar from 'components/backstage/backstage_navbar.jsx';
+import BackstageSidebar from 'components/backstage/backstage_sidebar.jsx';
+import InstalledIntegrations from 'components/backstage/installed_integrations.jsx';
import SignupTeamComplete from 'components/signup_team_complete/components/signup_team_complete.jsx';
import WelcomePage from 'components/signup_team_complete/components/team_signup_welcome_page.jsx';
@@ -242,6 +245,51 @@ function renderRootComponent() {
onEnter={onLoggedOut}
/>
<Route
+ path=':team/team_settings'
+ components={{
+ navbar: BackstageNavbar,
+ sidebar: BackstageSidebar,
+ center: null
+ }}
+ />
+ <Route path=':team/integrations'>
+ <IndexRedirect to='installed'/>
+ <Route
+ path='installed'
+ components={{
+ navbar: BackstageNavbar,
+ sidebar: BackstageSidebar,
+ center: InstalledIntegrations
+ }}
+ />
+ <Route path='add'>
+ <IndexRoute
+ components={{
+ navbar: BackstageNavbar,
+ sidebar: BackstageSidebar,
+ center: null
+ }}
+ />
+ <Route
+ path='incoming_webhook'
+ components={{
+ navbar: BackstageNavbar,
+ sidebar: BackstageSidebar,
+ center: null
+ }}
+ />
+ <Route
+ path='outgoing_webhook'
+ components={{
+ navbar: BackstageNavbar,
+ sidebar: BackstageSidebar,
+ center: null
+ }}
+ />
+ </Route>
+ </Route>
+>>>>>>> Added initial backstage components and InstalledIntegrations page
+ <Route
path='admin_console'
component={AdminConsole}
/>
diff --git a/webapp/sass/routes/_backstage.scss b/webapp/sass/routes/_backstage.scss
new file mode 100644
index 000000000..4619e4cbc
--- /dev/null
+++ b/webapp/sass/routes/_backstage.scss
@@ -0,0 +1,182 @@
+.backstage {
+ background-color: #f2f2f2;
+ height: 100%;
+ padding-left: 260px;
+ padding-top: 45px;
+}
+
+.backstage__navbar {
+ background: white;
+ border-bottom: lightgray 1px solid;
+ margin: 0 -15px;
+ padding: 10px;
+ z-index: 10;
+}
+
+.backstage__navbar__back {
+ color: black;
+
+ .fa {
+ font-weight: bold;
+ margin-right: 5px;
+ }
+}
+
+.backstage__sidebar {
+ position: absolute;
+ left: 0;
+ width: 260px;
+ height: 100%;
+ background-color: #f2f2f2;
+ padding-bottom: 20px;
+ padding-left: 20px;
+ padding-right: 20px;
+ padding-top: 45px;
+ z-index: 5;
+
+ ul {
+ padding: 0px;
+ list-style: none;
+ }
+}
+
+.backstage__sidebar__category {
+ border: lightgray 1px solid;
+
+ .category-title {
+ color: gray;
+ display: block;
+ padding: 5px 10px;
+ position: relative;
+ }
+
+ .category-title--active {
+ color: black;
+ }
+
+ .category-title__text {
+ position: absolute;
+ left: 2em;
+ }
+
+ .sections {
+ background: white;
+ border-top: lightgray 1px solid;
+ }
+
+ .section-title {
+ display: block;
+ padding-left: 2em;
+ }
+
+ .subsection {
+ }
+
+ .subsection-title {
+ display: block;
+ padding-left: 3em;
+ }
+
+ .section-title--active, .subsection-title--active {
+ background-color:#2388d6;
+ color: white;
+ }
+}
+
+.backstage__sidebar__category + .backstage__sidebar__category {
+ border-top-width: 0px;
+}
+
+.installed-integrations {
+ height: 100%;
+ max-width: 958px;
+}
+
+.installed-integrations__header {
+ margin-bottom: 20px;
+ width: 100%;
+
+ .text {
+ display: inline;
+ }
+
+ .add-integrations-link {
+ float: right;
+ }
+}
+
+.installed-integrations__filters {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 8px;
+ width: 100%;
+
+ .type-filters {
+ flex-grow: 1;
+ flex-shrink: 0;
+
+ .type-filter {
+ &--selected {
+ color: black;
+ cursor: default;
+ font-weight: bold;
+ }
+ }
+
+ .divider {
+ margin-left: 8px;
+ margin-right: 8px;
+ }
+ }
+
+ .filter-box {
+ flex-grow: 0;
+ flex-shrink: 0;
+ }
+}
+
+.installed-integrations__list {
+ background-color: white;
+ border: lightgray 1px solid;
+ padding-bottom: 30px;
+ padding-left: 30px;
+ padding-right: 30px;
+ padding-top: 10px;
+}
+
+.installed-integrations__item {
+ border-bottom: lightgray 1px solid;
+ display: flex;
+ padding: 20px;
+
+ .details {
+ flex-grow: 1;
+ flex-shrink: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ .details-row + .details-row {
+ margin-top: 15px;
+ }
+
+ .name {
+ font-weight: bold;
+ margin-bottom: 1em;
+ }
+
+ .type {
+ margin-left: 6px;
+ }
+
+ .description {
+ color: gray;
+ margin-bottom: 1em;
+ }
+ }
+
+ .actions {
+ flex-grow: 0;
+ flex-shrink: 0;
+ padding-left: 20px;
+ }
+}
diff --git a/webapp/sass/routes/_module.scss b/webapp/sass/routes/_module.scss
index 48c1af1d9..b76dd147f 100644
--- a/webapp/sass/routes/_module.scss
+++ b/webapp/sass/routes/_module.scss
@@ -2,6 +2,7 @@
@import 'access-history';
@import 'activity-log';
@import 'admin-console';
+@import 'backstage';
@import 'docs';
@import 'error-page';
@import 'loading';
diff --git a/webapp/stores/file_store.jsx b/webapp/stores/file_store.jsx
index 2628685cc..6473e0474 100644
--- a/webapp/stores/file_store.jsx
+++ b/webapp/stores/file_store.jsx
@@ -13,11 +13,6 @@ class FileStore extends EventEmitter {
constructor() {
super();
- this.addChangeListener = this.addChangeListener.bind(this);
- this.removeChangeListener = this.removeChangeListener.bind(this);
- this.emitChange = this.emitChange.bind(this);
-
- this.handleEventPayload = this.handleEventPayload.bind(this);
this.dispatchToken = AppDispatcher.register(this.handleEventPayload);
this.fileInfo = new Map();
diff --git a/webapp/stores/integration_store.jsx b/webapp/stores/integration_store.jsx
new file mode 100644
index 000000000..4e9212bcb
--- /dev/null
+++ b/webapp/stores/integration_store.jsx
@@ -0,0 +1,80 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import Constants from 'utils/constants.jsx';
+import EventEmitter from 'events';
+import * as Utils from 'utils/utils.jsx';
+
+const ActionTypes = Constants.ActionTypes;
+
+const CHANGE_EVENT = 'changed';
+
+class IntegrationStore extends EventEmitter {
+ constructor() {
+ super();
+
+ this.dispatchToken = AppDispatcher.register(this.handleEventPayload.bind(this));
+
+ this.incomingWebhooks = [];
+ this.receivedIncomingWebhooks = false;
+
+ this.outgoingWebhooks = [];
+ this.receivedOutgoingWebhooks = false;
+ }
+
+ addChangeListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ }
+
+ removeChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+
+ emitChange() {
+ this.emit(CHANGE_EVENT);
+ }
+
+ hasReceivedIncomingWebhooks() {
+ return this.receivedIncomingWebhooks;
+ }
+
+ getIncomingWebhooks() {
+ return this.incomingWebhooks;
+ }
+
+ setIncomingWebhooks(incomingWebhooks) {
+ this.incomingWebhooks = Utils.freezeArray(incomingWebhooks);
+ this.receivedIncomingWebhooks = true;
+ }
+
+ hasReceivedOutgoingWebhooks() {
+ return this.receivedIncomingWebhooks;
+ }
+
+ getOutgoingWebhooks() {
+ return this.outgoingWebhooks;
+ }
+
+ setOutgoingWebhooks(outgoingWebhooks) {
+ this.outgoingWebhooks = Utils.freezeArray(outgoingWebhooks);
+ this.receivedOutgoingWebhooks = true;
+ }
+
+ handleEventPayload(payload) {
+ const action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECEIVED_INCOMING_WEBHOOKS:
+ this.setIncomingWebhooks(action.incomingWebhooks);
+ this.emitChange();
+ break;
+ case ActionTypes.RECEIVED_OUTGOING_WEBHOOKS:
+ this.setOutgoingWebhooks(action.outgoingWebhooks);
+ this.emitChange();
+ break;
+ }
+ }
+}
+
+export default new IntegrationStore();
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index 6140fd9e0..2154fbe43 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -1121,3 +1121,49 @@ export function getRecentAndNewUsersAnalytics(teamId) {
}
);
}
+
+export function listIncomingHooks() {
+ if (isCallInProgress('listIncomingHooks')) {
+ return;
+ }
+
+ callTracker.listIncomingHooks = utils.getTimestamp();
+
+ client.listIncomingHooks(
+ (data) => {
+ callTracker.listIncomingHooks = 0;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_INCOMING_WEBHOOKS,
+ incomingWebhooks: data
+ });
+ },
+ (err) => {
+ callTracker.listIncomingHooks = 0;
+ dispatchError(err, 'getIncomingHooks');
+ }
+ );
+}
+
+export function listOutgoingHooks() {
+ if (isCallInProgress('listOutgoingHooks')) {
+ return;
+ }
+
+ callTracker.listOutgoingHooks = utils.getTimestamp();
+
+ client.listOutgoingHooks(
+ (data) => {
+ callTracker.listOutgoingHooks = 0;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_OUTGOING_WEBHOOKS,
+ outgoingWebhooks: data
+ });
+ },
+ (err) => {
+ callTracker.listOutgoingHooks = 0;
+ dispatchError(err, 'getOutgoingHooks');
+ }
+ );
+}
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index bcd2fadb9..1f316369a 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -68,6 +68,8 @@ export default {
RECEIVED_PREFERENCE: null,
RECEIVED_PREFERENCES: null,
RECEIVED_FILE_INFO: null,
+ RECEIVED_INCOMING_WEBHOOKS: null,
+ RECEIVED_OUTGOING_WEBHOOKS: null,
RECEIVED_MSG: null,
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
index 83519a6ec..33a3d8b27 100644
--- a/webapp/utils/utils.jsx
+++ b/webapp/utils/utils.jsx
@@ -1398,3 +1398,13 @@ export function localizeMessage(id, defaultMessage) {
return id;
}
+
+export function freezeArray(arr) {
+ for (const obj of arr) {
+ Object.freeze(obj);
+ }
+
+ Object.freeze(arr);
+
+ return arr;
+}