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,
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) {
});
};
- handleGetUsers = (
- data: GetUsersParams
- ): Promise<{ pageRestResponse: Paging; users: RestUser<'admin'>[] }> => {
+ handleGetUsers: typeof getUsers<RestUserDetailed> = (data) => {
let pageRestResponse = {
pageIndex: 1,
pageSize: 0,
sonarQubeLastConnectionDateTo: data.sonarQubeLastConnectionDateTo,
sonarLintLastConnectionDateFrom: data.sonarLintLastConnectionDateFrom,
sonarLintLastConnectionDateTo: data.sonarLintLastConnectionDateTo,
- }) as RestUser<'admin'>[];
+ });
return this.reply({
pageRestResponse: {
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;
+++ /dev/null
-/*
- * 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 {
- CreateUserParams,
- DeactivateUserParams,
- GetUsersParams,
- Permission,
- RestUser,
- UpdateUserParams,
- createUser,
- deactivateUser,
- getUsers,
- updateUser,
-} from '../users';
-
-export function useUsersQueries<P extends Permission>(
- getParams: Omit<GetUsersParams, 'pageSize' | 'pageIndex'>,
- numberOfPages: number
-) {
- type QueryKey = ['user', 'list', number, Omit<GetUsersParams, '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<QueryKey>) =>
- getUsers<P>({ ...getParams, pageIndex: page }),
- })),
- });
-
- return results.reduce(
- (acc, { data, isLoading }) => ({
- users: acc.users.concat(data?.users ?? []),
- total: data?.pageRestResponse.total,
- isLoading: acc.isLoading || isLoading,
- }),
- { users: [] as RestUser<P>[], 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'] });
- },
- });
-}
* 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<CurrentUser> {
return getJSON('/api/users/current', undefined, true);
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);
}
return getJSON('/api/users/identity_providers').catch(throwGlobalError);
}
-export interface SearchUsersParams {
+export function searchUsers(data: {
p?: number;
ps?: number;
q?: string;
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<T extends RestUserBase>(data: {
q: string;
active?: boolean;
managed?: boolean;
sonarLintLastConnectionDateTo?: string;
pageSize?: number;
pageIndex?: number;
-}
-
-export type Permission = 'admin' | 'anonymous' | 'user';
-
-export type RestUser<T extends Permission> = 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<T extends Permission>(
- data: GetUsersParams
-): Promise<{ pageRestResponse: Paging; users: RestUser<T>[] }> {
+}): 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<void | Response> {
+}): Promise<void | Response> {
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<void | Response> {
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';
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';
}
}, [usersActivity]);
- const { users, total, isLoading } = useUsersQueries<'admin'>(
+ const { users, total, isLoading } = useUsersQueries<RestUserDetailed>(
{
q: search,
managed,
* 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 (
<div className="boxed-group boxed-group-inner">
<table className="data zebra" id="users-list">
identityProvider={identityProviders.find(
(provider) => user.externalProvider === provider.key
)}
- isCurrentUser={isLoggedIn(currentUser) && currentUser.login === user.login}
key={user.login}
user={user}
manageProvider={manageProvider}
renderUsersApp();
await act(async () => expect(await ui.charlieRow.find()).toHaveTextContent(/ExternalTest/));
- // logRoles(document.body);
expect(await ui.denisRow.find()).toHaveTextContent(/test2: UnknownExternalProvider/);
});
*/
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 {
*/
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,
} 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) {
* 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';
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<Props, State> {
- 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<string | undefined>(
+ 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<HTMLInputElement>) =>
- this.setState({ confirmPassword: event.currentTarget.value });
-
- handleNewPasswordChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
- this.setState({ newPassword: event.currentTarget.value });
-
- handleOldPasswordChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
- this.setState({ oldPassword: event.currentTarget.value });
-
- handleChangePassword = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ const handleChangePassword = (event: React.SyntheticEvent<HTMLFormElement>) => {
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 (
- <Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
- <form autoComplete="off" id="user-password-form" onSubmit={this.handleChangePassword}>
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
- <div className="modal-body">
- {errorTranslationKey && <Alert variant="error">{translate(errorTranslationKey)}</Alert>}
+ return (
+ <Modal contentLabel={header} onRequestClose={props.onClose} size="small">
+ <form autoComplete="off" id="user-password-form" onSubmit={handleChangePassword}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+ <div className="modal-body">
+ {errorTranslationKey && <Alert variant="error">{translate(errorTranslationKey)}</Alert>}
- <MandatoryFieldsExplanation className="modal-field" />
+ <MandatoryFieldsExplanation className="modal-field" />
- {this.props.isCurrentUser && (
- <div className="modal-field">
- <label htmlFor="old-user-password">
- {translate('my_profile.password.old')}
- <MandatoryFieldMarker />
- </label>
- {/* keep this fake field to hack browser autofill */}
- <input className="hidden" aria-hidden name="old-password-fake" type="password" />
- <input
- id="old-user-password"
- name="old-password"
- onChange={this.handleOldPasswordChange}
- required
- type="password"
- value={this.state.oldPassword}
- />
- </div>
- )}
+ {isCurrentUser && (
<div className="modal-field">
- <label htmlFor="user-password">
- {translate('my_profile.password.new')}
+ <label htmlFor="old-user-password">
+ {translate('my_profile.password.old')}
<MandatoryFieldMarker />
</label>
{/* keep this fake field to hack browser autofill */}
- <input className="hidden" aria-hidden name="password-fake" type="password" />
+ <input className="hidden" aria-hidden name="old-password-fake" type="password" />
<input
- id="user-password"
- name="password"
- onChange={this.handleNewPasswordChange}
+ id="old-user-password"
+ name="old-password"
+ onChange={(event) => setOldPassword(event.currentTarget.value)}
required
type="password"
- value={this.state.newPassword}
- />
- </div>
- <div className="modal-field">
- <label htmlFor="confirm-user-password">
- {translate('my_profile.password.confirm')}
- <MandatoryFieldMarker />
- </label>
- {/* keep this fake field to hack browser autofill */}
- <input className="hidden" aria-hidden name="confirm-password-fake" type="password" />
- <input
- id="confirm-user-password"
- name="confirm-password"
- onChange={this.handleConfirmPasswordChange}
- required
- type="password"
- value={this.state.confirmPassword}
+ value={oldPassword}
/>
</div>
+ )}
+ <div className="modal-field">
+ <label htmlFor="user-password">
+ {translate('my_profile.password.new')}
+ <MandatoryFieldMarker />
+ </label>
+ {/* keep this fake field to hack browser autofill */}
+ <input className="hidden" aria-hidden name="password-fake" type="password" />
+ <input
+ id="user-password"
+ name="password"
+ onChange={(event) => setNewPassword(event.currentTarget.value)}
+ required
+ type="password"
+ value={newPassword}
+ />
+ </div>
+ <div className="modal-field">
+ <label htmlFor="confirm-user-password">
+ {translate('my_profile.password.confirm')}
+ <MandatoryFieldMarker />
+ </label>
+ {/* keep this fake field to hack browser autofill */}
+ <input className="hidden" aria-hidden name="confirm-password-fake" type="password" />
+ <input
+ id="confirm-user-password"
+ name="confirm-password"
+ onChange={(event) => setConfirmPassword(event.currentTarget.value)}
+ required
+ type="password"
+ value={confirmPassword}
+ />
</div>
- <footer className="modal-foot">
- {submitting && <i className="spinner spacer-right" />}
- <SubmitButton disabled={submitting || !newPassword || newPassword !== confirmPassword}>
- {translate('change_verb')}
- </SubmitButton>
- <ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
- </footer>
- </form>
- </Modal>
- );
- }
+ </div>
+ <footer className="modal-foot">
+ {submitting && <i className="spinner spacer-right" />}
+ <SubmitButton disabled={submitting || !newPassword || newPassword !== confirmPassword}>
+ {translate('change_verb')}
+ </SubmitButton>
+ <ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink>
+ </footer>
+ </form>
+ </Modal>
+ );
}
*/
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;
}
* 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<string | undefined>(undefined);
<DeactivateForm onClose={() => setOpenForm(undefined)} user={user} />
)}
{openForm === 'password' && (
- <PasswordForm
- isCurrentUser={isCurrentUser}
- onClose={() => setOpenForm(undefined)}
- user={user}
- />
+ <PasswordForm onClose={() => setOpenForm(undefined)} user={user} />
)}
{openForm === 'update' && <UserForm onClose={() => setOpenForm(undefined)} user={user} />}
</>
* 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';
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<string>(user?.email || '');
- const [login, setLogin] = React.useState<string>(user?.login || '');
- const [name, setName] = React.useState<string>(user?.name || '');
+ const [email, setEmail] = React.useState<string>(user?.email ?? '');
+ const [login, setLogin] = React.useState<string>(user?.login ?? '');
+ const [name, setName] = React.useState<string>(user?.name ?? '');
const [password, setPassword] = React.useState<string>('');
- const [scmAccounts, setScmAccounts] = React.useState<string[]>(user?.scmAccounts || []);
+ const [scmAccounts, setScmAccounts] = React.useState<string[]>(user?.scmAccounts ?? []);
const [error, setError] = React.useState<string | undefined>(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);
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 }
* 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';
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,
{(manageProvider === undefined || !managed) && (
<td className="thin nowrap text-right text-middle">
- <UserActions isCurrentUser={isCurrentUser} user={user} manageProvider={manageProvider} />
+ <UserActions user={user} manageProvider={manageProvider} />
</td>
)}
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;
}
});
}
+/**
+ * Shortcut to do a DELETE request
+ */
+export function deleteJSON(url: string, data?: RequestData): Promise<any> {
+ return request(url)
+ .setMethod('DELETE')
+ .setData(data)
+ .submit()
+ .then((response) => checkStatus(response));
+}
+
function tryRequestAgain<T>(
repeatAPICall: () => Promise<T>,
tries: { max: number; slowThreshold: number },
*/
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';
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> = {}): AlmApplication {
return {
};
}
-export function mockRestUser(overrides: Partial<RestUser<'admin'>> = {}): RestUser<'admin'> {
+export function mockRestUser(overrides: Partial<RestUserDetailed> = {}): RestUserDetailed {
return {
id: Math.random().toString(),
login: 'buzz.aldrin',
--- /dev/null
+/*
+ * 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 { createUser, deleteUser, getUsers, updateUser } from '../api/users';
+import { RestUserBase } from '../types/users';
+
+const STALE_TIME = 4 * 60 * 1000;
+
+export function useUsersQueries<U extends RestUserBase>(
+ getParams: Omit<Parameters<typeof getUsers>[0], 'pageSize' | 'pageIndex'>,
+ numberOfPages: number
+) {
+ type QueryKey = [
+ 'user',
+ 'list',
+ number,
+ Omit<Parameters<typeof getUsers>[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<QueryKey>) =>
+ getUsers<U>({ ...getParams, pageIndex: page }),
+ staleTime: STALE_TIME,
+ })),
+ });
+
+ return results.reduce(
+ (acc, { data, isLoading }) => ({
+ users: acc.users.concat(data?.users ?? []),
+ total: data?.pageRestResponse.total,
+ isLoading: acc.isLoading || isLoading,
+ }),
+ { users: [] as U[], 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: Parameters<typeof createUser>[0]) => {
+ await createUser(data);
+ },
+ onSuccess() {
+ queryClient.invalidateQueries({ queryKey: ['user', 'list'] });
+ },
+ });
+}
+
+export function useUpdateUserMutation() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (data: Parameters<typeof updateUser>[0]) => {
+ await updateUser(data);
+ },
+ onSuccess() {
+ queryClient.invalidateQueries({ queryKey: ['user', 'list'] });
+ },
+ });
+}
+
+export function useDeactivateUserMutation() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (data: Parameters<typeof deleteUser>[0]) => {
+ await deleteUser(data);
+ },
+ onSuccess() {
+ queryClient.invalidateQueries({ queryKey: ['user', 'list'] });
+ },
+ });
+}
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',