From: guillaume-peoch-sonarsource Date: Mon, 24 Jul 2023 10:16:13 +0000 (+0200) Subject: SONAR-19967 Refactor User call using React Query X-Git-Tag: 10.2.0.77647~274 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=a0bade28a6c8a387afbe78a8fd3bc90bb00aa267;p=sonarqube.git SONAR-19967 Refactor User call using React Query --- diff --git a/server/sonar-web/src/main/js/api/queries/users.ts b/server/sonar-web/src/main/js/api/queries/users.ts new file mode 100644 index 00000000000..916c86bd2ba --- /dev/null +++ b/server/sonar-web/src/main/js/api/queries/users.ts @@ -0,0 +1,106 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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 { + QueryFunctionContext, + useMutation, + useQueries, + useQueryClient, +} from '@tanstack/react-query'; +import { range } from 'lodash'; +import { User } from '../../types/users'; +import { + CreateUserParams, + DeactivateUserParams, + SearchUsersParams, + UpdateUserParams, + createUser, + deactivateUser, + searchUsers, + updateUser, +} from '../users'; + +export function useUsersQueries( + searchParam: Omit, + numberOfPages: number +) { + type QueryKey = ['user', 'list', number, Omit]; + const results = useQueries({ + queries: range(1, numberOfPages + 1).map((page: number) => ({ + queryKey: ['user', 'list', page, searchParam], + queryFn: ({ queryKey: [_u, _l, page, searchParam] }: QueryFunctionContext) => + searchUsers({ ...searchParam, p: page }), + })), + }); + + return results.reduce( + (acc, { data, isLoading }) => ({ + users: acc.users.concat(data?.users ?? []), + total: data?.paging.total, + isLoading: acc.isLoading || isLoading, + }), + { users: [] as User[], total: 0, isLoading: false } + ); +} + +export function useInvalidateUsersList() { + const queryClient = useQueryClient(); + + return () => queryClient.invalidateQueries({ queryKey: ['user', 'list'] }); +} + +export function useCreateUserMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data: CreateUserParams) => { + await createUser(data); + }, + onSuccess() { + queryClient.invalidateQueries({ queryKey: ['user', 'list'] }); + }, + }); +} + +export function useUpdateUserMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data: UpdateUserParams) => { + await updateUser(data); + }, + onSuccess() { + queryClient.invalidateQueries({ queryKey: ['user', 'list'] }); + }, + }); +} + +export function useDeactivateUserMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data: DeactivateUserParams) => { + await deactivateUser(data); + }, + onSuccess() { + queryClient.invalidateQueries({ queryKey: ['user', 'list'] }); + }, + }); +} diff --git a/server/sonar-web/src/main/js/api/users.ts b/server/sonar-web/src/main/js/api/users.ts index 5afe097a7bb..d7f33159095 100644 --- a/server/sonar-web/src/main/js/api/users.ts +++ b/server/sonar-web/src/main/js/api/users.ts @@ -53,13 +53,17 @@ export interface UserGroup { selected: boolean; } -export function getUserGroups(data: { +export interface UserGroupsParams { login: string; p?: number; ps?: number; q?: string; selected?: string; -}): Promise<{ paging: Paging; groups: UserGroup[] }> { +} + +export function getUserGroups( + data: UserGroupsParams +): Promise<{ paging: Paging; groups: UserGroup[] }> { return getJSON('/api/users/groups', data); } @@ -67,7 +71,7 @@ export function getIdentityProviders(): Promise<{ identityProviders: IdentityPro return getJSON('/api/users/identity_providers').catch(throwGlobalError); } -export function searchUsers(data: { +export interface SearchUsersParams { p?: number; ps?: number; q?: string; @@ -76,38 +80,46 @@ export function searchUsers(data: { lastConnectedBefore?: string; slLastConnectedAfter?: string; slLastConnectedBefore?: string; -}): Promise<{ paging: Paging; users: User[] }> { +} + +export function searchUsers(data: SearchUsersParams): Promise<{ paging: Paging; users: User[] }> { data.q = data.q || undefined; return getJSON('/api/users/search', data).catch(throwGlobalError); } -export function createUser(data: { +export interface CreateUserParams { email?: string; local?: boolean; login: string; name: string; password?: string; scmAccount: string[]; -}): Promise { +} + +export function createUser(data: CreateUserParams): Promise { return post('/api/users/create', data); } -export function updateUser(data: { +export interface UpdateUserParams { email?: string; login: string; name?: string; scmAccount: string[]; -}): Promise<{ user: User }> { +} + +export function updateUser(data: UpdateUserParams): Promise<{ user: User }> { return postJSON('/api/users/update', { ...data, scmAccount: data.scmAccount.length > 0 ? data.scmAccount : '', }); } -export function deactivateUser(data: { +export interface DeactivateUserParams { login: string; anonymize?: boolean; -}): Promise<{ user: User }> { +} + +export function deactivateUser(data: DeactivateUserParams): Promise<{ user: User }> { return postJSON('/api/users/deactivate', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx index 7eb384a9bb0..1d8acf00f16 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx @@ -37,7 +37,7 @@ import AuthenticationFormField from './AuthenticationFormField'; import ConfigurationForm from './ConfigurationForm'; import GitHubConfigurationValidity from './GitHubConfigurationValidity'; import useGithubConfiguration, { GITHUB_JIT_FIELDS } from './hook/useGithubConfiguration'; -import { useCheckGitHubConfigQuery, useIdentityProvierQuery } from './queries/identity-provider'; +import { useCheckGitHubConfigQuery, useIdentityProviderQuery } from './queries/identity-provider'; interface GithubAuthenticationProps { definitions: ExtendedSettingDefinition[]; @@ -51,7 +51,7 @@ const GITHUB_EXCLUDED_FIELD = [ export default function GithubAuthenticationTab(props: GithubAuthenticationProps) { const { definitions } = props; - const { data } = useIdentityProvierQuery(); + const { data } = useIdentityProviderQuery(); const [showEditModal, setShowEditModal] = useState(false); const [showConfirmProvisioningModal, setShowConfirmProvisioningModal] = useState(false); diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx index edd22a7a4d6..48448523693 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx @@ -40,7 +40,7 @@ import useSamlConfiguration, { SAML_GROUP_NAME, SAML_SCIM_DEPRECATED, } from './hook/useSamlConfiguration'; -import { useIdentityProvierQuery, useToggleScimMutation } from './queries/identity-provider'; +import { useIdentityProviderQuery, useToggleScimMutation } from './queries/identity-provider'; interface SamlAuthenticationProps { definitions: ExtendedSettingDefinition[]; @@ -76,7 +76,7 @@ export default function SamlAuthenticationTab(props: SamlAuthenticationProps) { } = useSamlConfiguration(definitions); const toggleScim = useToggleScimMutation(); - const { data } = useIdentityProvierQuery(); + const { data } = useIdentityProviderQuery(); const { mutate: saveSetting } = useSaveValueMutation(); const hasDifferentProvider = data?.provider !== undefined && data.provider !== Provider.Scim; diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/queries/identity-provider.ts b/server/sonar-web/src/main/js/apps/settings/components/authentication/queries/identity-provider.ts index b42d4bd4f50..9c02a99a80c 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/queries/identity-provider.ts +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/queries/identity-provider.ts @@ -35,7 +35,7 @@ import { useSyncStatusQuery } from '../../../../../queries/github-sync'; import { Feature } from '../../../../../types/features'; import { SysInfoCluster } from '../../../../../types/types'; -export function useIdentityProvierQuery() { +export function useIdentityProviderQuery() { return useQuery(['identity_provider'], async () => { const info = (await getSystemInfo()) as SysInfoCluster; return { provider: info.System['External Users and Groups Provisioning'] }; diff --git a/server/sonar-web/src/main/js/apps/users/Header.tsx b/server/sonar-web/src/main/js/apps/users/Header.tsx index 559e388b306..4dc19d14ff4 100644 --- a/server/sonar-web/src/main/js/apps/users/Header.tsx +++ b/server/sonar-web/src/main/js/apps/users/Header.tsx @@ -26,7 +26,6 @@ import { translate } from '../../helpers/l10n'; import UserForm from './components/UserForm'; interface Props { - onUpdateUsers: () => void; manageProvider?: string; } @@ -66,9 +65,7 @@ export default function Header(props: Props) { /> )} - {openUserForm && ( - setOpenUserForm(false)} onUpdateUsers={props.onUpdateUsers} /> - )} + {openUserForm && setOpenUserForm(false)} />} ); } diff --git a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx index 50f5b15c847..348a19e2278 100644 --- a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx +++ b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx @@ -18,9 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { subDays, subSeconds } from 'date-fns'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Helmet } from 'react-helmet-async'; -import { getIdentityProviders, searchUsers } from '../../api/users'; +import { useUsersQueries } from '../../api/queries/users'; +import { getIdentityProviders } from '../../api/users'; import GitHubSynchronisationWarning from '../../app/components/GitHubSynchronisationWarning'; import HelpTooltip from '../../components/controls/HelpTooltip'; import ListFooter from '../../components/controls/ListFooter'; @@ -32,8 +33,7 @@ import { Provider, useManageProvider } from '../../components/hooks/useManagePro import DeferredSpinner from '../../components/ui/DeferredSpinner'; import { now, toISO8601WithOffsetString } from '../../helpers/dates'; import { translate } from '../../helpers/l10n'; -import { IdentityProvider, Paging } from '../../types/types'; -import { User } from '../../types/users'; +import { IdentityProvider } from '../../types/types'; import Header from './Header'; import UsersList from './UsersList'; import { USERS_ACTIVITY_OPTIONS, USER_INACTIVITY_DAYS_THRESHOLD } from './constants'; @@ -41,17 +41,11 @@ import { UserActivity } from './types'; export default function UsersApp() { const [identityProviders, setIdentityProviders] = useState([]); - - const [loading, setLoading] = useState(true); - const [paging, setPaging] = useState(); - const [users, setUsers] = useState([]); - + const [numberOfPages, setNumberOfPages] = useState(1); const [search, setSearch] = useState(''); const [usersActivity, setUsersActivity] = useState(UserActivity.AnyActivity); const [managed, setManaged] = useState(undefined); - const manageProvider = useManageProvider(); - const usersActivityParams = useMemo(() => { const nowDate = now(); const nowDateMinus30Days = subDays(nowDate, USER_INACTIVITY_DAYS_THRESHOLD); @@ -76,39 +70,16 @@ export default function UsersApp() { } }, [usersActivity]); - const fetchUsers = useCallback(async () => { - setLoading(true); - try { - const { paging, users } = await searchUsers({ - q: search, - managed, - ...usersActivityParams, - }); - setPaging(paging); - setUsers(users); - } finally { - setLoading(false); - } - }, [search, managed, usersActivityParams]); + const { users, total, isLoading } = useUsersQueries( + { + q: search, + managed, + ...usersActivityParams, + }, + numberOfPages + ); - const fetchMoreUsers = useCallback(async () => { - if (!paging) { - return; - } - setLoading(true); - try { - const { paging: nextPage, users: nextUsers } = await searchUsers({ - q: search, - managed, - ...usersActivityParams, - p: paging.pageIndex + 1, - }); - setPaging(nextPage); - setUsers([...users, ...nextUsers]); - } finally { - setLoading(false); - } - }, [search, managed, usersActivityParams, paging, users]); + const manageProvider = useManageProvider(); useEffect(() => { (async () => { @@ -117,27 +88,29 @@ export default function UsersApp() { })(); }, []); - useEffect(() => { - fetchUsers(); - }, [fetchUsers]); - return (
-
+
{manageProvider === Provider.Github && }
{ + setManaged(m); + setNumberOfPages(1); + }} /> setSearch(search)} + onChange={(search: string) => { + setSearch(search); + setNumberOfPages(1); + }} placeholder={translate('search.search_by_login_or_name')} value={search} /> @@ -145,9 +118,10 @@ export default function UsersApp() { -

{translateWithParameters('users.minimum_x_characters', 3)}

-
- )} + {!user && (
-
+ )} +
+ + setName(e.currentTarget.value)} + required + type="text" + value={name} + /> +
+
+ + setEmail(e.currentTarget.value)} + type="email" + value={email} + /> +
+ {!user && (
- + setPassword(e.currentTarget.value)} + required + type="password" + value={password} />
- {!user && ( -
- - +
+ {translate('my_profile.scm_accounts')} + {scmAccounts.map((scm, idx) => ( + + ))} +
+
- )} -
-
- {translate('my_profile.scm_accounts')} - {this.state.scmAccounts.map((scm, idx) => ( - - ))} -
- -
-
-

{translate('user.login_or_email_used_as_scm_account')}

-
+
+

{translate('user.login_or_email_used_as_scm_account')}

- -
- {submitting && } - - {user ? translate('update_verb') : translate('create')} - - {translate('cancel')} -
- - )} - - ); - } + + +
+ {submitting && } + + {user ? translate('update_verb') : translate('create')} + + {translate('cancel')} +
+ + )} + + ); } diff --git a/server/sonar-web/src/main/js/apps/users/components/UserGroups.tsx b/server/sonar-web/src/main/js/apps/users/components/UserGroups.tsx index 73cfd23aff4..1b56b5e9764 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UserGroups.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/UserGroups.tsx @@ -26,71 +26,54 @@ import GroupsForm from './GroupsForm'; interface Props { groups: string[]; - onUpdateUsers: () => void; user: User; manageProvider: string | undefined; } -interface State { - openForm: boolean; - showMore: boolean; -} - const GROUPS_LIMIT = 3; -export default class UserGroups extends React.PureComponent { - state: State = { openForm: false, showMore: false }; - - handleOpenForm = () => this.setState({ openForm: true }); - handleCloseForm = () => this.setState({ openForm: false }); +export default function UserGroups(props: Props) { + const { groups, user, manageProvider } = props; - toggleShowMore = () => { - this.setState((state) => ({ showMore: !state.showMore })); - }; + const [openForm, setOpenForm] = React.useState(false); + const [showMore, setShowMore] = React.useState(false); - render() { - const { groups, user, manageProvider } = this.props; - const { showMore, openForm } = this.state; - const limit = groups.length > GROUPS_LIMIT ? GROUPS_LIMIT - 1 : GROUPS_LIMIT; - return ( -
    - {groups.slice(0, limit).map((group) => ( + const limit = groups.length > GROUPS_LIMIT ? GROUPS_LIMIT - 1 : GROUPS_LIMIT; + return ( +
      + {groups.slice(0, limit).map((group) => ( +
    • + {group} +
    • + ))} + {groups.length > GROUPS_LIMIT && + showMore && + groups.slice(limit).map((group) => (
    • {group}
    • ))} - {groups.length > GROUPS_LIMIT && - showMore && - groups.slice(limit).map((group) => ( -
    • - {group} -
    • - ))} -
    • - {groups.length > GROUPS_LIMIT && !showMore && ( - - {translateWithParameters('more_x', groups.length - limit)} - - )} - {manageProvider === undefined && ( - - - - )} -
    • - {openForm && ( - +
    • + {groups.length > GROUPS_LIMIT && !showMore && ( + setShowMore(!showMore)} + > + {translateWithParameters('more_x', groups.length - limit)} + + )} + {manageProvider === undefined && ( + setOpenForm(true)} + tooltip={translate('users.update_groups')} + > + + )} -
    - ); - } + + {openForm && setOpenForm(false)} user={user} />} +
+ ); } diff --git a/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx b/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx index cd6bdef46f0..d298d81b530 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx @@ -34,8 +34,6 @@ import UserScmAccounts from './UserScmAccounts'; export interface UserListItemProps { identityProvider?: IdentityProvider; isCurrentUser: boolean; - onUpdateUsers: () => void; - updateTokensCount: (login: string, tokensCount: number) => void; user: User; manageProvider: string | undefined; } @@ -43,14 +41,7 @@ export interface UserListItemProps { export default function UserListItem(props: UserListItemProps) { const [openTokenForm, setOpenTokenForm] = React.useState(false); - const { - identityProvider, - onUpdateUsers, - user, - manageProvider, - isCurrentUser, - updateTokensCount, - } = props; + const { identityProvider, user, manageProvider, isCurrentUser } = props; return ( @@ -79,12 +70,7 @@ export default function UserListItem(props: UserListItemProps) { - + {user.tokensCount} @@ -100,22 +86,11 @@ export default function UserListItem(props: UserListItemProps) { {(manageProvider === undefined || !user.managed) && ( - + )} - {openTokenForm && ( - setOpenTokenForm(false)} - updateTokensCount={updateTokensCount} - user={user} - /> - )} + {openTokenForm && setOpenTokenForm(false)} user={user} />} ); }