From: guillaume-peoch-sonarsource Date: Thu, 9 Mar 2023 09:10:18 +0000 (+0100) Subject: SONAR-18689 Add Filters for all, local and managed users on user list X-Git-Tag: 10.0.0.68432~93 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=c96fc17ae41f274d56cd329af1373a94352d74bf;p=sonarqube.git SONAR-18689 Add Filters for all, local and managed users on user list --- diff --git a/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts index f575559c69a..0c8c719732c 100644 --- a/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts @@ -19,7 +19,7 @@ */ import { cloneDeep } from 'lodash'; -import { mockClusterSysInfo, mockIdentityProvider } from '../../helpers/testMocks'; +import { mockClusterSysInfo, mockIdentityProvider, mockUser } from '../../helpers/testMocks'; import { IdentityProvider, Paging, SysInfoCluster } from '../../types/types'; import { User } from '../../types/users'; import { getSystemInfo } from '../system'; @@ -27,26 +27,44 @@ import { getIdentityProviders, searchUsers } from '../users'; export default class UsersServiceMock { isManaged = true; + users = [ + mockUser({ + managed: true, + login: 'bob.marley', + name: 'Bob Marley', + }), + mockUser({ + managed: false, + login: 'alice.merveille', + name: 'Alice Merveille', + }), + ]; constructor() { jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo); jest.mocked(getIdentityProviders).mockImplementation(this.handleGetIdentityProviders); - jest.mocked(searchUsers).mockImplementation(this.handleSearchUsers); + jest.mocked(searchUsers).mockImplementation((p) => this.handleSearchUsers(p)); } setIsManaged(managed: boolean) { this.isManaged = managed; } - handleSearchUsers = (): Promise<{ paging: Paging; users: User[] }> => { - return this.reply({ - paging: { - pageIndex: 1, - pageSize: 100, - total: 0, - }, - users: [], - }); + handleSearchUsers = (data: any): Promise<{ paging: Paging; users: User[] }> => { + const paging = { + pageIndex: 1, + pageSize: 100, + total: 0, + }; + + if (this.isManaged) { + if (data.managed === undefined) { + return this.reply({ paging, users: this.users }); + } + const users = this.users.filter((user) => user.managed === data.managed); + return this.reply({ paging, users }); + } + return this.reply({ paging, users: this.users }); }; handleGetIdentityProviders = (): Promise<{ identityProviders: IdentityProvider[] }> => { diff --git a/server/sonar-web/src/main/js/api/users.ts b/server/sonar-web/src/main/js/api/users.ts index ca921bd026c..d3fbb2f7e8b 100644 --- a/server/sonar-web/src/main/js/api/users.ts +++ b/server/sonar-web/src/main/js/api/users.ts @@ -71,6 +71,7 @@ export function searchUsers(data: { p?: number; ps?: number; q?: string; + managed?: boolean; }): Promise<{ paging: Paging; users: User[] }> { data.q = data.q || undefined; return getJSON('/api/users/search', data).catch(throwGlobalError); diff --git a/server/sonar-web/src/main/js/apps/users/Search.tsx b/server/sonar-web/src/main/js/apps/users/Search.tsx deleted file mode 100644 index f7f0b5e9a69..00000000000 --- a/server/sonar-web/src/main/js/apps/users/Search.tsx +++ /dev/null @@ -1,49 +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 * as React from 'react'; -import SearchBox from '../../components/controls/SearchBox'; -import { translate } from '../../helpers/l10n'; -import { Query } from './utils'; - -interface Props { - query: Query; - updateQuery: (newQuery: Partial) => void; -} - -export default class Search extends React.PureComponent { - handleSearch = (search: string) => { - this.props.updateQuery({ search }); - }; - - render() { - const { query } = this.props; - - return ( - - ); - } -} 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 768b2c8f7e6..3eed4f2f5e4 100644 --- a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx +++ b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx @@ -22,14 +22,15 @@ 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 SearchBox from '../../components/controls/SearchBox'; import Suggestions from '../../components/embed-docs-modal/Suggestions'; import { Location, Router, withRouter } from '../../components/hoc/withRouter'; import { translate } from '../../helpers/l10n'; import { IdentityProvider, Paging, SysInfoCluster } from '../../types/types'; import { CurrentUser, User } from '../../types/users'; import Header from './Header'; -import Search from './Search'; import UsersList from './UsersList'; import { parseQuery, Query, serializeQuery } from './utils'; @@ -59,7 +60,10 @@ export class UsersApp extends React.PureComponent { } componentDidUpdate(prevProps: Props) { - if (prevProps.location.query.search !== this.props.location.query.search) { + if ( + prevProps.location.query.search !== this.props.location.query.search || + prevProps.location.query.managed !== this.props.location.query.managed + ) { this.fetchUsers(); } } @@ -91,9 +95,12 @@ export class UsersApp extends React.PureComponent { }); fetchUsers = () => { - const { location } = this.props; + const { search, managed } = parseQuery(this.props.location.query); this.setState({ loading: true }); - searchUsers({ q: parseQuery(location.query).search }).then(({ paging, users }) => { + searchUsers({ + q: search, + managed, + }).then(({ paging, users }) => { if (this.mounted) { this.setState({ loading: false, paging, users }); } @@ -103,10 +110,12 @@ export class UsersApp extends React.PureComponent { fetchMoreUsers = () => { const { paging } = this.state; if (paging) { + const { search, managed } = parseQuery(this.props.location.query); this.setState({ loading: true }); searchUsers({ p: paging.pageIndex + 1, - q: parseQuery(this.props.location.query).search, + q: search, + managed, }).then(({ paging, users }) => { if (this.mounted) { this.setState((state) => ({ loading: false, users: [...state.users, ...users], paging })); @@ -127,20 +136,48 @@ export class UsersApp extends React.PureComponent { }; render() { - const query = parseQuery(this.props.location.query); + const { search, managed } = parseQuery(this.props.location.query); const { loading, paging, users, manageProvider } = this.state; + // What if we have ONLY managed users? Should we not display the filter toggle? return (
- +
+ {manageProvider !== undefined && ( +
+ { + if (filterOption === 'all') { + this.updateQuery({ managed: undefined }); + } else { + this.updateQuery({ managed: filterOption as boolean }); + } + }} + /> +
+ )} + this.updateQuery({ search })} + placeholder={translate('search.search_by_login_or_name')} + value={search} + /> +
{paging !== undefined && ( void; updateTokensCount: (login: string, tokensCount: number) => void; users: User[]; + manageProvider: string | undefined; } export default function UsersList({ @@ -37,6 +38,7 @@ export default function UsersList({ onUpdateUsers, updateTokensCount, users, + manageProvider, }: Props) { return (
@@ -63,6 +65,7 @@ export default function UsersList({ onUpdateUsers={onUpdateUsers} updateTokensCount={updateTokensCount} user={user} + manageProvider={manageProvider} /> ))} diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx index 3917a11296e..1532ad653c0 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 userEvent from '@testing-library/user-event'; import * as React from 'react'; import { byRole, byText } from 'testing-library-selector'; import UsersServiceMock from '../../../api/mocks/UsersServiceMock'; @@ -33,24 +34,82 @@ const ui = { createUserButton: byRole('button', { name: 'users.create_user' }), infoManageMode: byText(/users\.page\.managed_description/), description: byText('users.page.description'), + allFilter: byRole('button', { name: 'all' }), + managedFilter: byRole('button', { name: 'users.managed' }), + localFilter: byRole('button', { name: 'users.local' }), + aliceRow: byRole('row', { name: 'AM Alice Merveille alice.merveille never' }), + aliceRowWithLocalBadge: byRole('row', { + name: 'AM Alice Merveille alice.merveille users.local never', + }), + bobRow: byRole('row', { name: 'BM Bob Marley bob.marley never' }), }; -it('should render list of user in non manage mode', async () => { - handler.setIsManaged(false); - renderUsersApp(); +describe('in non managed mode', () => { + beforeEach(() => { + handler.setIsManaged(false); + }); - expect(await ui.description.find()).toBeInTheDocument(); - expect(ui.createUserButton.get()).toBeEnabled(); + it('should allow the creation of user', async () => { + renderUsersApp(); + + expect(await ui.description.find()).toBeInTheDocument(); + expect(ui.createUserButton.get()).toBeEnabled(); + }); + + it('should render all users', async () => { + renderUsersApp(); + + expect(ui.aliceRowWithLocalBadge.query()).not.toBeInTheDocument(); + expect(await ui.aliceRow.find()).toBeInTheDocument(); + expect(await ui.bobRow.find()).toBeInTheDocument(); + }); }); -it('should render list of user in manage mode', async () => { - handler.setIsManaged(true); - renderUsersApp(); +describe('in manage mode', () => { + beforeEach(() => { + handler.setIsManaged(true); + }); + + it('should not be able to create a user"', async () => { + renderUsersApp(); + expect(await ui.createUserButton.get()).toBeDisabled(); + expect(await ui.infoManageMode.find()).toBeInTheDocument(); + }); + + it('should render list of all users', async () => { + renderUsersApp(); + + expect(await ui.allFilter.find()).toBeInTheDocument(); + + expect(ui.aliceRowWithLocalBadge.get()).toBeInTheDocument(); + expect(ui.bobRow.get()).toBeInTheDocument(); + }); + + it('should render list of managed users', async () => { + const user = userEvent.setup(); + renderUsersApp(); + + // The click downs't work without this line + expect(await ui.managedFilter.find()).toBeInTheDocument(); + await user.click(await ui.managedFilter.get()); + + expect(ui.aliceRowWithLocalBadge.query()).not.toBeInTheDocument(); + expect(ui.bobRow.get()).toBeInTheDocument(); + }); + + it('should render list of local users', async () => { + const user = userEvent.setup(); + renderUsersApp(); + + // The click downs't work without this line + expect(await ui.localFilter.find()).toBeInTheDocument(); + await user.click(await ui.localFilter.get()); - expect(await ui.infoManageMode.find()).toBeInTheDocument(); - expect(ui.createUserButton.get()).toBeDisabled(); + expect(ui.aliceRowWithLocalBadge.get()).toBeInTheDocument(); + expect(ui.bobRow.query()).not.toBeInTheDocument(); + }); }); function renderUsersApp() { - renderApp('admin/users', ); + return renderApp('admin/users', ); } diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx deleted file mode 100644 index e9cab0de5d9..00000000000 --- a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx +++ /dev/null @@ -1,84 +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 { Location } from '../../../components/hoc/withRouter'; -import { mockRouter } from '../../../helpers/testMocks'; -import { waitAndUpdate } from '../../../helpers/testUtils'; -import { UsersApp } from '../UsersApp'; - -jest.mock('../../../api/users', () => ({ - getIdentityProviders: jest.fn(() => - Promise.resolve({ - identityProviders: [ - { - backgroundColor: 'blue', - iconPath: 'icon/path', - key: 'foo', - name: 'Foo Provider', - }, - ], - }) - ), - searchUsers: jest.fn(() => - Promise.resolve({ - paging: { - pageIndex: 1, - pageSize: 1, - total: 2, - }, - users: [ - { - login: 'luke', - name: 'Luke', - active: true, - scmAccounts: [], - local: false, - }, - ], - }) - ), -})); - -const getIdentityProviders = require('../../../api/users').getIdentityProviders as jest.Mock; -const searchUsers = require('../../../api/users').searchUsers as jest.Mock; - -const currentUser = { isLoggedIn: true, login: 'luke', dismissedNotices: {} }; -const location = { pathname: '', query: {} } as Location; - -beforeEach(() => { - getIdentityProviders.mockClear(); - searchUsers.mockClear(); -}); - -it('should render correctly', async () => { - const wrapper = getWrapper(); - expect(wrapper).toMatchSnapshot(); - expect(getIdentityProviders).toHaveBeenCalled(); - expect(searchUsers).toHaveBeenCalled(); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); -}); - -function getWrapper(props: Partial = {}) { - return shallow( - - ); -} 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 index 8d67e0b7894..51b3d06e945 100644 --- 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 @@ -28,6 +28,7 @@ const users = [ active: true, scmAccounts: [], local: false, + managed: false, }, { login: 'obi', @@ -35,6 +36,7 @@ const users = [ active: true, scmAccounts: [], local: false, + managed: false, }, ]; @@ -57,6 +59,7 @@ function getWrapper(props = {}) { onUpdateUsers={jest.fn()} updateTokensCount={jest.fn()} users={users} + manageProvider={undefined} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap deleted file mode 100644 index 41245b66512..00000000000 --- a/server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap +++ /dev/null @@ -1,110 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -
- - -
- - -
-`; - -exports[`should render correctly 2`] = ` -
- - -
- - - -
-`; 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 index 175936df424..64566ee533b 100644 --- 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 @@ -52,6 +52,7 @@ exports[`should render correctly 1`] = ` "active": true, "local": false, "login": "luke", + "managed": false, "name": "Luke", "scmAccounts": [], } @@ -67,6 +68,7 @@ exports[`should render correctly 1`] = ` "active": true, "local": false, "login": "obi", + "managed": false, "name": "One", "scmAccounts": [], } 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 04a03dee287..4ec202012c5 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 @@ -31,67 +31,66 @@ import UserGroups from './UserGroups'; import UserListItemIdentity from './UserListItemIdentity'; import UserScmAccounts from './UserScmAccounts'; -interface Props { +export interface UserListItemProps { identityProvider?: IdentityProvider; isCurrentUser: boolean; onUpdateUsers: () => void; updateTokensCount: (login: string, tokensCount: number) => void; user: User; + manageProvider: string | undefined; } -interface State { - openTokenForm: boolean; -} - -export default class UserListItem extends React.PureComponent { - state: State = { openTokenForm: false }; - - handleOpenTokensForm = () => this.setState({ openTokenForm: true }); - handleCloseTokensForm = () => this.setState({ openTokenForm: false }); +export default function UserListItem(props: UserListItemProps) { + const [openTokenForm, setOpenTokenForm] = React.useState(false); - render() { - const { identityProvider, onUpdateUsers, user } = this.props; + const { + identityProvider, + onUpdateUsers, + user, + manageProvider, + isCurrentUser, + updateTokensCount, + } = props; - return ( - - - - - - - - - - - - - - - - {user.tokensCount} - - - - - - - - {this.state.openTokenForm && ( - - )} - - ); - } + return ( + + + + + + + + + + + + + + + + {user.tokensCount} + setOpenTokenForm(true)} + tooltip={translate('users.update_tokens')} + > + + + + + + + {openTokenForm && ( + setOpenTokenForm(false)} + updateTokensCount={updateTokensCount} + user={user} + /> + )} + + ); } diff --git a/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx b/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx index 6d4d6872ce8..f73eb688689 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx @@ -21,6 +21,7 @@ import { getTextColor } from 'design-system'; import * as React from 'react'; import { colors } from '../../../app/theme'; +import { translate } from '../../../helpers/l10n'; import { getBaseUrl } from '../../../helpers/system'; import { IdentityProvider } from '../../../types/types'; import { User } from '../../../types/users'; @@ -28,9 +29,10 @@ import { User } from '../../../types/users'; export interface Props { identityProvider?: IdentityProvider; user: User; + manageProvider?: string; } -export default function UserListItemIdentity({ identityProvider, user }: Props) { +export default function UserListItemIdentity({ identityProvider, user, manageProvider }: Props) { return (
@@ -41,11 +43,14 @@ export default function UserListItemIdentity({ identityProvider, user }: Props) {!user.local && user.externalProvider !== 'sonarqube' && ( )} + {user.managed === false && manageProvider !== undefined && ( + {translate('users.local')} + )} ); } -export function ExternalProvider({ identityProvider, user }: Props) { +export function ExternalProvider({ identityProvider, user }: Omit) { if (!identityProvider) { return (
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx index ab9a64daa91..e4925c08c9a 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx @@ -28,6 +28,7 @@ const user = { active: true, scmAccounts: [], local: false, + managed: false, }; it('should render correctly', () => { diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserGroups-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserGroups-test.tsx index 5683e8ef5c2..47ea651dbe4 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserGroups-test.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserGroups-test.tsx @@ -28,6 +28,7 @@ const user = { active: true, scmAccounts: [], local: false, + managed: false, }; const groups = ['foo', 'bar', 'baz', 'plop']; diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx index a3c0c1eb34d..cfd69e9e579 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx @@ -21,7 +21,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { click } from '../../../../helpers/testUtils'; import { User } from '../../../../types/users'; -import UserListItem from '../UserListItem'; +import UserListItem, { UserListItemProps } from '../UserListItem'; jest.mock('../../../../components/intl/DateFromNow'); jest.mock('../../../../components/intl/DateTimeFormatter'); @@ -33,6 +33,7 @@ const user: User = { login: 'obi', name: 'One', scmAccounts: [], + managed: false, }; it('should render correctly', () => { @@ -49,13 +50,14 @@ it('should open the correct forms', () => { expect(wrapper.find('TokensFormModal').exists()).toBe(true); }); -function shallowRender(props: Partial = {}) { +function shallowRender(props: Partial = {}) { return shallow( ); diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItemIdentity-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItemIdentity-test.tsx index 661feb1fc1e..a362fd780d4 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItemIdentity-test.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItemIdentity-test.tsx @@ -44,6 +44,7 @@ describe('#UserListItemIdentity', () => { login: 'obi', name: 'One', scmAccounts: [], + managed: false, }} {...props} /> @@ -78,6 +79,7 @@ describe('#ExternalProvider', () => { login: 'obi', name: 'One', scmAccounts: [], + managed: false, }} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItem-test.tsx.snap index a4e8b65bfc3..5970d8c3871 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItem-test.tsx.snap @@ -17,6 +17,7 @@ exports[`should render correctly 1`] = ` "lastConnectionDate": "2019-01-18T15:06:33+0100", "local": false, "login": "obi", + "managed": false, "name": "One", "scmAccounts": [], } @@ -49,6 +50,7 @@ exports[`should render correctly 1`] = ` "lastConnectionDate": "2019-01-18T15:06:33+0100", "local": false, "login": "obi", + "managed": false, "name": "One", "scmAccounts": [], } @@ -78,6 +80,7 @@ exports[`should render correctly 1`] = ` "lastConnectionDate": "2019-01-18T15:06:33+0100", "local": false, "login": "obi", + "managed": false, "name": "One", "scmAccounts": [], } @@ -104,6 +107,7 @@ exports[`should render correctly without last connection date 1`] = ` "lastConnectionDate": "2019-01-18T15:06:33+0100", "local": false, "login": "obi", + "managed": false, "name": "One", "scmAccounts": [], } @@ -136,6 +140,7 @@ exports[`should render correctly without last connection date 1`] = ` "lastConnectionDate": "2019-01-18T15:06:33+0100", "local": false, "login": "obi", + "managed": false, "name": "One", "scmAccounts": [], } @@ -165,6 +170,7 @@ exports[`should render correctly without last connection date 1`] = ` "lastConnectionDate": "2019-01-18T15:06:33+0100", "local": false, "login": "obi", + "managed": false, "name": "One", "scmAccounts": [], } diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItemIdentity-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItemIdentity-test.tsx.snap index eccdb8b8147..9fd139b23b9 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItemIdentity-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItemIdentity-test.tsx.snap @@ -73,6 +73,7 @@ exports[`#UserListItemIdentity should render correctly 1`] = ` "lastConnectionDate": "2019-01-18T15:06:33+0100", "local": false, "login": "obi", + "managed": false, "name": "One", "scmAccounts": [], } diff --git a/server/sonar-web/src/main/js/apps/users/utils.ts b/server/sonar-web/src/main/js/apps/users/utils.ts index 9af11a617b8..c3ceb0cc5b4 100644 --- a/server/sonar-web/src/main/js/apps/users/utils.ts +++ b/server/sonar-web/src/main/js/apps/users/utils.ts @@ -23,11 +23,13 @@ import { RawQuery } from '../../types/types'; export interface Query { search: string; + managed?: boolean; } export const parseQuery = memoize( (urlQuery: RawQuery): Query => ({ search: parseAsString(urlQuery['search']), + managed: urlQuery['managed'] !== undefined ? urlQuery['managed'] === 'true' : undefined, }) ); @@ -35,5 +37,6 @@ export const serializeQuery = memoize( (query: Query): RawQuery => cleanQuery({ search: query.search ? serializeString(query.search) : undefined, + managed: query.managed, }) ); diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index 9b2b7c65332..3cc53cbf2e6 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -651,6 +651,7 @@ export function mockUser(overrides: Partial = {}): User { local: true, login: 'john.doe', name: 'John Doe', + managed: false, ...overrides, }; } diff --git a/server/sonar-web/src/main/js/types/users.ts b/server/sonar-web/src/main/js/types/users.ts index eb94140e2b4..b5a5f55ba4c 100644 --- a/server/sonar-web/src/main/js/types/users.ts +++ b/server/sonar-web/src/main/js/types/users.ts @@ -77,6 +77,7 @@ export interface User extends UserBase { groups?: string[]; lastConnectionDate?: string; local: boolean; + managed: boolean; scmAccounts?: string[]; tokensCount?: number; } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 5ea15bc32c3..43a618575db 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -4384,6 +4384,8 @@ users.change_admin_password.form.confirm=Confirm password for user 'admin' users.change_admin_password.form.cannot_use_default_password=You must choose a password that is different from the default password. users.change_admin_password.form.success=The admin user's password was successfully changed. users.change_admin_password.form.continue_to_app=Continue to SonarQube +users.local=Local +users.managed=Managed #------------------------------------------------------------------------------ #