diff options
author | Harrison Healey <harrisonmhealey@gmail.com> | 2016-03-17 10:30:49 -0400 |
---|---|---|
committer | Harrison Healey <harrisonmhealey@gmail.com> | 2016-03-29 15:18:26 -0400 |
commit | c417fdc152e953982d9c9af2c04ca2c04ced41b3 (patch) | |
tree | 6bf1f8618474d3e60bbe844876de665407f80095 /webapp/components | |
parent | 9c36210edd7cae4026e3a2ee472cf2fa751a0f77 (diff) | |
download | chat-c417fdc152e953982d9c9af2c04ca2c04ced41b3.tar.gz chat-c417fdc152e953982d9c9af2c04ca2c04ced41b3.tar.bz2 chat-c417fdc152e953982d9c9af2c04ca2c04ced41b3.zip |
Added initial backstage components and InstalledIntegrations page
Diffstat (limited to 'webapp/components')
-rw-r--r-- | webapp/components/backstage/backstage_category.jsx | 68 | ||||
-rw-r--r-- | webapp/components/backstage/backstage_navbar.jsx | 62 | ||||
-rw-r--r-- | webapp/components/backstage/backstage_section.jsx | 122 | ||||
-rw-r--r-- | webapp/components/backstage/backstage_sidebar.jsx | 113 | ||||
-rw-r--r-- | webapp/components/backstage/installed_integrations.jsx | 304 | ||||
-rw-r--r-- | webapp/components/logged_in.jsx | 10 |
6 files changed, 676 insertions, 3 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 }; |