]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21384 Migrating users list inside security to adopt the new UI
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Tue, 2 Jan 2024 11:18:43 +0000 (12:18 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 4 Jan 2024 20:02:48 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/users/UsersApp.tsx
server/sonar-web/src/main/js/apps/users/UsersList.tsx
server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx
server/sonar-web/src/main/js/apps/users/components/UserActions.tsx
server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx
server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx
server/sonar-web/src/main/js/apps/users/components/UserScmAccounts.tsx

index 4dd2306a8a9abeca53e664b291e9348b1ae0f1fe..af4769e5e1f6e2a6965e8e09ba43d0b5f4c63922 100644 (file)
@@ -147,6 +147,7 @@ export default function UsersApp() {
         loadMore={fetchNextPage}
         ready={!isLoading}
         total={data?.pages[0].page.total}
+        useMIUIButtons
       />
     </main>
   );
index 2b53c10c50a4148b7baf0d41838ad2bf58f7f2b1..19a2bb20dd6a2518dd3be12ddd07b74183771f85 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { ActionCell, ContentCell, HelperHintIcon, Table, TableRow } from 'design-system';
 import * as React from 'react';
 import HelpTooltip from '../../components/controls/HelpTooltip';
 import { translate } from '../../helpers/l10n';
@@ -31,41 +32,37 @@ interface Props {
 }
 
 export default function UsersList({ identityProviders, users, manageProvider }: Props) {
+  const header = (
+    <TableRow>
+      <ContentCell>{translate('users.user_name')}</ContentCell>
+      <ContentCell>{translate('my_profile.scm_accounts')}</ContentCell>
+      <ContentCell>{translate('users.last_connection')}</ContentCell>
+      <ContentCell>
+        {translate('users.last_sonarlint_connection')}
+        <HelpTooltip overlay={translate('users.last_sonarlint_connection.help_text')}>
+          <HelperHintIcon />
+        </HelpTooltip>
+      </ContentCell>
+      <ContentCell>{translate('my_profile.groups')}</ContentCell>
+      <ContentCell>{translate('users.tokens')}</ContentCell>
+      {(manageProvider === undefined || users.some((u) => !u.managed)) && (
+        <ActionCell>{translate('actions')}</ActionCell>
+      )}
+    </TableRow>
+  );
+
   return (
-    <div className="boxed-group boxed-group-inner">
-      <table className="data zebra" id="users-list">
-        <thead>
-          <tr>
-            <th className="nowrap">{translate('users.user_name')}</th>
-            <th className="nowrap">{translate('my_profile.scm_accounts')}</th>
-            <th className="nowrap">{translate('users.last_connection')}</th>
-            <th className="nowrap">
-              {translate('users.last_sonarlint_connection')}
-              <HelpTooltip
-                className="sw-ml-1"
-                overlay={translate('users.last_sonarlint_connection.help_text')}
-              />
-            </th>
-            <th className="nowrap">{translate('my_profile.groups')}</th>
-            <th className="nowrap">{translate('users.tokens')}</th>
-            {(manageProvider === undefined || users.some((u) => !u.managed)) && (
-              <th className="nowrap">{translate('actions')}</th>
-            )}
-          </tr>
-        </thead>
-        <tbody>
-          {users.map((user) => (
-            <UserListItem
-              identityProvider={identityProviders.find(
-                (provider) => user.externalProvider === provider.key,
-              )}
-              key={user.login}
-              user={user}
-              manageProvider={manageProvider}
-            />
-          ))}
-        </tbody>
-      </table>
-    </div>
+    <Table columnCount={7} header={header} id="users-list">
+      {users.map((user) => (
+        <UserListItem
+          identityProvider={identityProviders.find(
+            (provider) => user.externalProvider === provider.key,
+          )}
+          key={user.login}
+          user={user}
+          manageProvider={manageProvider}
+        />
+      ))}
+    </Table>
   );
 }
index 7dfaebb95b28edde6aacf5d380927db6d850a762..33a0be26b466c6c9c923a173b73b51b7cd1daa83 100644 (file)
@@ -52,7 +52,7 @@ const ui = {
   aliceUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.alice.merveille' }),
   aliceUpdateButton: byRole('button', { name: 'users.manage_user.alice.merveille' }),
   denisUpdateButton: byRole('button', { name: 'users.manage_user.denis.villeneuve' }),
-  alicedDeactivateButton: byRole('button', { name: 'users.deactivate' }),
+  alicedDeactivateButton: byRole('menuitem', { name: 'users.deactivate' }),
   bobUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.bob.marley' }),
   bobUpdateButton: byRole('button', { name: 'users.manage_user.bob.marley' }),
   scmAddButton: byRole('button', { name: 'add_verb' }),
@@ -70,31 +70,34 @@ const ui = {
       name: `remove_x.users.create_user.scm_account_${value ? `x.${value}` : 'new'}`,
     }),
   userRows: byRole('row', {
-    name: (accessibleName) => /^[A-Z]+ /.test(accessibleName),
+    name: (accessibleName) => /^[A-Z]+[a-z]*/.test(accessibleName),
   }),
   aliceRow: byRole('row', {
-    name: (accessibleName) => accessibleName.startsWith('AM Alice Merveille alice.merveille '),
+    name: (accessibleName) =>
+      accessibleName.startsWith('Alice Merveille Alice Merveille alice.merveille '),
   }),
   aliceRowWithLocalBadge: byRole('row', {
     name: (accessibleName) =>
       accessibleName.startsWith(
-        'AM Alice Merveille alice.merveille alice.merveille@wonderland.com local ',
+        'Alice Merveille Alice Merveille alice.merveille alice.merveille@wonderland.com local ',
       ),
   }),
   bobRow: byRole('row', {
-    name: (accessibleName) => accessibleName.startsWith('BM Bob Marley bob.marley '),
+    name: (accessibleName) => accessibleName.startsWith('Bob Marley Bob Marley bob.marley '),
   }),
   charlieRow: byRole('row', {
-    name: (accessibleName) => accessibleName.startsWith('CC Charlie Cox charlie.cox'),
+    name: (accessibleName) => accessibleName.startsWith('Charlie Cox Charlie Cox charlie.cox'),
   }),
   denisRow: byRole('row', {
-    name: (accessibleName) => accessibleName.startsWith('DV Denis Villeneuve denis.villeneuve '),
+    name: (accessibleName) =>
+      accessibleName.startsWith('Denis Villeneuve Denis Villeneuve denis.villeneuve '),
   }),
   evaRow: byRole('row', {
-    name: (accessibleName) => accessibleName.startsWith('EG Eva Green eva.green '),
+    name: (accessibleName) => accessibleName.startsWith('Eva Green Eva Green eva.green '),
   }),
   franckRow: byRole('row', {
-    name: (accessibleName) => accessibleName.startsWith('FG Franck Grillo franck.grillo '),
+    name: (accessibleName) =>
+      accessibleName.startsWith('Franck Grillo Franck Grillo franck.grillo '),
   }),
   jackRow: byRole('row', { name: /Jack/ }),
 
@@ -338,7 +341,7 @@ describe('in non managed mode', () => {
     renderUsersApp();
 
     await user.click(await ui.aliceUpdateButton.find());
-    await user.click(await ui.aliceRow.byRole('button', { name: 'update_details' }).find());
+    await user.click(await ui.aliceRow.byRole('menuitem', { name: 'update_details' }).find());
     expect(await ui.dialogUpdateUser.find()).toBeInTheDocument();
 
     expect(ui.userNameInput.get()).toHaveValue('Alice Merveille');
@@ -357,7 +360,7 @@ describe('in non managed mode', () => {
     renderUsersApp();
 
     await user.click(await ui.aliceUpdateButton.find());
-    await user.click(await ui.aliceRow.byRole('button', { name: 'users.deactivate' }).find());
+    await user.click(await ui.aliceRow.byRole('menuitem', { name: 'users.deactivate' }).find());
     expect(await ui.dialogDeactivateUser.find()).toBeInTheDocument();
     expect(ui.deleteUserAlert.query()).not.toBeInTheDocument();
     await user.click(ui.deleteUserCheckbox.get());
@@ -375,7 +378,7 @@ describe('in non managed mode', () => {
 
     await user.click(await ui.aliceUpdateButton.find());
     await user.click(
-      await ui.aliceRow.byRole('button', { name: 'my_profile.password.title' }).find(),
+      await ui.aliceRow.byRole('menuitem', { name: 'my_profile.password.title' }).find(),
     );
     expect(await ui.dialogPasswords.find()).toBeInTheDocument();
 
@@ -427,7 +430,7 @@ describe('in non managed mode', () => {
     renderUsersApp([], currentUser);
 
     await user.click(await ui.denisUpdateButton.find());
-    await user.click(await ui.denisRow.byRole('button', { name: 'update_details' }).find());
+    await user.click(await ui.denisRow.byRole('menuitem', { name: 'update_details' }).find());
     expect(await ui.dialogUpdateUser.find()).toBeInTheDocument();
 
     expect(ui.userNameInput.get()).toHaveValue('Denis Villeneuve');
@@ -447,7 +450,7 @@ describe('in non managed mode', () => {
 
     expect(await ui.aliceRow.byText('alice.merveille@wonderland.com').find()).toBeInTheDocument();
     await user.click(await ui.aliceUpdateButton.find());
-    await user.click(await ui.aliceRow.byRole('button', { name: 'update_details' }).find());
+    await user.click(await ui.aliceRow.byRole('menuitem', { name: 'update_details' }).find());
     expect(await ui.dialogUpdateUser.find()).toBeInTheDocument();
 
     expect(ui.emailInput.get()).toHaveValue('alice.merveille@wonderland.com');
@@ -498,7 +501,7 @@ describe('in manage mode', () => {
       ui.bobRow.byRole('button', { name: 'my_profile.password.title' }).query(),
     ).not.toBeInTheDocument();
 
-    await user.click(ui.bobRow.byRole('button', { name: 'update_scm' }).get());
+    await user.click(ui.bobRow.byRole('menuitem', { name: 'update_scm' }).get());
 
     expect(ui.userNameInput.get()).toBeDisabled();
     expect(ui.emailInput.get()).toBeDisabled();
@@ -691,7 +694,7 @@ it('accessibility', async () => {
 
   // user update dialog should be accessible
   await user.click(await ui.aliceUpdateButton.find());
-  await user.click(await ui.aliceRow.byRole('button', { name: 'update_details' }).find());
+  await user.click(await ui.aliceRow.byRole('menuitem', { name: 'update_details' }).find());
   expect(await ui.dialogUpdateUser.find()).toBeInTheDocument();
   await expect(await ui.dialogUpdateUser.find()).toHaveNoA11yViolations();
   await user.click(ui.cancelButton.get());
@@ -711,7 +714,7 @@ it('accessibility', async () => {
   // user password dialog should be accessible
   await user.click(await ui.aliceUpdateButton.find());
   await user.click(
-    await ui.aliceRow.byRole('button', { name: 'my_profile.password.title' }).find(),
+    await ui.aliceRow.byRole('menuitem', { name: 'my_profile.password.title' }).find(),
   );
   expect(await ui.dialogPasswords.find()).toBeInTheDocument();
   await expect(await ui.dialogPasswords.find()).toHaveNoA11yViolations();
index 1d1fc383f533eb803c3a43e9722daa952c2de052..2bac4faced2d8453d32370227c67a0de1266cce7 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 {
+  ActionsDropdown,
+  ItemButton,
+  ItemDangerButton,
+  ItemDivider,
+  PopupZLevel,
+} from 'design-system';
 import * as React from 'react';
-import ActionsDropdown, {
-  ActionsDropdownDivider,
-  ActionsDropdownItem,
-} from '../../../components/controls/ActionsDropdown';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { Provider } from '../../../types/types';
 import { RestUserDetailed, isUserActive } from '../../../types/users';
@@ -45,28 +49,29 @@ export default function UserActions(props: Props) {
 
   return (
     <>
-      <ActionsDropdown label={translateWithParameters('users.manage_user', user.login)}>
-        <ActionsDropdownItem className="js-user-update" onClick={() => setOpenForm('update')}>
+      <ActionsDropdown
+        id={`user-settings-action-dropdown-${user.login}`}
+        toggleClassName="it__user-actions-toggle"
+        allowResizing
+        ariaLabel={translateWithParameters('users.manage_user', user.login)}
+        zLevel={PopupZLevel.Global}
+      >
+        <ItemButton className="it__user-update" onClick={() => setOpenForm('update')}>
           {isInstanceManaged ? translate('update_scm') : translate('update_details')}
-        </ActionsDropdownItem>
+        </ItemButton>
         {!isInstanceManaged && user.local && (
-          <ActionsDropdownItem
-            className="js-user-change-password"
-            onClick={() => setOpenForm('password')}
-          >
+          <ItemButton className="it__user-change-password" onClick={() => setOpenForm('password')}>
             {translate('my_profile.password.title')}
-          </ActionsDropdownItem>
+          </ItemButton>
         )}
-
-        {isUserActive(user) && !isInstanceManaged && <ActionsDropdownDivider />}
+        {isUserActive(user) && !isInstanceManaged && <ItemDivider />}
         {isUserActive(user) && (!isInstanceManaged || isUserLocal) && (
-          <ActionsDropdownItem
-            className="js-user-deactivate"
-            destructive
+          <ItemDangerButton
+            className="it__user-deactivate"
             onClick={() => setOpenForm('deactivate')}
           >
             {translate('users.deactivate')}
-          </ActionsDropdownItem>
+          </ItemDangerButton>
         )}
       </ActionsDropdown>
       {openForm === 'deactivate' && isUserActive(user) && (
index 4c61f7830b7ffe691fa2727ae2bcda3ab62758bb..3a429d3287a7b69b7acdd3992d2dceade71677e9 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 {
+  Avatar,
+  ContentCell,
+  InteractiveIcon,
+  MenuIcon,
+  Spinner,
+  TableRow,
+  Tooltip,
+} from 'design-system';
 import * as React from 'react';
-import { ButtonIcon } from '../../../components/controls/buttons';
-import BulletListIcon from '../../../components/icons/BulletListIcon';
 import DateFromNow from '../../../components/intl/DateFromNow';
-import LegacyAvatar from '../../../components/ui/LegacyAvatar';
-import Spinner from '../../../components/ui/Spinner';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { useUserGroupsCountQuery, useUserTokensQuery } from '../../../queries/users';
 import { IdentityProvider, Provider } from '../../../types/types';
@@ -56,61 +61,63 @@ export default function UserListItem(props: UserListItemProps) {
   const { data: groupsCount, isLoading: groupsAreLoading } = useUserGroupsCountQuery(login);
 
   return (
-    <tr>
-      <td className="thin text-middle">
+    <TableRow>
+      <ContentCell>
         <div className="sw-flex sw-items-center">
-          <LegacyAvatar className="sw-shrink-0 sw-mr-4" hash={avatar} name={name} size={36} />
+          <Avatar className="sw-shrink-0 sw-mr-4" hash={avatar} name={name} size="md" />
           <UserListItemIdentity
             identityProvider={identityProvider}
             user={user}
             manageProvider={manageProvider}
           />
         </div>
-      </td>
-      <td className="thin text-middle">
+      </ContentCell>
+      <ContentCell>
         <UserScmAccounts scmAccounts={scmAccounts || []} />
-      </td>
-      <td className="thin nowrap text-middle">
+      </ContentCell>
+      <ContentCell>
         <DateFromNow date={sonarQubeLastConnectionDate ?? ''} hourPrecision />
-      </td>
-      <td className="thin nowrap text-middle">
+      </ContentCell>
+      <ContentCell>
         <DateFromNow date={sonarLintLastConnectionDate ?? ''} hourPrecision />
-      </td>
-      <td className="thin nowrap text-middle">
+      </ContentCell>
+      <ContentCell>
         <Spinner loading={groupsAreLoading}>
           {groupsCount}
           {manageProvider === undefined && (
-            <ButtonIcon
-              aria-label={translateWithParameters('users.update_users_groups', user.login)}
-              className="js-user-groups spacer-left button-small"
-              onClick={() => setOpenGroupForm(true)}
-              tooltip={translate('users.update_groups')}
-            >
-              <BulletListIcon />
-            </ButtonIcon>
+            <Tooltip overlay={translate('users.update_groups')}>
+              <InteractiveIcon
+                Icon={MenuIcon}
+                className="it__user-groups sw-ml-2"
+                aria-label={translateWithParameters('users.update_users_groups', user.login)}
+                onClick={() => setOpenGroupForm(true)}
+                size="small"
+              />
+            </Tooltip>
           )}
         </Spinner>
-      </td>
-      <td className="thin nowrap text-middle">
+      </ContentCell>
+      <ContentCell>
         <Spinner loading={tokensAreLoading}>
           {tokens?.length}
-          <ButtonIcon
-            className="js-user-tokens spacer-left button-small"
-            onClick={() => setOpenTokenForm(true)}
-            tooltip={translateWithParameters('users.update_tokens')}
-            aria-label={translateWithParameters('users.update_tokens_for_x', name ?? login)}
-          >
-            <BulletListIcon />
-          </ButtonIcon>
+          <Tooltip overlay={translateWithParameters('users.update_tokens')}>
+            <InteractiveIcon
+              Icon={MenuIcon}
+              className="it__user-tokens sw-ml-2"
+              aria-label={translateWithParameters('users.update_tokens_for_x', name ?? login)}
+              onClick={() => setOpenTokenForm(true)}
+              size="small"
+            />
+          </Tooltip>
         </Spinner>
-      </td>
+      </ContentCell>
 
-      <td className="thin nowrap text-right text-middle">
+      <ContentCell>
         <UserActions user={user} manageProvider={manageProvider} />
-      </td>
+      </ContentCell>
 
       {openTokenForm && <TokensFormModal onClose={() => setOpenTokenForm(false)} user={user} />}
       {openGroupForm && <GroupsForm onClose={() => setOpenGroupForm(false)} user={user} />}
-    </tr>
+    </TableRow>
   );
 }
index 08fa00509c6a09f121e950f90b4573ed9ba6d3b9..60de244e2ae3f16e877b6543f67675a6d004ddb2 100644 (file)
@@ -17,7 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { getTextColor } from 'design-system';
+
+import { Badge, Note, getTextColor } from 'design-system';
 import * as React from 'react';
 import { colors } from '../../../app/theme';
 import { translate } from '../../../helpers/l10n';
@@ -34,17 +35,15 @@ export interface Props {
 export default function UserListItemIdentity({ identityProvider, user, manageProvider }: Props) {
   return (
     <div>
-      <div>
-        <strong className="js-user-name">{user.name}</strong>
-        <span className="js-user-login note little-spacer-left">{user.login}</span>
+      <div className="sw-flex sw-flex-col">
+        <strong className="it__user-name sw-body-sm-highlight">{user.name}</strong>
+        <Note className="it__user-login">{user.login}</Note>
       </div>
-      {user.email && <div className="js-user-email little-spacer-top">{user.email}</div>}
+      {user.email && <div className="it__user-email sw-mt-1">{user.email}</div>}
       {!user.local && user.externalProvider !== 'sonarqube' && (
         <ExternalProvider identityProvider={identityProvider} user={user} />
       )}
-      {!user.managed && manageProvider !== undefined && (
-        <span className="badge">{translate('local')}</span>
-      )}
+      {!user.managed && manageProvider !== undefined && <Badge>{translate('local')}</Badge>}
     </div>
   );
 }
@@ -52,7 +51,7 @@ export default function UserListItemIdentity({ identityProvider, user, managePro
 export function ExternalProvider({ identityProvider, user }: Omit<Props, 'manageProvider'>) {
   if (!identityProvider) {
     return (
-      <div className="js-user-identity-provider little-spacer-top">
+      <div className="it__user-identity-provider sw-mt-1">
         <span>
           {user.externalProvider}: {user.externalLogin}
         </span>
@@ -61,9 +60,8 @@ export function ExternalProvider({ identityProvider, user }: Omit<Props, 'manage
   }
 
   return (
-    <div className="js-user-identity-provider little-spacer-top">
+    <div className="it__user-identity-provider sw-mt-1">
       <div
-        className="identity-provider"
         style={{
           backgroundColor: identityProvider.backgroundColor,
           color: getTextColor(identityProvider.backgroundColor, colors.secondFontColor),
@@ -71,7 +69,7 @@ export function ExternalProvider({ identityProvider, user }: Omit<Props, 'manage
       >
         <img
           alt={identityProvider.name}
-          className="little-spacer-right"
+          className="sw-mr-1"
           height="14"
           src={getBaseUrl() + identityProvider.iconPath}
           width="14"
index 0a0187219c28894f333b1f168e0231223de372f0..61a2014f33fdaf5ac5b90064274d37a9334b8c4e 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Link } from 'design-system';
 import * as React from 'react';
 import { translateWithParameters } from '../../../helpers/l10n';
 
@@ -42,24 +43,24 @@ export default class UserScmAccounts extends React.PureComponent<Props, State> {
     const { scmAccounts } = this.props;
     const limit = scmAccounts.length > SCM_LIMIT ? SCM_LIMIT - 1 : SCM_LIMIT;
     return (
-      <ul className="js-scm-accounts">
-        {scmAccounts.slice(0, limit).map((scmAccount, idx) => (
-          <li className="little-spacer-bottom" key={idx}>
+      <ul className="it__scm-accounts">
+        {scmAccounts.slice(0, limit).map((scmAccount) => (
+          <li className="sw-mb-1" key={scmAccount}>
             {scmAccount}
           </li>
         ))}
         {scmAccounts.length > SCM_LIMIT &&
           (this.state.showMore ? (
-            scmAccounts.slice(limit).map((scmAccount, idx) => (
-              <li className="little-spacer-bottom" key={idx + limit}>
+            scmAccounts.slice(limit).map((scmAccount) => (
+              <li className="sw-mb-1" key={scmAccount}>
                 {scmAccount}
               </li>
             ))
           ) : (
-            <li className="little-spacer-bottom">
-              <a className="js-user-more-scm" href="#" onClick={this.toggleShowMore}>
+            <li className="sw-mb-1">
+              <Link to="#" onClick={this.toggleShowMore}>
                 {translateWithParameters('more_x', scmAccounts.length - limit)}
-              </a>
+              </Link>
             </li>
           ))}
       </ul>