From 086f2c1ced0da956219181958767683e1d4bb63b Mon Sep 17 00:00:00 2001 From: guillaume-peoch-sonarsource Date: Thu, 23 Nov 2023 15:07:03 +0100 Subject: [PATCH] SONAR-21086 Use new endpoint '/api/v2/authorizations/groups' --- .../main/js/api/mocks/GroupsServiceMock.ts | 75 ++++++++----------- .../sonar-web/src/main/js/api/user_groups.ts | 32 +++++--- .../src/main/js/apps/groups/GroupsApp.tsx | 18 ++--- .../groups/components/DeleteGroupForm.tsx | 9 +-- .../js/apps/groups/components/GroupForm.tsx | 10 +-- .../src/main/js/helpers/react-query.ts | 9 +++ .../src/main/js/helpers/testMocks.ts | 2 +- .../sonar-web/src/main/js/queries/groups.ts | 47 ++++-------- server/sonar-web/src/main/js/queries/users.ts | 9 +-- server/sonar-web/src/main/js/types/types.ts | 4 +- 10 files changed, 98 insertions(+), 117 deletions(-) diff --git a/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts index d86e38376cf..442ec1b12fc 100644 --- a/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts @@ -81,8 +81,8 @@ export default class GroupsServiceMock { jest.mocked(getIdentityProviders).mockImplementation(this.handleGetIdentityProviders); jest.mocked(getUsersGroups).mockImplementation((p) => this.handleSearchUsersGroups(p)); jest.mocked(createGroup).mockImplementation((g) => this.handleCreateGroup(g)); - jest.mocked(deleteGroup).mockImplementation((g) => this.handleDeleteGroup(g)); - jest.mocked(updateGroup).mockImplementation((g) => this.handleUpdateGroup(g)); + jest.mocked(deleteGroup).mockImplementation((id) => this.handleDeleteGroup(id)); + jest.mocked(updateGroup).mockImplementation((id, data) => this.handleUpdateGroup(id, data)); jest.mocked(getUsersInGroup).mockImplementation(this.handlegetUsersInGroup); jest.mocked(addUserToGroup).mockImplementation(this.handleAddUserToGroup); jest.mocked(removeUserFromGroup).mockImplementation(this.handleRemoveUserFromGroup); @@ -111,39 +111,34 @@ export default class GroupsServiceMock { return this.reply(newGroup); }; - handleDeleteGroup = (group: { name: string }): Promise> => { - if (!this.groups.some((g) => g.name === group.name)) { + handleDeleteGroup: typeof deleteGroup = (id: string) => { + if (!this.groups.some((g) => g.id === id)) { return Promise.reject(); } - const groupToDelete = this.groups.find((g) => g.name === group.name); + const groupToDelete = this.groups.find((g) => g.id === id); if (groupToDelete?.managed) { return Promise.reject(); } - this.groups = this.groups.filter((g) => g.name !== group.name); - return this.reply({}); + this.groups = this.groups.filter((g) => g.id !== id); + return this.reply(undefined); }; - handleUpdateGroup = (group: { - currentName: string; - name?: string; - description?: string; - }): Promise> => { - if (!this.groups.some((g) => group.currentName === g.name)) { + handleUpdateGroup: typeof updateGroup = (id, data): Promise> => { + const group = this.groups.find((g) => g.id === id); + if (group === undefined) { return Promise.reject(); } - this.groups.map((g) => { - if (g.name === group.currentName) { - if (group.name !== undefined) { - g.name = group.name; - } - if (group.description !== undefined) { - g.description = group.description; - } - } - }); + if (data.description !== undefined) { + group.description = data.description; + } + + if (data.name !== undefined) { + group.name = data.name; + } + return this.reply({}); }; @@ -173,39 +168,35 @@ export default class GroupsServiceMock { }); }; - handleSearchUsersGroups = (data: { - f?: string; - p?: number; - ps?: number; - q?: string; - managed: boolean | undefined; - }): Promise<{ groups: Group[]; paging: Paging }> => { - const { paging } = this; - if (data.p !== undefined && data.p !== paging.pageIndex) { - this.setPaging({ pageIndex: paging.pageIndex++ }); + handleSearchUsersGroups = ( + params: Parameters[0], + ): Promise<{ groups: Group[]; page: Paging }> => { + const { paging: page } = this; + if (params.pageIndex !== undefined && params.pageIndex !== page.pageIndex) { + this.setPaging({ pageIndex: page.pageIndex++ }); const groups = [ mockGroup({ name: `local-group ${this.groups.length + 4}` }), mockGroup({ name: `local-group ${this.groups.length + 5}` }), ]; - return this.reply({ paging, groups }); + return this.reply({ page, groups }); } if (this.isManaged) { - if (data.managed === undefined) { + if (params.managed === undefined) { return this.reply({ - paging, - groups: this.groups.filter((g) => (data?.q ? g.name.includes(data.q) : true)), + page, + groups: this.groups.filter((g) => (params?.q ? g.name.includes(params.q) : true)), }); } - const groups = this.groups.filter((group) => group.managed === data.managed); + const groups = this.groups.filter((group) => group.managed === params.managed); return this.reply({ - paging, - groups: groups.filter((g) => (data?.q ? g.name.includes(data.q) : true)), + page, + groups: groups.filter((g) => (params?.q ? g.name.includes(params.q) : true)), }); } return this.reply({ - paging, - groups: this.groups.filter((g) => (data?.q ? g.name.includes(data.q) : true)), + page, + groups: this.groups.filter((g) => (params?.q ? g.name.includes(params.q) : true)), }); }; diff --git a/server/sonar-web/src/main/js/api/user_groups.ts b/server/sonar-web/src/main/js/api/user_groups.ts index 61faf130605..ade6218916c 100644 --- a/server/sonar-web/src/main/js/api/user_groups.ts +++ b/server/sonar-web/src/main/js/api/user_groups.ts @@ -17,18 +17,20 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import axios from 'axios'; import { throwGlobalError } from '../helpers/error'; -import { getJSON, post, postJSON } from '../helpers/request'; +import { axiosToCatch, getJSON, post } from '../helpers/request'; import { Group, Paging, UserGroupMember } from '../types/types'; -export function getUsersGroups(data: { - f?: string; - p?: number; - ps?: number; +const GROUPS_ENDPOINT = '/api/v2/authorizations/groups'; + +export function getUsersGroups(params: { q?: string; managed: boolean | undefined; -}): Promise<{ groups: Group[]; paging: Paging }> { - return getJSON('/api/user_groups/search', data).catch(throwGlobalError); + pageIndex?: number; + pageSize?: number; +}): Promise<{ groups: Group[]; page: Paging }> { + return axios.get(GROUPS_ENDPOINT, { params }); } export function getUsersInGroup(data: { @@ -53,13 +55,19 @@ export function removeUserFromGroup(data: { name: string; login?: string }) { } export function createGroup(data: { description?: string; name: string }): Promise { - return postJSON('/api/user_groups/create', data).then((r) => r.group, throwGlobalError); + return axios.post(GROUPS_ENDPOINT, data).then((r) => r.group); } -export function updateGroup(data: { description?: string; currentName: string; name?: string }) { - return post('/api/user_groups/update', data).catch(throwGlobalError); +export function updateGroup( + id: string, + data: { + name?: string; + description?: string; + }, +) { + return axiosToCatch.patch(`${GROUPS_ENDPOINT}/${id}`, data); } -export function deleteGroup(data: { name: string }) { - return post('/api/user_groups/delete', data).catch(throwGlobalError); +export function deleteGroup(id: string) { + return axios.delete(`${GROUPS_ENDPOINT}/${id}`); } diff --git a/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx b/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx index aa4f890f768..6aef407edc8 100644 --- a/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx +++ b/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx @@ -33,18 +33,16 @@ import List from './components/List'; import './groups.css'; export default function GroupsApp() { - const [numberOfPages, setNumberOfPages] = useState(1); const [search, setSearch] = useState(''); const [managed, setManaged] = useState(); const manageProvider = useManageProvider(); - const { groups, total, isLoading } = useGroupsQueries( - { - q: search, - managed, - }, - numberOfPages, - ); + const { data, isLoading, fetchNextPage } = useGroupsQueries({ + q: search, + managed, + }); + + const groups = data?.pages.flatMap((page) => page.groups) ?? []; return ( <> @@ -76,9 +74,9 @@ export default function GroupsApp() { setNumberOfPages((n) => n + 1)} + loadMore={fetchNextPage} ready={!isLoading} - total={total} + total={data?.pages[0].page.total} /> diff --git a/server/sonar-web/src/main/js/apps/groups/components/DeleteGroupForm.tsx b/server/sonar-web/src/main/js/apps/groups/components/DeleteGroupForm.tsx index 6b491854dbe..a4b4ad084a2 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/DeleteGroupForm.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/DeleteGroupForm.tsx @@ -36,12 +36,9 @@ export default function DeleteGroupForm(props: Props) { const { mutate: deleteGroup } = useDeleteGroupMutation(); const onSubmit = () => { - deleteGroup( - { name: group.name }, - { - onSuccess: props.onClose, - }, - ); + deleteGroup(group.id, { + onSuccess: props.onClose, + }); }; const header = translate('groups.delete_group'); diff --git a/server/sonar-web/src/main/js/apps/groups/components/GroupForm.tsx b/server/sonar-web/src/main/js/apps/groups/components/GroupForm.tsx index 84eff7ad012..97faf125d87 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/GroupForm.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/GroupForm.tsx @@ -25,7 +25,6 @@ import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker'; import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation'; import Spinner from '../../../components/ui/Spinner'; import { translate } from '../../../helpers/l10n'; -import { omitNil } from '../../../helpers/request'; import { useCreateGroupMutation, useUpdateGroupMutation } from '../../../queries/groups'; import { Group } from '../../../types/types'; @@ -60,10 +59,11 @@ export default function GroupForm(props: Props) { } updateGroup( { - currentName: group.name, - description, - // pass `name` only if it has changed, otherwise the WS fails - ...omitNil({ name: name !== group.name ? name : undefined }), + id: group.id, + data: { + name, + description, + }, }, { onSuccess: props.onClose }, ); diff --git a/server/sonar-web/src/main/js/helpers/react-query.ts b/server/sonar-web/src/main/js/helpers/react-query.ts index d8efca16d91..0dc5e38c9fc 100644 --- a/server/sonar-web/src/main/js/helpers/react-query.ts +++ b/server/sonar-web/src/main/js/helpers/react-query.ts @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { UseQueryResult } from '@tanstack/react-query'; +import { Paging } from '../types/types'; const notUndefined = (x: T | undefined): x is T => x !== undefined; @@ -32,3 +33,11 @@ export const mapReactQueryResult = ( data: notUndefined(res.data) ? mapper(res.data) : res.data, } as UseQueryResult; }; + +export const getNextPageParam = (params: T) => + params.page.total <= params.page.pageIndex * params.page.pageSize + ? undefined + : params.page.pageIndex + 1; + +export const getPreviousPageParam = (params: T) => + params.page.pageIndex === 1 ? undefined : params.page.pageIndex - 1; diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index a815fac07a0..f064faf24ad 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -293,7 +293,7 @@ export function mockLoggedInUser(overrides: Partial = {}): LoggedI export function mockGroup(overrides: Partial = {}): Group { return { - membersCount: 1, + id: Math.random().toString(), name: 'Foo', managed: false, ...overrides, diff --git a/server/sonar-web/src/main/js/queries/groups.ts b/server/sonar-web/src/main/js/queries/groups.ts index a7592593612..1b0e258276e 100644 --- a/server/sonar-web/src/main/js/queries/groups.ts +++ b/server/sonar-web/src/main/js/queries/groups.ts @@ -18,14 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { - QueryFunctionContext, - useMutation, - useQueries, - useQuery, - useQueryClient, -} from '@tanstack/react-query'; -import { range } from 'lodash'; +import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { createGroup, deleteGroup, @@ -33,37 +26,19 @@ import { getUsersInGroup, updateGroup, } from '../api/user_groups'; -import { Group } from '../types/types'; +import { getNextPageParam, getPreviousPageParam } from '../helpers/react-query'; const STALE_TIME = 4 * 60 * 1000; export function useGroupsQueries( getParams: Omit[0], 'pageSize' | 'pageIndex'>, - numberOfPages: number, ) { - type QueryKey = [ - 'group', - 'list', - number, - Omit[0], 'pageSize' | 'pageIndex'>, - ]; - const results = useQueries({ - queries: range(1, numberOfPages + 1).map((page: number) => ({ - queryKey: ['group', 'list', page, getParams], - queryFn: ({ queryKey: [_u, _l, page, getParams] }: QueryFunctionContext) => - getUsersGroups({ ...getParams, p: page }), - staleTime: STALE_TIME, - })), + return useInfiniteQuery({ + queryKey: ['group', 'list', getParams], + queryFn: ({ pageParam = 1 }) => getUsersGroups({ ...getParams, pageIndex: pageParam }), + getNextPageParam, + getPreviousPageParam, }); - - return results.reduce<{ groups: Group[]; total: number | undefined; isLoading: boolean }>( - (acc, { data, isLoading }) => ({ - groups: acc.groups.concat(data?.groups ?? []), - total: data?.paging.total, - isLoading: acc.isLoading || isLoading, - }), - { groups: [], total: 0, isLoading: false }, - ); } export function useMembersCountQuery(name: string) { @@ -89,7 +64,13 @@ export function useUpdateGroupMutation() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (data: Parameters[0]) => updateGroup(data), + mutationFn: ({ + id, + data, + }: { + id: Parameters[0]; + data: Parameters[1]; + }) => updateGroup(id, data), onSuccess() { queryClient.invalidateQueries({ queryKey: ['group', 'list'] }); }, diff --git a/server/sonar-web/src/main/js/queries/users.ts b/server/sonar-web/src/main/js/queries/users.ts index 897292435a0..6dbedd6c805 100644 --- a/server/sonar-web/src/main/js/queries/users.ts +++ b/server/sonar-web/src/main/js/queries/users.ts @@ -30,6 +30,7 @@ import { updateUser, } from '../api/users'; import { useCurrentUser } from '../app/components/current-user/CurrentUserContext'; +import { getNextPageParam, getPreviousPageParam } from '../helpers/react-query'; import { UserToken } from '../types/token'; import { NoticeType, RestUserBase } from '../types/users'; @@ -41,12 +42,8 @@ export function useUsersQueries( return useInfiniteQuery({ queryKey: ['user', 'list', getParams], queryFn: ({ pageParam = 1 }) => getUsers({ ...getParams, pageIndex: pageParam }), - getNextPageParam: (lastPage) => - lastPage.page.total <= lastPage.page.pageIndex * lastPage.page.pageSize - ? undefined - : lastPage.page.pageIndex + 1, - getPreviousPageParam: (firstPage) => - firstPage.page.pageIndex === 1 ? undefined : firstPage.page.pageIndex - 1, + getNextPageParam, + getPreviousPageParam, }); } diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts index 910fb3147be..9bae3b878eb 100644 --- a/server/sonar-web/src/main/js/types/types.ts +++ b/server/sonar-web/src/main/js/types/types.ts @@ -223,10 +223,10 @@ export interface FlowLocation { } export interface Group { + id: string; default?: boolean; - description?: string; - membersCount: number; name: string; + description?: string; managed: boolean; } -- 2.39.5