diff options
author | Sarath Nair <91882341+sarath-nair-sonarsource@users.noreply.github.com> | 2024-10-03 14:17:13 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-10-03 20:02:51 +0000 |
commit | e4d86bcecaa3fc20d37971f11be44b0150195692 (patch) | |
tree | 78789e87cbe728bed7adde61aca58ac30a36e38c /server/sonar-web | |
parent | 4b6cae980fe81ca8e6368785873159a868832426 (diff) | |
download | sonarqube-e4d86bcecaa3fc20d37971f11be44b0150195692.tar.gz sonarqube-e4d86bcecaa3fc20d37971f11be44b0150195692.zip |
SONAR-22486 Show group membership in user list when auto provision is enabled (#11933)
Diffstat (limited to 'server/sonar-web')
3 files changed, 116 insertions, 15 deletions
diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx index d6d7940c7df..b0a6876f2c1 100644 --- a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx @@ -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 () => { diff --git a/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx b/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx index 51b8b12c5bb..6fbb0e6ccd6 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx @@ -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 index 00000000000..adf8b46e3a3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/ViewGroupsModal.tsx @@ -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')} + /> + ); +} |