From 3ad3b0ad0d24d9b95314398e455b18fb5c719479 Mon Sep 17 00:00:00 2001 From: guillaume-peoch-sonarsource Date: Wed, 26 Jul 2023 16:49:26 +0200 Subject: [PATCH] SONAR-19967 Use DELETE api/v2/users/:login on the FE --- .../src/main/js/api/mocks/UsersServiceMock.ts | 18 +- server/sonar-web/src/main/js/api/users.ts | 90 +++----- .../src/main/js/apps/users/UsersApp.tsx | 5 +- .../src/main/js/apps/users/UsersList.tsx | 10 +- .../js/apps/users/__tests__/UsersApp-it.tsx | 1 - .../apps/users/components/DeactivateForm.tsx | 2 +- .../js/apps/users/components/GroupsForm.tsx | 7 +- .../js/apps/users/components/PasswordForm.tsx | 210 ++++++++---------- .../apps/users/components/TokensFormModal.tsx | 6 +- .../js/apps/users/components/UserActions.tsx | 14 +- .../js/apps/users/components/UserForm.tsx | 23 +- .../js/apps/users/components/UserListItem.tsx | 9 +- .../users/components/UserListItemIdentity.tsx | 4 +- .../sonar-web/src/main/js/helpers/request.ts | 11 + .../src/main/js/helpers/testMocks.ts | 5 +- .../src/main/js/{api => }/queries/users.ts | 40 ++-- server/sonar-web/src/main/js/types/users.ts | 24 ++ 17 files changed, 217 insertions(+), 262 deletions(-) rename server/sonar-web/src/main/js/{api => }/queries/users.ts (74%) diff --git a/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts index 71f4e226847..decdb9c4d53 100644 --- a/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts @@ -21,17 +21,15 @@ import { isAfter, isBefore } from 'date-fns'; import { cloneDeep, isEmpty, isUndefined, omitBy } from 'lodash'; import { mockClusterSysInfo, mockIdentityProvider, mockRestUser } from '../../helpers/testMocks'; -import { IdentityProvider, Paging, SysInfoCluster } from '../../types/types'; -import { ChangePasswordResults, User } from '../../types/users'; +import { IdentityProvider, SysInfoCluster } from '../../types/types'; +import { ChangePasswordResults, RestUserDetailed, User } from '../../types/users'; import { getSystemInfo } from '../system'; import { addUserToGroup, removeUserFromGroup } from '../user_groups'; import { - GetUsersParams, - RestUser, UserGroup, changePassword, createUser, - deactivateUser, + deleteUser, getIdentityProviders, getUserGroups, getUsers, @@ -133,7 +131,7 @@ export default class UsersServiceMock { jest.mocked(addUserToGroup).mockImplementation(this.handleAddUserToGroup); jest.mocked(removeUserFromGroup).mockImplementation(this.handleRemoveUserFromGroup); jest.mocked(changePassword).mockImplementation(this.handleChangePassword); - jest.mocked(deactivateUser).mockImplementation(this.handleDeactivateUser); + jest.mocked(deleteUser).mockImplementation(this.handleDeactivateUser); } setIsManaged(managed: boolean) { @@ -208,9 +206,7 @@ export default class UsersServiceMock { }); }; - handleGetUsers = ( - data: GetUsersParams - ): Promise<{ pageRestResponse: Paging; users: RestUser<'admin'>[] }> => { + handleGetUsers: typeof getUsers = (data) => { let pageRestResponse = { pageIndex: 1, pageSize: 0, @@ -240,7 +236,7 @@ export default class UsersServiceMock { sonarQubeLastConnectionDateTo: data.sonarQubeLastConnectionDateTo, sonarLintLastConnectionDateFrom: data.sonarLintLastConnectionDateFrom, sonarLintLastConnectionDateTo: data.sonarLintLastConnectionDateTo, - }) as RestUser<'admin'>[]; + }); return this.reply({ pageRestResponse: { @@ -373,7 +369,7 @@ export default class UsersServiceMock { return this.reply({}); }; - handleDeactivateUser: typeof deactivateUser = (data) => { + handleDeactivateUser: typeof deleteUser = (data) => { const index = this.users.findIndex((u) => u.login === data.login); const user = this.users.splice(index, 1)[0]; user.active = false; diff --git a/server/sonar-web/src/main/js/api/users.ts b/server/sonar-web/src/main/js/api/users.ts index 72c569b8dda..d75c811ff95 100644 --- a/server/sonar-web/src/main/js/api/users.ts +++ b/server/sonar-web/src/main/js/api/users.ts @@ -18,9 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { throwGlobalError } from '../helpers/error'; -import { getJSON, HttpStatus, parseJSON, post, postJSON } from '../helpers/request'; +import { deleteJSON, getJSON, HttpStatus, parseJSON, post, postJSON } from '../helpers/request'; import { IdentityProvider, Paging } from '../types/types'; -import { ChangePasswordResults, CurrentUser, HomePage, NoticeType, User } from '../types/users'; +import { + ChangePasswordResults, + CurrentUser, + HomePage, + NoticeType, + RestUserBase, + RestUserDetailed, + User, +} from '../types/users'; export function getCurrentUser(): Promise { return getJSON('/api/users/current', undefined, true); @@ -53,17 +61,13 @@ export interface UserGroup { selected: boolean; } -export interface UserGroupsParams { +export function getUserGroups(data: { login: string; p?: number; ps?: number; q?: string; selected?: string; -} - -export function getUserGroups( - data: UserGroupsParams -): Promise<{ paging: Paging; groups: UserGroup[] }> { +}): Promise<{ paging: Paging; groups: UserGroup[] }> { return getJSON('/api/users/groups', data); } @@ -71,7 +75,7 @@ export function getIdentityProviders(): Promise<{ identityProviders: IdentityPro return getJSON('/api/users/identity_providers').catch(throwGlobalError); } -export interface SearchUsersParams { +export function searchUsers(data: { p?: number; ps?: number; q?: string; @@ -80,14 +84,12 @@ export interface SearchUsersParams { lastConnectedBefore?: string; slLastConnectedAfter?: string; slLastConnectedBefore?: string; -} - -export function searchUsers(data: SearchUsersParams): Promise<{ paging: Paging; users: User[] }> { +}): Promise<{ paging: Paging; users: User[] }> { data.q = data.q || undefined; return getJSON('/api/users/search', data).catch(throwGlobalError); } -export interface GetUsersParams { +export function getUsers(data: { q: string; active?: boolean; managed?: boolean; @@ -97,81 +99,41 @@ export interface GetUsersParams { sonarLintLastConnectionDateTo?: string; pageSize?: number; pageIndex?: number; -} - -export type Permission = 'admin' | 'anonymous' | 'user'; - -export type RestUser = T extends 'admin' - ? { - id: string; - login: string; - name: string; - email: string; - active: boolean; - local: boolean; - externalProvider: string; - avatar: string; - managed: boolean; - externalLogin: string; - sonarQubeLastConnectionDate: string | null; - sonarLintLastConnectionDate: string | null; - scmAccounts: string[]; - groupsCount: number; - tokensCount: number; - } - : T extends 'anonymous' - ? { id: string; login: string; name: string } - : { - id: string; - login: string; - name: string; - email: string; - active: boolean; - local: boolean; - externalProvider: string; - avatar: string; - }; - -export function getUsers( - data: GetUsersParams -): Promise<{ pageRestResponse: Paging; users: RestUser[] }> { +}): Promise<{ pageRestResponse: Paging; users: T[] }> { return getJSON('/api/v2/users', data).catch(throwGlobalError); } -export interface CreateUserParams { +export function createUser(data: { email?: string; local?: boolean; login: string; name: string; password?: string; scmAccount: string[]; -} - -export function createUser(data: CreateUserParams): Promise { +}): Promise { return post('/api/users/create', data); } -export interface UpdateUserParams { +export function updateUser(data: { email?: string; login: string; name?: string; scmAccount: string[]; -} - -export function updateUser(data: UpdateUserParams): Promise<{ user: User }> { +}): Promise<{ user: User }> { return postJSON('/api/users/update', { ...data, scmAccount: data.scmAccount.length > 0 ? data.scmAccount : '', }); } -export interface DeactivateUserParams { +export function deleteUser({ + login, + anonymize, +}: { login: string; anonymize?: boolean; -} - -export function deactivateUser(data: DeactivateUserParams): Promise<{ user: RestUser<'admin'> }> { - return postJSON('/api/users/deactivate', data).catch(throwGlobalError); +}): Promise<{ user: RestUserDetailed }> { + return deleteJSON(`/api/v2/users/${login}`, { anonymize }).catch(throwGlobalError); } export function setHomePage(homepage: HomePage): Promise { 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 e087a7dd8e2..7ba7969debb 100644 --- a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx +++ b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx @@ -20,7 +20,6 @@ import { subDays, subSeconds } from 'date-fns'; import React, { useEffect, useMemo, useState } from 'react'; import { Helmet } from 'react-helmet-async'; -import { useUsersQueries } from '../../api/queries/users'; import { getIdentityProviders } from '../../api/users'; import GitHubSynchronisationWarning from '../../app/components/GitHubSynchronisationWarning'; import HelpTooltip from '../../components/controls/HelpTooltip'; @@ -33,7 +32,9 @@ 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 { useUsersQueries } from '../../queries/users'; import { IdentityProvider } from '../../types/types'; +import { RestUserDetailed } from '../../types/users'; import Header from './Header'; import UsersList from './UsersList'; import { USERS_ACTIVITY_OPTIONS, USER_INACTIVITY_DAYS_THRESHOLD } from './constants'; @@ -70,7 +71,7 @@ export default function UsersApp() { } }, [usersActivity]); - const { users, total, isLoading } = useUsersQueries<'admin'>( + const { users, total, isLoading } = useUsersQueries( { q: search, managed, diff --git a/server/sonar-web/src/main/js/apps/users/UsersList.tsx b/server/sonar-web/src/main/js/apps/users/UsersList.tsx index c3d01379d49..33ce5102cd8 100644 --- a/server/sonar-web/src/main/js/apps/users/UsersList.tsx +++ b/server/sonar-web/src/main/js/apps/users/UsersList.tsx @@ -18,24 +18,19 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { RestUser } from '../../api/users'; -import { CurrentUserContext } from '../../app/components/current-user/CurrentUserContext'; import HelpTooltip from '../../components/controls/HelpTooltip'; import { translate } from '../../helpers/l10n'; import { IdentityProvider } from '../../types/types'; -import { isLoggedIn } from '../../types/users'; +import { RestUserDetailed } from '../../types/users'; import UserListItem from './components/UserListItem'; interface Props { identityProviders: IdentityProvider[]; - users: RestUser<'admin'>[]; + users: RestUserDetailed[]; manageProvider: string | undefined; } export default function UsersList({ identityProviders, users, manageProvider }: Props) { - const userContext = React.useContext(CurrentUserContext); - const currentUser = userContext?.currentUser; - return (
@@ -64,7 +59,6 @@ export default function UsersList({ identityProviders, users, manageProvider }: identityProvider={identityProviders.find( (provider) => user.externalProvider === provider.key )} - isCurrentUser={isLoggedIn(currentUser) && currentUser.login === user.login} key={user.login} user={user} manageProvider={manageProvider} diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx index dc7b33c2f85..73e454c64a3 100644 --- a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx @@ -592,7 +592,6 @@ it('should render external identity Providers', async () => { renderUsersApp(); await act(async () => expect(await ui.charlieRow.find()).toHaveTextContent(/ExternalTest/)); - // logRoles(document.body); expect(await ui.denisRow.find()).toHaveTextContent(/test2: UnknownExternalProvider/); }); diff --git a/server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx b/server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx index aa334af4bb4..b7fbf2077bf 100644 --- a/server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx @@ -19,13 +19,13 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useDeactivateUserMutation } from '../../../api/queries/users'; import DocLink from '../../../components/common/DocLink'; import Checkbox from '../../../components/controls/Checkbox'; import Modal from '../../../components/controls/Modal'; import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; import { Alert } from '../../../components/ui/Alert'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { useDeactivateUserMutation } from '../../../queries/users'; import { UserActive } from '../../../types/users'; export interface Props { diff --git a/server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx b/server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx index fda16770551..e27d00ec0d3 100644 --- a/server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx @@ -19,9 +19,8 @@ */ import { find, without } from 'lodash'; import * as React from 'react'; -import { useInvalidateUsersList } from '../../../api/queries/users'; import { addUserToGroup, removeUserFromGroup } from '../../../api/user_groups'; -import { RestUser, UserGroup, getUserGroups } from '../../../api/users'; +import { UserGroup, getUserGroups } from '../../../api/users'; import Modal from '../../../components/controls/Modal'; import SelectList, { SelectListFilter, @@ -29,10 +28,12 @@ import SelectList, { } from '../../../components/controls/SelectList'; import { ResetButtonLink } from '../../../components/controls/buttons'; import { translate } from '../../../helpers/l10n'; +import { useInvalidateUsersList } from '../../../queries/users'; +import { RestUserDetailed } from '../../../types/users'; interface Props { onClose: () => void; - user: RestUser<'admin'>; + user: RestUserDetailed; } export default function GroupsForm(props: Props) { diff --git a/server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx b/server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx index 0387363c3c6..6e9a17d3237 100644 --- a/server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx @@ -18,7 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { RestUser, changePassword } from '../../../api/users'; +import { changePassword } from '../../../api/users'; +import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext'; import Modal from '../../../components/controls/Modal'; import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; import { Alert } from '../../../components/ui/Alert'; @@ -26,151 +27,124 @@ import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker'; import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation'; import { addGlobalSuccessMessage } from '../../../helpers/globalMessages'; import { translate } from '../../../helpers/l10n'; -import { ChangePasswordResults } from '../../../types/users'; +import { ChangePasswordResults, RestUserDetailed, isLoggedIn } from '../../../types/users'; interface Props { - isCurrentUser: boolean; onClose: () => void; - user: RestUser<'admin'>; + user: RestUserDetailed; } -interface State { - confirmPassword: string; - errorTranslationKey?: string; - newPassword: string; - oldPassword: string; - submitting: boolean; -} - -export default class PasswordForm extends React.PureComponent { - mounted = false; - state: State = { - confirmPassword: '', - newPassword: '', - oldPassword: '', - submitting: false, - }; +export default function PasswordForm(props: Props) { + const { user } = props; + const [confirmPassword, setConfirmPassword] = React.useState(''); + const [errorTranslationKey, setErrorTranslationKey] = React.useState( + undefined + ); + const [newPassword, setNewPassword] = React.useState(''); + const [oldPassword, setOldPassword] = React.useState(''); + const [submitting, setSubmitting] = React.useState(false); - componentDidMount() { - this.mounted = true; - } + const userContext = React.useContext(CurrentUserContext); + const currentUser = userContext?.currentUser; + const isCurrentUser = isLoggedIn(currentUser) && currentUser.login === user.login; - componentWillUnmount() { - this.mounted = false; - } - - handleError = (result: ChangePasswordResults) => { - if (this.mounted) { - if (result === ChangePasswordResults.OldPasswordIncorrect) { - this.setState({ errorTranslationKey: 'user.old_password_incorrect', submitting: false }); - } else if (result === ChangePasswordResults.NewPasswordSameAsOld) { - this.setState({ errorTranslationKey: 'user.new_password_same_as_old', submitting: false }); - } + const handleError = (result: ChangePasswordResults) => { + if (result === ChangePasswordResults.OldPasswordIncorrect) { + setErrorTranslationKey('user.old_password_incorrect'); + setSubmitting(false); + } else if (result === ChangePasswordResults.NewPasswordSameAsOld) { + setErrorTranslationKey('user.new_password_same_as_old'); + setSubmitting(false); } }; - handleConfirmPasswordChange = (event: React.SyntheticEvent) => - this.setState({ confirmPassword: event.currentTarget.value }); - - handleNewPasswordChange = (event: React.SyntheticEvent) => - this.setState({ newPassword: event.currentTarget.value }); - - handleOldPasswordChange = (event: React.SyntheticEvent) => - this.setState({ oldPassword: event.currentTarget.value }); - - handleChangePassword = (event: React.SyntheticEvent) => { + const handleChangePassword = (event: React.SyntheticEvent) => { event.preventDefault(); - if ( - this.state.newPassword.length > 0 && - this.state.newPassword === this.state.confirmPassword - ) { - this.setState({ submitting: true }); + if (newPassword.length > 0 && newPassword === confirmPassword) { + setSubmitting(true); changePassword({ - login: this.props.user.login, - password: this.state.newPassword, - previousPassword: this.state.oldPassword, + login: user.login, + password: newPassword, + previousPassword: oldPassword, }).then(() => { addGlobalSuccessMessage(translate('my_profile.password.changed')); - this.props.onClose(); - }, this.handleError); + props.onClose(); + }, handleError); } }; - render() { - const { errorTranslationKey, submitting, newPassword, confirmPassword } = this.state; + const header = translate('my_profile.password.title'); - const header = translate('my_profile.password.title'); - return ( - -
-
-

{header}

-
-
- {errorTranslationKey && {translate(errorTranslationKey)}} + return ( + + +
+

{header}

+
+
+ {errorTranslationKey && {translate(errorTranslationKey)}} - + - {this.props.isCurrentUser && ( -
- - {/* keep this fake field to hack browser autofill */} - - -
- )} + {isCurrentUser && (
-
-
- - {/* keep this fake field to hack browser autofill */} - -
+ )} +
+ + {/* keep this fake field to hack browser autofill */} + + setNewPassword(event.currentTarget.value)} + required + type="password" + value={newPassword} + /> +
+
+ + {/* keep this fake field to hack browser autofill */} + + setConfirmPassword(event.currentTarget.value)} + required + type="password" + value={confirmPassword} + />
-
- {submitting && } - - {translate('change_verb')} - - {translate('cancel')} -
- - - ); - } +
+
+ {submitting && } + + {translate('change_verb')} + + {translate('cancel')} +
+ +
+ ); } diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx index 87c219710f7..0524be649a7 100644 --- a/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx @@ -19,15 +19,15 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useInvalidateUsersList } from '../../../api/queries/users'; -import { RestUser } from '../../../api/users'; import Modal from '../../../components/controls/Modal'; import { ResetButtonLink } from '../../../components/controls/buttons'; import { translate } from '../../../helpers/l10n'; +import { useInvalidateUsersList } from '../../../queries/users'; +import { RestUserDetailed } from '../../../types/users'; import TokensForm from './TokensForm'; interface Props { - user: RestUser<'admin'>; + user: RestUserDetailed; onClose: () => void; } diff --git a/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx b/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx index aa61b550194..8d9937de08b 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/UserActions.tsx @@ -18,25 +18,23 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { RestUser } from '../../../api/users'; import ActionsDropdown, { ActionsDropdownDivider, ActionsDropdownItem, } from '../../../components/controls/ActionsDropdown'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { isUserActive } from '../../../types/users'; +import { RestUserDetailed, isUserActive } from '../../../types/users'; import DeactivateForm from './DeactivateForm'; import PasswordForm from './PasswordForm'; import UserForm from './UserForm'; interface Props { - isCurrentUser: boolean; - user: RestUser<'admin'>; + user: RestUserDetailed; manageProvider: string | undefined; } export default function UserActions(props: Props) { - const { isCurrentUser, user, manageProvider } = props; + const { user, manageProvider } = props; const [openForm, setOpenForm] = React.useState(undefined); @@ -90,11 +88,7 @@ export default function UserActions(props: Props) { setOpenForm(undefined)} user={user} /> )} {openForm === 'password' && ( - setOpenForm(undefined)} - user={user} - /> + setOpenForm(undefined)} user={user} /> )} {openForm === 'update' && setOpenForm(undefined)} user={user} />} diff --git a/server/sonar-web/src/main/js/apps/users/components/UserForm.tsx b/server/sonar-web/src/main/js/apps/users/components/UserForm.tsx index fc18984a583..b63ad7288ee 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UserForm.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/UserForm.tsx @@ -18,8 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { useCreateUserMutation, useUpdateUserMutation } from '../../../api/queries/users'; -import { RestUser } from '../../../api/users'; import SimpleModal from '../../../components/controls/SimpleModal'; import { Button, ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; import { Alert } from '../../../components/ui/Alert'; @@ -28,28 +26,33 @@ import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsEx import { throwGlobalError } from '../../../helpers/error'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { parseError } from '../../../helpers/request'; +import { useCreateUserMutation, useUpdateUserMutation } from '../../../queries/users'; +import { RestUserDetailed } from '../../../types/users'; import UserScmAccountInput from './UserScmAccountInput'; export interface Props { onClose: () => void; - user?: RestUser<'admin'>; + user?: RestUserDetailed; } +const BAD_REQUEST = 400; +const INTERNAL_SERVER_ERROR = 500; + export default function UserForm(props: Props) { const { user } = props; const { mutate: createUser } = useCreateUserMutation(); const { mutate: updateUser } = useUpdateUserMutation(); - const [email, setEmail] = React.useState(user?.email || ''); - const [login, setLogin] = React.useState(user?.login || ''); - const [name, setName] = React.useState(user?.name || ''); + const [email, setEmail] = React.useState(user?.email ?? ''); + const [login, setLogin] = React.useState(user?.login ?? ''); + const [name, setName] = React.useState(user?.name ?? ''); const [password, setPassword] = React.useState(''); - const [scmAccounts, setScmAccounts] = React.useState(user?.scmAccounts || []); + const [scmAccounts, setScmAccounts] = React.useState(user?.scmAccounts ?? []); const [error, setError] = React.useState(undefined); const handleError = (response: Response) => { - if (![400, 500].includes(response.status)) { + if (![BAD_REQUEST, INTERNAL_SERVER_ERROR].includes(response.status)) { throwGlobalError(response); } else { parseError(response).then((errorMsg) => setError(errorMsg), throwGlobalError); @@ -74,9 +77,9 @@ export default function UserForm(props: Props) { updateUser( { - email: user!.local ? email : undefined, + email: user?.local ? email : undefined, login, - name: user!.local ? name : undefined, + name: user?.local ? name : undefined, scmAccount: scmAccounts, }, { onSuccess: props.onClose, onError: handleError } 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 e5c6a7f4c88..e0f314eb742 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 @@ -18,13 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { RestUser } from '../../../api/users'; import { ButtonIcon } from '../../../components/controls/buttons'; import BulletListIcon from '../../../components/icons/BulletListIcon'; import DateFromNow from '../../../components/intl/DateFromNow'; import LegacyAvatar from '../../../components/ui/LegacyAvatar'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { IdentityProvider } from '../../../types/types'; +import { RestUserDetailed } from '../../../types/users'; import GroupsForm from './GroupsForm'; import TokensFormModal from './TokensFormModal'; import UserActions from './UserActions'; @@ -33,13 +33,12 @@ import UserScmAccounts from './UserScmAccounts'; export interface UserListItemProps { identityProvider?: IdentityProvider; - isCurrentUser: boolean; - user: RestUser<'admin'>; + user: RestUserDetailed; manageProvider: string | undefined; } export default function UserListItem(props: UserListItemProps) { - const { identityProvider, user, manageProvider, isCurrentUser } = props; + const { identityProvider, user, manageProvider } = props; const { name, login, @@ -103,7 +102,7 @@ export default function UserListItem(props: UserListItemProps) { {(manageProvider === undefined || !managed) && (
)} diff --git a/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx b/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx index e21060242eb..c432e59b623 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx @@ -20,15 +20,15 @@ import { getTextColor } from 'design-system'; import * as React from 'react'; -import { RestUser } from '../../../api/users'; import { colors } from '../../../app/theme'; import { translate } from '../../../helpers/l10n'; import { getBaseUrl } from '../../../helpers/system'; import { IdentityProvider } from '../../../types/types'; +import { RestUserDetailed } from '../../../types/users'; export interface Props { identityProvider?: IdentityProvider; - user: RestUser<'admin'>; + user: RestUserDetailed; manageProvider?: string; } diff --git a/server/sonar-web/src/main/js/helpers/request.ts b/server/sonar-web/src/main/js/helpers/request.ts index ff58851874f..9b40c1f14ed 100644 --- a/server/sonar-web/src/main/js/helpers/request.ts +++ b/server/sonar-web/src/main/js/helpers/request.ts @@ -272,6 +272,17 @@ export function post(url: string, data?: RequestData, bypassRedirect?: boolean): }); } +/** + * Shortcut to do a DELETE request + */ +export function deleteJSON(url: string, data?: RequestData): Promise { + return request(url) + .setMethod('DELETE') + .setData(data) + .submit() + .then((response) => checkStatus(response)); +} + function tryRequestAgain( repeatAPICall: () => Promise, tries: { max: number; slowThreshold: number }, diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index c3ee78cb3a4..474e43af881 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -19,7 +19,6 @@ */ import { To } from 'react-router-dom'; import { CompareResponse } from '../api/quality-profiles'; -import { RestUser } from '../api/users'; import { RuleDescriptionSections } from '../apps/coding-rules/rule'; import { Exporter, Profile, ProfileChangelogEvent } from '../apps/quality-profiles/types'; import { LogsLevels } from '../apps/system/utils'; @@ -57,7 +56,7 @@ import { UserGroupMember, UserSelected, } from '../types/types'; -import { CurrentUser, LoggedInUser, User } from '../types/users'; +import { CurrentUser, LoggedInUser, RestUserDetailed, User } from '../types/users'; export function mockAlmApplication(overrides: Partial = {}): AlmApplication { return { @@ -681,7 +680,7 @@ export function mockUser(overrides: Partial = {}): User { }; } -export function mockRestUser(overrides: Partial> = {}): RestUser<'admin'> { +export function mockRestUser(overrides: Partial = {}): RestUserDetailed { return { id: Math.random().toString(), login: 'buzz.aldrin', diff --git a/server/sonar-web/src/main/js/api/queries/users.ts b/server/sonar-web/src/main/js/queries/users.ts similarity index 74% rename from server/sonar-web/src/main/js/api/queries/users.ts rename to server/sonar-web/src/main/js/queries/users.ts index bc18ed5cbc9..51e3ce0acb9 100644 --- a/server/sonar-web/src/main/js/api/queries/users.ts +++ b/server/sonar-web/src/main/js/queries/users.ts @@ -25,29 +25,27 @@ import { useQueryClient, } from '@tanstack/react-query'; import { range } from 'lodash'; -import { - CreateUserParams, - DeactivateUserParams, - GetUsersParams, - Permission, - RestUser, - UpdateUserParams, - createUser, - deactivateUser, - getUsers, - updateUser, -} from '../users'; +import { createUser, deleteUser, getUsers, updateUser } from '../api/users'; +import { RestUserBase } from '../types/users'; + +const STALE_TIME = 4 * 60 * 1000; -export function useUsersQueries

( - getParams: Omit, +export function useUsersQueries( + getParams: Omit[0], 'pageSize' | 'pageIndex'>, numberOfPages: number ) { - type QueryKey = ['user', 'list', number, Omit]; + type QueryKey = [ + 'user', + 'list', + number, + Omit[0], 'pageSize' | 'pageIndex'> + ]; const results = useQueries({ queries: range(1, numberOfPages + 1).map((page: number) => ({ queryKey: ['user', 'list', page, getParams], queryFn: ({ queryKey: [_u, _l, page, getParams] }: QueryFunctionContext) => - getUsers

({ ...getParams, pageIndex: page }), + getUsers({ ...getParams, pageIndex: page }), + staleTime: STALE_TIME, })), }); @@ -57,7 +55,7 @@ export function useUsersQueries

( total: data?.pageRestResponse.total, isLoading: acc.isLoading || isLoading, }), - { users: [] as RestUser

[], total: 0, isLoading: false } + { users: [] as U[], total: 0, isLoading: false } ); } @@ -71,7 +69,7 @@ export function useCreateUserMutation() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (data: CreateUserParams) => { + mutationFn: async (data: Parameters[0]) => { await createUser(data); }, onSuccess() { @@ -84,7 +82,7 @@ export function useUpdateUserMutation() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (data: UpdateUserParams) => { + mutationFn: async (data: Parameters[0]) => { await updateUser(data); }, onSuccess() { @@ -97,8 +95,8 @@ export function useDeactivateUserMutation() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (data: DeactivateUserParams) => { - await deactivateUser(data); + mutationFn: async (data: Parameters[0]) => { + await deleteUser(data); }, onSuccess() { queryClient.invalidateQueries({ queryKey: ['user', 'list'] }); diff --git a/server/sonar-web/src/main/js/types/users.ts b/server/sonar-web/src/main/js/types/users.ts index c40de0fde6c..ab2421f700e 100644 --- a/server/sonar-web/src/main/js/types/users.ts +++ b/server/sonar-web/src/main/js/types/users.ts @@ -91,6 +91,30 @@ export interface UserBase { name?: string; } +export interface RestUserBase { + id: string; + login: string; + name: string; +} + +export interface RestUser extends RestUserBase { + email: string; + active: boolean; + local: boolean; + externalProvider: string; + avatar: string; +} + +export interface RestUserDetailed extends RestUser { + managed: boolean; + externalLogin: string; + sonarQubeLastConnectionDate: string | null; + sonarLintLastConnectionDate: string | null; + scmAccounts: string[]; + groupsCount: number; + tokensCount: number; +} + export const enum ChangePasswordResults { OldPasswordIncorrect = 'old_password_incorrect', NewPasswordSameAsOld = 'new_password_same_as_old', -- 2.39.5

- +