diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2018-01-11 13:41:50 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-01-11 13:41:50 +0100 |
commit | 5323f2c5bd56b52a03cf7c651470d71b0cd37eb3 (patch) | |
tree | f2f1b328ff5ed11cb256b3ce129b7218639d1e10 /server/sonar-web | |
parent | 898f6155cabbbe43a4cd50d01ec90754ff139929 (diff) | |
download | sonarqube-5323f2c5bd56b52a03cf7c651470d71b0cd37eb3.tar.gz sonarqube-5323f2c5bd56b52a03cf7c651470d71b0cd37eb3.zip |
Reuse react based tokens form in account space (#2954)
Diffstat (limited to 'server/sonar-web')
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> </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} /> |