]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21086 Use new endpoint '/api/v2/authorizations/groups'
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>
Thu, 23 Nov 2023 14:07:03 +0000 (15:07 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 24 Nov 2023 20:02:44 +0000 (20:02 +0000)
server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts
server/sonar-web/src/main/js/api/user_groups.ts
server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx
server/sonar-web/src/main/js/apps/groups/components/DeleteGroupForm.tsx
server/sonar-web/src/main/js/apps/groups/components/GroupForm.tsx
server/sonar-web/src/main/js/helpers/react-query.ts
server/sonar-web/src/main/js/helpers/testMocks.ts
server/sonar-web/src/main/js/queries/groups.ts
server/sonar-web/src/main/js/queries/users.ts
server/sonar-web/src/main/js/types/types.ts

index d86e38376cf70e46a64af4da3fa51198e31908b4..442ec1b12fcf6f1bf98e7612dcaa561e1a466749 100644 (file)
@@ -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<Record<string, never>> => {
-    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<Record<string, never>> => {
-    if (!this.groups.some((g) => group.currentName === g.name)) {
+  handleUpdateGroup: typeof updateGroup = (id, data): Promise<Record<string, never>> => {
+    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<typeof getUsersGroups>[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)),
     });
   };
 
index 61faf130605ffd0e0ea8da1ba8e58cae684b8b22..ade6218916c813e6a86b46e79097387635e03aa6 100644 (file)
  * 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<Group> {
-  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}`);
 }
index aa4f890f7688b1eb056a970952658ab24e2ab74c..6aef407edc8ea7144d114e6c20cce2d77c5a3359 100644 (file)
@@ -33,18 +33,16 @@ import List from './components/List';
 import './groups.css';
 
 export default function GroupsApp() {
-  const [numberOfPages, setNumberOfPages] = useState<number>(1);
   const [search, setSearch] = useState<string>('');
   const [managed, setManaged] = useState<boolean | undefined>();
   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() {
           <ListFooter
             count={groups.length}
             loading={isLoading}
-            loadMore={() => setNumberOfPages((n) => n + 1)}
+            loadMore={fetchNextPage}
             ready={!isLoading}
-            total={total}
+            total={data?.pages[0].page.total}
           />
         </div>
       </main>
index 6b491854dbefc6ffbcb409fc0168dea880680b35..a4b4ad084a27258a0f31291ea06223de6f504746 100644 (file)
@@ -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');
index 84eff7ad0120069ce63426146e5fac263995ab02..97faf125d87cacc1bfd8db8fbc2ad1be09b58d39 100644 (file)
@@ -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 },
     );
index d8efca16d91f497e16ee861ae406563ee4a94378..0dc5e38c9fcd4eef635821126e0600cbe9342700 100644 (file)
@@ -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 = <T>(x: T | undefined): x is T => x !== undefined;
 
@@ -32,3 +33,11 @@ export const mapReactQueryResult = <T, R>(
     data: notUndefined(res.data) ? mapper(res.data) : res.data,
   } as UseQueryResult<R>;
 };
+
+export const getNextPageParam = <T extends { page: Paging }>(params: T) =>
+  params.page.total <= params.page.pageIndex * params.page.pageSize
+    ? undefined
+    : params.page.pageIndex + 1;
+
+export const getPreviousPageParam = <T extends { page: Paging }>(params: T) =>
+  params.page.pageIndex === 1 ? undefined : params.page.pageIndex - 1;
index a815fac07a0d74133d91f71d3967e83b3ff95a6b..f064faf24ad8c6bdb9af08215dc1f4d05922575a 100644 (file)
@@ -293,7 +293,7 @@ export function mockLoggedInUser(overrides: Partial<LoggedInUser> = {}): LoggedI
 
 export function mockGroup(overrides: Partial<Group> = {}): Group {
   return {
-    membersCount: 1,
+    id: Math.random().toString(),
     name: 'Foo',
     managed: false,
     ...overrides,
index a75925936124659fc5c8f6d1a1ab8f3a66776f93..1b0e258276e584be3046b8ab279038a1f36f6061 100644 (file)
  * 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<Parameters<typeof getUsersGroups>[0], 'pageSize' | 'pageIndex'>,
-  numberOfPages: number,
 ) {
-  type QueryKey = [
-    'group',
-    'list',
-    number,
-    Omit<Parameters<typeof getUsersGroups>[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<QueryKey>) =>
-        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<typeof updateGroup>[0]) => updateGroup(data),
+    mutationFn: ({
+      id,
+      data,
+    }: {
+      id: Parameters<typeof updateGroup>[0];
+      data: Parameters<typeof updateGroup>[1];
+    }) => updateGroup(id, data),
     onSuccess() {
       queryClient.invalidateQueries({ queryKey: ['group', 'list'] });
     },
index 897292435a0dc6365c8c9130df3e21f9ab93273b..6dbedd6c805bd1fa6653a9686c28c0e353b94f75 100644 (file)
@@ -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<U extends RestUserBase>(
   return useInfiniteQuery({
     queryKey: ['user', 'list', getParams],
     queryFn: ({ pageParam = 1 }) => getUsers<U>({ ...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,
   });
 }
 
index 910fb3147bed6aa9f173982438dfd69341421475..9bae3b878eb802df0687aa2602a511fe11ec724a 100644 (file)
@@ -223,10 +223,10 @@ export interface FlowLocation {
 }
 
 export interface Group {
+  id: string;
   default?: boolean;
-  description?: string;
-  membersCount: number;
   name: string;
+  description?: string;
   managed: boolean;
 }