diff --git a/webapp/components/backstage/backstage_category.jsx b/webapp/components/backstage/backstage_category.jsx
new file mode 100644
index 0000000000..e8b0b57ae8
--- /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 = (
+
+ {
+ React.Children.map(children, (child) => {
+ return React.cloneElement(child, {
+ parentLink: link
+ });
+ })
+ }
+
+ );
+ }
+
+ return (
+
+
+
+
+ {title}
+
+
+ {clonedChildren}
+
+ );
+ }
+}
diff --git a/webapp/components/backstage/backstage_navbar.jsx b/webapp/components/backstage/backstage_navbar.jsx
new file mode 100644
index 0000000000..8ba8669c57
--- /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 (
+
+
+
+
+
+
+
+ {'TODO: Switch Teams'}
+
+ );
+ }
+}
diff --git a/webapp/components/backstage/backstage_section.jsx b/webapp/components/backstage/backstage_section.jsx
new file mode 100644
index 0000000000..41ce766bdc
--- /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 = ;
+ } else {
+ toggle = ;
+ }
+ }
+
+ let clonedChildren = null;
+ if (children.length > 0 && expanded) {
+ clonedChildren = (
+
+ {
+ React.Children.map(children, (child) => {
+ return React.cloneElement(child, {
+ parentLink: link,
+ subsection: true
+ });
+ })
+ }
+
+ );
+ }
+
+ let className = 'section';
+ if (subsection) {
+ className = 'subsection';
+ }
+
+ return (
+
+
+ {toggle}
+
+ {title}
+
+
+ {clonedChildren}
+
+ );
+ }
+}
diff --git a/webapp/components/backstage/backstage_sidebar.jsx b/webapp/components/backstage/backstage_sidebar.jsx
new file mode 100644
index 0000000000..672005333e
--- /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 (
+
+
+
+ }
+ />
+
+ }
+ >
+
+ )}
+ />
+
+ )}
+ collapsible={true}
+ >
+
+ )}
+ />
+
+ )}
+ />
+
+
+
+
+ );
+ }
+}
+
diff --git a/webapp/components/backstage/installed_integrations.jsx b/webapp/components/backstage/installed_integrations.jsx
new file mode 100644
index 0000000000..cfb68c6603
--- /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(
+ this.setFilter(e, '')}
+ >
+
+
+ );
+ }
+
+ if (incomingWebhooks.length > 0) {
+ fields.push(
+
+ {'|'}
+
+ );
+
+ let filterClassName = 'type-filter';
+ if (this.state.filter === 'incomingWebhooks') {
+ filterClassName += ' type-filter--selected';
+ }
+
+ fields.push(
+ this.setFilter(e, 'incomingWebhooks')}
+ >
+
+
+ );
+ }
+
+ if (outgoingWebhooks.length > 0) {
+ fields.push(
+
+ {'|'}
+
+ );
+
+ let filterClassName = 'type-filter';
+ if (this.state.filter === 'outgoingWebhooks') {
+ filterClassName += ' type-filter--selected';
+ }
+
+ fields.push(
+ this.setFilter(e, 'outgoingWebhooks')}
+ >
+
+
+ );
+ }
+
+ return (
+
+ {fields}
+
+ );
+ }
+
+ 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(
+
+ );
+ }
+ }
+
+ if (!this.state.filter || this.state.filter === 'outgoingWebhooks') {
+ for (const outgoingWebhook of outgoingWebhooks) {
+ integrations.push(
+
+ );
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {this.renderTypeFilters(this.state.incomingWebhooks, this.state.outgoingWebhooks)}
+
+
+
+ {integrations}
+
+
+
+ );
+ }
+}
+
+function IncomingWebhook({incomingWebhook}) {
+ const channel = ChannelStore.get(incomingWebhook.channel_id);
+ const channelName = channel ? channel.display_name : 'cannot find channel';
+
+ return (
+
+
+
+
+ {channelName}
+
+
+
+
+
+
+
+ {Utils.getWindowLocationOrigin() + '/hooks/' + incomingWebhook.id}
+
+
+
+
+ );
+}
+
+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 (
+
+
+
+
+ {channelName}
+
+
+
+
+
+
+
+ {Utils.getWindowLocationOrigin() + '/hooks/' + outgoingWebhook.id}
+
+
+
+
+ );
+}
+
+OutgoingWebhook.propTypes = {
+ outgoingWebhook: React.PropTypes.object.isRequired
+};
diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx
index 53db501bf0..fd09aac9ea 100644
--- a/webapp/components/logged_in.jsx
+++ b/webapp/components/logged_in.jsx
@@ -199,6 +199,9 @@ export default class LoggedIn extends React.Component {
if (this.props.children) {
content = this.props.children;
} else {
+ content.push(
+ this.props.navbar
+ );
content.push(
this.props.sidebar
);
@@ -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 ce59a95c9a..c10099967d 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';
@@ -241,6 +244,51 @@ function renderRootComponent() {
path=':team/logout'
onEnter={onLoggedOut}
/>
+
+
+
+
+
+
+
+
+
+
+>>>>>>> Added initial backstage components and InstalledIntegrations page
{
+ 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 bcd2fadb9f..1f316369a2 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 83519a6ec5..33a3d8b27e 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;
+}