]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11985 Use a confirmation modal for token revoking
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Tue, 18 Jun 2019 08:03:00 +0000 (10:03 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 28 Jun 2019 06:45:41 +0000 (08:45 +0200)
server/sonar-web/src/main/js/apps/account/components/Tokens.tsx
server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Tokens-test.tsx.snap
server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx
server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx
server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx
server/sonar-web/src/main/js/apps/users/components/__tests__/TokensForm-test.tsx
server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensForm-test.tsx.snap
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormItem-test.tsx.snap
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 4e385bd815ba2fd73d37c130e81e3888a8e4fe2d..aeef02fbf00ebad627f85f3ba64ca55bea0ede7a 100644 (file)
@@ -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>
   );
index e7bd83f820991b17bfc7a7d8a772d05466822899..67efe5168da4a0c0f807b5cede55cb3039a38fcd 100644 (file)
@@ -18,6 +18,7 @@ exports[`renders 1`] = `
       />
     </div>
     <TokensForm
+      deleteConfirmation="modal"
       login="user"
     />
   </div>
index ef7b4103bf188e3e783d94299ff1277c39ec43e4..cee5f77432b06d115acee41322cc890a896b9260 100644 (file)
@@ -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>
index 09673e108dcba5b37d0a57a9f47b4d3bb378dc23..4c42e26003d1bef45c290061c22acef58145c8b8 100644 (file)
  * 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>
     );
index 567a143b99c9ce2b412d2a6edbddfaebcdf29514..89e6652db2bef474d8cea5a217a253e7ea2b8cc8 100644 (file)
@@ -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>
index 71d9002127c687f12fb51d7752008b0545a844f6..19ebe0feea0a9502e3d3b37eae65bcfc41450bbe 100644 (file)
@@ -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} />
+  );
 }
index 1a65f7c4756d5314f97da126a5f714e0642c0867..71614fc6ff73ebb87d263ccaf631f675f5282f0c 100644 (file)
@@ -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}
+    />
   );
 }
index 401bae31b0a7adb4063f277b0e6fae4a33ed5ec8..f45ecb861b4405f2beb972066269a5435f083d8c 100644 (file)
@@ -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]}
index 499fed7e30fe9ab1908fd2c764c3120bb7362f76..7a38670adf73dc9c6f8f173143166b63ec022db7 100644 (file)
@@ -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}
index 77879640815151561eee2e59e5f837b7f58e51f7..436ea23f0619a6e80bf97472bc6e15c36d8ad87f 100644 (file)
@@ -26,6 +26,7 @@ exports[`should render correctly 1`] = `
     className="modal-body modal-container"
   >
     <TokensForm
+      deleteConfirmation="inline"
       login="john.doe"
       updateTokensCount={[MockFunction]}
     />
index a5dbd86a899a95b5c376b61bbc1e5352af908e03..4fbc0c17990ec6248d97001a58b7f8e8bfc70f6f 100644 (file)
@@ -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