Browse Source

SONAR-21114 Migrate groups of a user dialog to Web API v2

tags/10.4.0.87286
Viktor Vorona 5 months ago
parent
commit
0893f59871

+ 0
- 29
server/sonar-web/src/main/js/api/legacy-group-membership.ts View File

@@ -1,29 +0,0 @@
/*
* 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 { throwGlobalError } from '../helpers/error';
import { post } from '../helpers/request';

export function addUserToGroup(data: { name: string; login?: string }) {
return post('/api/user_groups/add_user', data).catch(throwGlobalError);
}

export function removeUserFromGroup(data: { name: string; login?: string }) {
return post('/api/user_groups/remove_user', data).catch(throwGlobalError);
}

+ 12
- 41
server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts View File

@@ -18,12 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { cloneDeep } from 'lodash';
import {
mockGroup,
mockIdentityProvider,
mockPaging,
mockUserGroupMember,
} from '../../helpers/testMocks';
import { mockGroup, mockIdentityProvider } from '../../helpers/testMocks';
import { Group, IdentityProvider, Paging, Provider } from '../../types/types';
import { createGroup, deleteGroup, getUsersGroups, updateGroup } from '../user_groups';

@@ -31,26 +26,14 @@ jest.mock('../user_groups');

export default class GroupsServiceMock {
provider: Provider | undefined;
paging: Paging;
groups: Group[];
readOnlyGroups = [
mockGroup({ name: 'managed-group', managed: true, id: '1' }),
mockGroup({ name: 'local-group', managed: false, id: '2' }),
];

defaultUsers = [
mockUserGroupMember({ name: 'alice', login: 'alice.dev' }),
mockUserGroupMember({ name: 'bob', login: 'bob.dev' }),
mockUserGroupMember({ selected: false }),
];

constructor() {
this.groups = cloneDeep(this.readOnlyGroups);
this.paging = mockPaging({
pageIndex: 1,
pageSize: 2,
total: 200,
});

jest.mocked(getUsersGroups).mockImplementation((p) => this.handleSearchUsersGroups(p));
jest.mocked(createGroup).mockImplementation((g) => this.handleCreateGroup(g));
@@ -62,10 +45,6 @@ export default class GroupsServiceMock {
this.groups = cloneDeep(this.readOnlyGroups);
}

setPaging(paging: Partial<Paging>) {
this.paging = { ...this.paging, ...paging };
}

handleCreateGroup = (group: { name: string; description?: string }): Promise<Group> => {
const newGroup = mockGroup(group);
this.groups.push(newGroup);
@@ -106,26 +85,18 @@ export default class GroupsServiceMock {
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({ page, groups });
}
if (params.managed === undefined) {
return this.reply({
page,
groups: this.groups.filter((g) => (params?.q ? g.name.includes(params.q) : true)),
});
}
const groups = this.groups.filter((group) => group.managed === params.managed);
const pageIndex = params.pageIndex ?? 1;
const pageSize = params.pageSize ?? 10;
const groups = this.groups
.filter((g) => !params.q || g.name.includes(params.q))
.filter((g) => params.managed === undefined || g.managed === params.managed);
return this.reply({
page,
groups: groups.filter((g) => (params?.q ? g.name.includes(params.q) : true)),
page: {
pageIndex,
pageSize,
total: groups.length,
},
groups: groups.slice((pageIndex - 1) * pageSize, pageIndex * pageSize),
});
};


+ 0
- 90
server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts View File

@@ -28,15 +28,12 @@ import {
NoticeType,
RestUserDetailed,
} from '../../types/users';
import { addUserToGroup, removeUserFromGroup } from '../legacy-group-membership';
import {
UserGroup,
changePassword,
deleteUser,
dismissNotice,
getCurrentUser,
getIdentityProviders,
getUserGroups,
getUsers,
postUser,
updateUser,
@@ -44,7 +41,6 @@ import {
import GroupMembershipsServiceMock from './GroupMembersipsServiceMock';

jest.mock('../users');
jest.mock('../legacy-group-membership');

const DEFAULT_USERS = [
mockRestUser({
@@ -101,44 +97,12 @@ const DEFAULT_USERS = [
}),
];

const DEFAULT_GROUPS: UserGroup[] = [
{
id: 1001,
name: 'test1',
description: 'test1',
selected: true,
default: true,
},
{
id: 1002,
name: 'test2',
description: 'test2',
selected: true,
default: false,
},
{
id: 1003,
name: 'test3',
description: 'test3',
selected: true,
default: false,
},
{
id: 1004,
name: 'test4',
description: 'test4',
selected: false,
default: false,
},
];

const DEFAULT_PASSWORD = 'test';

export default class UsersServiceMock {
isManaged = true;
users = cloneDeep(DEFAULT_USERS);
currentUser = mockLoggedInUser();
groups = cloneDeep(DEFAULT_GROUPS);
password = DEFAULT_PASSWORD;
groupMembershipsServiceMock?: GroupMembershipsServiceMock = undefined;
constructor(groupMembershipsServiceMock?: GroupMembershipsServiceMock) {
@@ -147,9 +111,6 @@ export default class UsersServiceMock {
jest.mocked(getUsers).mockImplementation(this.handleGetUsers);
jest.mocked(postUser).mockImplementation(this.handlePostUser);
jest.mocked(updateUser).mockImplementation(this.handleUpdateUser);
jest.mocked(getUserGroups).mockImplementation(this.handleGetUserGroups);
jest.mocked(addUserToGroup).mockImplementation(this.handleAddUserToGroup);
jest.mocked(removeUserFromGroup).mockImplementation(this.handleRemoveUserFromGroup);
jest.mocked(changePassword).mockImplementation(this.handleChangePassword);
jest.mocked(deleteUser).mockImplementation(this.handleDeactivateUser);
jest.mocked(dismissNotice).mockImplementation(this.handleDismissNotification);
@@ -294,56 +255,6 @@ export default class UsersServiceMock {
});
};

handleGetUserGroups: typeof getUserGroups = (data) => {
if (data.login !== 'alice.merveille') {
return this.reply({
paging: { pageIndex: 1, pageSize: 10, total: 0 },
groups: [],
});
}
const filteredGroups = this.groups
.filter((g) => g.name.includes(data.q ?? ''))
.filter((g) => {
switch (data.selected) {
case 'all':
return true;
case 'deselected':
return !g.selected;
default:
return g.selected;
}
});

return this.reply({
paging: { pageIndex: 1, pageSize: 10, total: filteredGroups.length },
groups: filteredGroups,
});
};

handleAddUserToGroup: typeof addUserToGroup = ({ name }) => {
this.groups = this.groups.map((g) => (g.name === name ? { ...g, selected: true } : g));
return this.reply({});
};

handleRemoveUserFromGroup: typeof removeUserFromGroup = ({ name }) => {
let isDefault = false;
this.groups = this.groups.map((g) => {
if (g.name === name) {
if (g.default) {
isDefault = true;
return g;
}
return { ...g, selected: false };
}
return g;
});
return isDefault
? Promise.reject({
errors: [{ msg: 'Cannot remove Default group' }],
})
: this.reply({});
};

handleChangePassword: typeof changePassword = (data) => {
if (data.previousPassword !== this.password) {
return Promise.reject(ChangePasswordResults.OldPasswordIncorrect);
@@ -381,7 +292,6 @@ export default class UsersServiceMock {
reset = () => {
this.isManaged = true;
this.users = cloneDeep(DEFAULT_USERS);
this.groups = cloneDeep(DEFAULT_GROUPS);
this.password = DEFAULT_PASSWORD;
this.currentUser = mockLoggedInUser();
};

+ 1
- 1
server/sonar-web/src/main/js/api/user_groups.ts View File

@@ -24,7 +24,7 @@ const GROUPS_ENDPOINT = '/api/v2/authorizations/groups';

export function getUsersGroups(params: {
q?: string;
managed: boolean | undefined;
managed?: boolean;
pageIndex?: number;
pageSize?: number;
}): Promise<{ groups: Group[]; page: Paging }> {

+ 0
- 18
server/sonar-web/src/main/js/api/users.ts View File

@@ -55,24 +55,6 @@ export function changePassword(data: {
});
}

export interface UserGroup {
default: boolean;
description: string;
id: number;
name: string;
selected: boolean;
}

export function getUserGroups(data: {
login: string;
p?: number;
ps?: number;
q?: string;
selected?: string;
}): Promise<{ paging: Paging; groups: UserGroup[] }> {
return getJSON('/api/users/groups', data);
}

export function getIdentityProviders(): Promise<{ identityProviders: IdentityProvider[] }> {
return getJSON('/api/users/identity_providers').catch(throwGlobalError);
}

+ 7
- 4
server/sonar-web/src/main/js/apps/groups/__tests__/GroupsApp-it.tsx View File

@@ -25,7 +25,7 @@ import GroupMembershipsServiceMock from '../../../api/mocks/GroupMembersipsServi
import GroupsServiceMock from '../../../api/mocks/GroupsServiceMock';
import SystemServiceMock from '../../../api/mocks/SystemServiceMock';
import UsersServiceMock from '../../../api/mocks/UsersServiceMock';
import { mockGroupMembership, mockRestUser } from '../../../helpers/testMocks';
import { mockGroup, mockGroupMembership, mockRestUser } from '../../../helpers/testMocks';
import { renderApp } from '../../../helpers/testReactTestingUtils';
import { byRole, byText } from '../../../helpers/testSelector';
import { Feature } from '../../../types/features';
@@ -257,14 +257,17 @@ describe('in non managed mode', () => {

it('should be able load more group', async () => {
const user = userEvent.setup();
handler.groups = new Array(15)
.fill(null)
.map((_, index) => mockGroup({ id: index.toString(), name: `group${index}` }));
renderGroupsApp();

expect(await ui.localGroupRow.find()).toBeInTheDocument();
expect(await screen.findAllByRole('row')).toHaveLength(3);
expect(await ui.showMore.find()).toBeInTheDocument();
expect(await screen.findAllByRole('row')).toHaveLength(11);

await user.click(await ui.showMore.find());

expect(await screen.findAllByRole('row')).toHaveLength(5);
expect(await screen.findAllByRole('row')).toHaveLength(16);
});
});


+ 37
- 11
server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx View File

@@ -17,17 +17,25 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { screen, waitFor, within } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import selectEvent from 'react-select-event';
import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
import GithubProvisioningServiceMock from '../../../api/mocks/GithubProvisioningServiceMock';
import GroupMembershipsServiceMock from '../../../api/mocks/GroupMembersipsServiceMock';
import GroupsServiceMock from '../../../api/mocks/GroupsServiceMock';
import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
import SystemServiceMock from '../../../api/mocks/SystemServiceMock';
import UserTokensMock from '../../../api/mocks/UserTokensMock';
import UsersServiceMock from '../../../api/mocks/UsersServiceMock';
import { mockCurrentUser, mockLoggedInUser, mockRestUser } from '../../../helpers/testMocks';
import {
mockCurrentUser,
mockGroup,
mockGroupMembership,
mockLoggedInUser,
mockRestUser,
} from '../../../helpers/testMocks';
import { renderApp } from '../../../helpers/testReactTestingUtils';
import { byLabelText, byRole, byText } from '../../../helpers/testSelector';
import { Feature } from '../../../types/features';
@@ -42,6 +50,8 @@ const systemHandler = new SystemServiceMock();
const componentsHandler = new ComponentsServiceMock();
const settingsHandler = new SettingsServiceMock();
const githubHandler = new GithubProvisioningServiceMock();
const membershipHandler = new GroupMembershipsServiceMock();
const groupsHandler = new GroupsServiceMock();

const ui = {
createUserButton: byRole('button', { name: 'users.create_user' }),
@@ -107,7 +117,7 @@ const ui = {
selectedFilter: byRole('radio', { name: 'selected' }),
unselectedFilter: byRole('radio', { name: 'unselected' }),

getGroups: () => within(ui.dialogGroups.get()).getAllByRole('checkbox'),
groups: byRole('dialog', { name: 'users.update_groups' }).byRole('checkbox'),
dialogTokens: byRole('dialog', { name: /users.user_X_tokens/ }),
dialogPasswords: byRole('dialog', { name: 'my_profile.password.title' }),
dialogUpdateUser: byRole('dialog', { name: 'users.update_user' }),
@@ -147,6 +157,8 @@ beforeEach(() => {
settingsHandler.reset();
systemHandler.reset();
githubHandler.reset();
membershipHandler.reset();
groupsHandler.reset();
});

describe('different filters combinations', () => {
@@ -297,24 +309,38 @@ describe('in non managed mode', () => {

it('should be able to edit the groups of a user', async () => {
const user = userEvent.setup();
groupsHandler.groups = new Array(105).fill(null).map((_, index) =>
mockGroup({
id: index.toString(),
name: `group${index}`,
// eslint-disable-next-line jest/no-conditional-in-test
description: index === 0 ? 'description99' : undefined,
}),
);
membershipHandler.memberships = [
mockGroupMembership({ userId: '2', groupId: '1' }),
mockGroupMembership({ userId: '2', groupId: '2' }),
mockGroupMembership({ userId: '2', groupId: '3' }),
];
renderUsersApp();
expect(await ui.aliceRow.byText('3').find()).toBeInTheDocument();

await user.click(await ui.aliceUpdateGroupButton.find());
expect(await ui.dialogGroups.find()).toBeInTheDocument();

expect(ui.getGroups()).toHaveLength(3);
expect(await ui.groups.findAll()).toHaveLength(3);

await user.click(await ui.allFilter.find());
expect(ui.getGroups()).toHaveLength(4);
expect(ui.groups.getAll()).toHaveLength(105);

await user.click(ui.unselectedFilter.get());
expect(ui.groups.getAll()).toHaveLength(102);
expect(ui.reloadButton.query()).not.toBeInTheDocument();
await user.click(ui.getGroups()[0]);
await user.click(ui.groups.getAt(0));
expect(await ui.reloadButton.find()).toBeInTheDocument();

await user.click(ui.selectedFilter.get());
expect(ui.getGroups()).toHaveLength(4);
expect(ui.groups.getAll()).toHaveLength(4);

await user.click(ui.doneButton.get());
expect(ui.dialogGroups.query()).not.toBeInTheDocument();
@@ -324,14 +350,14 @@ describe('in non managed mode', () => {

await user.click(ui.selectedFilter.get());

await user.click(ui.getGroups()[1]);
await user.click(ui.groups.getAt(1));
expect(await ui.reloadButton.find()).toBeInTheDocument();
await user.click(ui.reloadButton.get());
expect(ui.getGroups()).toHaveLength(3);
expect(ui.groups.getAll()).toHaveLength(3);

await user.type(ui.dialogGroups.byRole('searchbox').get(), '4');
await user.type(ui.dialogGroups.byRole('searchbox').get(), '99');

expect(ui.getGroups()).toHaveLength(1);
expect(ui.groups.getAll()).toHaveLength(2);

await user.click(ui.doneButton.get());
expect(ui.dialogGroups.query()).not.toBeInTheDocument();

+ 54
- 51
server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx View File

@@ -19,15 +19,18 @@
*/

import { LightPrimary, Modal, Note } from 'design-system';
import { find, without } from 'lodash';
import { find } from 'lodash';
import * as React from 'react';
import { UserGroup, getUserGroups } from '../../../api/users';
import SelectList, {
SelectListFilter,
SelectListSearchParams,
} from '../../../components/controls/SelectList';
import { translate } from '../../../helpers/l10n';
import { useAddUserToGroupMutation, useRemoveUserToGroupMutation } from '../../../queries/users';
import {
useAddGroupMembershipMutation,
useRemoveGroupMembershipMutation,
useUserGroupsQuery,
} from '../../../queries/group-memberships';
import { RestUserDetailed } from '../../../types/users';

interface Props {
@@ -37,60 +40,58 @@ interface Props {

export default function GroupsForm(props: Props) {
const { user } = props;
const [needToReload, setNeedToReload] = React.useState<boolean>(false);
const [lastSearchParams, setLastSearchParams] = React.useState<
SelectListSearchParams | undefined
>(undefined);
const [groups, setGroups] = React.useState<UserGroup[]>([]);
const [groupsTotalCount, setGroupsTotalCount] = React.useState<number | undefined>(undefined);
const [selectedGroups, setSelectedGroups] = React.useState<string[]>([]);
const { mutateAsync: addUserToGroup } = useAddUserToGroupMutation();
const { mutateAsync: removeUserFromGroup } = useRemoveUserToGroupMutation();
const [query, setQuery] = React.useState<string>('');
const [filter, setFilter] = React.useState<SelectListFilter>(SelectListFilter.Selected);
const [changedGroups, setChangedGroups] = React.useState<Map<string, boolean>>(new Map());
const {
data: groups,
isLoading,
refetch,
} = useUserGroupsQuery({
q: query,
filter,
userId: user.id,
});
const { mutateAsync: addUserToGroup } = useAddGroupMembershipMutation();
const { mutateAsync: removeUserFromGroup } = useRemoveGroupMembershipMutation();

const fetchUsers = (searchParams: SelectListSearchParams) =>
getUserGroups({
login: user.login,
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 allGroups = more ? [...groups, ...data.groups] : data.groups;
const newSeletedGroups = data.groups.filter((gp) => gp.selected).map((gp) => gp.name);
const allSelectedGroups = more ? [...selectedGroups, ...newSeletedGroups] : newSeletedGroups;
const onSearch = (searchParams: SelectListSearchParams) => {
if (query === searchParams.query && filter === searchParams.filter) {
refetch();
} else {
setQuery(searchParams.query);
setFilter(searchParams.filter);
}

setLastSearchParams(searchParams);
setNeedToReload(false);
setGroups(allGroups);
setGroupsTotalCount(data.paging.total);
setSelectedGroups(allSelectedGroups);
});
setChangedGroups(new Map());
};

const handleSelect = (name: string) =>
const handleSelect = (groupId: string) =>
addUserToGroup({
name,
login: user.login,
userId: user.id,
groupId,
}).then(() => {
setNeedToReload(true);
setSelectedGroups([...selectedGroups, name]);
const newChangedGroups = new Map(changedGroups);
newChangedGroups.set(groupId, true);
setChangedGroups(newChangedGroups);
});

const handleUnselect = (name: string) =>
const handleUnselect = (groupId: string) =>
removeUserFromGroup({
name,
login: user.login,
groupId,
userId: user.id,
}).then(() => {
setNeedToReload(true);
setSelectedGroups(without(selectedGroups, name));
const newChangedGroups = new Map(changedGroups);
newChangedGroups.set(groupId, false);
setChangedGroups(newChangedGroups);
});

const renderElement = (name: string): React.ReactNode => {
const group = find(groups, { name });
const renderElement = (groupId: string): React.ReactNode => {
const group = find(groups, { id: groupId });
return (
<div>
{group === undefined ? (
<LightPrimary>{name}</LightPrimary>
<LightPrimary>{groupId}</LightPrimary>
) : (
<>
<LightPrimary>{group.name}</LightPrimary>
@@ -110,17 +111,19 @@ export default function GroupsForm(props: Props) {
body={
<div className="sw-pt-1">
<SelectList
elements={groups.map((group) => group.name)}
elementsTotalCount={groupsTotalCount}
needToReload={
needToReload && lastSearchParams && lastSearchParams.filter !== SelectListFilter.All
}
onSearch={fetchUsers}
elements={groups?.map((group) => group.id.toString()) ?? []}
elementsTotalCount={groups?.length}
needToReload={changedGroups.size > 0 && filter !== SelectListFilter.All}
onSearch={onSearch}
onSelect={handleSelect}
onUnselect={handleUnselect}
renderElement={renderElement}
selectedElements={selectedGroups}
withPaging
selectedElements={
groups
?.filter((g) => (changedGroups.has(g.id) ? changedGroups.get(g.id) : g.selected))
.map((g) => g.id) ?? []
}
loading={isLoading}
/>
</div>
}

+ 5
- 3
server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx View File

@@ -30,7 +30,8 @@ import {
import * as React from 'react';
import DateFromNow from '../../../components/intl/DateFromNow';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { useUserGroupsCountQuery, useUserTokensQuery } from '../../../queries/users';
import { useUserGroupsCountQuery } from '../../../queries/group-memberships';
import { useUserTokensQuery } from '../../../queries/users';
import { IdentityProvider, Provider } from '../../../types/types';
import { RestUserDetailed } from '../../../types/users';
import GroupsForm from './GroupsForm';
@@ -45,9 +46,10 @@ export interface UserListItemProps {
manageProvider: Provider | undefined;
}

export default function UserListItem(props: UserListItemProps) {
export default function UserListItem(props: Readonly<UserListItemProps>) {
const { identityProvider, user, manageProvider } = props;
const {
id,
name,
login,
avatar,
@@ -59,7 +61,7 @@ export default function UserListItem(props: UserListItemProps) {
const [openTokenForm, setOpenTokenForm] = React.useState(false);
const [openGroupForm, setOpenGroupForm] = React.useState(false);
const { data: tokens, isLoading: tokensAreLoading } = useUserTokensQuery(login);
const { data: groupsCount, isLoading: groupsAreLoading } = useUserGroupsCountQuery(login);
const { data: groupsCount, isLoading: groupsAreLoading } = useUserGroupsCountQuery(id);

return (
<TableRow>

+ 78
- 0
server/sonar-web/src/main/js/queries/group-memberships.ts View File

@@ -28,9 +28,11 @@ import { SelectListFilter } from '../components/controls/SelectList';
import { translateWithParameters } from '../helpers/l10n';
import { getNextPageParam, getPreviousPageParam } from '../helpers/react-query';
import { RestUserDetailed } from '../types/users';
import { useGroupsQueries } from './groups';

const DOMAIN = 'group-memberships';
const GROUP_SUB_DOMAIN = 'users-of-group';
const USER_SUB_DOMAIN = 'groups-of-user';

export function useGroupMembersQuery(params: {
filter?: SelectListFilter;
@@ -78,6 +80,65 @@ export function useGroupMembersQuery(params: {
});
}

export function useUserGroupsQuery(params: {
filter?: SelectListFilter;
q?: string;
userId: string;
}) {
const { q, filter, userId } = params;
const {
data: groupsPages,
isLoading: loadingGroups,
fetchNextPage: fetchNextPageGroups,
hasNextPage: hasNextPageGroups,
} = useGroupsQueries({});
const {
data: membershipsPages,
isLoading: loadingMemberships,
fetchNextPage: fetchNextPageMemberships,
hasNextPage: hasNextPageMemberships,
} = useInfiniteQuery({
queryKey: [DOMAIN, USER_SUB_DOMAIN, 'memberships', userId],
queryFn: ({ pageParam = 1 }) =>
getGroupMemberships({ userId, pageSize: 100, pageIndex: pageParam }),
getNextPageParam,
getPreviousPageParam,
});
if (hasNextPageGroups) {
fetchNextPageGroups();
}
if (hasNextPageMemberships) {
fetchNextPageMemberships();
}
return useQuery({
queryKey: [DOMAIN, USER_SUB_DOMAIN, params],
queryFn: () => {
const memberships =
membershipsPages?.pages.flatMap((page) => page.groupMemberships).flat() ?? [];
const groups = (groupsPages?.pages.flatMap((page) => page.groups).flat() ?? [])
.filter(
(group) =>
q === undefined ||
group.name.toLowerCase().includes(q.toLowerCase()) ||
group.description?.toLowerCase().includes(q.toLowerCase()),
)
.map((group) => ({
...group,
selected: memberships.some((membership) => membership.groupId === group.id),
}));
switch (filter) {
case SelectListFilter.All:
return groups;
case SelectListFilter.Unselected:
return groups.filter((group) => !group.selected);
default:
return groups.filter((group) => group.selected);
}
},
enabled: !loadingGroups && !hasNextPageGroups && !loadingMemberships && !hasNextPageMemberships,
});
}

export function useGroupMembersCountQuery(groupId: string) {
return useQuery({
queryKey: [DOMAIN, GROUP_SUB_DOMAIN, 'count', groupId],
@@ -85,6 +146,13 @@ export function useGroupMembersCountQuery(groupId: string) {
});
}

export function useUserGroupsCountQuery(userId: string) {
return useQuery({
queryKey: [DOMAIN, USER_SUB_DOMAIN, 'count', userId],
queryFn: () => getGroupMemberships({ userId, pageSize: 0 }).then((r) => r.page.total),
});
}

export function useAddGroupMembershipMutation() {
const queryClient = useQueryClient();

@@ -95,6 +163,11 @@ export function useAddGroupMembershipMutation() {
[DOMAIN, GROUP_SUB_DOMAIN, 'count', data.groupId],
(oldData) => (oldData !== undefined ? oldData + 1 : undefined),
);
queryClient.setQueryData<number>(
[DOMAIN, USER_SUB_DOMAIN, 'count', data.userId],
(oldData) => (oldData !== undefined ? oldData + 1 : undefined),
);
queryClient.invalidateQueries([DOMAIN, USER_SUB_DOMAIN, 'memberships', data.userId]);
},
});
}
@@ -117,6 +190,11 @@ export function useRemoveGroupMembershipMutation() {
[DOMAIN, GROUP_SUB_DOMAIN, 'count', data.groupId],
(oldData) => (oldData !== undefined ? oldData - 1 : undefined),
);
queryClient.setQueryData<number>(
[DOMAIN, USER_SUB_DOMAIN, 'count', data.userId],
(oldData) => (oldData !== undefined ? oldData - 1 : undefined),
);
queryClient.invalidateQueries([DOMAIN, USER_SUB_DOMAIN, 'memberships', data.userId]);
},
});
}

+ 1
- 40
server/sonar-web/src/main/js/queries/users.ts View File

@@ -18,16 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { addUserToGroup, removeUserFromGroup } from '../api/legacy-group-membership';
import { generateToken, getTokens, revokeToken } from '../api/user-tokens';
import {
deleteUser,
dismissNotice,
getUserGroups,
getUsers,
postUser,
updateUser,
} from '../api/users';
import { deleteUser, dismissNotice, getUsers, postUser, updateUser } from '../api/users';
import { useCurrentUser } from '../app/components/current-user/CurrentUserContext';
import { getNextPageParam, getPreviousPageParam } from '../helpers/react-query';
import { UserToken } from '../types/token';
@@ -56,13 +48,6 @@ export function useUserTokensQuery(login: string) {
});
}

export function useUserGroupsCountQuery(login: string) {
return useQuery({
queryKey: ['user', login, 'groups', 'total'],
queryFn: () => getUserGroups({ login, ps: 1 }).then((r) => r.paging.total),
});
}

export function usePostUserMutation() {
const queryClient = useQueryClient();

@@ -136,30 +121,6 @@ export function useRevokeTokenMutation() {
});
}

export function useAddUserToGroupMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Parameters<typeof addUserToGroup>[0]) => addUserToGroup(data),
onSuccess(_, data) {
queryClient.setQueryData<number>(['user', data.login, 'groups', 'total'], (oldData) =>
oldData !== undefined ? oldData + 1 : undefined,
);
},
});
}

export function useRemoveUserToGroupMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Parameters<typeof removeUserFromGroup>[0]) => removeUserFromGroup(data),
onSuccess(_, data) {
queryClient.setQueryData<number>(['user', data.login, 'groups', 'total'], (oldData) =>
oldData !== undefined ? oldData - 1 : undefined,
);
},
});
}

export function useDismissNoticeMutation() {
const { updateDismissedNotices } = useCurrentUser();


Loading…
Cancel
Save