From: Stas Vilchik Date: Thu, 11 Jan 2018 12:41:50 +0000 (+0100) Subject: Reuse react based tokens form in account space (#2954) X-Git-Tag: 7.0-RC1~30 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=5323f2c5bd56b52a03cf7c651470d71b0cd37eb3;p=sonarqube.git Reuse react based tokens form in account space (#2954) --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java b/server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java index 4479a6e55fe..7f880d4cf13 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java @@ -35,12 +35,13 @@ import org.sonarqube.ws.UserTokens; import org.sonarqube.ws.UserTokens.GenerateWsResponse; import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; +import static org.sonar.api.utils.DateUtils.formatDateTime; import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException; -import static org.sonar.server.ws.WsUtils.checkRequest; -import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonar.server.usertoken.ws.UserTokensWsParameters.ACTION_GENERATE; import static org.sonar.server.usertoken.ws.UserTokensWsParameters.PARAM_LOGIN; import static org.sonar.server.usertoken.ws.UserTokensWsParameters.PARAM_NAME; +import static org.sonar.server.ws.WsUtils.checkRequest; +import static org.sonar.server.ws.WsUtils.writeProtobuf; public class GenerateAction implements UserTokensWsAction { private static final int MAX_TOKEN_NAME_LENGTH = 100; @@ -151,6 +152,7 @@ public class GenerateAction implements UserTokensWsAction { return UserTokens.GenerateWsResponse.newBuilder() .setLogin(userTokenDto.getLogin()) .setName(userTokenDto.getName()) + .setCreatedAt(formatDateTime(userTokenDto.getCreatedAt())) .setToken(token) .build(); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/SearchAction.java index b486f7dc5ed..4ead6902c20 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/SearchAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/SearchAction.java @@ -19,7 +19,6 @@ */ package org.sonar.server.usertoken.ws; -import java.util.Date; import java.util.List; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; @@ -31,10 +30,10 @@ import org.sonar.server.user.UserSession; import org.sonarqube.ws.UserTokens.SearchWsResponse; import static org.sonar.api.utils.DateUtils.formatDateTime; -import static org.sonar.server.ws.WsUtils.checkFound; -import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonar.server.usertoken.ws.UserTokensWsParameters.ACTION_SEARCH; import static org.sonar.server.usertoken.ws.UserTokensWsParameters.PARAM_LOGIN; +import static org.sonar.server.ws.WsUtils.checkFound; +import static org.sonar.server.ws.WsUtils.writeProtobuf; public class SearchAction implements UserTokensWsAction { private final DbClient dbClient; @@ -88,7 +87,7 @@ public class SearchAction implements UserTokensWsAction { userTokenBuilder .clear() .setName(userTokenDto.getName()) - .setCreatedAt(formatDateTime(new Date(userTokenDto.getCreatedAt()))); + .setCreatedAt(formatDateTime(userTokenDto.getCreatedAt())); searchWsResponse.addUserTokens(userTokenBuilder); } diff --git a/server/sonar-server/src/main/resources/org/sonar/server/usertoken/ws/generate-example.json b/server/sonar-server/src/main/resources/org/sonar/server/usertoken/ws/generate-example.json index 16d5236762c..cbf4aacb362 100644 --- a/server/sonar-server/src/main/resources/org/sonar/server/usertoken/ws/generate-example.json +++ b/server/sonar-server/src/main/resources/org/sonar/server/usertoken/ws/generate-example.json @@ -1,5 +1,6 @@ { "login": "grace.hopper", "name": "Third Party Application", + "createdAt": "2018-01-10T14:06:05+0100", "token": "123456789" } diff --git a/server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java index a639da33bb6..d30470869f6 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java @@ -83,7 +83,7 @@ public class GenerateActionTest { .setParam(PARAM_NAME, TOKEN_NAME) .execute().getInput(); - assertJson(response).isSimilarTo(getClass().getResource("generate-example.json")); + assertJson(response).ignoreFields("createdAt").isSimilarTo(getClass().getResource("generate-example.json")); } @Test @@ -93,6 +93,7 @@ public class GenerateActionTest { GenerateWsResponse response = newRequest(null, TOKEN_NAME); assertThat(response.getLogin()).isEqualTo(GRACE_HOPPER); + assertThat(response.getCreatedAt()).isNotEmpty(); } @Test 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 { 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 { 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 { 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 (
- + {user.local && }
); 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.js deleted file mode 100644 index fc3e859bc97..00000000000 --- a/server/sonar-web/src/main/js/apps/account/components/Tokens.js +++ /dev/null @@ -1,53 +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 Backbone from 'backbone'; -import React, { Component } from 'react'; -import TokensView from '../tokens-view'; - -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 - }); - - this.tokensView = new TokensView({ - el: this.refs.container, - model: account - }).render(); - } - - render() { - return
; - } -} diff --git a/server/sonar-web/src/main/js/apps/account/components/Tokens.tsx b/server/sonar-web/src/main/js/apps/account/components/Tokens.tsx new file mode 100644 index 00000000000..db6a74adf1c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/account/components/Tokens.tsx @@ -0,0 +1,41 @@ +/* + * 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 TokenForm from '../../users/components/TokensForm'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + login: string; +} + +export default function Tokens({ login }: Props) { + return ( +
+

{translate('users.tokens')}

+
+
+ {translate('my_account.tokens_description')} +
+ + +
+
+ ); +} 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()).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`] = ` +
+

+ users.tokens +

+
+
+ my_account.tokens_description +
+ +
+
+`; 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 @@ -
-

{{t 'users.tokens'}}

- -
-
-

{{t 'my_account.tokens_description'}}

-
- - {{#notNull tokens}} - - - - - - - - - - {{#each tokens}} - - - - - - {{else}} - - - - {{/each}} - -
{{t 'name'}}{{t 'created'}} 
-
- {{limitString name}} -
-
- {{d createdAt}} - -
-
- {{#if deleting}} - - {{else}} - - {{/if}} -
-
-
- {{t 'users.no_tokens'}} -
- {{/notNull}} - - {{#each errors}} -
{{msg}}
- {{/each}} - -
- - - -
- - {{#if newToken}} -
-
- {{tp 'users.tokens.new_token_created' newToken.name}} -
- - - - - - -
- - -
{{newToken.token}}
-
-
- {{/if}} -
-
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 { 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 { 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 []} />
@@ -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 { mounted: boolean; state: State = { generating: false, - hasChanged: false, loading: true, newTokenName: '', tokens: [] @@ -60,9 +55,9 @@ export default class TokensForm extends React.PureComponent { 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 { ); }; - handleCloseClick = (evt: React.SyntheticEvent) => { - 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) => { 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 { } }; - 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) => @@ -130,16 +128,15 @@ export default class TokensForm extends React.PureComponent { return tokens.map(token => ( )); } render() { const { generating, loading, newToken, newTokenName, tokens } = this.state; - const header = translate('users.tokens'); const customSpinner = ( @@ -148,55 +145,43 @@ export default class TokensForm extends React.PureComponent { ); return ( - -
-

{header}

-
-
-

{translate('users.generate_tokens')}

-
- - -
+ <> +

{translate('users.generate_tokens')}

+
+ + +
- {newToken && } + {newToken && } - - - - - - - - - - {this.renderItems()} - - -
{translate('name')}{translate('created')} -
-
- -
+ + + + + + + + + + {this.renderItems()} + + +
{translate('name')}{translate('created')} +
+ ); } } 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 { 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 { + handleCloseClick = (evt: React.SyntheticEvent) => { + evt.preventDefault(); + this.props.onClose(); + }; + + render() { + const header = translate('users.tokens'); + return ( + +
+

{header}

+
+
+ +
+ +
+ ); + } +} 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 { /> {this.state.openTokenForm && ( - )} 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} /> diff --git a/sonar-ws/src/main/protobuf/ws-user_tokens.proto b/sonar-ws/src/main/protobuf/ws-user_tokens.proto index e51cd80174c..d25e2af36bb 100644 --- a/sonar-ws/src/main/protobuf/ws-user_tokens.proto +++ b/sonar-ws/src/main/protobuf/ws-user_tokens.proto @@ -29,6 +29,7 @@ message GenerateWsResponse { optional string login = 1; optional string name = 2; optional string token = 3; + optional string createdAt = 4; } // WS api/user_tokens/search