aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2018-01-11 13:41:50 +0100
committerGitHub <noreply@github.com>2018-01-11 13:41:50 +0100
commit5323f2c5bd56b52a03cf7c651470d71b0cd37eb3 (patch)
treef2f1b328ff5ed11cb256b3ce129b7218639d1e10 /server/sonar-web
parent898f6155cabbbe43a4cd50d01ec90754ff139929 (diff)
downloadsonarqube-5323f2c5bd56b52a03cf7c651470d71b0cd37eb3.tar.gz
sonarqube-5323f2c5bd56b52a03cf7c651470d71b0cd37eb3.zip
Reuse react based tokens form in account space (#2954)
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/api/user-tokens.ts22
-rw-r--r--server/sonar-web/src/main/js/app/types.ts1
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/Security.js2
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/Tokens.tsx (renamed from server/sonar-web/src/main/js/apps/account/components/Tokens.js)48
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/__tests__/Tokens-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Tokens-test.tsx.snap23
-rw-r--r--server/sonar-web/src/main/js/apps/account/templates/account-tokens.hbs81
-rw-r--r--server/sonar-web/src/main/js/apps/account/tokens-view.js108
-rw-r--r--server/sonar-web/src/main/js/apps/users/UsersApp.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/users/UsersList.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/UsersList.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersList.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx131
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx78
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx3
18 files changed, 240 insertions, 314 deletions
diff --git a/server/sonar-web/src/main/js/api/user-tokens.ts b/server/sonar-web/src/main/js/api/user-tokens.ts
index 0d7f6a95ace..bbe02d17311 100644
--- a/server/sonar-web/src/main/js/api/user-tokens.ts
+++ b/server/sonar-web/src/main/js/api/user-tokens.ts
@@ -45,26 +45,24 @@ export interface UserToken {
createdAt: string;
}
-/**
- * List tokens for given user login
- */
+/** List tokens for given user login */
export function getTokens(login: string): Promise<UserToken[]> {
return getJSON('/api/user_tokens/search', { login }).then(r => r.userTokens, throwGlobalError);
}
-/**
- * Generate a user token
- */
-export function generateToken(data: {
+export interface NewToken {
+ createdAt: string;
+ login: string;
name: string;
- login?: string;
-}): Promise<{ login: string; name: string; token: string }> {
+ token: string;
+}
+
+/** Generate a user token */
+export function generateToken(data: { name: string; login?: string }): Promise<NewToken> {
return postJSON('/api/user_tokens/generate', data).catch(throwGlobalError);
}
-/**
- * Revoke a user token
- */
+/** Revoke a user token */
export function revokeToken(data: { name: string; login?: string }): Promise<void | Response> {
return post('/api/user_tokens/revoke', data).catch(throwGlobalError);
}
diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts
index 69a81b64d0f..228038e736e 100644
--- a/server/sonar-web/src/main/js/app/types.ts
+++ b/server/sonar-web/src/main/js/app/types.ts
@@ -184,6 +184,7 @@ export interface LoggedInUser extends CurrentUser {
email?: string;
homepage?: HomePage;
isLoggedIn: true;
+ login: string;
name: string;
}
diff --git a/server/sonar-web/src/main/js/apps/account/components/Security.js b/server/sonar-web/src/main/js/apps/account/components/Security.js
index 2063cc2cc8d..af77c5eff8d 100644
--- a/server/sonar-web/src/main/js/apps/account/components/Security.js
+++ b/server/sonar-web/src/main/js/apps/account/components/Security.js
@@ -31,7 +31,7 @@ function Security(props) {
return (
<div className="account-body account-container">
<Helmet title={translate('my_account.security')} />
- <Tokens user={user} />
+ <Tokens login={user.login} />
{user.local && <Password user={user} />}
</div>
);
diff --git a/server/sonar-web/src/main/js/apps/account/components/Tokens.js b/server/sonar-web/src/main/js/apps/account/components/Tokens.tsx
index fc3e859bc97..db6a74adf1c 100644
--- a/server/sonar-web/src/main/js/apps/account/components/Tokens.js
+++ b/server/sonar-web/src/main/js/apps/account/components/Tokens.tsx
@@ -17,37 +17,25 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import Backbone from 'backbone';
-import React, { Component } from 'react';
-import TokensView from '../tokens-view';
+import * as React from 'react';
+import TokenForm from '../../users/components/TokensForm';
+import { translate } from '../../../helpers/l10n';
-export default class Tokens extends Component {
- componentDidMount() {
- this.renderView();
- }
-
- componentWillUnmount() {
- this.destroyView();
- }
-
- destroyView() {
- if (this.destroyView) {
- this.tokensView.destroy();
- }
- }
-
- renderView() {
- const account = new Backbone.Model({
- id: this.props.user.login
- });
+interface Props {
+ login: string;
+}
- this.tokensView = new TokensView({
- el: this.refs.container,
- model: account
- }).render();
- }
+export default function Tokens({ login }: Props) {
+ return (
+ <div className="boxed-group">
+ <h2>{translate('users.tokens')}</h2>
+ <div className="boxed-group-inner">
+ <div className="big-spacer-bottom big-spacer-right markdown">
+ {translate('my_account.tokens_description')}
+ </div>
- render() {
- return <div ref="container" />;
- }
+ <TokenForm login={login} />
+ </div>
+ </div>
+ );
}
diff --git a/server/sonar-web/src/main/js/apps/account/components/__tests__/Tokens-test.tsx b/server/sonar-web/src/main/js/apps/account/components/__tests__/Tokens-test.tsx
new file mode 100644
index 00000000000..3b82e5f3000
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/components/__tests__/Tokens-test.tsx
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import Tokens from '../Tokens';
+
+it('renders', () => {
+ expect(shallow(<Tokens login="user" />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Tokens-test.tsx.snap b/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Tokens-test.tsx.snap
new file mode 100644
index 00000000000..f495a2fa244
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Tokens-test.tsx.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<div
+ className="boxed-group"
+>
+ <h2>
+ users.tokens
+ </h2>
+ <div
+ className="boxed-group-inner"
+ >
+ <div
+ className="big-spacer-bottom big-spacer-right markdown"
+ >
+ my_account.tokens_description
+ </div>
+ <TokensForm
+ login="user"
+ />
+ </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/account/templates/account-tokens.hbs b/server/sonar-web/src/main/js/apps/account/templates/account-tokens.hbs
deleted file mode 100644
index 65bbf91e160..00000000000
--- a/server/sonar-web/src/main/js/apps/account/templates/account-tokens.hbs
+++ /dev/null
@@ -1,81 +0,0 @@
-<div class="boxed-group">
- <h2>{{t 'users.tokens'}}</h2>
-
-<div class="boxed-group-inner">
- <div class="big-spacer-bottom big-spacer-right markdown">
- <p>{{t 'my_account.tokens_description'}}</p>
- </div>
-
- {{#notNull tokens}}
- <table class="data">
- <thead>
- <tr>
- <th>{{t 'name'}}</th>
- <th class="text-right">{{t 'created'}}</th>
- <th>&nbsp;</th>
- </tr>
- </thead>
- <tbody>
- {{#each tokens}}
- <tr>
- <td>
- <div title="{{name}}">
- {{limitString name}}
- </div>
- </td>
- <td class="thin nowrap text-right">
- {{d createdAt}}
- </td>
- <td class="thin nowrap text-right">
- <div class="big-spacer-left">
- <form class="js-revoke-token-form" data-token="{{name}}">
- {{#if deleting}}
- <button class="button-red active input-small">{{t 'users.tokens.sure'}}</button>
- {{else}}
- <button class="button-red input-small">{{t 'users.tokens.revoke'}}</button>
- {{/if}}
- </form>
- </div>
- </td>
- </tr>
- {{else}}
- <tr>
- <td colspan="3">
- <span class="note">{{t 'users.no_tokens'}}</span>
- </td>
- </tr>
- {{/each}}
- </tbody>
- </table>
- {{/notNull}}
-
- {{#each errors}}
- <div class="alert alert-danger">{{msg}}</div>
- {{/each}}
-
- <form class="js-generate-token-form spacer-top panel bg-muted">
- <label>{{t 'users.generate_new_token'}}:</label>
- <input type="text" required maxlength="100" placeholder="{{t 'users.enter_token_name'}}">
- <button>{{t 'users.generate'}}</button>
- </form>
-
- {{#if newToken}}
- <div class="panel panel-white big-spacer-top">
- <div class="alert alert-warning">
- {{tp 'users.tokens.new_token_created' newToken.name}}
- </div>
-
- <table class="data">
- <tr>
- <td class="thin">
- <button class="js-copy-to-clipboard" data-clipboard-text="{{newToken.token}}">{{t 'copy'}}</button>
- </td>
- <td class="nowrap">
- <div class="monospaced text-success">{{newToken.token}}</div>
- </td>
- </tr>
- </table>
- </div>
- {{/if}}
-</div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/account/tokens-view.js b/server/sonar-web/src/main/js/apps/account/tokens-view.js
deleted file mode 100644
index 76f3cc2409e..00000000000
--- a/server/sonar-web/src/main/js/apps/account/tokens-view.js
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import Marionette from 'backbone.marionette';
-import Clipboard from 'clipboard';
-import Template from './templates/account-tokens.hbs';
-import { getTokens, generateToken, revokeToken } from '../../api/user-tokens';
-import { translate } from '../../helpers/l10n';
-
-export default Marionette.ItemView.extend({
- template: Template,
-
- events() {
- return {
- 'submit .js-generate-token-form': 'onGenerateTokenFormSubmit',
- 'submit .js-revoke-token-form': 'onRevokeTokenFormSubmit'
- };
- },
-
- initialize() {
- this.tokens = null;
- this.newToken = null;
- this.errors = [];
- this.requestTokens();
- },
-
- requestTokens() {
- return getTokens(this.model.id).then(tokens => {
- this.tokens = tokens;
- this.render();
- });
- },
-
- onGenerateTokenFormSubmit(e) {
- e.preventDefault();
- this.errors = [];
- this.newToken = null;
- const tokenName = this.$('.js-generate-token-form input').val();
- generateToken({ name: tokenName, login: this.model.id }).then(
- response => {
- this.newToken = response;
- this.requestTokens();
- },
- () => {}
- );
- },
-
- onRevokeTokenFormSubmit(e) {
- e.preventDefault();
- const tokenName = $(e.currentTarget).data('token');
- const token = this.tokens.find(token => token.name === `${tokenName}`);
- if (token) {
- if (token.deleting) {
- revokeToken({ name: tokenName, login: this.model.id }).then(
- () => this.requestTokens(),
- () => {}
- );
- } else {
- token.deleting = true;
- this.render();
- }
- }
- },
-
- onRender() {
- const copyButton = this.$('.js-copy-to-clipboard');
- if (copyButton.length) {
- const clipboard = new Clipboard(copyButton.get(0));
- clipboard.on('success', () => {
- copyButton
- .tooltip({
- title: translate('users.tokens.copied'),
- placement: 'bottom',
- trigger: 'manual'
- })
- .tooltip('show');
- setTimeout(() => copyButton.tooltip('hide'), 1000);
- });
- }
- this.newToken = null;
- },
-
- serializeData() {
- return {
- ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
- tokens: this.tokens,
- newToken: this.newToken,
- errors: this.errors
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx
index 24c638ca7b6..45a3f77e7cd 100644
--- a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx
+++ b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx
@@ -113,6 +113,12 @@ export default class UsersApp extends React.PureComponent<Props, State> {
this.context.router.push({ ...this.props.location, query });
};
+ updateTokensCount = (login: string, tokensCount: number) => {
+ this.setState(state => ({
+ users: state.users.map(user => (user.login === login ? { ...user, tokensCount } : user))
+ }));
+ };
+
render() {
const query = parseQuery(this.props.location.query);
const { loading, paging, users } = this.state;
@@ -126,6 +132,7 @@ export default class UsersApp extends React.PureComponent<Props, State> {
identityProviders={this.state.identityProviders}
onUpdateUsers={this.fetchUsers}
organizationsEnabled={this.props.organizationsEnabled}
+ updateTokensCount={this.updateTokensCount}
users={users}
/>
{paging !== undefined && (
diff --git a/server/sonar-web/src/main/js/apps/users/UsersList.tsx b/server/sonar-web/src/main/js/apps/users/UsersList.tsx
index 9609acd0a9b..8938ae00e04 100644
--- a/server/sonar-web/src/main/js/apps/users/UsersList.tsx
+++ b/server/sonar-web/src/main/js/apps/users/UsersList.tsx
@@ -27,6 +27,7 @@ interface Props {
identityProviders: IdentityProvider[];
onUpdateUsers: () => void;
organizationsEnabled: boolean;
+ updateTokensCount: (login: string, tokensCount: number) => void;
users: User[];
}
@@ -35,6 +36,7 @@ export default function UsersList({
identityProviders,
onUpdateUsers,
organizationsEnabled,
+ updateTokensCount,
users
}: Props) {
return (
@@ -60,6 +62,7 @@ export default function UsersList({
key={user.login}
onUpdateUsers={onUpdateUsers}
organizationsEnabled={organizationsEnabled}
+ updateTokensCount={updateTokensCount}
user={user}
/>
))}
diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/UsersList.tsx b/server/sonar-web/src/main/js/apps/users/__tests__/UsersList.tsx
index aa3ed1b718a..2a3d301e275 100644
--- a/server/sonar-web/src/main/js/apps/users/__tests__/UsersList.tsx
+++ b/server/sonar-web/src/main/js/apps/users/__tests__/UsersList.tsx
@@ -63,6 +63,7 @@ function getWrapper(props = {}) {
]}
onUpdateUsers={jest.fn()}
organizationsEnabled={true}
+ updateTokensCount={jest.fn()}
users={users}
{...props}
/>
diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap
index 8aff49f07ed..c955a2eef06 100644
--- a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap
@@ -32,6 +32,7 @@ exports[`should render correctly 1`] = `
identityProviders={Array []}
onUpdateUsers={[Function]}
organizationsEnabled={true}
+ updateTokensCount={[Function]}
users={Array []}
/>
</div>
@@ -78,6 +79,7 @@ exports[`should render correctly 2`] = `
}
onUpdateUsers={[Function]}
organizationsEnabled={true}
+ updateTokensCount={[Function]}
users={
Array [
Object {
diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersList.tsx.snap b/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersList.tsx.snap
index 64c13cf48e9..5fc532a3be4 100644
--- a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersList.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersList.tsx.snap
@@ -37,6 +37,7 @@ exports[`should render correctly 1`] = `
key="luke"
onUpdateUsers={[Function]}
organizationsEnabled={true}
+ updateTokensCount={[Function]}
user={
Object {
"active": true,
@@ -52,6 +53,7 @@ exports[`should render correctly 1`] = `
key="obi"
onUpdateUsers={[Function]}
organizationsEnabled={true}
+ updateTokensCount={[Function]}
user={
Object {
"active": true,
diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx
index 4d723aa723f..2f27df08c77 100644
--- a/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx
+++ b/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx
@@ -18,23 +18,19 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import Modal from '../../../components/controls/Modal';
import TokensFormItem from './TokensFormItem';
import TokensFormNewToken from './TokensFormNewToken';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
-import { User } from '../../../api/users';
import { getTokens, generateToken, UserToken } from '../../../api/user-tokens';
import { translate } from '../../../helpers/l10n';
interface Props {
- user: User;
- onClose: () => void;
- onUpdateUsers: () => void;
+ login: string;
+ updateTokensCount?: (login: string, tokensCount: number) => void;
}
interface State {
generating: boolean;
- hasChanged: boolean;
loading: boolean;
newToken?: { name: string; token: string };
newTokenName: string;
@@ -45,7 +41,6 @@ export default class TokensForm extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = {
generating: false,
- hasChanged: false,
loading: true,
newTokenName: '',
tokens: []
@@ -60,9 +55,9 @@ export default class TokensForm extends React.PureComponent<Props, State> {
this.mounted = false;
}
- fetchTokens = ({ user } = this.props) => {
+ fetchTokens = () => {
this.setState({ loading: true });
- getTokens(user.login).then(
+ getTokens(this.props.login).then(
tokens => {
if (this.mounted) {
this.setState({ loading: false, tokens });
@@ -76,27 +71,26 @@ export default class TokensForm extends React.PureComponent<Props, State> {
);
};
- handleCloseClick = (evt: React.SyntheticEvent<HTMLAnchorElement>) => {
- evt.preventDefault();
- this.handleClose();
- };
-
- handleClose = () => {
- if (this.state.hasChanged) {
- this.props.onUpdateUsers();
+ updateTokensCount = () => {
+ if (this.props.updateTokensCount) {
+ this.props.updateTokensCount(this.props.login, this.state.tokens.length);
}
- this.props.onClose();
};
handleGenerateToken = (evt: React.SyntheticEvent<HTMLFormElement>) => {
evt.preventDefault();
if (this.state.newTokenName.length > 0) {
this.setState({ generating: true });
- generateToken({ name: this.state.newTokenName, login: this.props.user.login }).then(
+ generateToken({ name: this.state.newTokenName, login: this.props.login }).then(
newToken => {
if (this.mounted) {
- this.fetchTokens();
- this.setState({ generating: false, hasChanged: true, newToken, newTokenName: '' });
+ this.setState(state => {
+ const tokens = [
+ ...state.tokens,
+ { name: newToken.name, createdAt: newToken.createdAt }
+ ];
+ return { generating: false, newToken, newTokenName: '', tokens };
+ }, this.updateTokensCount);
}
},
() => {
@@ -108,9 +102,13 @@ export default class TokensForm extends React.PureComponent<Props, State> {
}
};
- handleRevokeToken = () => {
- this.setState({ hasChanged: true });
- this.fetchTokens();
+ handleRevokeToken = (revokedToken: UserToken) => {
+ this.setState(
+ state => ({
+ tokens: state.tokens.filter(token => token.name !== revokedToken.name)
+ }),
+ this.updateTokensCount
+ );
};
handleNewTokenChange = (evt: React.SyntheticEvent<HTMLInputElement>) =>
@@ -130,16 +128,15 @@ export default class TokensForm extends React.PureComponent<Props, State> {
return tokens.map(token => (
<TokensFormItem
key={token.name}
+ login={this.props.login}
token={token}
onRevokeToken={this.handleRevokeToken}
- user={this.props.user}
/>
));
}
render() {
const { generating, loading, newToken, newTokenName, tokens } = this.state;
- const header = translate('users.tokens');
const customSpinner = (
<tr>
<td>
@@ -148,55 +145,43 @@ export default class TokensForm extends React.PureComponent<Props, State> {
</tr>
);
return (
- <Modal contentLabel={header} onRequestClose={this.handleClose}>
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
- <div className="modal-body modal-container">
- <h3 className="spacer-bottom">{translate('users.generate_tokens')}</h3>
- <form id="generate-token-form" onSubmit={this.handleGenerateToken} autoComplete="off">
- <input
- className="spacer-right"
- type="text"
- maxLength={100}
- onChange={this.handleNewTokenChange}
- placeholder={translate('users.enter_token_name')}
- required={true}
- value={newTokenName}
- />
- <button
- className="js-generate-token"
- disabled={generating || newTokenName.length <= 0}
- type="submit">
- {translate('users.generate')}
- </button>
- </form>
+ <>
+ <h3 className="spacer-bottom">{translate('users.generate_tokens')}</h3>
+ <form id="generate-token-form" onSubmit={this.handleGenerateToken} autoComplete="off">
+ <input
+ className="spacer-right"
+ type="text"
+ maxLength={100}
+ onChange={this.handleNewTokenChange}
+ placeholder={translate('users.enter_token_name')}
+ required={true}
+ value={newTokenName}
+ />
+ <button
+ className="js-generate-token"
+ disabled={generating || newTokenName.length <= 0}
+ type="submit">
+ {translate('users.generate')}
+ </button>
+ </form>
- {newToken && <TokensFormNewToken token={newToken} />}
+ {newToken && <TokensFormNewToken token={newToken} />}
- <table className="data zebra big-spacer-top ">
- <thead>
- <tr>
- <th>{translate('name')}</th>
- <th className="text-right">{translate('created')}</th>
- <th />
- </tr>
- </thead>
- <tbody>
- <DeferredSpinner
- customSpinner={customSpinner}
- loading={loading && tokens.length <= 0}>
- {this.renderItems()}
- </DeferredSpinner>
- </tbody>
- </table>
- </div>
- <footer className="modal-foot">
- <a className="js-modal-close" href="#" onClick={this.handleCloseClick}>
- {translate('Done')}
- </a>
- </footer>
- </Modal>
+ <table className="data zebra big-spacer-top ">
+ <thead>
+ <tr>
+ <th>{translate('name')}</th>
+ <th className="text-right">{translate('created')}</th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ <DeferredSpinner customSpinner={customSpinner} loading={loading && tokens.length <= 0}>
+ {this.renderItems()}
+ </DeferredSpinner>
+ </tbody>
+ </table>
+ </>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx
index f94a65caf8c..e1426619ba9 100644
--- a/server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx
+++ b/server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx
@@ -21,15 +21,14 @@ import * as React from 'react';
import Tooltip from '../../../components/controls/Tooltip';
import DateFormatter from '../../../components/intl/DateFormatter';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
-import { User } from '../../../api/users';
import { revokeToken, UserToken } from '../../../api/user-tokens';
import { limitComponentName } from '../../../helpers/path';
import { translate } from '../../../helpers/l10n';
interface Props {
+ login: string;
+ onRevokeToken: (token: UserToken) => void;
token: UserToken;
- user: User;
- onRevokeToken: () => void;
}
interface State {
@@ -52,8 +51,8 @@ export default class TokensFormItem extends React.PureComponent<Props, State> {
handleRevoke = () => {
if (this.state.deleting) {
this.setState({ loading: true });
- revokeToken({ login: this.props.user.login, name: this.props.token.name }).then(
- this.props.onRevokeToken,
+ revokeToken({ login: this.props.login, name: this.props.token.name }).then(
+ () => this.props.onRevokeToken(this.props.token),
() => {
if (this.mounted) {
this.setState({ loading: false, deleting: false });
diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx
new file mode 100644
index 00000000000..3befffc30da
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import Modal from '../../../components/controls/Modal';
+import TokensForm from './TokensForm';
+import { User } from '../../../api/users';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ user: User;
+ onClose: () => void;
+ updateTokensCount: (login: string, tokensCount: number) => void;
+}
+
+export default class TokensFormModal extends React.PureComponent<Props> {
+ handleCloseClick = (evt: React.SyntheticEvent<HTMLAnchorElement>) => {
+ evt.preventDefault();
+ this.props.onClose();
+ };
+
+ render() {
+ const header = translate('users.tokens');
+ return (
+ <Modal contentLabel={header} onRequestClose={this.props.onClose}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+ <div className="modal-body modal-container">
+ <TokensForm
+ login={this.props.user.login}
+ updateTokensCount={this.props.updateTokensCount}
+ />
+ </div>
+ <footer className="modal-foot">
+ <a className="js-modal-close" href="#" onClick={this.handleCloseClick}>
+ {translate('Done')}
+ </a>
+ </footer>
+ </Modal>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx b/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx
index f6ebe464f4c..a5ed91dac8e 100644
--- a/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx
+++ b/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx
@@ -21,7 +21,7 @@ import * as React from 'react';
import Avatar from '../../../components/ui/Avatar';
import BulletListIcon from '../../../components/icons-components/BulletListIcon';
import { ButtonIcon } from '../../../components/ui/buttons';
-import TokensForm from './TokensForm';
+import TokensFormModal from './TokensFormModal';
import UserActions from './UserActions';
import UserGroups from './UserGroups';
import UserListItemIdentity from './UserListItemIdentity';
@@ -34,6 +34,7 @@ interface Props {
isCurrentUser: boolean;
onUpdateUsers: () => void;
organizationsEnabled: boolean;
+ updateTokensCount: (login: string, tokensCount: number) => void;
user: User;
}
@@ -81,10 +82,10 @@ export default class UserListItem extends React.PureComponent<Props, State> {
/>
</td>
{this.state.openTokenForm && (
- <TokensForm
+ <TokensFormModal
user={user}
onClose={this.handleCloseTokensForm}
- onUpdateUsers={onUpdateUsers}
+ updateTokensCount={this.props.updateTokensCount}
/>
)}
</tr>
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx
index 632020a9068..7482e1f239a 100644
--- a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx
+++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx
@@ -45,7 +45,7 @@ it('should display a change password button', () => {
it('should open the correct forms', () => {
const wrapper = getWrapper();
click(wrapper.find('.js-user-tokens'));
- expect(wrapper.find('TokensForm').exists()).toBeTruthy();
+ expect(wrapper.find('TokensFormModal').exists()).toBeTruthy();
});
function getWrapper(props = {}) {
@@ -54,6 +54,7 @@ function getWrapper(props = {}) {
isCurrentUser={false}
onUpdateUsers={jest.fn()}
organizationsEnabled={false}
+ updateTokensCount={jest.fn()}
user={user}
{...props}
/>