summaryrefslogtreecommitdiffstats
path: root/webapp/components/integrations
diff options
context:
space:
mode:
authorenahum <nahumhbl@gmail.com>2016-08-03 12:19:27 -0500
committerHarrison Healey <harrisonmhealey@gmail.com>2016-08-03 13:19:27 -0400
commit5bc3cea6fe4a909735753692d0c4cd960e8ab516 (patch)
tree85715d9fcbc146a9672d84c9a1ea1e96b6e71231 /webapp/components/integrations
parentea027c8de44d44b6ac4e66ab802e675d315b0be5 (diff)
downloadchat-5bc3cea6fe4a909735753692d0c4cd960e8ab516.tar.gz
chat-5bc3cea6fe4a909735753692d0c4cd960e8ab516.tar.bz2
chat-5bc3cea6fe4a909735753692d0c4cd960e8ab516.zip
PLT-3484 OAuth2 Service Provider (#3632)
* PLT-3484 OAuth2 Service Provider * PM text review for OAuth 2.0 Service Provider * PLT-3484 OAuth2 Service Provider UI tweaks (#3668) * Tweaks to help text * Pushing OAuth improvements (#3680) * Re-arrange System Console for OAuth 2.0 Provider
Diffstat (limited to 'webapp/components/integrations')
-rw-r--r--webapp/components/integrations/components/add_oauth_app.jsx435
-rw-r--r--webapp/components/integrations/components/installed_oauth_app.jsx219
-rw-r--r--webapp/components/integrations/components/installed_oauth_apps.jsx108
-rw-r--r--webapp/components/integrations/components/integrations.jsx30
4 files changed, 789 insertions, 3 deletions
diff --git a/webapp/components/integrations/components/add_oauth_app.jsx b/webapp/components/integrations/components/add_oauth_app.jsx
new file mode 100644
index 000000000..7e56aea8f
--- /dev/null
+++ b/webapp/components/integrations/components/add_oauth_app.jsx
@@ -0,0 +1,435 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as OAuthActions from 'actions/oauth_actions.jsx';
+
+import BackstageHeader from 'components/backstage/components/backstage_header.jsx';
+import {FormattedMessage} from 'react-intl';
+import FormError from 'components/form_error.jsx';
+import {browserHistory, Link} from 'react-router/es6';
+import SpinnerButton from 'components/spinner_button.jsx';
+
+export default class AddOAuthApp extends React.Component {
+ static get propTypes() {
+ return {
+ team: React.propTypes.object.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.updateName = this.updateName.bind(this);
+ this.updateTrusted = this.updateTrusted.bind(this);
+ this.updateDescription = this.updateDescription.bind(this);
+ this.updateHomepage = this.updateHomepage.bind(this);
+ this.updateIconUrl = this.updateIconUrl.bind(this);
+ this.updateCallbackUrls = this.updateCallbackUrls.bind(this);
+
+ this.imageLoaded = this.imageLoaded.bind(this);
+ this.image = new Image();
+ this.image.onload = this.imageLoaded;
+
+ this.state = {
+ name: '',
+ description: '',
+ homepage: '',
+ icon_url: '',
+ callbackUrls: '',
+ is_trusted: false,
+ has_icon: false,
+ saving: false,
+ serverError: '',
+ clientError: null
+ };
+ }
+
+ imageLoaded() {
+ this.setState({
+ has_icon: true,
+ icon_url: this.refs.icon_url.value
+ });
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+
+ if (this.state.saving) {
+ return;
+ }
+
+ this.setState({
+ saving: true,
+ serverError: '',
+ clientError: ''
+ });
+
+ if (!this.state.name) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_oauth_app.nameRequired'
+ defaultMessage='Name for the OAuth 2.0 application is required.'
+ />
+ )
+ });
+
+ return;
+ }
+
+ if (!this.state.description) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_oauth_app.descriptionRequired'
+ defaultMessage='Description for the OAuth 2.0 application is required.'
+ />
+ )
+ });
+
+ return;
+ }
+
+ if (!this.state.homepage) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_oauth_app.homepageRequired'
+ defaultMessage='Homepage for the OAuth 2.0 application is required.'
+ />
+ )
+ });
+
+ return;
+ }
+
+ const callbackUrls = [];
+ for (let callbackUrl of this.state.callbackUrls.split('\n')) {
+ callbackUrl = callbackUrl.trim();
+
+ if (callbackUrl.length > 0) {
+ callbackUrls.push(callbackUrl);
+ }
+ }
+
+ if (callbackUrls.length === 0) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_oauth_app.callbackUrlsRequired'
+ defaultMessage='One or more callback URLs are required.'
+ />
+ )
+ });
+
+ return;
+ }
+
+ const app = {
+ name: this.state.name,
+ callback_urls: callbackUrls,
+ homepage: this.state.homepage,
+ description: this.state.description,
+ is_trusted: this.state.is_trusted,
+ icon_url: this.state.icon_url
+ };
+
+ OAuthActions.registerOAuthApp(
+ app,
+ () => {
+ browserHistory.push('/' + this.props.team.name + '/integrations/oauth2-apps');
+ },
+ (err) => {
+ this.setState({
+ saving: false,
+ serverError: err.message
+ });
+ }
+ );
+ }
+
+ updateName(e) {
+ this.setState({
+ name: e.target.value
+ });
+ }
+
+ updateTrusted(e) {
+ this.setState({
+ is_trusted: e.target.value === 'true'
+ });
+ }
+
+ updateDescription(e) {
+ this.setState({
+ description: e.target.value
+ });
+ }
+
+ updateHomepage(e) {
+ this.setState({
+ homepage: e.target.value
+ });
+ }
+
+ updateIconUrl(e) {
+ this.setState({
+ has_icon: false,
+ icon_url: ''
+ });
+ this.image.src = e.target.value;
+ }
+
+ updateCallbackUrls(e) {
+ this.setState({
+ callbackUrls: e.target.value
+ });
+ }
+
+ render() {
+ let icon;
+ if (this.state.has_icon) {
+ icon = (
+ <div className='integration__icon'>
+ <img src={this.state.icon_url}/>
+ </div>
+ );
+ }
+
+ return (
+ <div className='backstage-content'>
+ <BackstageHeader>
+ <Link to={'/' + this.props.team.name + '/integrations/oauth2-apps'}>
+ <FormattedMessage
+ id='installed_oauth_apps.header'
+ defaultMessage='Installed OAuth2 Apps'
+ />
+ </Link>
+ <FormattedMessage
+ id='add_oauth_app.header'
+ defaultMessage='Add'
+ />
+ </BackstageHeader>
+ <div className='backstage-form'>
+ {icon}
+ <form className='form-horizontal'>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='is_trusted'
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.trusted'
+ defaultMessage='Is Trusted'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ value='true'
+ name='is_trusted'
+ checked={this.state.is_trusted}
+ onChange={this.updateTrusted}
+ />
+ <FormattedMessage
+ id='installed_oauth_apps.trusted.yes'
+ defaultMessage='Yes'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ value='false'
+ name='is_trusted'
+ checked={!this.state.is_trusted}
+ onChange={this.updateTrusted}
+ />
+ <FormattedMessage
+ id='installed_oauth_apps.trusted.no'
+ defaultMessage='No'
+ />
+ </label>
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_oauth_app.trusted.help'
+ defaultMessage="When true, the OAuth 2.0 application is considered trusted by the Mattermost server and doesn't require the user to accept authorization. When false, an additional window will appear, asking the user to accept or deny the authorization."
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='name'
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.name'
+ defaultMessage='Display Name'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='name'
+ type='text'
+ maxLength='64'
+ className='form-control'
+ value={this.state.name}
+ onChange={this.updateName}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_oauth_app.name.help'
+ defaultMessage='Display name for your OAuth 2.0 application made of up to 64 characters.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='description'
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.description'
+ defaultMessage='Description'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='description'
+ type='text'
+ maxLength='512'
+ className='form-control'
+ value={this.state.description}
+ onChange={this.updateDescription}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_oauth_app.description.help'
+ defaultMessage='Description for your OAuth 2.0 application.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='homepage'
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.homepage'
+ defaultMessage='Homepage'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='homepage'
+ type='url'
+ maxLength='256'
+ className='form-control'
+ value={this.state.homepage}
+ onChange={this.updateHomepage}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_oauth_app.homepage.help'
+ defaultMessage='The URL for the homepage of the OAuth 2.0 application. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='icon_url'
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.iconUrl'
+ defaultMessage='Icon URL'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='icon_url'
+ ref='icon_url'
+ type='url'
+ maxLength='512'
+ className='form-control'
+ onChange={this.updateIconUrl}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_oauth_app.icon.help'
+ defaultMessage='The URL for the homepage of the OAuth 2.0 application. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='callbackUrls'
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.callbackUrls'
+ defaultMessage='Callback URLs (One Per Line)'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <textarea
+ id='callbackUrls'
+ rows='3'
+ maxLength='1024'
+ className='form-control'
+ value={this.state.callbackUrls}
+ onChange={this.updateCallbackUrls}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_oauth_app.callbackUrls.help'
+ defaultMessage='The redirect URIs to which the service will redirect users after accepting or denying authorization of your application, and which will handle authorization codes or access tokens. Must be a valid URL and start with http:// or https://.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='backstage-form__footer'>
+ <FormError
+ type='backstage'
+ errors={[this.state.serverError, this.state.clientError]}
+ />
+ <Link
+ className='btn btn-sm'
+ to={'/' + this.props.team.name + '/integrations/oauth2-apps'}
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.cancel'
+ defaultMessage='Cancel'
+ />
+ </Link>
+ <SpinnerButton
+ className='btn btn-primary'
+ type='submit'
+ spinning={this.state.saving}
+ onClick={this.handleSubmit}
+ >
+ <FormattedMessage
+ id='installed_oauth_apps.save'
+ defaultMessage='Save'
+ />
+ </SpinnerButton>
+ </div>
+ </form>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/integrations/components/installed_oauth_app.jsx b/webapp/components/integrations/components/installed_oauth_app.jsx
new file mode 100644
index 000000000..37fc061f7
--- /dev/null
+++ b/webapp/components/integrations/components/installed_oauth_app.jsx
@@ -0,0 +1,219 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+
+const FAKE_SECRET = '***************';
+
+export default class InstalledOAuthApp extends React.Component {
+ static get propTypes() {
+ return {
+ oauthApp: React.PropTypes.object.isRequired,
+ onDelete: React.PropTypes.func.isRequired,
+ filter: React.PropTypes.string
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleShowClientSecret = this.handleShowClientSecret.bind(this);
+ this.handleHideClientScret = this.handleHideClientScret.bind(this);
+ this.handleDelete = this.handleDelete.bind(this);
+
+ this.matchesFilter = this.matchesFilter.bind(this);
+
+ this.state = {
+ clientSecret: FAKE_SECRET
+ };
+ }
+
+ handleShowClientSecret(e) {
+ e.preventDefault();
+ this.setState({clientSecret: this.props.oauthApp.client_secret});
+ }
+
+ handleHideClientScret(e) {
+ e.preventDefault();
+ this.setState({clientSecret: FAKE_SECRET});
+ }
+
+ handleDelete(e) {
+ e.preventDefault();
+
+ this.props.onDelete(this.props.oauthApp);
+ }
+
+ matchesFilter(oauthApp, filter) {
+ if (!filter) {
+ return true;
+ }
+
+ return oauthApp.name.toLowerCase().indexOf(filter) !== -1;
+ }
+
+ render() {
+ const oauthApp = this.props.oauthApp;
+
+ if (!this.matchesFilter(oauthApp, this.props.filter)) {
+ return null;
+ }
+
+ let name;
+ if (oauthApp.name) {
+ name = oauthApp.name;
+ } else {
+ name = (
+ <FormattedMessage
+ id='installed_integrations.unnamed_oauth_app'
+ defaultMessage='Unnamed OAuth 2.0 Application'
+ />
+ );
+ }
+
+ let description;
+ if (oauthApp.description) {
+ description = (
+ <div className='item-details__row'>
+ <span className='item-details__description'>
+ {oauthApp.description}
+ </span>
+ </div>
+ );
+ }
+
+ const urls = (
+ <div className='item-details__row'>
+ <span className='item-details__url'>
+ <FormattedMessage
+ id='installed_integrations.callback_urls'
+ defaultMessage='Callback URLs: {urls}'
+ values={{
+ urls: oauthApp.callback_urls.join(', ')
+ }}
+ />
+ </span>
+ </div>
+ );
+
+ let isTrusted;
+ if (oauthApp.is_trusted) {
+ isTrusted = Utils.localizeMessage('installed_oauth_apps.trusted.yes', 'Yes');
+ } else {
+ isTrusted = Utils.localizeMessage('installed_oauth_apps.trusted.no', 'No');
+ }
+
+ let action;
+ if (this.state.clientSecret === FAKE_SECRET) {
+ action = (
+ <a
+ href='#'
+ onClick={this.handleShowClientSecret}
+ >
+ <FormattedMessage
+ id='installed_integrations.showSecret'
+ defaultMessage='Show Secret'
+ />
+ </a>
+ );
+ } else {
+ action = (
+ <a
+ href='#'
+ onClick={this.handleHideClientScret}
+ >
+ <FormattedMessage
+ id='installed_integrations.hideSecret'
+ defaultMessage='Hide Secret'
+ />
+ </a>
+ );
+ }
+
+ let icon;
+ if (oauthApp.icon_url) {
+ icon = (
+ <div className='integration__icon integration-list__icon'>
+ <img src={oauthApp.icon_url}/>
+ </div>
+ );
+ }
+
+ return (
+ <div className='backstage-list__item'>
+ {icon}
+ <div className='item-details'>
+ <div className='item-details__row'>
+ <span className='item-details__name'>
+ {name}
+ </span>
+ </div>
+ {description}
+ <div className='item-details__row'>
+ <span className='item-details__url'>
+ <FormattedHTMLMessage
+ id='installed_oauth_apps.is_trusted'
+ defaultMessage='Is Trusted: <strong>{isTrusted}</strong>'
+ values={{
+ isTrusted
+ }}
+ />
+ </span>
+ </div>
+ <div className='item-details__row'>
+ <span className='item-details__token'>
+ <FormattedHTMLMessage
+ id='installed_integrations.client_id'
+ defaultMessage='Client ID: <strong>{clientId}</strong>'
+ values={{
+ clientId: oauthApp.id
+ }}
+ />
+ </span>
+ </div>
+ <div className='item-details__row'>
+ <span className='item-details__token'>
+ <FormattedHTMLMessage
+ id='installed_integrations.client_secret'
+ defaultMessage='Client Secret: <strong>{clientSecret}</strong>'
+ values={{
+ clientSecret: this.state.clientSecret
+ }}
+ />
+ </span>
+ </div>
+ {urls}
+ <div className='item-details__row'>
+ <span className='item-details__creation'>
+ <FormattedMessage
+ id='installed_integrations.creation'
+ defaultMessage='Created by {creator} on {createAt, date, full}'
+ values={{
+ creator: Utils.displayUsername(oauthApp.creator_id),
+ createAt: oauthApp.create_at
+ }}
+ />
+ </span>
+ </div>
+ </div>
+ <div className='item-actions'>
+ {action}
+ {' - '}
+ <a
+ href='#'
+ onClick={this.handleDelete}
+ >
+ <FormattedMessage
+ id='installed_integrations.delete'
+ defaultMessage='Delete'
+ />
+ </a>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/integrations/components/installed_oauth_apps.jsx b/webapp/components/integrations/components/installed_oauth_apps.jsx
new file mode 100644
index 000000000..7a3b512dd
--- /dev/null
+++ b/webapp/components/integrations/components/installed_oauth_apps.jsx
@@ -0,0 +1,108 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import UserStore from 'stores/user_store.jsx';
+import IntegrationStore from 'stores/integration_store.jsx';
+import * as OAuthActions from 'actions/oauth_actions.jsx';
+import {localizeMessage} from 'utils/utils.jsx';
+
+import BackstageList from 'components/backstage/components/backstage_list.jsx';
+import {FormattedMessage} from 'react-intl';
+import InstalledOAuthApp from './installed_oauth_app.jsx';
+
+export default class InstalledOAuthApps extends React.Component {
+ static get propTypes() {
+ return {
+ team: React.propTypes.object.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleIntegrationChange = this.handleIntegrationChange.bind(this);
+
+ this.deleteOAuthApp = this.deleteOAuthApp.bind(this);
+
+ const userId = UserStore.getCurrentId();
+
+ this.state = {
+ oauthApps: IntegrationStore.getOAuthApps(userId),
+ loading: !IntegrationStore.hasReceivedOAuthApps(userId)
+ };
+ }
+
+ componentDidMount() {
+ IntegrationStore.addChangeListener(this.handleIntegrationChange);
+
+ if (window.mm_config.EnableOAuthServiceProvider === 'true') {
+ OAuthActions.listOAuthApps(UserStore.getCurrentId());
+ }
+ }
+
+ componentWillUnmount() {
+ IntegrationStore.removeChangeListener(this.handleIntegrationChange);
+ }
+
+ handleIntegrationChange() {
+ const userId = UserStore.getCurrentId();
+
+ this.setState({
+ oauthApps: IntegrationStore.getOAuthApps(userId),
+ loading: !IntegrationStore.hasReceivedOAuthApps(userId)
+ });
+ }
+
+ deleteOAuthApp(app) {
+ const userId = UserStore.getCurrentId();
+ OAuthActions.deleteOAuthApp(app.id, userId);
+ }
+
+ render() {
+ const oauthApps = this.state.oauthApps.map((app) => {
+ return (
+ <InstalledOAuthApp
+ key={app.id}
+ oauthApp={app}
+ onDelete={this.deleteOAuthApp}
+ />
+ );
+ });
+
+ return (
+ <BackstageList
+ header={
+ <FormattedMessage
+ id='installed_oauth_apps.header'
+ defaultMessage='OAuth 2.0 Applications'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='installed_oauth_apps.help'
+ defaultMessage='OAuth 2.0 Applications are available to everyone on your server.'
+ />
+ }
+ addText={
+ <FormattedMessage
+ id='installed_oauth_apps.add'
+ defaultMessage='Add OAuth 2.0 Application'
+ />
+ }
+ addLink={'/' + this.props.team.name + '/integrations/oauth2-apps/add'}
+ emptyText={
+ <FormattedMessage
+ id='installed_oauth_apps.empty'
+ defaultMessage='No OAuth 2.0 Applications found'
+ />
+ }
+ searchPlaceholder={localizeMessage('installed_oauth_apps.search', 'Search OAuth 2.0 Applications')}
+ loading={this.state.loading}
+ >
+ {oauthApps}
+ </BackstageList>
+ );
+ }
+}
diff --git a/webapp/components/integrations/components/integrations.jsx b/webapp/components/integrations/components/integrations.jsx
index 7894ced5d..ec923c4f0 100644
--- a/webapp/components/integrations/components/integrations.jsx
+++ b/webapp/components/integrations/components/integrations.jsx
@@ -7,6 +7,7 @@ import {FormattedMessage} from 'react-intl';
import IntegrationOption from './integration_option.jsx';
import WebhookIcon from 'images/webhook_icon.jpg';
+import AppIcon from 'images/oauth_icon.png';
export default class Integrations extends React.Component {
static get propTypes() {
@@ -17,8 +18,9 @@ export default class Integrations extends React.Component {
render() {
const options = [];
+ const config = window.mm_config;
- if (window.mm_config.EnableIncomingWebhooks === 'true') {
+ if (config.EnableIncomingWebhooks === 'true') {
options.push(
<IntegrationOption
key='incomingWebhook'
@@ -40,7 +42,7 @@ export default class Integrations extends React.Component {
);
}
- if (window.mm_config.EnableOutgoingWebhooks === 'true') {
+ if (config.EnableOutgoingWebhooks === 'true') {
options.push(
<IntegrationOption
key='outgoingWebhook'
@@ -62,7 +64,7 @@ export default class Integrations extends React.Component {
);
}
- if (window.mm_config.EnableCommands === 'true') {
+ if (config.EnableCommands === 'true') {
options.push(
<IntegrationOption
key='command'
@@ -84,6 +86,28 @@ export default class Integrations extends React.Component {
);
}
+ if (config.EnableOAuthServiceProvider === 'true') {
+ options.push(
+ <IntegrationOption
+ key='oauth2Apps'
+ image={AppIcon}
+ title={
+ <FormattedMessage
+ id='integrations.oauthApps.title'
+ defaultMessage='OAuth 2.0 Applications'
+ />
+ }
+ description={
+ <FormattedMessage
+ id='integrations.oauthApps.description'
+ defaultMessage='Auth 2.0 allows external applications to make authorized requests to the Mattermost API.'
+ />
+ }
+ link={'/' + this.props.team.name + '/integrations/oauth2-apps'}
+ />
+ );
+ }
+
return (
<div className='backstage-content row'>
<div className='backstage-header'>