aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/users
diff options
context:
space:
mode:
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>2023-03-27 10:06:34 +0200
committersonartech <sonartech@sonarsource.com>2023-03-30 20:03:07 +0000
commit33cf03a9e6316e69e53c3960d43dde522d17583f (patch)
tree39639d5abf37cf5a3a72380620e15b49917f7010 /server/sonar-web/src/main/js/apps/users
parent4726b0404d6e7b101f5d808c9ee3707ad1e03494 (diff)
downloadsonarqube-33cf03a9e6316e69e53c3960d43dde522d17583f.tar.gz
sonarqube-33cf03a9e6316e69e53c3960d43dde522d17583f.zip
SONAR-18888 UserApp and GroupApp refactorization
Diffstat (limited to 'server/sonar-web/src/main/js/apps/users')
-rw-r--r--server/sonar-web/src/main/js/apps/users/UsersApp.tsx236
-rw-r--r--server/sonar-web/src/main/js/apps/users/UsersList.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/Header-test.tsx37
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx45
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/UsersList-test.tsx66
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/Header-test.tsx.snap29
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersList-test.tsx.snap80
7 files changed, 123 insertions, 380 deletions
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 36b1daac07c..7cec61d1f3b 100644
--- a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx
+++ b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx
@@ -17,180 +17,110 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import * as React from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import { Helmet } from 'react-helmet-async';
-import { getSystemInfo } from '../../api/system';
import { getIdentityProviders, searchUsers } from '../../api/users';
-import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
-import ButtonToggle from '../../components/controls/ButtonToggle';
import ListFooter from '../../components/controls/ListFooter';
+import { ManagedFilter } from '../../components/controls/ManagedFilter';
import SearchBox from '../../components/controls/SearchBox';
import Suggestions from '../../components/embed-docs-modal/Suggestions';
-import { Location, Router, withRouter } from '../../components/hoc/withRouter';
+import { useManageProvider } from '../../components/hooks/useManageProvider';
+import DeferredSpinner from '../../components/ui/DeferredSpinner';
import { translate } from '../../helpers/l10n';
-import { IdentityProvider, Paging, SysInfoCluster } from '../../types/types';
-import { CurrentUser, User } from '../../types/users';
+import { IdentityProvider, Paging } from '../../types/types';
+import { User } from '../../types/users';
import Header from './Header';
import UsersList from './UsersList';
-import { parseQuery, Query, serializeQuery } from './utils';
-interface Props {
- currentUser: CurrentUser;
- location: Location;
- router: Router;
-}
-
-interface State {
- identityProviders: IdentityProvider[];
- manageProvider?: string;
- loading: boolean;
- paging?: Paging;
- users: User[];
-}
+export default function UsersApp() {
+ const [identityProviders, setIdentityProviders] = useState<IdentityProvider[]>([]);
-export class UsersApp extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { identityProviders: [], loading: true, users: [] };
+ const [loading, setLoading] = useState(true);
+ const [paging, setPaging] = useState<Paging>();
+ const [users, setUsers] = useState<User[]>([]);
- componentDidMount() {
- this.mounted = true;
- this.fetchIdentityProviders();
- this.fetchManageInstance();
- this.fetchUsers();
- }
+ const [search, setSearch] = useState('');
+ const [managed, setManaged] = useState<boolean | undefined>(undefined);
- componentDidUpdate(prevProps: Props) {
- if (
- prevProps.location.query.search !== this.props.location.query.search ||
- prevProps.location.query.managed !== this.props.location.query.managed
- ) {
- this.fetchUsers();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
+ const manageProvider = useManageProvider();
- finishLoading = () => {
- if (this.mounted) {
- this.setState({ loading: false });
+ const fetchUsers = useCallback(async () => {
+ setLoading(true);
+ try {
+ const { paging, users } = await searchUsers({ q: search, managed });
+ setPaging(paging);
+ setUsers(users);
+ } finally {
+ setLoading(false);
}
- };
+ }, [search, managed]);
- async fetchManageInstance() {
- const info = (await getSystemInfo()) as SysInfoCluster;
- if (this.mounted) {
- this.setState({
- manageProvider: info.System['External Users and Groups Provisioning'],
- });
+ const fetchMoreUsers = useCallback(async () => {
+ if (!paging) {
+ return;
}
- }
-
- fetchIdentityProviders = () =>
- getIdentityProviders().then(({ identityProviders }) => {
- if (this.mounted) {
- this.setState({ identityProviders });
- }
- });
-
- fetchUsers = () => {
- const { search, managed } = parseQuery(this.props.location.query);
- this.setState({ loading: true });
- searchUsers({
- q: search,
- managed,
- }).then(({ paging, users }) => {
- if (this.mounted) {
- this.setState({ loading: false, paging, users });
- }
- }, this.finishLoading);
- };
-
- fetchMoreUsers = () => {
- const { paging } = this.state;
- if (paging) {
- const { search, managed } = parseQuery(this.props.location.query);
- this.setState({ loading: true });
- searchUsers({
- p: paging.pageIndex + 1,
+ setLoading(true);
+ try {
+ const { paging: nextPage, users: nextUsers } = await searchUsers({
q: search,
managed,
- }).then(({ paging, users }) => {
- if (this.mounted) {
- this.setState((state) => ({ loading: false, users: [...state.users, ...users], paging }));
- }
- }, this.finishLoading);
+ p: paging.pageIndex + 1,
+ });
+ setPaging(nextPage);
+ setUsers([...users, ...nextUsers]);
+ } finally {
+ setLoading(false);
}
- };
-
- updateQuery = (newQuery: Partial<Query>) => {
- const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery });
- this.props.router.push({ ...this.props.location, query });
- };
-
- updateTokensCount = (login: string, tokensCount: number) => {
- this.setState((state) => ({
- users: state.users.map((user) => (user.login === login ? { ...user, tokensCount } : user)),
- }));
- };
-
- render() {
- const { search, managed } = parseQuery(this.props.location.query);
- const { loading, paging, users, manageProvider } = this.state;
-
- return (
- <main className="page page-limited" id="users-page">
- <Suggestions suggestions="users" />
- <Helmet defer={false} title={translate('users.page')} />
- <Header onUpdateUsers={this.fetchUsers} manageProvider={manageProvider} />
- <div className="display-flex-justify-start big-spacer-bottom big-spacer-top">
- {manageProvider !== undefined && (
- <div className="big-spacer-right">
- <ButtonToggle
- value={managed === undefined ? 'all' : managed}
- disabled={loading}
- options={[
- { label: translate('all'), value: 'all' },
- { label: translate('managed'), value: true },
- { label: translate('local'), value: false },
- ]}
- onCheck={(filterOption) => {
- if (filterOption === 'all') {
- this.updateQuery({ managed: undefined });
- } else {
- this.updateQuery({ managed: filterOption as boolean });
- }
- }}
- />
- </div>
- )}
- <SearchBox
- id="users-search"
- onChange={(search: string) => this.updateQuery({ search })}
- placeholder={translate('search.search_by_login_or_name')}
- value={search}
- />
- </div>
+ }, [search, managed, paging, users]);
+
+ useEffect(() => {
+ (async () => {
+ const { identityProviders } = await getIdentityProviders();
+ setIdentityProviders(identityProviders);
+ })();
+ }, []);
+
+ useEffect(() => {
+ fetchUsers();
+ }, [search, managed]);
+
+ return (
+ <main className="page page-limited" id="users-page">
+ <Suggestions suggestions="users" />
+ <Helmet defer={false} title={translate('users.page')} />
+ <Header onUpdateUsers={fetchUsers} manageProvider={manageProvider} />
+ <div className="display-flex-justify-start big-spacer-bottom big-spacer-top">
+ <ManagedFilter
+ manageProvider={manageProvider}
+ loading={loading}
+ managed={managed}
+ setManaged={setManaged}
+ />
+ <SearchBox
+ id="users-search"
+ minLength={2}
+ onChange={(search: string) => setSearch(search)}
+ placeholder={translate('search.search_by_login_or_name')}
+ value={search}
+ />
+ </div>
+ <DeferredSpinner loading={loading}>
<UsersList
- currentUser={this.props.currentUser}
- identityProviders={this.state.identityProviders}
- onUpdateUsers={this.fetchUsers}
- updateTokensCount={this.updateTokensCount}
+ identityProviders={identityProviders}
+ onUpdateUsers={fetchUsers}
+ updateTokensCount={fetchUsers}
users={users}
manageProvider={manageProvider}
/>
- {paging !== undefined && (
- <ListFooter
- count={users.length}
- loadMore={this.fetchMoreUsers}
- ready={!loading}
- total={paging.total}
- />
- )}
- </main>
- );
- }
+ </DeferredSpinner>
+ {paging !== undefined && (
+ <ListFooter
+ count={users.length}
+ loadMore={fetchMoreUsers}
+ ready={!loading}
+ total={paging.total}
+ />
+ )}
+ </main>
+ );
}
-
-export default withRouter(withCurrentUserContext(UsersApp));
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 d0c4e130803..8d20e705bbe 100644
--- a/server/sonar-web/src/main/js/apps/users/UsersList.tsx
+++ b/server/sonar-web/src/main/js/apps/users/UsersList.tsx
@@ -18,13 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { CurrentUserContext } from '../../app/components/current-user/CurrentUserContext';
import { translate } from '../../helpers/l10n';
import { IdentityProvider } from '../../types/types';
-import { User } from '../../types/users';
+import { isLoggedIn, User } from '../../types/users';
import UserListItem from './components/UserListItem';
interface Props {
- currentUser: { isLoggedIn: boolean; login?: string };
identityProviders: IdentityProvider[];
onUpdateUsers: () => void;
updateTokensCount: (login: string, tokensCount: number) => void;
@@ -33,13 +33,15 @@ interface Props {
}
export default function UsersList({
- currentUser,
identityProviders,
onUpdateUsers,
updateTokensCount,
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">
@@ -62,7 +64,7 @@ export default function UsersList({
identityProvider={identityProviders.find(
(provider) => user.externalProvider === provider.key
)}
- isCurrentUser={currentUser.isLoggedIn && currentUser.login === user.login}
+ isCurrentUser={isLoggedIn(currentUser) && currentUser.login === user.login}
key={user.login}
onUpdateUsers={onUpdateUsers}
updateTokensCount={updateTokensCount}
diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/users/__tests__/Header-test.tsx
deleted file mode 100644
index cab66f21b2d..00000000000
--- a/server/sonar-web/src/main/js/apps/users/__tests__/Header-test.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { click } from '../../../helpers/testUtils';
-import Header from '../Header';
-
-it('should render correctly', () => {
- expect(getWrapper()).toMatchSnapshot();
-});
-
-it('should open the user creation form', () => {
- const wrapper = getWrapper();
- click(wrapper.find('#users-create'));
- expect(wrapper.find('UserForm').exists()).toBe(true);
-});
-
-function getWrapper(props = {}) {
- return shallow(<Header onUpdateUsers={jest.fn()} {...props} />);
-}
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 4661ee408db..23fce172487 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
@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { act, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { byLabelText, byRole, byText } from 'testing-library-selector';
@@ -37,6 +38,7 @@ const ui = {
allFilter: byRole('button', { name: 'all' }),
managedFilter: byRole('button', { name: 'managed' }),
localFilter: byRole('button', { name: 'local' }),
+ showMore: byRole('button', { name: 'show_more' }),
aliceRow: byRole('row', { name: 'AM Alice Merveille alice.merveille never' }),
aliceRowWithLocalBadge: byRole('row', {
name: 'AM Alice Merveille alice.merveille local never',
@@ -99,22 +101,37 @@ describe('in non managed mode', () => {
renderUsersApp();
expect(await ui.aliceUpdateGroupButton.find()).toBeInTheDocument();
- expect(await ui.bobUpdateGroupButton.find()).toBeInTheDocument();
+ expect(ui.bobUpdateGroupButton.get()).toBeInTheDocument();
});
it('should be able to update / change password / deactivate a user', async () => {
renderUsersApp();
expect(await ui.aliceUpdateButton.find()).toBeInTheDocument();
- expect(await ui.bobUpdateButton.find()).toBeInTheDocument();
+ expect(ui.bobUpdateButton.get()).toBeInTheDocument();
});
it('should render all users', async () => {
renderUsersApp();
+ expect(await ui.aliceRow.find()).toBeInTheDocument();
+ expect(ui.bobRow.get()).toBeInTheDocument();
expect(ui.aliceRowWithLocalBadge.query()).not.toBeInTheDocument();
+ });
+
+ it('should be able load more users', async () => {
+ const user = userEvent.setup();
+ renderUsersApp();
+
expect(await ui.aliceRow.find()).toBeInTheDocument();
- expect(await ui.bobRow.find()).toBeInTheDocument();
+ expect(ui.bobRow.get()).toBeInTheDocument();
+ expect(screen.getAllByRole('row')).toHaveLength(4);
+
+ await act(async () => {
+ await user.click(await ui.showMore.find());
+ });
+
+ expect(screen.getAllByRole('row')).toHaveLength(6);
});
});
@@ -125,8 +142,9 @@ describe('in manage mode', () => {
it('should not be able to create a user"', async () => {
renderUsersApp();
- expect(await ui.createUserButton.get()).toBeDisabled();
+
expect(await ui.infoManageMode.find()).toBeInTheDocument();
+ expect(ui.createUserButton.get()).toBeDisabled();
});
it("should not be able to add/remove a user's group", async () => {
@@ -134,8 +152,7 @@ describe('in manage mode', () => {
expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument();
expect(ui.aliceUpdateGroupButton.query()).not.toBeInTheDocument();
-
- expect(await ui.bobRow.find()).toBeInTheDocument();
+ expect(ui.bobRow.get()).toBeInTheDocument();
expect(ui.bobUpdateGroupButton.query()).not.toBeInTheDocument();
});
@@ -152,7 +169,7 @@ describe('in manage mode', () => {
expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument();
await user.click(ui.aliceUpdateButton.get());
- expect(await ui.alicedDeactivateButton.get()).toBeInTheDocument();
+ expect(await ui.alicedDeactivateButton.find()).toBeInTheDocument();
});
it('should render list of all users', async () => {
@@ -168,19 +185,25 @@ describe('in manage mode', () => {
const user = userEvent.setup();
renderUsersApp();
- await user.click(await ui.managedFilter.find());
+ expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument();
+
+ await act(async () => {
+ await user.click(await ui.managedFilter.find());
+ });
+ expect(await ui.bobRow.find()).toBeInTheDocument();
expect(ui.aliceRowWithLocalBadge.query()).not.toBeInTheDocument();
- expect(ui.bobRow.get()).toBeInTheDocument();
});
it('should render list of local users', async () => {
const user = userEvent.setup();
renderUsersApp();
- await user.click(await ui.localFilter.find());
+ await act(async () => {
+ await user.click(await ui.localFilter.find());
+ });
- expect(ui.aliceRowWithLocalBadge.get()).toBeInTheDocument();
+ expect(await ui.aliceRowWithLocalBadge.find()).toBeInTheDocument();
expect(ui.bobRow.query()).not.toBeInTheDocument();
});
});
diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/UsersList-test.tsx b/server/sonar-web/src/main/js/apps/users/__tests__/UsersList-test.tsx
deleted file mode 100644
index 51b3d06e945..00000000000
--- a/server/sonar-web/src/main/js/apps/users/__tests__/UsersList-test.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import UsersList from '../UsersList';
-
-const users = [
- {
- login: 'luke',
- name: 'Luke',
- active: true,
- scmAccounts: [],
- local: false,
- managed: false,
- },
- {
- login: 'obi',
- name: 'One',
- active: true,
- scmAccounts: [],
- local: false,
- managed: false,
- },
-];
-
-it('should render correctly', () => {
- expect(getWrapper()).toMatchSnapshot();
-});
-
-function getWrapper(props = {}) {
- return shallow(
- <UsersList
- currentUser={{ isLoggedIn: true, login: 'luke' }}
- identityProviders={[
- {
- backgroundColor: 'blue',
- iconPath: 'icon/path',
- key: 'foo',
- name: 'Foo Provider',
- },
- ]}
- onUpdateUsers={jest.fn()}
- updateTokensCount={jest.fn()}
- users={users}
- manageProvider={undefined}
- {...props}
- />
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/Header-test.tsx.snap
deleted file mode 100644
index 88dc40cb905..00000000000
--- a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/Header-test.tsx.snap
+++ /dev/null
@@ -1,29 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<div
- className="page-header null-spacer-bottom"
->
- <h2
- className="page-title"
- >
- users.page
- </h2>
- <div
- className="page-actions"
- >
- <Button
- disabled={false}
- id="users-create"
- onClick={[Function]}
- >
- users.create_user
- </Button>
- </div>
- <p
- className="page-description"
- >
- users.page.description
- </p>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersList-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersList-test.tsx.snap
deleted file mode 100644
index 64566ee533b..00000000000
--- a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersList-test.tsx.snap
+++ /dev/null
@@ -1,80 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<div
- className="boxed-group boxed-group-inner"
->
- <table
- className="data zebra"
- id="users-list"
- >
- <thead>
- <tr>
- <th />
- <th
- className="nowrap"
- />
- <th
- className="nowrap"
- >
- my_profile.scm_accounts
- </th>
- <th
- className="nowrap"
- >
- users.last_connection
- </th>
- <th
- className="nowrap"
- >
- my_profile.groups
- </th>
- <th
- className="nowrap"
- >
- users.tokens
- </th>
- <th
- className="nowrap"
- >
-  
- </th>
- </tr>
- </thead>
- <tbody>
- <UserListItem
- isCurrentUser={true}
- key="luke"
- onUpdateUsers={[MockFunction]}
- updateTokensCount={[MockFunction]}
- user={
- {
- "active": true,
- "local": false,
- "login": "luke",
- "managed": false,
- "name": "Luke",
- "scmAccounts": [],
- }
- }
- />
- <UserListItem
- isCurrentUser={false}
- key="obi"
- onUpdateUsers={[MockFunction]}
- updateTokensCount={[MockFunction]}
- user={
- {
- "active": true,
- "local": false,
- "login": "obi",
- "managed": false,
- "name": "One",
- "scmAccounts": [],
- }
- }
- />
- </tbody>
- </table>
-</div>
-`;