aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/groups
diff options
context:
space:
mode:
authorViktor Vorona <viktor.vorona@sonarsource.com>2023-11-27 11:09:07 +0100
committersonartech <sonartech@sonarsource.com>2023-11-28 20:02:43 +0000
commit34d70b67a50d90c9b8380898fe645aac19c9c5db (patch)
treeb73958a2ff453f39c0c343221b11bf0753006734 /server/sonar-web/src/main/js/apps/groups
parent683569ab1f206005514ea88e65ff343be61b838d (diff)
downloadsonarqube-34d70b67a50d90c9b8380898fe645aac19c9c5db.tar.gz
sonarqube-34d70b67a50d90c9b8380898fe645aac19c9c5db.zip
SONAR-21073 Use new /group-memberships endpoints on UI
Diffstat (limited to 'server/sonar-web/src/main/js/apps/groups')
-rw-r--r--server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx79
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx123
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/ViewMembersModal.tsx51
4 files changed, 149 insertions, 108 deletions
diff --git a/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx b/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx
index 03153e6d142..265da70be5f 100644
--- a/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx
@@ -22,15 +22,22 @@ import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import AuthenticationServiceMock from '../../../api/mocks/AuthenticationServiceMock';
+import GroupMembershipsServiceMock from '../../../api/mocks/GroupMembersipsServiceMock';
import GroupsServiceMock from '../../../api/mocks/GroupsServiceMock';
+import SystemServiceMock from '../../../api/mocks/SystemServiceMock';
+import UsersServiceMock from '../../../api/mocks/UsersServiceMock';
import { Provider } from '../../../components/hooks/useManageProvider';
+import { mockGroupMembership, mockRestUser } from '../../../helpers/testMocks';
import { renderApp } from '../../../helpers/testReactTestingUtils';
import { byRole, byText } from '../../../helpers/testSelector';
import { Feature } from '../../../types/features';
import { TaskStatuses } from '../../../types/tasks';
import GroupsApp from '../GroupsApp';
+const systemHandler = new SystemServiceMock();
const handler = new GroupsServiceMock();
+const groupMembershipsHandler = new GroupMembershipsServiceMock();
+const userHandler = new UsersServiceMock(groupMembershipsHandler);
const authenticationHandler = new AuthenticationServiceMock();
const ui = {
@@ -61,22 +68,23 @@ const ui = {
createGroupDialog: byRole('dialog', { name: 'groups.create_group' }),
membersViewDialog: byRole('dialog', { name: 'users.list' }),
membersDialog: byRole('dialog', { name: 'users.update' }),
- getMembers: () => within(ui.membersDialog.get()).getAllByRole('checkbox'),
+ getMembers: () => within(ui.membersDialog.get()).findAllByRole('checkbox'),
- managedGroupRow: byRole('row', { name: 'managed-group 3' }),
+ managedGroupRow: byRole('table').byRole('row', { name: 'managed-group 3' }),
githubManagedGroupRow: byRole('row', { name: 'managed-group github 3' }),
managedGroupEditMembersButton: byRole('button', { name: 'groups.users.edit.managed-group' }),
managedGroupViewMembersButton: byRole('button', { name: 'groups.users.view.managed-group' }),
- memberAliceUser: byText('alice'),
- memberBobUser: byText('bob'),
+ memberAliceUser: byText('alice.merveille'),
+ memberBobUser: byText('bob.marley'),
memberSearchInput: byRole('searchbox', { name: 'search_verb' }),
managedEditButton: byRole('button', { name: 'groups.edit.managed-group' }),
localGroupRow: byRole('row', { name: 'local-group 3' }),
+ localGroupWithALotOfSelected: byRole('row', { name: 'local-group 15' }),
localGroupEditMembersButton: byRole('button', { name: 'groups.users.edit.local-group' }),
- localGroupRow2: byRole('row', { name: 'local-group 2 3 group 2 is loco!' }),
+ localGroupRow2: byRole('row', { name: 'local-group 2 0 group 2 is loco!' }),
editedLocalGroupRow: byRole('row', { name: 'local-group 3 3 group 3 rocks!' }),
localEditButton: byRole('button', { name: 'groups.edit.local-group' }),
localGroupRowWithLocalBadge: byRole('row', {
@@ -91,12 +99,23 @@ const ui = {
beforeEach(() => {
handler.reset();
+ systemHandler.reset();
authenticationHandler.reset();
+ userHandler.reset();
+ groupMembershipsHandler.reset();
+ groupMembershipsHandler.memberships = [
+ mockGroupMembership({ groupId: '1', userId: '1' }),
+ mockGroupMembership({ groupId: '1', userId: '2' }),
+ mockGroupMembership({ groupId: '1', userId: '3' }),
+ mockGroupMembership({ groupId: '2', userId: '1' }),
+ mockGroupMembership({ groupId: '2', userId: '2' }),
+ mockGroupMembership({ groupId: '2', userId: '3' }),
+ ];
});
describe('in non managed mode', () => {
beforeEach(() => {
- handler.setIsManaged(false);
+ systemHandler.setProvider(null);
});
it('should render all groups', async () => {
@@ -172,28 +191,57 @@ describe('in non managed mode', () => {
expect(await ui.membersDialog.find()).toBeInTheDocument();
- expect(ui.getMembers()).toHaveLength(2);
+ expect(await ui.getMembers()).toHaveLength(3);
await user.click(ui.allFilter.get());
- expect(ui.getMembers()).toHaveLength(3);
+ expect(await ui.getMembers()).toHaveLength(6);
+ expect((await ui.getMembers()).filter((m) => (m as HTMLInputElement).checked)).toHaveLength(3);
await user.click(ui.unselectedFilter.get());
+ expect(await ui.getMembers()).toHaveLength(3);
expect(ui.reloadButton.query()).not.toBeInTheDocument();
- await user.click(ui.getMembers()[0]);
+ await user.click((await ui.getMembers())[0]);
expect(await ui.reloadButton.find()).toBeInTheDocument();
await user.click(ui.selectedFilter.get());
- expect(ui.getMembers()).toHaveLength(3);
+ expect(await ui.getMembers()).toHaveLength(4);
expect(ui.reloadButton.query()).not.toBeInTheDocument();
- await user.click(ui.getMembers()[0]);
+ await user.click((await ui.getMembers())[0]);
expect(await ui.reloadButton.find()).toBeInTheDocument();
await user.click(ui.reloadButton.get());
- expect(ui.getMembers()).toHaveLength(2);
+ expect(await ui.getMembers()).toHaveLength(3);
await user.click(ui.doneButton.get());
expect(ui.membersDialog.query()).not.toBeInTheDocument();
});
+ it('should be able to load more members of a group', async () => {
+ const user = userEvent.setup();
+ userHandler.users = new Array(20)
+ .fill(null)
+ .map((_, i) => mockRestUser({ login: `user${i}`, id: `${i}` }));
+ groupMembershipsHandler.memberships = new Array(15)
+ .fill(null)
+ .map((_, i) => mockGroupMembership({ groupId: '2', userId: `${i}` }));
+ renderGroupsApp();
+
+ expect(await ui.localGroupWithALotOfSelected.find()).toBeInTheDocument();
+ expect(await ui.localGroupEditMembersButton.find()).toBeInTheDocument();
+ await user.click(ui.localGroupEditMembersButton.get());
+
+ expect(await ui.membersDialog.find()).toBeInTheDocument();
+
+ expect(await ui.getMembers()).toHaveLength(10);
+ await user.click(ui.membersDialog.by(ui.showMore).get());
+ expect(await ui.getMembers()).toHaveLength(15);
+ expect(ui.membersDialog.by(ui.showMore).query()).not.toBeInTheDocument();
+
+ await user.click(ui.unselectedFilter.get());
+ expect(await ui.getMembers()).toHaveLength(5);
+ await user.click(ui.doneButton.get());
+ expect(ui.membersDialog.query()).not.toBeInTheDocument();
+ });
+
it('should be able search a group', async () => {
const user = userEvent.setup();
renderGroupsApp();
@@ -222,7 +270,7 @@ describe('in non managed mode', () => {
describe('in manage mode', () => {
beforeEach(() => {
- handler.setIsManaged(true);
+ systemHandler.setProvider(Provider.Scim);
});
it('should not be able to create a group', async () => {
@@ -265,7 +313,7 @@ describe('in manage mode', () => {
await user.click(ui.managedGroupViewMembersButton.get());
expect(await ui.membersViewDialog.find()).toBeInTheDocument();
- expect(ui.memberAliceUser.get()).toBeInTheDocument();
+ expect(ui.membersViewDialog.by(ui.memberAliceUser).get()).toBeInTheDocument();
expect(ui.memberBobUser.get()).toBeInTheDocument();
await user.type(ui.memberSearchInput.get(), 'b');
@@ -307,7 +355,7 @@ describe('in manage mode', () => {
describe('Github Provisioning', () => {
beforeEach(() => {
authenticationHandler.handleActivateGithubProvisioning();
- handler.setProvider(Provider.Github);
+ systemHandler.setProvider(Provider.Github);
});
it('should display a success status when the synchronisation is a success', async () => {
@@ -367,7 +415,6 @@ describe('in manage mode', () => {
});
it('should render a github icon for github groups', async () => {
- handler.setProvider(Provider.Github);
const user = userEvent.setup();
renderGroupsApp();
diff --git a/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx b/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx
index 875bade13e3..c1818628841 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx
@@ -17,9 +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 { find, without } from 'lodash';
+import { find } from 'lodash';
import * as React from 'react';
-import { addUserToGroup, getUsersInGroup, removeUserFromGroup } from '../../../api/user_groups';
import Modal from '../../../components/controls/Modal';
import SelectList, {
SelectListFilter,
@@ -27,7 +26,13 @@ import SelectList, {
} from '../../../components/controls/SelectList';
import { ResetButtonLink } from '../../../components/controls/buttons';
import { translate } from '../../../helpers/l10n';
-import { Group, UserSelected } from '../../../types/types';
+import {
+ useAddGroupMembershipMutation,
+ useGroupMembersQuery,
+ useRemoveGroupMembershipMutation,
+} from '../../../queries/group-memberships';
+import { Group } from '../../../types/types';
+import { RestUserBase } from '../../../types/users';
interface Props {
group: Group;
@@ -35,70 +40,75 @@ interface Props {
}
export default function EditMembersModal(props: Readonly<Props>) {
- const [needToReload, setNeedToReload] = React.useState(false);
- const [users, setUsers] = React.useState<UserSelected[]>([]);
- const [selectedUsers, setSelectedUsers] = React.useState<string[]>([]);
- const [usersTotalCount, setUsersTotalCount] = React.useState<number | undefined>(undefined);
- const [lastSearchParams, setLastSearchParams] = React.useState<
- SelectListSearchParams | undefined
- >(undefined);
-
const { group } = props;
- const modalHeader = translate('users.update');
- const fetchUsers = (searchParams: SelectListSearchParams) =>
- getUsersInGroup({
- name: props.group.name,
- p: searchParams.page,
- ps: searchParams.pageSize,
- q: searchParams.query !== '' ? searchParams.query : undefined,
- selected: searchParams.filter,
- }).then((data) => {
- const more = searchParams.page != null && searchParams.page > 1;
+ const [query, setQuery] = React.useState<string>('');
+ const [changedUsers, setChangedUsers] = React.useState<Map<string, boolean>>(new Map());
+ const [filter, setFilter] = React.useState<SelectListFilter>(SelectListFilter.Selected);
+ const { mutateAsync: addUserToGroup } = useAddGroupMembershipMutation();
+ const { mutateAsync: removeUserFromGroup } = useRemoveGroupMembershipMutation();
+ const {
+ data,
+ isLoading,
+ fetchNextPage,
+ remove: emptyQueryCache,
+ } = useGroupMembersQuery({
+ q: query,
+ groupId: group.id,
+ filter,
+ });
- setUsers(more ? [...users, ...data.users] : data.users);
- const newSelectedUsers = data.users.filter((user) => user.selected).map((user) => user.login);
- setSelectedUsers(more ? [...selectedUsers, ...newSelectedUsers] : newSelectedUsers);
- setNeedToReload(false);
- setLastSearchParams(searchParams);
- setUsersTotalCount(data.paging.total);
- });
+ const users: (RestUserBase & { selected?: boolean })[] =
+ data?.pages.flatMap((page) => page.users) ?? [];
+
+ const modalHeader = translate('users.update');
- const handleSelect = (login: string) =>
+ const handleSelect = (userId: string) =>
addUserToGroup({
- name: group.name,
- login,
+ groupId: group.id,
+ userId,
}).then(() => {
- setNeedToReload(true);
- setSelectedUsers([...selectedUsers, login]);
+ const newChangedUsers = new Map(changedUsers);
+ newChangedUsers.set(userId, true);
+ setChangedUsers(newChangedUsers);
});
- const handleUnselect = (login: string) =>
+ const handleUnselect = (userId: string) =>
removeUserFromGroup({
- name: group.name,
- login,
+ userId,
+ groupId: group.id,
}).then(() => {
- setNeedToReload(true);
- setSelectedUsers(without(selectedUsers, login));
+ const newChangedUsers = new Map(changedUsers);
+ newChangedUsers.set(userId, false);
+ setChangedUsers(newChangedUsers);
});
- const renderElement = (login: string): React.ReactNode => {
- const user = find(users, { login });
+ const renderElement = (id: string): React.ReactNode => {
+ const user = find(users, { id });
+ if (!user) {
+ return null;
+ }
+
return (
<div className="select-list-list-item">
- {user === undefined ? (
- login
- ) : (
- <>
- {user.name}
- <br />
- <span className="note">{user.login}</span>
- </>
- )}
+ {user.name}
+ <br />
+ <span className="note">{user.login}</span>
</div>
);
};
+ const onSearch = (searchParams: SelectListSearchParams) => {
+ setQuery(searchParams.query);
+ setFilter(searchParams.filter);
+ if (searchParams.page === 1) {
+ emptyQueryCache();
+ setChangedUsers(new Map());
+ } else {
+ fetchNextPage();
+ }
+ };
+
return (
<Modal
className="group-menbers-modal"
@@ -111,17 +121,18 @@ export default function EditMembersModal(props: Readonly<Props>) {
<div className="modal-body modal-container">
<SelectList
- elements={users.map((user) => user.login)}
- elementsTotalCount={usersTotalCount}
- needToReload={
- needToReload && lastSearchParams && lastSearchParams.filter !== SelectListFilter.All
- }
- onSearch={fetchUsers}
+ elements={users.map((user) => user.id)}
+ elementsTotalCount={data?.pages[0].page.total}
+ needToReload={changedUsers.size > 0 && filter !== SelectListFilter.All}
+ onSearch={onSearch}
onSelect={handleSelect}
onUnselect={handleUnselect}
renderElement={renderElement}
- selectedElements={selectedUsers}
+ selectedElements={users
+ .filter((u) => (changedUsers.has(u.id) ? changedUsers.get(u.id) : u.selected))
+ .map((u) => u.id)}
withPaging
+ loading={isLoading}
/>
</div>
diff --git a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
index 561c46536ce..57452ea1138 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
@@ -27,7 +27,7 @@ import ActionsDropdown, {
import { Provider } from '../../../components/hooks/useManageProvider';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/system';
-import { useMembersCountQuery } from '../../../queries/groups';
+import { useGroupMembersCountQuery } from '../../../queries/group-memberships';
import { Group } from '../../../types/types';
import DeleteGroupForm from './DeleteGroupForm';
import GroupForm from './GroupForm';
@@ -45,7 +45,7 @@ export default function ListItem(props: ListItemProps) {
const [groupToDelete, setGroupToDelete] = useState<Group | undefined>();
const [groupToEdit, setGroupToEdit] = useState<Group | undefined>();
- const { data: membersCount, isLoading, refetch } = useMembersCountQuery(group.name);
+ const { data: membersCount, isLoading, refetch } = useGroupMembersCountQuery(group.id);
const isManaged = () => {
return manageProvider !== undefined;
diff --git a/server/sonar-web/src/main/js/apps/groups/components/ViewMembersModal.tsx b/server/sonar-web/src/main/js/apps/groups/components/ViewMembersModal.tsx
index b5af92b945c..dd5f62a36c1 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/ViewMembersModal.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/components/ViewMembersModal.tsx
@@ -19,14 +19,13 @@
*/
import { Spinner } from 'design-system';
import * as React from 'react';
-import { getUsersInGroup } from '../../../api/user_groups';
import ListFooter from '../../../components/controls/ListFooter';
import Modal from '../../../components/controls/Modal';
import SearchBox from '../../../components/controls/SearchBox';
-import { SelectListFilter } from '../../../components/controls/SelectList';
import { ResetButtonLink } from '../../../components/controls/buttons';
import { translate } from '../../../helpers/l10n';
-import { Group, UserGroupMember } from '../../../types/types';
+import { useGroupMembersQuery } from '../../../queries/group-memberships';
+import { Group } from '../../../types/types';
interface Props {
isManaged: boolean;
@@ -34,33 +33,16 @@ interface Props {
onClose: () => void;
}
-export default function ViewMembersModal(props: Props) {
+export default function ViewMembersModal(props: Readonly<Props>) {
const { isManaged, group } = props;
- const [loading, setLoading] = React.useState(false);
- const [page, setPage] = React.useState(1);
const [query, setQuery] = React.useState<string>();
- const [total, setTotal] = React.useState<number>();
- const [users, setUsers] = React.useState<UserGroupMember[]>([]);
+ const { data, isLoading, fetchNextPage } = useGroupMembersQuery({
+ q: query,
+ groupId: group.id,
+ });
- React.useEffect(() => {
- (async () => {
- setLoading(true);
- const data = await getUsersInGroup({
- name: group.name,
- p: page,
- q: query,
- selected: SelectListFilter.Selected,
- });
- if (page > 1) {
- setUsers([...users, ...data.users]);
- } else {
- setUsers(data.users);
- }
- setTotal(data.paging.total);
- setLoading(false);
- })();
- }, [query, page]);
+ const users = data?.pages.flatMap((page) => page.users) ?? [];
const modalHeader = translate('users.list');
return (
@@ -76,16 +58,13 @@ export default function ViewMembersModal(props: Props) {
<div className="modal-body modal-container">
<SearchBox
className="view-search-box"
- loading={loading}
- onChange={(q) => {
- setQuery(q);
- setPage(1);
- }}
+ loading={isLoading}
+ onChange={setQuery}
placeholder={translate('search_verb')}
value={query}
/>
<div className="select-list-list-container spacer-top sw-overflow-auto">
- <Spinner loading={loading}>
+ <Spinner loading={isLoading}>
<ul className="menu">
{users.map((user) => (
<li key={user.login} className="display-flex-center">
@@ -106,8 +85,12 @@ export default function ViewMembersModal(props: Props) {
</ul>
</Spinner>
</div>
- {total !== undefined && (
- <ListFooter count={users.length} loadMore={() => setPage((p) => p + 1)} total={total} />
+ {data !== undefined && (
+ <ListFooter
+ count={users.length}
+ loadMore={fetchNextPage}
+ total={data?.pages[0].page.total}
+ />
)}
</div>