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' }),
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' }),
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 () => {
import UserActions from './UserActions';
import UserListItemIdentity from './UserListItemIdentity';
import UserScmAccounts from './UserScmAccounts';
+import ViewGroupsModal from './ViewGroupsModal';
export interface UserListItemProps {
identityProvider?: IdentityProvider;
<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>
</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>
);
}
--- /dev/null
+/*
+ * 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')}
+ />
+ );
+}
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}