aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
diff options
context:
space:
mode:
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>2023-03-09 10:10:18 +0100
committersonartech <sonartech@sonarsource.com>2023-03-22 20:04:07 +0000
commitc96fc17ae41f274d56cd329af1373a94352d74bf (patch)
tree709b273f35559b424a5d18230d9725e638df8dc8 /server/sonar-web/src/main/js/apps
parent2879fc6789ada0ebfe449f47572a89ba7a83e9ed (diff)
downloadsonarqube-c96fc17ae41f274d56cd329af1373a94352d74bf.tar.gz
sonarqube-c96fc17ae41f274d56cd329af1373a94352d74bf.zip
SONAR-18689 Add Filters for all, local and managed users on user list
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
-rw-r--r--server/sonar-web/src/main/js/apps/users/Search.tsx49
-rw-r--r--server/sonar-web/src/main/js/apps/users/UsersApp.tsx51
-rw-r--r--server/sonar-web/src/main/js/apps/users/UsersList.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx81
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-test.tsx84
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/UsersList-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersApp-test.tsx.snap110
-rw-r--r--server/sonar-web/src/main/js/apps/users/__tests__/__snapshots__/UsersList-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx107
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/__tests__/UserActions-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/__tests__/UserGroups-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItem-test.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/__tests__/UserListItemIdentity-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItem-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserListItemIdentity-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/apps/users/utils.ts3
17 files changed, 200 insertions, 319 deletions
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<Query>) => void;
-}
-
-export default class Search extends React.PureComponent<Props> {
- handleSearch = (search: string) => {
- this.props.updateQuery({ search });
- };
-
- render() {
- const { query } = this.props;
-
- return (
- <div className="panel panel-vertical bordered-bottom spacer-bottom" id="users-search">
- <SearchBox
- minLength={2}
- onChange={this.handleSearch}
- placeholder={translate('search.search_by_login_or_name')}
- value={query.search}
- />
- </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 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<Props, State> {
}
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<Props, State> {
});
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<Props, State> {
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<Props, State> {
};
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 (
<main className="page page-limited" id="users-page">
<Suggestions suggestions="users" />
<Helmet defer={false} title={translate('users.page')} />
<Header loading={loading} onUpdateUsers={this.fetchUsers} manageProvider={manageProvider} />
- <Search query={query} updateQuery={this.updateQuery} />
+ <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}
+ options={[
+ { label: translate('all'), value: 'all' },
+ { label: translate('users.managed'), value: true },
+ { label: translate('users.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>
<UsersList
currentUser={this.props.currentUser}
identityProviders={this.state.identityProviders}
onUpdateUsers={this.fetchUsers}
updateTokensCount={this.updateTokensCount}
users={users}
+ manageProvider={manageProvider}
/>
{paging !== undefined && (
<ListFooter
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 affc9381c9b..7dbb05134aa 100644
--- a/server/sonar-web/src/main/js/apps/users/UsersList.tsx
+++ b/server/sonar-web/src/main/js/apps/users/UsersList.tsx
@@ -29,6 +29,7 @@ interface Props {
onUpdateUsers: () => 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 (
<div className="boxed-group boxed-group-inner">
@@ -63,6 +65,7 @@ export default function UsersList({
onUpdateUsers={onUpdateUsers}
updateTokensCount={updateTokensCount}
user={user}
+ manageProvider={manageProvider}
/>
))}
</tbody>
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', <UsersApp />);
+ return renderApp('admin/users', <UsersApp />);
}
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<any>;
-const searchUsers = require('../../../api/users').searchUsers as jest.Mock<any>;
-
-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<UsersApp['props']> = {}) {
- return shallow(
- <UsersApp currentUser={currentUser} location={location} router={mockRouter()} {...props} />
- );
-}
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`] = `
-<main
- className="page page-limited"
- id="users-page"
->
- <Suggestions
- suggestions="users"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="users.page"
- />
- <Header
- loading={true}
- onUpdateUsers={[Function]}
- />
- <Search
- query={
- {
- "search": "",
- }
- }
- updateQuery={[Function]}
- />
- <UsersList
- currentUser={
- {
- "dismissedNotices": {},
- "isLoggedIn": true,
- "login": "luke",
- }
- }
- identityProviders={[]}
- onUpdateUsers={[Function]}
- updateTokensCount={[Function]}
- users={[]}
- />
-</main>
-`;
-
-exports[`should render correctly 2`] = `
-<main
- className="page page-limited"
- id="users-page"
->
- <Suggestions
- suggestions="users"
- />
- <Helmet
- defer={false}
- encodeSpecialCharacters={true}
- prioritizeSeoTags={false}
- title="users.page"
- />
- <Header
- loading={false}
- onUpdateUsers={[Function]}
- />
- <Search
- query={
- {
- "search": "",
- }
- }
- updateQuery={[Function]}
- />
- <UsersList
- currentUser={
- {
- "dismissedNotices": {},
- "isLoggedIn": true,
- "login": "luke",
- }
- }
- identityProviders={
- [
- {
- "backgroundColor": "blue",
- "iconPath": "icon/path",
- "key": "foo",
- "name": "Foo Provider",
- },
- ]
- }
- onUpdateUsers={[Function]}
- updateTokensCount={[Function]}
- users={
- [
- {
- "active": true,
- "local": false,
- "login": "luke",
- "name": "Luke",
- "scmAccounts": [],
- },
- ]
- }
- />
- <ListFooter
- count={1}
- loadMore={[Function]}
- ready={true}
- total={2}
- />
-</main>
-`;
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<Props, State> {
- 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 (
- <tr>
- <td className="thin nowrap text-middle">
- <Avatar hash={user.avatar} name={user.name} size={36} />
- </td>
- <UserListItemIdentity identityProvider={identityProvider} user={user} />
- <td className="thin nowrap text-middle">
- <UserScmAccounts scmAccounts={user.scmAccounts || []} />
- </td>
- <td className="thin nowrap text-middle">
- <DateFromNow date={user.lastConnectionDate} hourPrecision={true} />
- </td>
- <td className="thin nowrap text-middle">
- <UserGroups groups={user.groups || []} onUpdateUsers={onUpdateUsers} user={user} />
- </td>
- <td className="thin nowrap text-middle">
- {user.tokensCount}
- <ButtonIcon
- className="js-user-tokens spacer-left button-small"
- onClick={this.handleOpenTokensForm}
- tooltip={translate('users.update_tokens')}
- >
- <BulletListIcon />
- </ButtonIcon>
- </td>
- <td className="thin nowrap text-right text-middle">
- <UserActions
- isCurrentUser={this.props.isCurrentUser}
- onUpdateUsers={onUpdateUsers}
- user={user}
- />
- </td>
- {this.state.openTokenForm && (
- <TokensFormModal
- onClose={this.handleCloseTokensForm}
- updateTokensCount={this.props.updateTokensCount}
- user={user}
- />
- )}
- </tr>
- );
- }
+ return (
+ <tr>
+ <td className="thin nowrap text-middle">
+ <Avatar hash={user.avatar} name={user.name} size={36} />
+ </td>
+ <UserListItemIdentity
+ identityProvider={identityProvider}
+ user={user}
+ manageProvider={manageProvider}
+ />
+ <td className="thin nowrap text-middle">
+ <UserScmAccounts scmAccounts={user.scmAccounts || []} />
+ </td>
+ <td className="thin nowrap text-middle">
+ <DateFromNow date={user.lastConnectionDate} hourPrecision={true} />
+ </td>
+ <td className="thin nowrap text-middle">
+ <UserGroups groups={user.groups || []} onUpdateUsers={onUpdateUsers} user={user} />
+ </td>
+ <td className="thin nowrap text-middle">
+ {user.tokensCount}
+ <ButtonIcon
+ className="js-user-tokens spacer-left button-small"
+ onClick={() => setOpenTokenForm(true)}
+ tooltip={translate('users.update_tokens')}
+ >
+ <BulletListIcon />
+ </ButtonIcon>
+ </td>
+ <td className="thin nowrap text-right text-middle">
+ <UserActions isCurrentUser={isCurrentUser} onUpdateUsers={onUpdateUsers} user={user} />
+ </td>
+ {openTokenForm && (
+ <TokensFormModal
+ onClose={() => setOpenTokenForm(false)}
+ updateTokensCount={updateTokensCount}
+ user={user}
+ />
+ )}
+ </tr>
+ );
}
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 (
<td className="text-middle">
<div>
@@ -41,11 +43,14 @@ export default function UserListItemIdentity({ identityProvider, user }: Props)
{!user.local && user.externalProvider !== 'sonarqube' && (
<ExternalProvider identityProvider={identityProvider} user={user} />
)}
+ {user.managed === false && manageProvider !== undefined && (
+ <span className="badge">{translate('users.local')}</span>
+ )}
</td>
);
}
-export function ExternalProvider({ identityProvider, user }: Props) {
+export function ExternalProvider({ identityProvider, user }: Omit<Props, 'manageProvider'>) {
if (!identityProvider) {
return (
<div className="js-user-identity-provider little-spacer-top">
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<UserListItem['props']> = {}) {
+function shallowRender(props: Partial<UserListItemProps> = {}) {
return shallow(
<UserListItem
isCurrentUser={false}
onUpdateUsers={jest.fn()}
updateTokensCount={jest.fn()}
user={user}
+ manageProvider={undefined}
{...props}
/>
);
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,
})
);