Quellcode durchsuchen

SONAR-11985 Use a confirmation modal for token revoking

tags/8.0
Wouter Admiraal vor 5 Jahren
Ursprung
Commit
5fdfa1d192

+ 2
- 2
server/sonar-web/src/main/js/apps/account/components/Tokens.tsx Datei anzeigen

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import InstanceMessage from '../../../components/common/InstanceMessage';
import TokenForm from '../../users/components/TokensForm';
import TokensForm from '../../users/components/TokensForm';
import { translate } from '../../../helpers/l10n';

interface Props {
@@ -35,7 +35,7 @@ export default function Tokens({ login }: Props) {
<InstanceMessage message={translate('my_account.tokens_description')} />
</div>

<TokenForm login={login} />
<TokensForm deleteConfirmation="modal" login={login} />
</div>
</div>
);

+ 1
- 0
server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Tokens-test.tsx.snap Datei anzeigen

@@ -18,6 +18,7 @@ exports[`renders 1`] = `
/>
</div>
<TokensForm
deleteConfirmation="modal"
login="user"
/>
</div>

+ 4
- 2
server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx Datei anzeigen

@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import TokensFormItem from './TokensFormItem';
import TokensFormItem, { TokenDeleteConfirmation } from './TokensFormItem';
import TokensFormNewToken from './TokensFormNewToken';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import { SubmitButton } from '../../../components/ui/buttons';
@@ -26,6 +26,7 @@ import { getTokens, generateToken } from '../../../api/user-tokens';
import { translate } from '../../../helpers/l10n';

interface Props {
deleteConfirmation: TokenDeleteConfirmation;
login: string;
updateTokensCount?: (login: string, tokensCount: number) => void;
}
@@ -128,6 +129,7 @@ export default class TokensForm extends React.PureComponent<Props, State> {
}
return tokens.map(token => (
<TokensFormItem
deleteConfirmation={this.props.deleteConfirmation}
key={token.name}
login={this.props.login}
onRevokeToken={this.handleRevokeToken}
@@ -171,7 +173,7 @@ export default class TokensForm extends React.PureComponent<Props, State> {

{newToken && <TokensFormNewToken token={newToken} />}

<table className="data zebra big-spacer-top ">
<table className="data zebra big-spacer-top">
<thead>
<tr>
<th>{translate('name')}</th>

+ 60
- 24
server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx Datei anzeigen

@@ -18,29 +18,34 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { Button } from '../../../components/ui/buttons';
import ConfirmButton from '../../../components/controls/ConfirmButton';
import DateFormatter from '../../../components/intl/DateFormatter';
import DateFromNowHourPrecision from '../../../components/intl/DateFromNowHourPrecision';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import Tooltip from '../../../components/controls/Tooltip';
import { Button } from '../../../components/ui/buttons';
import { limitComponentName } from '../../../helpers/path';
import { revokeToken } from '../../../api/user-tokens';
import { translate } from '../../../helpers/l10n';

export type TokenDeleteConfirmation = 'inline' | 'modal';

interface Props {
deleteConfirmation: TokenDeleteConfirmation;
login: string;
onRevokeToken: (token: T.UserToken) => void;
token: T.UserToken;
}

interface State {
deleting: boolean;
loading: boolean;
showConfirmation: boolean;
}

export default class TokensFormItem extends React.PureComponent<Props, State> {
mounted = false;
state: State = { deleting: false, loading: false };
state: State = { loading: false, showConfirmation: false };

componentDidMount() {
this.mounted = true;
@@ -50,25 +55,33 @@ export default class TokensFormItem extends React.PureComponent<Props, State> {
this.mounted = false;
}

handleRevoke = () => {
if (this.state.deleting) {
this.setState({ loading: true });
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 });
}
handleClick = () => {
if (this.state.showConfirmation) {
this.handleRevoke().then(() => {
if (this.mounted) {
this.setState({ showConfirmation: false });
}
);
});
} else {
this.setState({ deleting: true });
this.setState({ showConfirmation: true });
}
};

handleRevoke = () => {
this.setState({ loading: true });
return revokeToken({ login: this.props.login, name: this.props.token.name }).then(
() => this.props.onRevokeToken(this.props.token),
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
};

render() {
const { token } = this.props;
const { loading } = this.state;
const { deleteConfirmation, token } = this.props;
const { loading, showConfirmation } = this.state;
return (
<tr>
<td>
@@ -86,14 +99,37 @@ export default class TokensFormItem extends React.PureComponent<Props, State> {
<DeferredSpinner loading={loading}>
<i className="spinner-placeholder" />
</DeferredSpinner>
<Button
className="button-red input-small spacer-left"
disabled={loading}
onClick={this.handleRevoke}>
{this.state.deleting
? translate('users.tokens.sure')
: translate('users.tokens.revoke')}
</Button>
{deleteConfirmation === 'modal' ? (
<ConfirmButton
confirmButtonText={translate('users.tokens.revoke_token')}
isDestructive={true}
modalBody={
<FormattedMessage
defaultMessage={translate('users.tokens.sure_X')}
id="users.tokens.sure_X"
values={{ token: <strong>{token.name}</strong> }}
/>
}
modalHeader={translate('users.tokens.revoke_token')}
onConfirm={this.handleRevoke}>
{({ onClick }) => (
<Button
className="spacer-left button-red input-small"
disabled={loading}
onClick={onClick}
title={translate('users.tokens.revoke_token')}>
{translate('users.tokens.revoke')}
</Button>
)}
</ConfirmButton>
) : (
<Button
className="button-red input-small spacer-left"
disabled={loading}
onClick={this.handleClick}>
{showConfirmation ? translate('users.tokens.sure') : translate('users.tokens.revoke')}
</Button>
)}
</td>
</tr>
);

+ 5
- 1
server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx Datei anzeigen

@@ -43,7 +43,11 @@ export default function TokensFormModal(props: Props) {
</h2>
</header>
<div className="modal-body modal-container">
<TokensForm login={props.user.login} updateTokensCount={props.updateTokensCount} />
<TokensForm
deleteConfirmation="inline"
login={props.user.login}
updateTokensCount={props.updateTokensCount}
/>
</div>
<footer className="modal-foot">
<ResetButtonLink onClick={props.onClose}>{translate('Done')}</ResetButtonLink>

+ 3
- 1
server/sonar-web/src/main/js/apps/users/components/__tests__/TokensForm-test.tsx Datei anzeigen

@@ -79,5 +79,7 @@ it('should revoke tokens', async () => {
});

function shallowRender(props: Partial<TokensForm['props']> = {}) {
return shallow<TokensForm>(<TokensForm login="luke" updateTokensCount={jest.fn()} {...props} />);
return shallow<TokensForm>(
<TokensForm deleteConfirmation="inline" login="luke" updateTokensCount={jest.fn()} {...props} />
);
}

+ 19
- 3
server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx Datei anzeigen

@@ -43,11 +43,12 @@ beforeEach(() => {

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(shallowRender({ deleteConfirmation: 'modal' })).toMatchSnapshot();
});

it('should revoke the token', async () => {
it('should revoke the token using inline confirmation', async () => {
const onRevokeToken = jest.fn();
const wrapper = shallowRender({ onRevokeToken });
const wrapper = shallowRender({ deleteConfirmation: 'inline', onRevokeToken });
expect(wrapper.find('Button')).toMatchSnapshot();
click(wrapper.find('Button'));
expect(wrapper.find('Button')).toMatchSnapshot();
@@ -58,8 +59,23 @@ it('should revoke the token', async () => {
expect(onRevokeToken).toHaveBeenCalledWith(userToken);
});

it('should revoke the token using modal confirmation', async () => {
const onRevokeToken = jest.fn();
const wrapper = shallowRender({ deleteConfirmation: 'modal', onRevokeToken });
wrapper.find('ConfirmButton').prop<Function>('onConfirm')();
expect(revokeToken).toHaveBeenCalledWith({ login: 'luke', name: 'foo' });
await waitAndUpdate(wrapper);
expect(onRevokeToken).toHaveBeenCalledWith(userToken);
});

function shallowRender(props: Partial<TokensFormItem['props']> = {}) {
return shallow(
<TokensFormItem login="luke" onRevokeToken={jest.fn()} token={userToken} {...props} />
<TokensFormItem
deleteConfirmation="inline"
login="luke"
onRevokeToken={jest.fn()}
token={userToken}
{...props}
/>
);
}

+ 4
- 2
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensForm-test.tsx.snap Datei anzeigen

@@ -30,7 +30,7 @@ exports[`should render correctly 1`] = `
</SubmitButton>
</form>
<table
className="data zebra big-spacer-top "
className="data zebra big-spacer-top"
>
<thead>
<tr>
@@ -106,7 +106,7 @@ exports[`should render correctly 2`] = `
</SubmitButton>
</form>
<table
className="data zebra big-spacer-top "
className="data zebra big-spacer-top"
>
<thead>
<tr>
@@ -139,6 +139,7 @@ exports[`should render correctly 2`] = `
timeout={100}
>
<TokensFormItem
deleteConfirmation="inline"
key="foo"
login="luke"
onRevokeToken={[Function]}
@@ -151,6 +152,7 @@ exports[`should render correctly 2`] = `
}
/>
<TokensFormItem
deleteConfirmation="inline"
key="bar"
login="luke"
onRevokeToken={[Function]}

+ 64
- 2
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormItem-test.tsx.snap Datei anzeigen

@@ -48,7 +48,69 @@ exports[`should render correctly 1`] = `
</tr>
`;

exports[`should revoke the token 1`] = `
exports[`should render correctly 2`] = `
<tr>
<td>
<Tooltip
overlay="foo"
>
<span>
foo
</span>
</Tooltip>
</td>
<td
className="nowrap"
>
<DateFromNowHourPrecision
date="2019-01-18T15:06:33+0100"
/>
</td>
<td
className="thin nowrap text-right"
>
<DateFormatter
date="2019-01-15T15:06:33+0100"
long={true}
/>
</td>
<td
className="thin nowrap text-right"
>
<DeferredSpinner
loading={false}
timeout={100}
>
<i
className="spinner-placeholder"
/>
</DeferredSpinner>
<ConfirmButton
confirmButtonText="users.tokens.revoke_token"
isDestructive={true}
modalBody={
<FormattedMessage
defaultMessage="users.tokens.sure_X"
id="users.tokens.sure_X"
values={
Object {
"token": <strong>
foo
</strong>,
}
}
/>
}
modalHeader="users.tokens.revoke_token"
onConfirm={[Function]}
>
<Component />
</ConfirmButton>
</td>
</tr>
`;

exports[`should revoke the token using inline confirmation 1`] = `
<Button
className="button-red input-small spacer-left"
disabled={false}
@@ -58,7 +120,7 @@ exports[`should revoke the token 1`] = `
</Button>
`;

exports[`should revoke the token 2`] = `
exports[`should revoke the token using inline confirmation 2`] = `
<Button
className="button-red input-small spacer-left"
disabled={false}

+ 1
- 0
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap Datei anzeigen

@@ -26,6 +26,7 @@ exports[`should render correctly 1`] = `
className="modal-body modal-container"
>
<TokensForm
deleteConfirmation="inline"
login="john.doe"
updateTokensCount={[MockFunction]}
/>

+ 3
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties Datei anzeigen

@@ -3098,9 +3098,11 @@ users.remove=Remove user
users.search_description=Search users by login or name
users.update=Update users
users.tokens=Tokens
users.user_X_tokens=Tokens of {user}
users.tokens.sure=Sure?
users.tokens.sure_X=Are you sure you want to revoke token {token}?
users.tokens.revoke=Revoke
users.user_X_tokens=Tokens of {user}
users.tokens.revoke_token=Revoke token
users.no_tokens=No tokens
users.generate=Generate
users.generate_tokens=Generate Tokens

Laden…
Abbrechen
Speichern