diff options
author | guillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com> | 2023-03-27 10:06:34 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-03-30 20:03:07 +0000 |
commit | 33cf03a9e6316e69e53c3960d43dde522d17583f (patch) | |
tree | 39639d5abf37cf5a3a72380620e15b49917f7010 /server/sonar-web/src/main/js/apps/users | |
parent | 4726b0404d6e7b101f5d808c9ee3707ad1e03494 (diff) | |
download | sonarqube-33cf03a9e6316e69e53c3960d43dde522d17583f.tar.gz sonarqube-33cf03a9e6316e69e53c3960d43dde522d17583f.zip |
SONAR-18888 UserApp and GroupApp refactorization
Diffstat (limited to 'server/sonar-web/src/main/js/apps/users')
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> -`; |