diff options
author | Viktor Vorona <viktor.vorona@sonarsource.com> | 2023-11-27 11:09:07 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-11-28 20:02:43 +0000 |
commit | 34d70b67a50d90c9b8380898fe645aac19c9c5db (patch) | |
tree | b73958a2ff453f39c0c343221b11bf0753006734 /server/sonar-web/src/main/js/apps/groups | |
parent | 683569ab1f206005514ea88e65ff343be61b838d (diff) | |
download | sonarqube-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')
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> |