Browse Source

SONAR-11985 Use a confirmation modal for token revoking

tags/8.0
Wouter Admiraal 5 years ago
parent
commit
5fdfa1d192

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

*/ */
import * as React from 'react'; import * as React from 'react';
import InstanceMessage from '../../../components/common/InstanceMessage'; import InstanceMessage from '../../../components/common/InstanceMessage';
import TokenForm from '../../users/components/TokensForm';
import TokensForm from '../../users/components/TokensForm';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';


interface Props { interface Props {
<InstanceMessage message={translate('my_account.tokens_description')} /> <InstanceMessage message={translate('my_account.tokens_description')} />
</div> </div>


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

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

/> />
</div> </div>
<TokensForm <TokensForm
deleteConfirmation="modal"
login="user" login="user"
/> />
</div> </div>

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

* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import * as React from 'react'; import * as React from 'react';
import TokensFormItem from './TokensFormItem';
import TokensFormItem, { TokenDeleteConfirmation } from './TokensFormItem';
import TokensFormNewToken from './TokensFormNewToken'; import TokensFormNewToken from './TokensFormNewToken';
import DeferredSpinner from '../../../components/common/DeferredSpinner'; import DeferredSpinner from '../../../components/common/DeferredSpinner';
import { SubmitButton } from '../../../components/ui/buttons'; import { SubmitButton } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';


interface Props { interface Props {
deleteConfirmation: TokenDeleteConfirmation;
login: string; login: string;
updateTokensCount?: (login: string, tokensCount: number) => void; updateTokensCount?: (login: string, tokensCount: number) => void;
} }
} }
return tokens.map(token => ( return tokens.map(token => (
<TokensFormItem <TokensFormItem
deleteConfirmation={this.props.deleteConfirmation}
key={token.name} key={token.name}
login={this.props.login} login={this.props.login}
onRevokeToken={this.handleRevokeToken} onRevokeToken={this.handleRevokeToken}


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


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

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

* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import * as React from 'react'; 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 DateFormatter from '../../../components/intl/DateFormatter';
import DateFromNowHourPrecision from '../../../components/intl/DateFromNowHourPrecision'; import DateFromNowHourPrecision from '../../../components/intl/DateFromNowHourPrecision';
import DeferredSpinner from '../../../components/common/DeferredSpinner'; import DeferredSpinner from '../../../components/common/DeferredSpinner';
import Tooltip from '../../../components/controls/Tooltip'; import Tooltip from '../../../components/controls/Tooltip';
import { Button } from '../../../components/ui/buttons';
import { limitComponentName } from '../../../helpers/path'; import { limitComponentName } from '../../../helpers/path';
import { revokeToken } from '../../../api/user-tokens'; import { revokeToken } from '../../../api/user-tokens';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';


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

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


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


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


componentDidMount() { componentDidMount() {
this.mounted = true; this.mounted = true;
this.mounted = false; 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 { } 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() { render() {
const { token } = this.props;
const { loading } = this.state;
const { deleteConfirmation, token } = this.props;
const { loading, showConfirmation } = this.state;
return ( return (
<tr> <tr>
<td> <td>
<DeferredSpinner loading={loading}> <DeferredSpinner loading={loading}>
<i className="spinner-placeholder" /> <i className="spinner-placeholder" />
</DeferredSpinner> </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> </td>
</tr> </tr>
); );

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

</h2> </h2>
</header> </header>
<div className="modal-body modal-container"> <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> </div>
<footer className="modal-foot"> <footer className="modal-foot">
<ResetButtonLink onClick={props.onClose}>{translate('Done')}</ResetButtonLink> <ResetButtonLink onClick={props.onClose}>{translate('Done')}</ResetButtonLink>

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

}); });


function shallowRender(props: Partial<TokensForm['props']> = {}) { 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 View File



it('should render correctly', () => { it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot(); 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 onRevokeToken = jest.fn();
const wrapper = shallowRender({ onRevokeToken });
const wrapper = shallowRender({ deleteConfirmation: 'inline', onRevokeToken });
expect(wrapper.find('Button')).toMatchSnapshot(); expect(wrapper.find('Button')).toMatchSnapshot();
click(wrapper.find('Button')); click(wrapper.find('Button'));
expect(wrapper.find('Button')).toMatchSnapshot(); expect(wrapper.find('Button')).toMatchSnapshot();
expect(onRevokeToken).toHaveBeenCalledWith(userToken); 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']> = {}) { function shallowRender(props: Partial<TokensFormItem['props']> = {}) {
return shallow( 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 View File

</SubmitButton> </SubmitButton>
</form> </form>
<table <table
className="data zebra big-spacer-top "
className="data zebra big-spacer-top"
> >
<thead> <thead>
<tr> <tr>
</SubmitButton> </SubmitButton>
</form> </form>
<table <table
className="data zebra big-spacer-top "
className="data zebra big-spacer-top"
> >
<thead> <thead>
<tr> <tr>
timeout={100} timeout={100}
> >
<TokensFormItem <TokensFormItem
deleteConfirmation="inline"
key="foo" key="foo"
login="luke" login="luke"
onRevokeToken={[Function]} onRevokeToken={[Function]}
} }
/> />
<TokensFormItem <TokensFormItem
deleteConfirmation="inline"
key="bar" key="bar"
login="luke" login="luke"
onRevokeToken={[Function]} onRevokeToken={[Function]}

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

</tr> </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 <Button
className="button-red input-small spacer-left" className="button-red input-small spacer-left"
disabled={false} disabled={false}
</Button> </Button>
`; `;


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

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

className="modal-body modal-container" className="modal-body modal-container"
> >
<TokensForm <TokensForm
deleteConfirmation="inline"
login="john.doe" login="john.doe"
updateTokensCount={[MockFunction]} updateTokensCount={[MockFunction]}
/> />

+ 3
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

users.search_description=Search users by login or name users.search_description=Search users by login or name
users.update=Update users users.update=Update users
users.tokens=Tokens users.tokens=Tokens
users.user_X_tokens=Tokens of {user}
users.tokens.sure=Sure? users.tokens.sure=Sure?
users.tokens.sure_X=Are you sure you want to revoke token {token}?
users.tokens.revoke=Revoke users.tokens.revoke=Revoke
users.user_X_tokens=Tokens of {user}
users.tokens.revoke_token=Revoke token
users.no_tokens=No tokens users.no_tokens=No tokens
users.generate=Generate users.generate=Generate
users.generate_tokens=Generate Tokens users.generate_tokens=Generate Tokens

Loading…
Cancel
Save