@@ -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(); | |||
} |
@@ -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); | |||
} | |||
@@ -1,5 +1,6 @@ | |||
{ | |||
"login": "grace.hopper", | |||
"name": "Third Party Application", | |||
"createdAt": "2018-01-10T14:06:05+0100", | |||
"token": "123456789" | |||
} |
@@ -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 |
@@ -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); | |||
} |
@@ -184,6 +184,7 @@ export interface LoggedInUser extends CurrentUser { | |||
email?: string; | |||
homepage?: HomePage; | |||
isLoggedIn: true; | |||
login: string; | |||
name: string; | |||
} | |||
@@ -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> | |||
); |
@@ -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> | |||
); | |||
} |
@@ -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(); | |||
}); |
@@ -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> | |||
`; |
@@ -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> |
@@ -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 | |||
}; | |||
} | |||
}); |
@@ -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 && ( |
@@ -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} | |||
/> | |||
))} |
@@ -63,6 +63,7 @@ function getWrapper(props = {}) { | |||
]} | |||
onUpdateUsers={jest.fn()} | |||
organizationsEnabled={true} | |||
updateTokensCount={jest.fn()} | |||
users={users} | |||
{...props} | |||
/> |
@@ -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 { |
@@ -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, |
@@ -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> | |||
</> | |||
); | |||
} | |||
} |
@@ -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 }); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> |
@@ -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} | |||
/> |
@@ -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 |