Преглед на файлове

Reuse react based tokens form in account space (#2954)

tags/7.0-RC1
Stas Vilchik преди 6 години
родител
ревизия
5323f2c5bd
No account linked to committer's email address
променени са 23 файла, в които са добавени 251 реда и са изтрити 321 реда
  1. 4
    2
      server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java
  2. 3
    4
      server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/SearchAction.java
  3. 1
    0
      server/sonar-server/src/main/resources/org/sonar/server/usertoken/ws/generate-example.json
  4. 2
    1
      server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java
  5. 10
    12
      server/sonar-web/src/main/js/api/user-tokens.ts
  6. 1
    0
      server/sonar-web/src/main/js/app/types.ts
  7. 1
    1
      server/sonar-web/src/main/js/apps/account/components/Security.js
  8. 18
    30
      server/sonar-web/src/main/js/apps/account/components/Tokens.tsx
  9. 26
    0
      server/sonar-web/src/main/js/apps/account/components/__tests__/Tokens-test.tsx
  10. 23
    0
      server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Tokens-test.tsx.snap
  11. 0
    81
      server/sonar-web/src/main/js/apps/account/templates/account-tokens.hbs
  12. 0
    108
      server/sonar-web/src/main/js/apps/account/tokens-view.js
  13. 7
    0
      server/sonar-web/src/main/js/apps/users/UsersApp.tsx
  14. 3
    0
      server/sonar-web/src/main/js/apps/users/UsersList.tsx
  15. 1
    0
      server/sonar-web/src/main/js/apps/users/__tests__/UsersList.tsx
  16. 2
    0
      server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap
  17. 2
    0
      server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersList.tsx.snap
  18. 58
    73
      server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx
  19. 4
    5
      server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx
  20. 78
    0
      server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx
  21. 4
    3
      server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx
  22. 2
    1
      server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx
  23. 1
    0
      sonar-ws/src/main/protobuf/ws-user_tokens.proto

+ 4
- 2
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();
}

+ 3
- 4
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);
}


+ 1
- 0
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"
}

+ 2
- 1
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

+ 10
- 12
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);
}

+ 1
- 0
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;
}


+ 1
- 1
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>
);

server/sonar-web/src/main/js/apps/account/components/Tokens.js → 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>
);
}

+ 26
- 0
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();
});

+ 23
- 0
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>
`;

+ 0
- 81
server/sonar-web/src/main/js/apps/account/templates/account-tokens.hbs Целия файл

@@ -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>

+ 0
- 108
server/sonar-web/src/main/js/apps/account/tokens-view.js Целия файл

@@ -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
};
}
});

+ 7
- 0
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 && (

+ 3
- 0
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}
/>
))}

+ 1
- 0
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}
/>

+ 2
- 0
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 {

+ 2
- 0
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,

+ 58
- 73
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>
</>
);
}
}

+ 4
- 5
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 });

+ 78
- 0
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>
);
}
}

+ 4
- 3
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>

+ 2
- 1
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}
/>

+ 1
- 0
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

Loading…
Отказ
Запис