]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22486 Show group membership in user list when auto provision is enabled (...
authorSarath Nair <91882341+sarath-nair-sonarsource@users.noreply.github.com>
Thu, 3 Oct 2024 12:17:13 +0000 (14:17 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 3 Oct 2024 20:02:51 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx
server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx
server/sonar-web/src/main/js/apps/users/components/ViewGroupsModal.tsx [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index d6d7940c7df4501bbc364a6f1fb1287fd18e491d..b0a6876f2c17a0b28c74fbd12e2dcece9d2ae7de 100644 (file)
@@ -64,10 +64,12 @@ const ui = {
   localFilter: byRole('radio', { name: 'local' }),
   showMore: byRole('button', { name: 'show_more' }),
   aliceUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.alice.merveille' }),
+  aliceViewGroupButton: byRole('button', { name: 'users.view_users_groups.alice.merveille' }),
   aliceUpdateButton: byRole('button', { name: 'users.manage_user.alice.merveille' }),
   denisUpdateButton: byRole('button', { name: 'users.manage_user.denis.villeneuve' }),
   alicedDeactivateButton: byText('users.deactivate'),
   bobUpdateGroupButton: byRole('button', { name: 'users.update_users_groups.bob.marley' }),
+  bobViewGroupButton: byRole('button', { name: 'users.view_users_groups.bob.marley' }),
   bobUpdateButton: byRole('button', { name: 'users.manage_user.bob.marley' }),
   scmAddButton: byRole('button', { name: 'add_verb' }),
   createUserDialogButton: byRole('button', { name: 'create' }),
@@ -117,6 +119,8 @@ const ui = {
   jackRow: byRole('row', { name: /Jack/ }),
 
   dialogGroups: byRole('dialog', { name: 'users.update_groups' }),
+  dialogViewGroups: byRole('dialog', { name: 'users.view_groups' }),
+  buttonCloseDialogViewGroups: byRole('button', { name: 'modal.close' }),
   allFilter: byRole('radio', { name: 'all' }),
   selectedFilter: byRole('radio', { name: 'selected' }),
   unselectedFilter: byRole('radio', { name: 'unselected' }),
@@ -509,13 +513,18 @@ describe('in manage mode', () => {
     expect(ui.createUserButton.get()).toBeDisabled();
   });
 
-  it("should not be able to add/remove a user's group", async () => {
+  it("should be able to view only a user's group", async () => {
+    const user = userEvent.setup({ skipHover: true });
     renderUsersApp();
 
     expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument();
-    expect(ui.aliceUpdateGroupButton.query()).not.toBeInTheDocument();
+    await user.click(ui.aliceViewGroupButton.get());
+    expect(ui.dialogViewGroups.get()).toBeInTheDocument();
+    expect(ui.dialogViewGroups.byRole('checkbox').query()).not.toBeInTheDocument();
+    await user.click(ui.buttonCloseDialogViewGroups.get());
     expect(ui.bobRow.get()).toBeInTheDocument();
-    expect(ui.bobUpdateGroupButton.query()).not.toBeInTheDocument();
+    await user.click(ui.bobViewGroupButton.get());
+    expect(ui.dialogViewGroups.byRole('checkbox').query()).not.toBeInTheDocument();
   });
 
   it('should not be able to update scm account', async () => {
index 51b8b12c5bb4092774c1df6e1e33a8fb519b8e2b..6fbb0e6ccd6fc2733998cfe2f7b1dc2380cc1ef3 100644 (file)
@@ -37,6 +37,7 @@ import TokensFormModal from './TokensFormModal';
 import UserActions from './UserActions';
 import UserListItemIdentity from './UserListItemIdentity';
 import UserScmAccounts from './UserScmAccounts';
+import ViewGroupsModal from './ViewGroupsModal';
 
 export interface UserListItemProps {
   identityProvider?: IdentityProvider;
@@ -85,17 +86,24 @@ export default function UserListItem(props: Readonly<UserListItemProps>) {
       <ContentCell>
         <Spinner isLoading={groupsAreLoading}>
           {groupsCount}
-          {manageProvider === undefined && (
-            <ButtonIcon
-              Icon={IconMoreVertical}
-              tooltipContent={translate('users.update_groups')}
-              className="it__user-groups sw-ml-2"
-              ariaLabel={translateWithParameters('users.update_users_groups', user.login)}
-              onClick={() => setOpenGroupForm(true)}
-              size={ButtonSize.Medium}
-              variety={ButtonVariety.DefaultGhost}
-            />
-          )}
+          <ButtonIcon
+            Icon={IconMoreVertical}
+            tooltipContent={
+              manageProvider === undefined
+                ? translate('users.update_groups')
+                : translate('users.view_groups')
+            }
+            className="it__user-groups sw-ml-2"
+            ariaLabel={translateWithParameters(
+              manageProvider === undefined
+                ? 'users.update_users_groups'
+                : 'users.view_users_groups',
+              user.login,
+            )}
+            onClick={() => setOpenGroupForm(true)}
+            size={ButtonSize.Medium}
+            variety={ButtonVariety.DefaultGhost}
+          />
         </Spinner>
       </ContentCell>
       <ContentCell>
@@ -119,7 +127,12 @@ export default function UserListItem(props: Readonly<UserListItemProps>) {
       </ActionCell>
 
       {openTokenForm && <TokensFormModal onClose={() => setOpenTokenForm(false)} user={user} />}
-      {openGroupForm && <GroupsForm onClose={() => setOpenGroupForm(false)} user={user} />}
+      {openGroupForm && manageProvider === undefined && (
+        <GroupsForm onClose={() => setOpenGroupForm(false)} user={user} />
+      )}
+      {openGroupForm && manageProvider !== undefined && (
+        <ViewGroupsModal onClose={() => setOpenGroupForm(false)} user={user} />
+      )}
     </TableRow>
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/users/components/ViewGroupsModal.tsx b/server/sonar-web/src/main/js/apps/users/components/ViewGroupsModal.tsx
new file mode 100644 (file)
index 0000000..adf8b46
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { Button, Modal, ModalSize, Spinner, Text } from '@sonarsource/echoes-react';
+import { Badge, InputSearch } from 'design-system/lib';
+import * as React from 'react';
+import { translate } from '../../../helpers/l10n';
+import { useUserGroupsQuery } from '../../../queries/group-memberships';
+import { RestUserDetailed } from '../../../types/users';
+
+interface Props {
+  onClose: () => void;
+  user: RestUserDetailed;
+}
+
+export default function ViewGroupsModal(props: Readonly<Props>) {
+  const { onClose, user } = props;
+  const [query, setQuery] = React.useState<string>('');
+  const { data: groups, isLoading } = useUserGroupsQuery({
+    q: query,
+    userId: user.id,
+  });
+
+  const modalBody = (
+    <>
+      <InputSearch
+        className="sw-w-full"
+        loading={isLoading}
+        onChange={setQuery}
+        placeholder={translate('search_verb')}
+        value={query}
+      />
+      <div className="sw-mt-6">
+        <Spinner isLoading={isLoading} />
+        <ul className="sw-flex sw-flex-col sw-gap-4">
+          {(groups || []).map(({ id, description, managed, name }) => (
+            <li key={id} className="sw-flex sw-items-center">
+              <span className="sw-flex sw-gap-2 sw-justify-between sw-items-center sw-w-full">
+                <span>
+                  {name}
+                  <br />
+                  <Text isSubdued> {description} </Text>
+                </span>
+                {!managed && <Badge>{translate('local')}</Badge>}
+              </span>
+            </li>
+          ))}
+        </ul>
+      </div>
+    </>
+  );
+
+  return (
+    <Modal
+      content={modalBody}
+      isOpen
+      onOpenChange={onClose}
+      secondaryButton={<Button onClick={onClose}>{translate('close')}</Button>}
+      size={ModalSize.Default}
+      title={translate('users.view_groups')}
+    />
+  );
+}
index a7384ba9f022b18b61e0b7351381b34360c23dc6..13b129c101b043d796ed4f52f50d5072894fb00b 100644 (file)
@@ -5491,7 +5491,9 @@ users.last_connection=Last connection
 users.last_sonarlint_connection=Last SonarLint connection
 users.last_sonarlint_connection.help_text=The time of the last connection from SonarLint indicates that the user used SonarLint in connected mode.
 users.update_users_groups=Update {0}'s group membership
+users.view_users_groups=View {0}'s group membership
 users.update_groups=Update Groups
+users.view_groups=View Groups
 users.manage_user=Update {0}
 users.update_tokens=Update tokens
 users.update_tokens_for_x=Update tokens for user {0}