]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16567 Display the token expiration date and status in the list
authorJeremy Davis <jeremy.davis@sonarsource.com>
Wed, 29 Jun 2022 15:48:18 +0000 (17:48 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 4 Jul 2022 20:02:46 +0000 (20:02 +0000)
server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts
server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
server/sonar-web/src/main/js/apps/account/account.css
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/__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/helpers/mocks/token.ts
server/sonar-web/src/main/js/types/token.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 691818e90c5d9ffaf7309322449dd56003c7db78..b602b25e48d78f4b45f46be8e22d402b3276a9c5 100644 (file)
@@ -69,6 +69,7 @@ export default class UserTokensMock {
       login,
       type,
       projectKey,
+      isExpired: false,
       token: Math.random()
         .toString(RANDOM_RADIX)
         .slice(RANDOM_PREFIX),
index b2f4304d0ced804b8a2aa65986cef0ffad6c9e85..2d0a7c4bf878c96bbe669bc563fff5207accdec7 100644 (file)
@@ -24,6 +24,7 @@ import selectEvent from 'react-select-event';
 import { getMyProjects, getScannableProjects } from '../../../api/components';
 import NotificationsMock from '../../../api/mocks/NotificationsMock';
 import UserTokensMock from '../../../api/mocks/UserTokensMock';
+import { mockUserToken } from '../../../helpers/mocks/token';
 import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
 import { renderApp } from '../../../helpers/testReactTestingUtils';
 import { Permissions } from '../../../types/permissions';
@@ -295,6 +296,33 @@ describe('security page', () => {
     }
   );
 
+  it('should flag expired tokens as such', async () => {
+    tokenMock.tokens.push(
+      mockUserToken({
+        name: 'expired token',
+        isExpired: true,
+        expirationDate: '2021-01-23T19:25:19+0000'
+      })
+    );
+
+    renderAccountApp(
+      mockLoggedInUser({ permissions: { global: [Permissions.Scan] } }),
+      securityPagePath
+    );
+
+    expect(await screen.findByText('users.tokens')).toBeInTheDocument();
+
+    // expired token is flagged as such
+    const expiredTokenRow = screen.getByRole('row', { name: /expired token/ });
+    expect(within(expiredTokenRow).getByText('my_account.tokens.expired')).toBeInTheDocument();
+
+    // unexpired token is not flagged
+    const unexpiredTokenRow = screen.getAllByRole('row')[0];
+    expect(
+      within(unexpiredTokenRow).queryByText('my_account.tokens.expired')
+    ).not.toBeInTheDocument();
+  });
+
   it("should not suggest creating a Project token if the user doesn't have at least one scannable Projects", async () => {
     (getScannableProjects as jest.Mock).mockResolvedValueOnce({
       projects: []
index 18fcd48bc2c884a1bd7c7dabfe5185aacf01cd7f..7bbfd2eaaeed1cf00d9007e03c434609fb5cfa75 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 .account-container {
-  width: 800px;
+  width: 1000px;
   margin-left: auto;
   margin-right: auto;
 }
   border-top: 1px solid var(--barBorderColor);
 }
 
-.account-projects-list {
-  margin-left: -20px;
-  margin-right: -20px;
-}
-
 .account-projects-list > li {
   padding: 15px 20px;
 }
index 7fdb7e4f64886f8e115376ab873363c21ade5bf6..6f32d5f24e95c854fbf2b3ce803de5635b584705 100644 (file)
@@ -127,6 +127,7 @@ export class TokensForm extends React.PureComponent<Props, State> {
             {
               name: newToken.name,
               createdAt: newToken.createdAt,
+              isExpired: false,
               type: newTokenType,
               ...(newTokenType === TokenType.Project && {
                 project: { key: selectedProject.key, name: selectedProject.name }
@@ -254,7 +255,7 @@ export class TokensForm extends React.PureComponent<Props, State> {
     if (tokens.length <= 0) {
       return (
         <tr>
-          <td className="note" colSpan={3}>
+          <td className="note" colSpan={7}>
             {translate('users.no_tokens')}
           </td>
         </tr>
@@ -295,7 +296,8 @@ export class TokensForm extends React.PureComponent<Props, State> {
               <th>{translate('my_account.project_name')}</th>
               <th>{translate('my_account.tokens_last_usage')}</th>
               <th className="text-right">{translate('created')}</th>
-              <th />
+              <th className="text-right">{translate('my_account.tokens.expiration')}</th>
+              <th aria-label={translate('actions')} />
             </tr>
           </thead>
           <tbody>
index 71ddd4742547c10492827874ffabed533da49542..2615a0f824c9dbdda1a76fc53f3eb7424d8345bd 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import classNames from 'classnames';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { revokeToken } from '../../../api/user-tokens';
 import { Button } from '../../../components/controls/buttons';
 import ConfirmButton from '../../../components/controls/ConfirmButton';
+import WarningIcon from '../../../components/icons/WarningIcon';
 import DateFormatter from '../../../components/intl/DateFormatter';
 import DateFromNow from '../../../components/intl/DateFromNow';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
@@ -82,9 +84,15 @@ export default class TokensFormItem extends React.PureComponent<Props, State> {
     const { deleteConfirmation, token } = this.props;
     const { loading, showConfirmation } = this.state;
     return (
-      <tr>
+      <tr className={classNames({ 'text-muted-2': token.isExpired })}>
         <td title={token.name} className="hide-overflow nowrap">
           {token.name}
+          {token.isExpired && (
+            <div className="spacer-top text-warning">
+              <WarningIcon className="little-spacer-right" />
+              {translate('my_account.tokens.expired')}
+            </div>
+          )}
         </td>
         <td title={translate('users.tokens', token.type)} className="hide-overflow thin">
           {translate('users.tokens', token.type, 'short')}
@@ -98,6 +106,9 @@ export default class TokensFormItem extends React.PureComponent<Props, State> {
         <td className="thin nowrap text-right">
           <DateFormatter date={token.createdAt} long={true} />
         </td>
+        <td className={classNames('thin nowrap text-right', { 'text-warning': token.isExpired })}>
+          {token.expirationDate ? <DateFormatter date={token.expirationDate} long={true} /> : '–'}
+        </td>
         <td className="thin nowrap text-right">
           {deleteConfirmation === 'modal' ? (
             <ConfirmButton
index be01090b33d012876ce27717e4364f5b12540877..974d398732bc72a4167a7e84bd3d59652b946617 100644 (file)
@@ -50,7 +50,14 @@ exports[`should render correctly 1`] = `
         >
           created
         </th>
-        <th />
+        <th
+          className="text-right"
+        >
+          my_account.tokens.expiration
+        </th>
+        <th
+          aria-label="actions"
+        />
       </tr>
     </thead>
     <tbody>
@@ -69,7 +76,7 @@ exports[`should render correctly 1`] = `
         <tr>
           <td
             className="note"
-            colSpan={3}
+            colSpan={7}
           >
             users.no_tokens
           </td>
@@ -130,7 +137,14 @@ exports[`should render correctly 2`] = `
         >
           created
         </th>
-        <th />
+        <th
+          className="text-right"
+        >
+          my_account.tokens.expiration
+        </th>
+        <th
+          aria-label="actions"
+        />
       </tr>
     </thead>
     <tbody>
index 1aa8ba418b7950bb987988ff23da971fb3333e21..9ca6159cd850a07d8c55c6883cb3b13b9843c40f 100644 (file)
@@ -1,7 +1,9 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`should render correctly 1`] = `
-<tr>
+<tr
+  className=""
+>
   <td
     className="hide-overflow nowrap"
     title="foo"
@@ -33,6 +35,11 @@ exports[`should render correctly 1`] = `
       long={true}
     />
   </td>
+  <td
+    className="thin nowrap text-right"
+  >
+    –
+  </td>
   <td
     className="thin nowrap text-right"
   >
@@ -52,7 +59,9 @@ exports[`should render correctly 1`] = `
 `;
 
 exports[`should render correctly 2`] = `
-<tr>
+<tr
+  className=""
+>
   <td
     className="hide-overflow nowrap"
     title="foo"
@@ -84,6 +93,11 @@ exports[`should render correctly 2`] = `
       long={true}
     />
   </td>
+  <td
+    className="thin nowrap text-right"
+  >
+    –
+  </td>
   <td
     className="thin nowrap text-right"
   >
index b44fe21df1c9ad692ada04fa0858db96630b6152..f7bbecf6e0886d33060eb8d556adcda75383f27f 100644 (file)
@@ -25,6 +25,7 @@ export function mockUserToken(overrides: Partial<UserToken> = {}): UserToken {
     name: 'Token name',
     createdAt: '2019-06-14T09:45:52+0200',
     type: TokenType.User,
+    isExpired: false,
     ...overrides
   };
 }
index 6f0ec100b6b53645e8f9613b317ec9c62c4c57cd..acc95fe5754e40164514ec585c79865af023ce1c 100644 (file)
@@ -28,6 +28,8 @@ export interface UserToken {
   name: string;
   createdAt: string;
   lastConnectionDate?: string;
+  expirationDate?: string;
+  isExpired: boolean;
   type: TokenType;
   project?: { name: string; key: string };
 }
index 655aaf22d2b906e605ece789a538dd97fe1af4a2..2aad77b3586998c74ef7fdc119ec8b3135305bf2 100644 (file)
@@ -2027,6 +2027,8 @@ my_account.tokens_description=If you want to enforce security by not providing c
 my_account.token_type=Type
 my_account.project_name=Project
 my_account.tokens_last_usage=Last use
+my_account.tokens.expiration=Expiration
+my_account.tokens.expired=Token is expired
 my_account.projects=Projects
 my_account.projects.description=Those projects are the ones you are administering.
 my_account.projects.no_results=You are not administering any project yet.