diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2019-06-18 10:03:00 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2019-06-28 08:45:41 +0200 |
commit | 5fdfa1d19245ba022c1c28b9ee8b47a5b4b01dc5 (patch) | |
tree | 3c2c1af21abb77e08fca8622872491e653a3c6a7 | |
parent | 0244544e2fc2953e08799c555c2d40a8cd2005a0 (diff) | |
download | sonarqube-5fdfa1d19245ba022c1c28b9ee8b47a5b4b01dc5.tar.gz sonarqube-5fdfa1d19245ba022c1c28b9ee8b47a5b4b01dc5.zip |
SONAR-11985 Use a confirmation modal for token revoking
11 files changed, 166 insertions, 38 deletions
diff --git a/server/sonar-web/src/main/js/apps/account/components/Tokens.tsx b/server/sonar-web/src/main/js/apps/account/components/Tokens.tsx index 4e385bd815b..aeef02fbf00 100644 --- a/server/sonar-web/src/main/js/apps/account/components/Tokens.tsx +++ b/server/sonar-web/src/main/js/apps/account/components/Tokens.tsx @@ -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> ); 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 index e7bd83f8209..67efe5168da 100644 --- 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 @@ -18,6 +18,7 @@ exports[`renders 1`] = ` /> </div> <TokensForm + deleteConfirmation="modal" login="user" /> </div> 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 ef7b4103bf1..cee5f77432b 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,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> 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 09673e108dc..4c42e26003d 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 @@ -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> ); 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 index 567a143b99c..89e6652db2b 100644 --- a/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensForm-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensForm-test.tsx index 71d9002127c..19ebe0feea0 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensForm-test.tsx @@ -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} /> + ); } diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx index 1a65f7c4756..71614fc6ff7 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx @@ -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} + /> ); } diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensForm-test.tsx.snap index 401bae31b0a..f45ecb861b4 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensForm-test.tsx.snap @@ -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]} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormItem-test.tsx.snap index 499fed7e30f..7a38670adf7 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormItem-test.tsx.snap @@ -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} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap index 77879640815..436ea23f061 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap @@ -26,6 +26,7 @@ exports[`should render correctly 1`] = ` className="modal-body modal-container" > <TokensForm + deleteConfirmation="inline" login="john.doe" updateTokensCount={[MockFunction]} /> diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index a5dbd86a899..4fbc0c17990 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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 |