From c417fdc152e953982d9c9af2c04ca2c04ced41b3 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Thu, 17 Mar 2016 10:30:49 -0400 Subject: Added initial backstage components and InstalledIntegrations page --- webapp/components/backstage/backstage_category.jsx | 68 +++++ webapp/components/backstage/backstage_navbar.jsx | 62 +++++ webapp/components/backstage/backstage_section.jsx | 122 +++++++++ webapp/components/backstage/backstage_sidebar.jsx | 113 ++++++++ .../backstage/installed_integrations.jsx | 304 +++++++++++++++++++++ webapp/components/logged_in.jsx | 10 +- webapp/root.jsx | 48 ++++ webapp/sass/routes/_backstage.scss | 182 ++++++++++++ webapp/sass/routes/_module.scss | 1 + webapp/stores/file_store.jsx | 5 - webapp/stores/integration_store.jsx | 80 ++++++ webapp/utils/async_client.jsx | 46 ++++ webapp/utils/constants.jsx | 2 + webapp/utils/utils.jsx | 10 + 14 files changed, 1045 insertions(+), 8 deletions(-) create mode 100644 webapp/components/backstage/backstage_category.jsx create mode 100644 webapp/components/backstage/backstage_navbar.jsx create mode 100644 webapp/components/backstage/backstage_section.jsx create mode 100644 webapp/components/backstage/backstage_sidebar.jsx create mode 100644 webapp/components/backstage/installed_integrations.jsx create mode 100644 webapp/sass/routes/_backstage.scss create mode 100644 webapp/stores/integration_store.jsx 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 = ( + + ); + } + + return ( +
  • + + + + {title} + + + {clonedChildren} +
  • + ); + } +} 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 ( +
    + + + + + + + {'TODO: Switch Teams'} +
    + ); + } +} 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 = ; + } 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 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 ( +
    +
      + + } + /> + + } + > + + )} + /> + + )} + collapsible={true} + > + + )} + /> + + )} + /> + + +
    +
    + ); + } +} + 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( + 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 53db501bf..fd09aac9e 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 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'; @@ -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 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; +} -- cgit v1.2.3-1-g7c22