Added initial backstage components and InstalledIntegrations page
Этот коммит содержится в:
68
webapp/components/backstage/backstage_category.jsx
Обычный файл
68
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
62
webapp/components/backstage/backstage_navbar.jsx
Обычный файл
62
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
122
webapp/components/backstage/backstage_section.jsx
Обычный файл
122
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
113
webapp/components/backstage/backstage_sidebar.jsx
Обычный файл
113
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
304
webapp/components/backstage/installed_integrations.jsx
Обычный файл
304
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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Ссылка в новой задаче
Block a user