aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>2023-07-24 12:16:13 +0200
committersonartech <sonartech@sonarsource.com>2023-07-31 20:03:31 +0000
commita0bade28a6c8a387afbe78a8fd3bc90bb00aa267 (patch)
tree2b2f33b7ae73f3fc459559cdae16075fd710c8bb
parent5f30a8ab495390c57c12ccab5fc228b4591a23ed (diff)
downloadsonarqube-a0bade28a6c8a387afbe78a8fd3bc90bb00aa267.tar.gz
sonarqube-a0bade28a6c8a387afbe78a8fd3bc90bb00aa267.zip
SONAR-19967 Refactor User call using React Query
-rw-r--r--server/sonar-web/src/main/js/api/queries/users.ts106
-rw-r--r--server/sonar-web/src/main/js/api/users.ts32
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/GithubAuthenticationTab.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/SamlAuthenticationTab.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/queries/identity-provider.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/users/Header.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/users/UsersApp.tsx97
-rw-r--r--server/sonar-web/src/main/js/apps/users/UsersList.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx143
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx166
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/UserActions.tsx102
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/UserForm.tsx355
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/UserGroups.tsx91
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx33
15 files changed, 534 insertions, 638 deletions
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<SearchUsersParams, 'p' | 'ps'>,
+ numberOfPages: number
+) {
+ type QueryKey = ['user', 'list', number, Omit<SearchUsersParams, 'p' | 'ps'>];
+ const results = useQueries({
+ queries: range(1, numberOfPages + 1).map((page: number) => ({
+ queryKey: ['user', 'list', page, searchParam],
+ queryFn: ({ queryKey: [_u, _l, page, searchParam] }: QueryFunctionContext<QueryKey>) =>
+ 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<void | Response> {
+}
+
+export function createUser(data: CreateUserParams): Promise<void | Response> {
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) {
/>
</Alert>
)}
- {openUserForm && (
- <UserForm onClose={() => setOpenUserForm(false)} onUpdateUsers={props.onUpdateUsers} />
- )}
+ {openUserForm && <UserForm onClose={() => setOpenUserForm(false)} />}
</div>
);
}
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<IdentityProvider[]>([]);
-
- const [loading, setLoading] = useState(true);
- const [paging, setPaging] = useState<Paging>();
- const [users, setUsers] = useState<User[]>([]);
-
+ const [numberOfPages, setNumberOfPages] = useState<number>(1);
const [search, setSearch] = useState('');
const [usersActivity, setUsersActivity] = useState<UserActivity>(UserActivity.AnyActivity);
const [managed, setManaged] = useState<boolean | undefined>(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 (
<main className="page page-limited" id="users-page">
<Suggestions suggestions="users" />
<Helmet defer={false} title={translate('users.page')} />
- <Header onUpdateUsers={fetchUsers} manageProvider={manageProvider} />
+ <Header manageProvider={manageProvider} />
{manageProvider === Provider.Github && <GitHubSynchronisationWarning short />}
<div className="display-flex-justify-start big-spacer-bottom big-spacer-top">
<ManagedFilter
manageProvider={manageProvider}
- loading={loading}
+ loading={isLoading}
managed={managed}
- setManaged={setManaged}
+ setManaged={(m) => {
+ setManaged(m);
+ setNumberOfPages(1);
+ }}
/>
<SearchBox
id="users-search"
minLength={2}
- onChange={(search: string) => 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() {
<Select
id="users-activity-filter"
className="input-large"
- isDisabled={loading}
+ isDisabled={isLoading}
onChange={(userActivity: LabelValueSelectOption<UserActivity>) => {
setUsersActivity(userActivity.value);
+ setNumberOfPages(1);
}}
options={USERS_ACTIVITY_OPTIONS}
isSearchable={false}
@@ -166,23 +140,20 @@ export default function UsersApp() {
/>
</div>
</div>
- <DeferredSpinner loading={loading}>
+ <DeferredSpinner loading={isLoading}>
<UsersList
identityProviders={identityProviders}
- onUpdateUsers={fetchUsers}
- updateTokensCount={fetchUsers}
users={users}
manageProvider={manageProvider}
/>
</DeferredSpinner>
- {paging !== undefined && (
- <ListFooter
- count={users.length}
- loadMore={fetchMoreUsers}
- ready={!loading}
- total={paging.total}
- />
- )}
+
+ <ListFooter
+ count={users.length}
+ loadMore={() => setNumberOfPages((n) => n + 1)}
+ ready={!isLoading}
+ total={total}
+ />
</main>
);
}
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 d1f3349c960..e3fed01d219 100644
--- a/server/sonar-web/src/main/js/apps/users/UsersList.tsx
+++ b/server/sonar-web/src/main/js/apps/users/UsersList.tsx
@@ -27,19 +27,11 @@ import UserListItem from './components/UserListItem';
interface Props {
identityProviders: IdentityProvider[];
- onUpdateUsers: () => void;
- updateTokensCount: (login: string, tokensCount: number) => void;
users: User[];
manageProvider: string | undefined;
}
-export default function UsersList({
- identityProviders,
- onUpdateUsers,
- updateTokensCount,
- users,
- manageProvider,
-}: Props) {
+export default function UsersList({ identityProviders, users, manageProvider }: Props) {
const userContext = React.useContext(CurrentUserContext);
const currentUser = userContext?.currentUser;
@@ -73,8 +65,6 @@ export default function UsersList({
)}
isCurrentUser={isLoggedIn(currentUser) && currentUser.login === user.login}
key={user.login}
- onUpdateUsers={onUpdateUsers}
- updateTokensCount={updateTokensCount}
user={user}
manageProvider={manageProvider}
/>
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 796d52abdf9..aa334af4bb4 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,108 +19,81 @@
*/
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
-import { deactivateUser } from '../../../api/users';
+import { useDeactivateUserMutation } from '../../../api/queries/users';
import DocLink from '../../../components/common/DocLink';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
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 { UserActive } from '../../../types/users';
export interface Props {
onClose: () => void;
- onUpdateUsers: () => void;
user: UserActive;
}
-interface State {
- submitting: boolean;
- anonymize: boolean;
-}
-
-export default class DeactivateForm extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { submitting: false, anonymize: false };
-
- componentDidMount() {
- this.mounted = true;
- }
+export default function DeactivateForm(props: Props) {
+ const { user } = props;
+ const [anonymize, setAnonymize] = React.useState(false);
- componentWillUnmount() {
- this.mounted = false;
- }
+ const { mutate: deactivateUser, isLoading } = useDeactivateUserMutation();
- handleAnonymize = (checked: boolean) => {
- this.setState({ anonymize: checked });
- };
-
- handleDeactivate = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ const handleDeactivate = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
- this.setState({ submitting: true });
- deactivateUser({ login: this.props.user.login, anonymize: this.state.anonymize }).then(
- () => {
- this.props.onUpdateUsers();
- this.props.onClose();
- },
- () => {
- if (this.mounted) {
- this.setState({ submitting: false });
- }
+ deactivateUser(
+ { login: user.login, anonymize },
+ {
+ onSuccess: props.onClose,
}
);
};
- render() {
- const { user } = this.props;
- const { submitting, anonymize } = this.state;
-
- const header = translate('users.deactivate_user');
- return (
- <Modal contentLabel={header} onRequestClose={this.props.onClose}>
- <form autoComplete="off" id="deactivate-user-form" onSubmit={this.handleDeactivate}>
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
- <div className="modal-body display-flex-column">
- {translateWithParameters('users.deactivate_user.confirmation', user.name, user.login)}
- <Checkbox
- id="delete-user"
- className="big-spacer-top"
- checked={anonymize}
- onCheck={this.handleAnonymize}
- >
- <label className="little-spacer-left" htmlFor="delete-user">
- {translate('users.delete_user')}
- </label>
- </Checkbox>
- {anonymize && (
- <Alert variant="warning" className="big-spacer-top">
- <FormattedMessage
- defaultMessage={translate('users.delete_user.help')}
- id="delete-user-warning"
- values={{
- link: (
- <DocLink to="/instance-administration/authentication/overview/">
- {translate('users.delete_user.help.link')}
- </DocLink>
- ),
- }}
- />
- </Alert>
- )}
- </div>
- <footer className="modal-foot">
- {submitting && <i className="spinner spacer-right" />}
- <SubmitButton className="js-confirm button-red" disabled={submitting}>
- {translate('users.deactivate')}
- </SubmitButton>
- <ResetButtonLink className="js-modal-close" onClick={this.props.onClose}>
- {translate('cancel')}
- </ResetButtonLink>
- </footer>
- </form>
- </Modal>
- );
- }
+ const header = translate('users.deactivate_user');
+ return (
+ <Modal contentLabel={header} onRequestClose={props.onClose}>
+ <form autoComplete="off" id="deactivate-user-form" onSubmit={handleDeactivate}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+ <div className="modal-body display-flex-column">
+ {translateWithParameters('users.deactivate_user.confirmation', user.name, user.login)}
+ <Checkbox
+ id="delete-user"
+ className="big-spacer-top"
+ checked={anonymize}
+ onCheck={(checked) => setAnonymize(checked)}
+ >
+ <label className="little-spacer-left" htmlFor="delete-user">
+ {translate('users.delete_user')}
+ </label>
+ </Checkbox>
+ {anonymize && (
+ <Alert variant="warning" className="big-spacer-top">
+ <FormattedMessage
+ defaultMessage={translate('users.delete_user.help')}
+ id="delete-user-warning"
+ values={{
+ link: (
+ <DocLink to="/instance-administration/authentication/overview/">
+ {translate('users.delete_user.help.link')}
+ </DocLink>
+ ),
+ }}
+ />
+ </Alert>
+ )}
+ </div>
+ <footer className="modal-foot">
+ {isLoading && <i className="spinner spacer-right" />}
+ <SubmitButton className="js-confirm button-red" disabled={isLoading}>
+ {translate('users.deactivate')}
+ </SubmitButton>
+ <ResetButtonLink className="js-modal-close" onClick={props.onClose}>
+ {translate('cancel')}
+ </ResetButtonLink>
+ </footer>
+ </form>
+ </Modal>
+ );
}
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 d590e7da07a..478b9b73376 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,114 +19,80 @@
*/
import { find, without } from 'lodash';
import * as React from 'react';
-import { getUserGroups, UserGroup } from '../../../api/users';
+import { useInvalidateUsersList } from '../../../api/queries/users';
import { addUserToGroup, removeUserFromGroup } from '../../../api/user_groups';
-import { ResetButtonLink } from '../../../components/controls/buttons';
+import { UserGroup, getUserGroups } from '../../../api/users';
import Modal from '../../../components/controls/Modal';
import SelectList, {
SelectListFilter,
SelectListSearchParams,
} from '../../../components/controls/SelectList';
+import { ResetButtonLink } from '../../../components/controls/buttons';
import { translate } from '../../../helpers/l10n';
import { User } from '../../../types/users';
interface Props {
onClose: () => void;
- onUpdateUsers: () => void;
user: User;
}
-interface State {
- needToReload: boolean;
- lastSearchParams?: SelectListSearchParams;
- groups: UserGroup[];
- groupsTotalCount?: number;
- selectedGroups: string[];
-}
-
-export default class GroupsForm extends React.PureComponent<Props, State> {
- mounted = false;
-
- constructor(props: Props) {
- super(props);
-
- this.state = {
- needToReload: false,
- groups: [],
- selectedGroups: [],
- };
- }
-
- componentDidMount() {
- this.mounted = true;
- }
+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[]>([]);
- componentWillUnmount() {
- this.mounted = false;
- }
+ const invalidateUserList = useInvalidateUsersList();
- fetchUsers = (searchParams: SelectListSearchParams) =>
+ const fetchUsers = (searchParams: SelectListSearchParams) =>
getUserGroups({
- login: this.props.user.login,
+ login: user.login,
p: searchParams.page,
ps: searchParams.pageSize,
q: searchParams.query !== '' ? searchParams.query : undefined,
selected: searchParams.filter,
}).then((data) => {
- if (this.mounted) {
- this.setState((prevState) => {
- const more = searchParams.page != null && searchParams.page > 1;
-
- const groups = more ? [...prevState.groups, ...data.groups] : data.groups;
- const newSeletedGroups = data.groups.filter((gp) => gp.selected).map((gp) => gp.name);
- const selectedGroups = more
- ? [...prevState.selectedGroups, ...newSeletedGroups]
- : newSeletedGroups;
-
- return {
- lastSearchParams: searchParams,
- needToReload: false,
- groups,
- groupsTotalCount: data.paging.total,
- selectedGroups,
- };
- });
- }
+ 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;
+
+ setLastSearchParams(searchParams);
+ setNeedToReload(false);
+ setGroups(allGroups);
+ setGroupsTotalCount(data.paging.total);
+ setSelectedGroups(allSelectedGroups);
});
- handleSelect = (name: string) =>
+ const handleSelect = (name: string) =>
addUserToGroup({
name,
- login: this.props.user.login,
+ login: user.login,
}).then(() => {
- if (this.mounted) {
- this.setState((state: State) => ({
- needToReload: true,
- selectedGroups: [...state.selectedGroups, name],
- }));
- }
+ setNeedToReload(true);
+ setSelectedGroups([...selectedGroups, name]);
});
- handleUnselect = (name: string) =>
+ const handleUnselect = (name: string) =>
removeUserFromGroup({
name,
- login: this.props.user.login,
+ login: user.login,
}).then(() => {
- if (this.mounted) {
- this.setState((state: State) => ({
- needToReload: true,
- selectedGroups: without(state.selectedGroups, name),
- }));
- }
+ setNeedToReload(true);
+ setSelectedGroups(without(selectedGroups, name));
});
- handleClose = () => {
- this.props.onUpdateUsers();
- this.props.onClose();
+ const handleClose = () => {
+ invalidateUserList();
+ props.onClose();
};
- renderElement = (name: string): React.ReactNode => {
- const group = find(this.state.groups, { name });
+ const renderElement = (name: string): React.ReactNode => {
+ const group = find(groups, { name });
return (
<div className="select-list-list-item">
{group === undefined ? (
@@ -142,37 +108,33 @@ export default class GroupsForm extends React.PureComponent<Props, State> {
);
};
- render() {
- const header = translate('users.update_groups');
+ const header = translate('users.update_groups');
- return (
- <Modal contentLabel={header} onRequestClose={this.handleClose}>
- <div className="modal-head">
- <h2>{header}</h2>
- </div>
+ return (
+ <Modal contentLabel={header} onRequestClose={handleClose}>
+ <div className="modal-head">
+ <h2>{header}</h2>
+ </div>
- <div className="modal-body modal-container">
- <SelectList
- elements={this.state.groups.map((group) => group.name)}
- elementsTotalCount={this.state.groupsTotalCount}
- needToReload={
- this.state.needToReload &&
- this.state.lastSearchParams &&
- this.state.lastSearchParams.filter !== SelectListFilter.All
- }
- onSearch={this.fetchUsers}
- onSelect={this.handleSelect}
- onUnselect={this.handleUnselect}
- renderElement={this.renderElement}
- selectedElements={this.state.selectedGroups}
- withPaging
- />
- </div>
+ <div className="modal-body modal-container">
+ <SelectList
+ elements={groups.map((group) => group.name)}
+ elementsTotalCount={groupsTotalCount}
+ needToReload={
+ needToReload && lastSearchParams && lastSearchParams.filter !== SelectListFilter.All
+ }
+ onSearch={fetchUsers}
+ onSelect={handleSelect}
+ onUnselect={handleUnselect}
+ renderElement={renderElement}
+ selectedElements={selectedGroups}
+ withPaging
+ />
+ </div>
- <footer className="modal-foot">
- <ResetButtonLink onClick={this.handleClose}>{translate('done')}</ResetButtonLink>
- </footer>
- </Modal>
- );
- }
+ <footer className="modal-foot">
+ <ResetButtonLink onClick={handleClose}>{translate('done')}</ResetButtonLink>
+ </footer>
+ </Modal>
+ );
}
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 d924351573a..6d6f79f267e 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,8 +19,9 @@
*/
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
-import { ResetButtonLink } from '../../../components/controls/buttons';
+import { useInvalidateUsersList } from '../../../api/queries/users';
import Modal from '../../../components/controls/Modal';
+import { ResetButtonLink } from '../../../components/controls/buttons';
import { translate } from '../../../helpers/l10n';
import { User } from '../../../types/users';
import TokensForm from './TokensForm';
@@ -28,12 +29,21 @@ import TokensForm from './TokensForm';
interface Props {
user: User;
onClose: () => void;
- updateTokensCount: (login: string, tokensCount: number) => void;
}
export default function TokensFormModal(props: Props) {
+ const [hasTokenCountChanged, setHasTokenCountChanged] = React.useState(false);
+ const invalidateUserList = useInvalidateUsersList();
+
+ const handleClose = () => {
+ if (hasTokenCountChanged) {
+ invalidateUserList();
+ }
+ props.onClose();
+ };
+
return (
- <Modal size="large" contentLabel={translate('users.tokens')} onRequestClose={props.onClose}>
+ <Modal size="large" contentLabel={translate('users.tokens')} onRequestClose={handleClose}>
<header className="modal-head">
<h2>
<FormattedMessage
@@ -47,12 +57,12 @@ export default function TokensFormModal(props: Props) {
<TokensForm
deleteConfirmation="inline"
login={props.user.login}
- updateTokensCount={props.updateTokensCount}
+ updateTokensCount={() => setHasTokenCountChanged(true)}
displayTokenTypeInput={false}
/>
</div>
<footer className="modal-foot">
- <ResetButtonLink onClick={props.onClose}>{translate('done')}</ResetButtonLink>
+ <ResetButtonLink onClick={handleClose}>{translate('done')}</ResetButtonLink>
</footer>
</Modal>
);
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 627c8b45cfa..e1872825607 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
@@ -23,67 +23,50 @@ import ActionsDropdown, {
ActionsDropdownItem,
} from '../../../components/controls/ActionsDropdown';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { isUserActive, User } from '../../../types/users';
+import { User, isUserActive } from '../../../types/users';
import DeactivateForm from './DeactivateForm';
import PasswordForm from './PasswordForm';
import UserForm from './UserForm';
interface Props {
isCurrentUser: boolean;
- onUpdateUsers: () => void;
user: User;
manageProvider: string | undefined;
}
-interface State {
- openForm?: string;
-}
-
-export default class UserActions extends React.PureComponent<Props, State> {
- state: State = {};
+export default function UserActions(props: Props) {
+ const { isCurrentUser, user, manageProvider } = props;
- handleOpenDeactivateForm = () => {
- this.setState({ openForm: 'deactivate' });
- };
-
- handleOpenPasswordForm = () => {
- this.setState({ openForm: 'password' });
- };
-
- handleOpenUpdateForm = () => {
- this.setState({ openForm: 'update' });
- };
-
- handleCloseForm = () => {
- this.setState({ openForm: undefined });
- };
+ const [openForm, setOpenForm] = React.useState<string | undefined>(undefined);
- isInstanceManaged = () => {
- return this.props.manageProvider !== undefined;
+ const isInstanceManaged = () => {
+ return manageProvider !== undefined;
};
- isUserLocal = () => {
- return this.isInstanceManaged() && !this.props.user.managed;
+ const isUserLocal = () => {
+ return isInstanceManaged() && !user.managed;
};
- isUserManaged = () => {
- return this.isInstanceManaged() && this.props.user.managed;
+ const isUserManaged = () => {
+ return isInstanceManaged() && user.managed;
};
- renderActions = () => {
- const { user } = this.props;
+ if (isUserManaged()) {
+ return null;
+ }
- return (
+ return (
+ <>
<ActionsDropdown label={translateWithParameters('users.manage_user', user.login)}>
- {!this.isInstanceManaged() && (
+ {!isInstanceManaged() && (
<>
- <ActionsDropdownItem className="js-user-update" onClick={this.handleOpenUpdateForm}>
+ <ActionsDropdownItem className="js-user-update" onClick={() => setOpenForm('update')}>
{translate('update_details')}
</ActionsDropdownItem>
{user.local && (
<ActionsDropdownItem
className="js-user-change-password"
- onClick={this.handleOpenPasswordForm}
+ onClick={() => setOpenForm('password')}
>
{translate('my_profile.password.title')}
</ActionsDropdownItem>
@@ -91,45 +74,28 @@ export default class UserActions extends React.PureComponent<Props, State> {
</>
)}
- {isUserActive(user) && !this.isInstanceManaged() && <ActionsDropdownDivider />}
- {isUserActive(user) && (!this.isInstanceManaged() || this.isUserLocal()) && (
+ {isUserActive(user) && !isInstanceManaged() && <ActionsDropdownDivider />}
+ {isUserActive(user) && (!isInstanceManaged() || isUserLocal()) && (
<ActionsDropdownItem
className="js-user-deactivate"
destructive
- onClick={this.handleOpenDeactivateForm}
+ onClick={() => setOpenForm('deactivate')}
>
{translate('users.deactivate')}
</ActionsDropdownItem>
)}
</ActionsDropdown>
- );
- };
-
- render() {
- const { openForm } = this.state;
- const { isCurrentUser, onUpdateUsers, user } = this.props;
-
- if (this.isUserManaged()) {
- return null;
- }
-
- return (
- <>
- {this.renderActions()}
- {openForm === 'deactivate' && isUserActive(user) && (
- <DeactivateForm
- onClose={this.handleCloseForm}
- onUpdateUsers={onUpdateUsers}
- user={user}
- />
- )}
- {openForm === 'password' && (
- <PasswordForm isCurrentUser={isCurrentUser} onClose={this.handleCloseForm} user={user} />
- )}
- {openForm === 'update' && (
- <UserForm onClose={this.handleCloseForm} onUpdateUsers={onUpdateUsers} user={user} />
- )}
- </>
- );
- }
+ {openForm === 'deactivate' && isUserActive(user) && (
+ <DeactivateForm onClose={() => setOpenForm(undefined)} user={user} />
+ )}
+ {openForm === 'password' && (
+ <PasswordForm
+ isCurrentUser={isCurrentUser}
+ onClose={() => setOpenForm(undefined)}
+ user={user}
+ />
+ )}
+ {openForm === 'update' && <UserForm onClose={() => 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 8896375624c..a1a4c11d7d4 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,9 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { createUser, updateUser } from '../../../api/users';
-import { Button, ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
+import { useCreateUserMutation, useUpdateUserMutation } from '../../../api/queries/users';
import SimpleModal from '../../../components/controls/SimpleModal';
+import { Button, ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
import { Alert } from '../../../components/ui/Alert';
import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
@@ -32,245 +32,196 @@ import UserScmAccountInput from './UserScmAccountInput';
export interface Props {
onClose: () => void;
- onUpdateUsers: () => void;
user?: User;
}
-interface State {
- email: string;
- error?: string;
- login: string;
- name: string;
- password: string;
- scmAccounts: string[];
-}
-
-export default class UserForm extends React.PureComponent<Props, State> {
- mounted = false;
-
- constructor(props: Props) {
- super(props);
- const { user } = props;
- if (user) {
- this.state = {
- email: user.email || '',
- login: user.login,
- name: user.name || '',
- password: '',
- scmAccounts: user.scmAccounts || [],
- };
- } else {
- this.state = {
- email: '',
- login: '',
- name: '',
- password: '',
- scmAccounts: [],
- };
- }
- }
+export default function UserForm(props: Props) {
+ const { user } = props;
- componentDidMount() {
- this.mounted = true;
- }
+ const { mutate: createUser } = useCreateUserMutation();
+ const { mutate: updateUser } = useUpdateUserMutation();
- componentWillUnmount() {
- this.mounted = false;
- }
+ 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 [error, setError] = React.useState<string | undefined>(undefined);
- handleError = (response: Response) => {
- if (!this.mounted || ![400, 500].includes(response.status)) {
+ const handleError = (response: Response) => {
+ if (![400, 500].includes(response.status)) {
return throwGlobalError(response);
- } else {
- return parseError(response).then(
- (errorMsg) => this.setState({ error: errorMsg }),
- throwGlobalError
- );
}
+ return parseError(response).then((errorMsg) => setError(errorMsg), throwGlobalError);
};
- handleEmailChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
- this.setState({ email: event.currentTarget.value });
-
- handleLoginChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
- this.setState({ login: event.currentTarget.value });
-
- handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
- this.setState({ name: event.currentTarget.value });
-
- handlePasswordChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
- this.setState({ password: event.currentTarget.value });
-
- handleCreateUser = () => {
- return createUser({
- email: this.state.email || undefined,
- login: this.state.login,
- name: this.state.name,
- password: this.state.password,
- scmAccount: this.state.scmAccounts,
- }).then(() => {
- this.props.onUpdateUsers();
- this.props.onClose();
- }, this.handleError);
+ const handleCreateUser = () => {
+ createUser(
+ {
+ email: email || undefined,
+ login,
+ name,
+ password,
+ scmAccount: scmAccounts,
+ },
+ { onSuccess: props.onClose, onError: handleError }
+ );
};
- handleUpdateUser = () => {
- const { user } = this.props;
- return updateUser({
- email: user!.local ? this.state.email : undefined,
- login: this.state.login,
- name: user!.local ? this.state.name : undefined,
- scmAccount: this.state.scmAccounts,
- }).then(() => {
- this.props.onUpdateUsers();
- this.props.onClose();
- }, this.handleError);
+ const handleUpdateUser = () => {
+ const { user } = props;
+
+ updateUser(
+ {
+ email: user!.local ? email : undefined,
+ login,
+ name: user!.local ? name : undefined,
+ scmAccount: scmAccounts,
+ },
+ { onSuccess: props.onClose, onError: handleError }
+ );
};
- handleAddScmAccount = () => {
- this.setState(({ scmAccounts }) => ({ scmAccounts: scmAccounts.concat('') }));
+ const handleAddScmAccount = () => {
+ setScmAccounts((scmAccounts) => scmAccounts.concat(''));
};
- handleUpdateScmAccount = (idx: number, scmAccount: string) =>
- this.setState(({ scmAccounts: oldScmAccounts }) => {
- const scmAccounts = oldScmAccounts.slice();
- scmAccounts[idx] = scmAccount;
- return { scmAccounts };
+ const handleUpdateScmAccount = (idx: number, scmAccount: string) => {
+ setScmAccounts((scmAccounts) => {
+ const newScmAccounts = scmAccounts.slice();
+ newScmAccounts[idx] = scmAccount;
+ return newScmAccounts;
});
+ };
- handleRemoveScmAccount = (idx: number) =>
- this.setState(({ scmAccounts }) => ({
- scmAccounts: scmAccounts.slice(0, idx).concat(scmAccounts.slice(idx + 1)),
- }));
+ const handleRemoveScmAccount = (idx: number) => {
+ setScmAccounts((scmAccounts) => scmAccounts.slice(0, idx).concat(scmAccounts.slice(idx + 1)));
+ };
- render() {
- const { user } = this.props;
- const { error } = this.state;
+ const header = user ? translate('users.update_user') : translate('users.create_user');
- const header = user ? translate('users.update_user') : translate('users.create_user');
- return (
- <SimpleModal
- header={header}
- onClose={this.props.onClose}
- onSubmit={user ? this.handleUpdateUser : this.handleCreateUser}
- size="small"
- >
- {({ onCloseClick, onFormSubmit, submitting }) => (
- <form autoComplete="off" id="user-form" onSubmit={onFormSubmit}>
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
+ return (
+ <SimpleModal
+ header={header}
+ onClose={props.onClose}
+ onSubmit={user ? handleUpdateUser : handleCreateUser}
+ size="small"
+ >
+ {({ onCloseClick, onFormSubmit, submitting }) => (
+ <form autoComplete="off" id="user-form" onSubmit={onFormSubmit}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
- <div className="modal-body modal-container">
- {error && <Alert variant="error">{error}</Alert>}
+ <div className="modal-body modal-container">
+ {error && <Alert variant="error">{error}</Alert>}
- {!error && user && !user.local && (
- <Alert variant="warning">{translate('users.cannot_update_delegated_user')}</Alert>
- )}
+ {!error && user && !user.local && (
+ <Alert variant="warning">{translate('users.cannot_update_delegated_user')}</Alert>
+ )}
- <MandatoryFieldsExplanation className="modal-field" />
+ <MandatoryFieldsExplanation className="modal-field" />
- {!user && (
- <div className="modal-field">
- <label htmlFor="create-user-login">
- {translate('login')}
- <MandatoryFieldMarker />
- </label>
- <input
- autoComplete="off"
- autoFocus
- id="create-user-login"
- maxLength={255}
- minLength={3}
- name="login"
- onChange={this.handleLoginChange}
- required
- type="text"
- value={this.state.login}
- />
- <p className="note">{translateWithParameters('users.minimum_x_characters', 3)}</p>
- </div>
- )}
+ {!user && (
<div className="modal-field">
- <label htmlFor="create-user-name">
- {translate('name')}
+ <label htmlFor="create-user-login">
+ {translate('login')}
<MandatoryFieldMarker />
</label>
<input
autoComplete="off"
- autoFocus={!!user}
- disabled={user && !user.local}
- id="create-user-name"
- maxLength={200}
- name="name"
- onChange={this.handleNameChange}
+ autoFocus
+ id="create-user-login"
+ maxLength={255}
+ minLength={3}
+ name="login"
+ onChange={(e) => setLogin(e.currentTarget.value)}
required
type="text"
- value={this.state.name}
+ value={login}
/>
+ <p className="note">{translateWithParameters('users.minimum_x_characters', 3)}</p>
</div>
+ )}
+ <div className="modal-field">
+ <label htmlFor="create-user-name">
+ {translate('name')}
+ <MandatoryFieldMarker />
+ </label>
+ <input
+ autoComplete="off"
+ autoFocus={!!user}
+ disabled={user && !user.local}
+ id="create-user-name"
+ maxLength={200}
+ name="name"
+ onChange={(e) => setName(e.currentTarget.value)}
+ required
+ type="text"
+ value={name}
+ />
+ </div>
+ <div className="modal-field">
+ <label htmlFor="create-user-email">{translate('users.email')}</label>
+ <input
+ autoComplete="off"
+ disabled={user && !user.local}
+ id="create-user-email"
+ maxLength={100}
+ name="email"
+ onChange={(e) => setEmail(e.currentTarget.value)}
+ type="email"
+ value={email}
+ />
+ </div>
+ {!user && (
<div className="modal-field">
- <label htmlFor="create-user-email">{translate('users.email')}</label>
+ <label htmlFor="create-user-password">
+ {translate('password')}
+ <MandatoryFieldMarker />
+ </label>
<input
autoComplete="off"
- disabled={user && !user.local}
- id="create-user-email"
- maxLength={100}
- name="email"
- onChange={this.handleEmailChange}
- type="email"
- value={this.state.email}
+ id="create-user-password"
+ name="password"
+ onChange={(e) => setPassword(e.currentTarget.value)}
+ required
+ type="password"
+ value={password}
/>
</div>
- {!user && (
- <div className="modal-field">
- <label htmlFor="create-user-password">
- {translate('password')}
- <MandatoryFieldMarker />
- </label>
- <input
- autoComplete="off"
- id="create-user-password"
- name="password"
- onChange={this.handlePasswordChange}
- required
- type="password"
- value={this.state.password}
+ )}
+ <div className="modal-field">
+ <fieldset>
+ <legend>{translate('my_profile.scm_accounts')}</legend>
+ {scmAccounts.map((scm, idx) => (
+ <UserScmAccountInput
+ idx={idx}
+ key={idx}
+ onChange={handleUpdateScmAccount}
+ onRemove={handleRemoveScmAccount}
+ scmAccount={scm}
/>
+ ))}
+ <div className="spacer-bottom">
+ <Button className="js-scm-account-add" onClick={handleAddScmAccount}>
+ {translate('add_verb')}
+ </Button>
</div>
- )}
- <div className="modal-field">
- <fieldset>
- <legend>{translate('my_profile.scm_accounts')}</legend>
- {this.state.scmAccounts.map((scm, idx) => (
- <UserScmAccountInput
- idx={idx}
- key={idx}
- onChange={this.handleUpdateScmAccount}
- onRemove={this.handleRemoveScmAccount}
- scmAccount={scm}
- />
- ))}
- <div className="spacer-bottom">
- <Button className="js-scm-account-add" onClick={this.handleAddScmAccount}>
- {translate('add_verb')}
- </Button>
- </div>
- </fieldset>
- <p className="note">{translate('user.login_or_email_used_as_scm_account')}</p>
- </div>
+ </fieldset>
+ <p className="note">{translate('user.login_or_email_used_as_scm_account')}</p>
</div>
-
- <footer className="modal-foot">
- {submitting && <i className="spinner spacer-right" />}
- <SubmitButton disabled={submitting}>
- {user ? translate('update_verb') : translate('create')}
- </SubmitButton>
- <ResetButtonLink onClick={onCloseClick}>{translate('cancel')}</ResetButtonLink>
- </footer>
- </form>
- )}
- </SimpleModal>
- );
- }
+ </div>
+
+ <footer className="modal-foot">
+ {submitting && <i className="spinner spacer-right" />}
+ <SubmitButton disabled={submitting}>
+ {user ? translate('update_verb') : translate('create')}
+ </SubmitButton>
+ <ResetButtonLink onClick={onCloseClick}>{translate('cancel')}</ResetButtonLink>
+ </footer>
+ </form>
+ )}
+ </SimpleModal>
+ );
}
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<Props, State> {
- 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 (
- <ul>
- {groups.slice(0, limit).map((group) => (
+ const limit = groups.length > GROUPS_LIMIT ? GROUPS_LIMIT - 1 : GROUPS_LIMIT;
+ return (
+ <ul>
+ {groups.slice(0, limit).map((group) => (
+ <li className="little-spacer-bottom" key={group}>
+ {group}
+ </li>
+ ))}
+ {groups.length > GROUPS_LIMIT &&
+ showMore &&
+ groups.slice(limit).map((group) => (
<li className="little-spacer-bottom" key={group}>
{group}
</li>
))}
- {groups.length > GROUPS_LIMIT &&
- showMore &&
- groups.slice(limit).map((group) => (
- <li className="little-spacer-bottom" key={group}>
- {group}
- </li>
- ))}
- <li className="little-spacer-bottom">
- {groups.length > GROUPS_LIMIT && !showMore && (
- <ButtonLink className="js-user-more-groups spacer-right" onClick={this.toggleShowMore}>
- {translateWithParameters('more_x', groups.length - limit)}
- </ButtonLink>
- )}
- {manageProvider === undefined && (
- <ButtonIcon
- aria-label={translateWithParameters('users.update_users_groups', user.login)}
- className="js-user-groups button-small"
- onClick={this.handleOpenForm}
- tooltip={translate('users.update_groups')}
- >
- <BulletListIcon />
- </ButtonIcon>
- )}
- </li>
- {openForm && (
- <GroupsForm
- onClose={this.handleCloseForm}
- onUpdateUsers={this.props.onUpdateUsers}
- user={user}
- />
+ <li className="little-spacer-bottom">
+ {groups.length > GROUPS_LIMIT && !showMore && (
+ <ButtonLink
+ className="js-user-more-groups spacer-right"
+ onClick={() => setShowMore(!showMore)}
+ >
+ {translateWithParameters('more_x', groups.length - limit)}
+ </ButtonLink>
+ )}
+ {manageProvider === undefined && (
+ <ButtonIcon
+ aria-label={translateWithParameters('users.update_users_groups', user.login)}
+ className="js-user-groups button-small"
+ onClick={() => setOpenForm(true)}
+ tooltip={translate('users.update_groups')}
+ >
+ <BulletListIcon />
+ </ButtonIcon>
)}
- </ul>
- );
- }
+ </li>
+ {openForm && <GroupsForm onClose={() => setOpenForm(false)} user={user} />}
+ </ul>
+ );
}
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 (
<tr>
@@ -79,12 +70,7 @@ export default function UserListItem(props: UserListItemProps) {
<DateFromNow date={user.sonarLintLastConnectionDate} hourPrecision />
</td>
<td className="thin nowrap text-middle">
- <UserGroups
- groups={user.groups ?? []}
- manageProvider={manageProvider}
- onUpdateUsers={onUpdateUsers}
- user={user}
- />
+ <UserGroups groups={user.groups ?? []} manageProvider={manageProvider} user={user} />
</td>
<td className="thin nowrap text-middle">
{user.tokensCount}
@@ -100,22 +86,11 @@ export default function UserListItem(props: UserListItemProps) {
{(manageProvider === undefined || !user.managed) && (
<td className="thin nowrap text-right text-middle">
- <UserActions
- isCurrentUser={isCurrentUser}
- onUpdateUsers={onUpdateUsers}
- user={user}
- manageProvider={manageProvider}
- />
+ <UserActions isCurrentUser={isCurrentUser} user={user} manageProvider={manageProvider} />
</td>
)}
- {openTokenForm && (
- <TokensFormModal
- onClose={() => setOpenTokenForm(false)}
- updateTokensCount={updateTokensCount}
- user={user}
- />
- )}
+ {openTokenForm && <TokensFormModal onClose={() => setOpenTokenForm(false)} user={user} />}
</tr>
);
}