From: Ambroise C Date: Tue, 11 Apr 2023 06:56:38 +0000 (+0200) Subject: SONAR-18964 Add User Activity filter on users list X-Git-Tag: 10.1.0.73491~431 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=b0fa5854cdf8d593a307d08891c454b8014e3041;p=sonarqube.git SONAR-18964 Add User Activity filter on users 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 efa961414bc..a46e8bb94bf 100644 --- a/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { isAfter, isBefore } from 'date-fns'; import { cloneDeep } from 'lodash'; import { mockClusterSysInfo, mockIdentityProvider, mockUser } from '../../helpers/testMocks'; import { IdentityProvider, Paging, SysInfoCluster } from '../../types/types'; @@ -30,11 +31,40 @@ const DEFAULT_USERS = [ managed: true, login: 'bob.marley', name: 'Bob Marley', + lastConnectionDate: '2023-06-27T17:08:59+0200', + sonarLintLastConnectionDate: '2023-06-27T17:08:59+0200', }), mockUser({ managed: false, login: 'alice.merveille', name: 'Alice Merveille', + lastConnectionDate: '2023-06-27T17:08:59+0200', + sonarLintLastConnectionDate: '2023-05-27T17:08:59+0200', + }), + mockUser({ + managed: false, + login: 'charlie.cox', + name: 'Charlie Cox', + lastConnectionDate: '2023-06-25T17:08:59+0200', + sonarLintLastConnectionDate: '2023-06-20T12:10:59+0200', + }), + mockUser({ + managed: true, + login: 'denis.villeneuve', + name: 'Denis Villeneuve', + lastConnectionDate: '2023-06-20T15:08:59+0200', + sonarLintLastConnectionDate: '2023-05-25T10:08:59+0200', + }), + mockUser({ + managed: true, + login: 'eva.green', + name: 'Eva Green', + lastConnectionDate: '2023-05-27T17:08:59+0200', + }), + mockUser({ + managed: false, + login: 'franck.grillo', + name: 'Franck Grillo', }), ]; @@ -52,31 +82,100 @@ export default class UsersServiceMock { this.isManaged = managed; } + getFilteredUsers = (filterParams: { + managed: boolean; + q: string; + lastConnectedAfter?: string; + lastConnectedBefore?: string; + slLastConnectedAfter?: string; + slLastConnectedBefore?: string; + }) => { + const { + managed, + q, + lastConnectedAfter, + lastConnectedBefore, + slLastConnectedAfter, + slLastConnectedBefore, + } = filterParams; + + return this.users.filter((user) => { + if (this.isManaged && managed !== undefined && user.managed !== managed) { + return false; + } + + if (q && (!user.login.includes(q) || (user.name && !user.name.includes(q)))) { + return false; + } + + if ( + lastConnectedAfter && + (user.lastConnectionDate === undefined || + isBefore(new Date(user.lastConnectionDate), new Date(lastConnectedAfter))) + ) { + return false; + } + + if ( + lastConnectedBefore && + user.lastConnectionDate !== undefined && + isAfter(new Date(user.lastConnectionDate), new Date(lastConnectedBefore)) + ) { + return false; + } + + if ( + slLastConnectedAfter && + (user.sonarLintLastConnectionDate === undefined || + isBefore(new Date(user.sonarLintLastConnectionDate), new Date(slLastConnectedAfter))) + ) { + return false; + } + + if ( + slLastConnectedBefore && + user.sonarLintLastConnectionDate !== undefined && + isAfter(new Date(user.sonarLintLastConnectionDate), new Date(slLastConnectedBefore)) + ) { + return false; + } + + return true; + }); + }; + handleSearchUsers = (data: any): Promise<{ paging: Paging; users: User[] }> => { let paging = { pageIndex: 1, - pageSize: 2, - total: 6, + pageSize: 0, + total: 10, }; if (data.p !== undefined && data.p !== paging.pageIndex) { - paging = { pageIndex: 2, pageSize: 2, total: 6 }; + paging = { pageIndex: 2, pageSize: 2, total: 10 }; const users = [ - mockUser({ name: `local-user ${this.users.length + 4}` }), - mockUser({ name: `local-user ${this.users.length + 5}` }), + mockUser({ + name: `Local User ${this.users.length + 4}`, + login: `local-user-${this.users.length + 4}`, + }), + mockUser({ + name: `Local User ${this.users.length + 5}`, + login: `local-user-${this.users.length + 5}`, + }), ]; return this.reply({ paging, users }); } - 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 }); + const users = this.getFilteredUsers(data); + return this.reply({ + paging: { + pageIndex: 1, + pageSize: users.length, + total: 10, + }, + users, + }); }; handleCreateUser = (data: { diff --git a/server/sonar-web/src/main/js/api/users.ts b/server/sonar-web/src/main/js/api/users.ts index d3fbb2f7e8b..444092043f7 100644 --- a/server/sonar-web/src/main/js/api/users.ts +++ b/server/sonar-web/src/main/js/api/users.ts @@ -72,6 +72,10 @@ export function searchUsers(data: { ps?: number; q?: string; managed?: boolean; + lastConnectedAfter?: string; + lastConnectedBefore?: string; + slLastConnectedAfter?: string; + slLastConnectedBefore?: string; }): 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/UsersApp.tsx b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx index 7cec61d1f3b..25cfbadde0b 100644 --- a/server/sonar-web/src/main/js/apps/users/UsersApp.tsx +++ b/server/sonar-web/src/main/js/apps/users/UsersApp.tsx @@ -17,20 +17,27 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import React, { useCallback, useEffect, useState } from 'react'; +import { subDays, subSeconds } from 'date-fns'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Helmet } from 'react-helmet-async'; +import { FormattedMessage } from 'react-intl'; import { getIdentityProviders, searchUsers } from '../../api/users'; +import HelpTooltip from '../../components/controls/HelpTooltip'; import ListFooter from '../../components/controls/ListFooter'; import { ManagedFilter } from '../../components/controls/ManagedFilter'; import SearchBox from '../../components/controls/SearchBox'; +import Select, { LabelValueSelectOption } from '../../components/controls/Select'; import Suggestions from '../../components/embed-docs-modal/Suggestions'; import { useManageProvider } from '../../components/hooks/useManageProvider'; import DeferredSpinner from '../../components/ui/DeferredSpinner'; +import { now, toNotSoISOString } from '../../helpers/dates'; import { translate } from '../../helpers/l10n'; import { IdentityProvider, Paging } from '../../types/types'; import { User } from '../../types/users'; import Header from './Header'; import UsersList from './UsersList'; +import { USERS_ACTIVITY_OPTIONS, USER_INACTIVITY_DAYS_THRESHOLD } from './constants'; +import { UserActivity } from './types'; export default function UsersApp() { const [identityProviders, setIdentityProviders] = useState([]); @@ -40,20 +47,49 @@ export default function UsersApp() { const [users, setUsers] = useState([]); const [search, setSearch] = useState(''); + const [usersActivity, setUsersActivity] = useState(UserActivity.AnyActivity); const [managed, setManaged] = useState(undefined); const manageProvider = useManageProvider(); + const usersActivityParams = useMemo(() => { + const nowDate = now(); + const nowDateMinus30Days = subDays(nowDate, USER_INACTIVITY_DAYS_THRESHOLD); + const nowDateMinus30DaysAnd1Second = subSeconds(nowDateMinus30Days, 1); + + switch (usersActivity) { + case UserActivity.ActiveSonarLintUser: + return { + slLastConnectedAfter: toNotSoISOString(nowDateMinus30Days), + }; + case UserActivity.ActiveSonarQubeUser: + return { + lastConnectedAfter: toNotSoISOString(nowDateMinus30Days), + slLastConnectedBefore: toNotSoISOString(nowDateMinus30DaysAnd1Second), + }; + case UserActivity.InactiveUser: + return { + lastConnectedBefore: toNotSoISOString(nowDateMinus30DaysAnd1Second), + }; + default: + return {}; + } + }, [usersActivity]); + const fetchUsers = useCallback(async () => { setLoading(true); try { - const { paging, users } = await searchUsers({ q: search, managed }); + const { paging, users } = await searchUsers({ + q: search, + managed, + ...usersActivityParams, + }); setPaging(paging); setUsers(users); } finally { setLoading(false); } - }, [search, managed]); + }, [search, managed, usersActivityParams]); const fetchMoreUsers = useCallback(async () => { if (!paging) { @@ -64,6 +100,7 @@ export default function UsersApp() { const { paging: nextPage, users: nextUsers } = await searchUsers({ q: search, managed, + ...usersActivityParams, p: paging.pageIndex + 1, }); setPaging(nextPage); @@ -71,7 +108,7 @@ export default function UsersApp() { } finally { setLoading(false); } - }, [search, managed, paging, users]); + }, [search, managed, usersActivityParams, paging, users]); useEffect(() => { (async () => { @@ -82,7 +119,7 @@ export default function UsersApp() { useEffect(() => { fetchUsers(); - }, [search, managed]); + }, [fetchUsers]); return (
@@ -103,6 +140,83 @@ export default function UsersApp() { placeholder={translate('search.search_by_login_or_name')} value={search} /> +
+