]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19967 Use GET api/v2/users on the FE
authorguillaume-peoch-sonarsource <guillaume.peoch@sonarsource.com>
Wed, 26 Jul 2023 09:41:36 +0000 (11:41 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 31 Jul 2023 20:03:32 +0000 (20:03 +0000)
15 files changed:
server/sonar-web/src/main/js/api/mocks/UsersServiceMock.ts
server/sonar-web/src/main/js/api/queries/users.ts
server/sonar-web/src/main/js/api/users.ts
server/sonar-web/src/main/js/apps/users/UsersApp.tsx
server/sonar-web/src/main/js/apps/users/UsersList.tsx
server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx
server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx
server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx
server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx
server/sonar-web/src/main/js/apps/users/components/UserActions.tsx
server/sonar-web/src/main/js/apps/users/components/UserForm.tsx
server/sonar-web/src/main/js/apps/users/components/UserGroups.tsx
server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx
server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx
server/sonar-web/src/main/js/helpers/testMocks.ts

index 98ded9a356bcb2fb553e928c78ec0468f70a758c..9ea5dbfec9ae29332aafd71aaefc71528679cd40 100644 (file)
 
 import { isAfter, isBefore } from 'date-fns';
 import { cloneDeep, isEmpty, isUndefined, omitBy } from 'lodash';
-import { mockClusterSysInfo, mockIdentityProvider, mockUser } from '../../helpers/testMocks';
+import { mockClusterSysInfo, mockIdentityProvider, mockRestUser } from '../../helpers/testMocks';
 import { IdentityProvider, Paging, SysInfoCluster } from '../../types/types';
 import { ChangePasswordResults, User } from '../../types/users';
 import { getSystemInfo } from '../system';
 import { addUserToGroup, removeUserFromGroup } from '../user_groups';
 import {
+  GetUsersParams,
+  RestUser,
   UserGroup,
   changePassword,
   createUser,
   deactivateUser,
   getIdentityProviders,
   getUserGroups,
-  searchUsers,
+  getUsers,
   updateUser,
 } from '../users';
 
@@ -41,48 +43,50 @@ jest.mock('../user_groups');
 jest.mock('../system');
 
 const DEFAULT_USERS = [
-  mockUser({
+  mockRestUser({
     managed: true,
     login: 'bob.marley',
     name: 'Bob Marley',
-    lastConnectionDate: '2023-06-27T17:08:59+0200',
+    sonarQubeLastConnectionDate: '2023-06-27T17:08:59+0200',
     sonarLintLastConnectionDate: '2023-06-27T17:08:59+0200',
   }),
-  mockUser({
+  mockRestUser({
     managed: false,
     login: 'alice.merveille',
     name: 'Alice Merveille',
-    lastConnectionDate: '2023-06-27T17:08:59+0200',
+    sonarQubeLastConnectionDate: '2023-06-27T17:08:59+0200',
     sonarLintLastConnectionDate: '2023-05-27T17:08:59+0200',
-    groups: ['group1', 'group2', 'group3', 'group4'],
+    email: 'alice.merveille@wonderland.com',
+    // groups: ['group1', 'group2', 'group3', 'group4'],
+    groupsCount: 4,
   }),
-  mockUser({
+  mockRestUser({
     managed: false,
     local: false,
     login: 'charlie.cox',
     name: 'Charlie Cox',
-    lastConnectionDate: '2023-06-25T17:08:59+0200',
+    sonarQubeLastConnectionDate: '2023-06-25T17:08:59+0200',
     sonarLintLastConnectionDate: '2023-06-20T12:10:59+0200',
     externalProvider: 'test',
-    externalIdentity: 'ExternalTest',
+    externalLogin: 'ExternalTest',
   }),
-  mockUser({
+  mockRestUser({
     managed: true,
     local: false,
     externalProvider: 'test2',
-    externalIdentity: 'UnknownExternalProvider',
+    externalLogin: 'UnknownExternalProvider',
     login: 'denis.villeneuve',
     name: 'Denis Villeneuve',
-    lastConnectionDate: '2023-06-20T15:08:59+0200',
+    sonarQubeLastConnectionDate: '2023-06-20T15:08:59+0200',
     sonarLintLastConnectionDate: '2023-05-25T10:08:59+0200',
   }),
-  mockUser({
+  mockRestUser({
     managed: true,
     login: 'eva.green',
     name: 'Eva Green',
-    lastConnectionDate: '2023-05-27T17:08:59+0200',
+    sonarQubeLastConnectionDate: '2023-05-27T17:08:59+0200',
   }),
-  mockUser({
+  mockRestUser({
     managed: false,
     login: 'franck.grillo',
     name: 'Franck Grillo',
@@ -123,7 +127,7 @@ export default class UsersServiceMock {
   constructor() {
     jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo);
     jest.mocked(getIdentityProviders).mockImplementation(this.handleGetIdentityProviders);
-    jest.mocked(searchUsers).mockImplementation((p) => this.handleSearchUsers(p));
+    jest.mocked(getUsers).mockImplementation((p) => this.handleGetUsers(p));
     jest.mocked(createUser).mockImplementation(this.handleCreateUser);
     jest.mocked(updateUser).mockImplementation(this.handleUpdateUser);
     jest.mocked(getUserGroups).mockImplementation(this.handleGetUserGroups);
@@ -137,21 +141,21 @@ export default class UsersServiceMock {
     this.isManaged = managed;
   }
 
-  getFilteredUsers = (filterParams: {
-    managed: boolean;
+  getFilteredRestUsers = (filterParams: {
     q: string;
-    lastConnectedAfter?: string;
-    lastConnectedBefore?: string;
-    slLastConnectedAfter?: string;
-    slLastConnectedBefore?: string;
+    managed?: boolean;
+    sonarQubeLastConnectionDateFrom?: string;
+    sonarQubeLastConnectionDateTo?: string;
+    sonarLintLastConnectionDateFrom?: string;
+    sonarLintLastConnectionDateTo?: string;
   }) => {
     const {
       managed,
       q,
-      lastConnectedAfter,
-      lastConnectedBefore,
-      slLastConnectedAfter,
-      slLastConnectedBefore,
+      sonarQubeLastConnectionDateFrom,
+      sonarQubeLastConnectionDateTo,
+      sonarLintLastConnectionDateFrom,
+      sonarLintLastConnectionDateTo,
     } = filterParams;
 
     return this.users.filter((user) => {
@@ -164,33 +168,39 @@ export default class UsersServiceMock {
       }
 
       if (
-        lastConnectedAfter &&
-        (user.lastConnectionDate === undefined ||
-          isBefore(new Date(user.lastConnectionDate), new Date(lastConnectedAfter)))
+        sonarQubeLastConnectionDateFrom &&
+        (user.sonarQubeLastConnectionDate === null ||
+          isBefore(
+            new Date(user.sonarQubeLastConnectionDate),
+            new Date(sonarQubeLastConnectionDateFrom)
+          ))
       ) {
         return false;
       }
 
       if (
-        lastConnectedBefore &&
-        user.lastConnectionDate !== undefined &&
-        isAfter(new Date(user.lastConnectionDate), new Date(lastConnectedBefore))
+        sonarQubeLastConnectionDateTo &&
+        user.sonarQubeLastConnectionDate &&
+        isAfter(new Date(user.sonarQubeLastConnectionDate), new Date(sonarQubeLastConnectionDateTo))
       ) {
         return false;
       }
 
       if (
-        slLastConnectedAfter &&
-        (user.sonarLintLastConnectionDate === undefined ||
-          isBefore(new Date(user.sonarLintLastConnectionDate), new Date(slLastConnectedAfter)))
+        sonarLintLastConnectionDateFrom &&
+        (user.sonarLintLastConnectionDate === null ||
+          isBefore(
+            new Date(user.sonarLintLastConnectionDate),
+            new Date(sonarLintLastConnectionDateFrom)
+          ))
       ) {
         return false;
       }
 
       if (
-        slLastConnectedBefore &&
-        user.sonarLintLastConnectionDate !== undefined &&
-        isAfter(new Date(user.sonarLintLastConnectionDate), new Date(slLastConnectedBefore))
+        sonarLintLastConnectionDateTo &&
+        user.sonarLintLastConnectionDate &&
+        isAfter(new Date(user.sonarLintLastConnectionDate), new Date(sonarLintLastConnectionDateTo))
       ) {
         return false;
       }
@@ -199,32 +209,42 @@ export default class UsersServiceMock {
     });
   };
 
-  handleSearchUsers = (data: any): Promise<{ paging: Paging; users: User[] }> => {
-    let paging = {
+  handleGetUsers = (
+    data: GetUsersParams
+  ): Promise<{ pageRestResponse: Paging; users: RestUser<'admin'>[] }> => {
+    let pageRestResponse = {
       pageIndex: 1,
       pageSize: 0,
       total: 10,
     };
 
-    if (data.p !== undefined && data.p !== paging.pageIndex) {
-      paging = { pageIndex: 2, pageSize: 2, total: 10 };
+    if (data.pageIndex !== undefined && data.pageIndex !== pageRestResponse.pageIndex) {
+      pageRestResponse = { pageIndex: 2, pageSize: 2, total: 10 };
       const users = [
-        mockUser({
+        mockRestUser({
           name: `Local User ${this.users.length + 4}`,
           login: `local-user-${this.users.length + 4}`,
         }),
-        mockUser({
+        mockRestUser({
           name: `Local User ${this.users.length + 5}`,
           login: `local-user-${this.users.length + 5}`,
         }),
       ];
 
-      return this.reply({ paging, users });
+      return this.reply({ pageRestResponse, users });
     }
 
-    const users = this.getFilteredUsers(data);
+    const users = this.getFilteredRestUsers({
+      managed: data.managed,
+      q: data.q,
+      sonarQubeLastConnectionDateFrom: data.sonarQubeLastConnectionDateFrom,
+      sonarQubeLastConnectionDateTo: data.sonarQubeLastConnectionDateTo,
+      sonarLintLastConnectionDateFrom: data.sonarLintLastConnectionDateFrom,
+      sonarLintLastConnectionDateTo: data.sonarLintLastConnectionDateTo,
+    }) as RestUser<'admin'>[];
+
     return this.reply({
-      paging: {
+      pageRestResponse: {
         pageIndex: 1,
         pageSize: users.length,
         total: 10,
@@ -248,7 +268,7 @@ export default class UsersServiceMock {
         json: () => Promise.resolve({ errors: [{ msg: 'Error: Empty SCM' }] }),
       });
     }
-    const newUser = mockUser({
+    const newUser = mockRestUser({
       email,
       local,
       login,
@@ -266,7 +286,7 @@ export default class UsersServiceMock {
     scmAccount: string[];
   }) => {
     const { email, login, name, scmAccount } = data;
-    const user = this.users.find((u) => u.login === login);
+    const user = this.users.find((u) => u.login === login) as User;
     if (!user) {
       return Promise.reject('No such user');
     }
index 916c86bd2ba10b093dd4d6578ef11853878c615a..bc18ed5cbc9ab80b02919b5c6c428b9a69e49271 100644 (file)
@@ -25,38 +25,39 @@ import {
   useQueryClient,
 } from '@tanstack/react-query';
 import { range } from 'lodash';
-import { User } from '../../types/users';
 import {
   CreateUserParams,
   DeactivateUserParams,
-  SearchUsersParams,
+  GetUsersParams,
+  Permission,
+  RestUser,
   UpdateUserParams,
   createUser,
   deactivateUser,
-  searchUsers,
+  getUsers,
   updateUser,
 } from '../users';
 
-export function useUsersQueries(
-  searchParam: Omit<SearchUsersParams, 'p' | 'ps'>,
+export function useUsersQueries<P extends Permission>(
+  getParams: Omit<GetUsersParams, 'pageSize' | 'pageIndex'>,
   numberOfPages: number
 ) {
-  type QueryKey = ['user', 'list', number, Omit<SearchUsersParams, 'p' | 'ps'>];
+  type QueryKey = ['user', 'list', number, Omit<GetUsersParams, 'pageSize' | 'pageIndex'>];
   const results = useQueries({
     queries: range(1, numberOfPages + 1).map((page: number) => ({
-      queryKey: ['user', 'list', page, searchParam],
-      queryFn: ({ queryKey: [_u, _l, page, searchParam] }: QueryFunctionContext<QueryKey>) =>
-        searchUsers({ ...searchParam, p: page }),
+      queryKey: ['user', 'list', page, getParams],
+      queryFn: ({ queryKey: [_u, _l, page, getParams] }: QueryFunctionContext<QueryKey>) =>
+        getUsers<P>({ ...getParams, pageIndex: page }),
     })),
   });
 
   return results.reduce(
     (acc, { data, isLoading }) => ({
       users: acc.users.concat(data?.users ?? []),
-      total: data?.paging.total,
+      total: data?.pageRestResponse.total,
       isLoading: acc.isLoading || isLoading,
     }),
-    { users: [] as User[], total: 0, isLoading: false }
+    { users: [] as RestUser<P>[], total: 0, isLoading: false }
   );
 }
 
index d7f331590957ccfe7b4b3df3a361d4f90036aeed..72c569b8dda2ac2f01cbe564ede011e077ef39a1 100644 (file)
@@ -87,6 +87,57 @@ export function searchUsers(data: SearchUsersParams): Promise<{ paging: Paging;
   return getJSON('/api/users/search', data).catch(throwGlobalError);
 }
 
+export interface GetUsersParams {
+  q: string;
+  active?: boolean;
+  managed?: boolean;
+  sonarQubeLastConnectionDateFrom?: string;
+  sonarQubeLastConnectionDateTo?: string;
+  sonarLintLastConnectionDateFrom?: string;
+  sonarLintLastConnectionDateTo?: string;
+  pageSize?: number;
+  pageIndex?: number;
+}
+
+export type Permission = 'admin' | 'anonymous' | 'user';
+
+export type RestUser<T extends Permission> = T extends 'admin'
+  ? {
+      id: string;
+      login: string;
+      name: string;
+      email: string;
+      active: boolean;
+      local: boolean;
+      externalProvider: string;
+      avatar: string;
+      managed: boolean;
+      externalLogin: string;
+      sonarQubeLastConnectionDate: string | null;
+      sonarLintLastConnectionDate: string | null;
+      scmAccounts: string[];
+      groupsCount: number;
+      tokensCount: number;
+    }
+  : T extends 'anonymous'
+  ? { id: string; login: string; name: string }
+  : {
+      id: string;
+      login: string;
+      name: string;
+      email: string;
+      active: boolean;
+      local: boolean;
+      externalProvider: string;
+      avatar: string;
+    };
+
+export function getUsers<T extends Permission>(
+  data: GetUsersParams
+): Promise<{ pageRestResponse: Paging; users: RestUser<T>[] }> {
+  return getJSON('/api/v2/users', data).catch(throwGlobalError);
+}
+
 export interface CreateUserParams {
   email?: string;
   local?: boolean;
@@ -119,7 +170,7 @@ export interface DeactivateUserParams {
   anonymize?: boolean;
 }
 
-export function deactivateUser(data: DeactivateUserParams): Promise<{ user: User }> {
+export function deactivateUser(data: DeactivateUserParams): Promise<{ user: RestUser<'admin'> }> {
   return postJSON('/api/users/deactivate', data).catch(throwGlobalError);
 }
 
index 348a19e2278190aaec72589bd4293ed7b809b322..e087a7dd8e218259cce721745782d293ace82ad7 100644 (file)
@@ -54,23 +54,23 @@ export default function UsersApp() {
     switch (usersActivity) {
       case UserActivity.ActiveSonarLintUser:
         return {
-          slLastConnectedAfter: toISO8601WithOffsetString(nowDateMinus30Days),
+          sonarLintLastConnectionDateFrom: toISO8601WithOffsetString(nowDateMinus30Days),
         };
       case UserActivity.ActiveSonarQubeUser:
         return {
-          lastConnectedAfter: toISO8601WithOffsetString(nowDateMinus30Days),
-          slLastConnectedBefore: toISO8601WithOffsetString(nowDateMinus30DaysAnd1Second),
+          sonarQubeLastConnectionDateFrom: toISO8601WithOffsetString(nowDateMinus30Days),
+          sonarLintLastConnectionDateTo: toISO8601WithOffsetString(nowDateMinus30DaysAnd1Second),
         };
       case UserActivity.InactiveUser:
         return {
-          lastConnectedBefore: toISO8601WithOffsetString(nowDateMinus30DaysAnd1Second),
+          sonarQubeLastConnectionDateTo: toISO8601WithOffsetString(nowDateMinus30DaysAnd1Second),
         };
       default:
         return {};
     }
   }, [usersActivity]);
 
-  const { users, total, isLoading } = useUsersQueries(
+  const { users, total, isLoading } = useUsersQueries<'admin'>(
     {
       q: search,
       managed,
index e3fed01d2195b66ac0d5e7253c30e0bfa234cbb5..c3d01379d49d73425287bb4d4c62304497e1635f 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { RestUser } from '../../api/users';
 import { CurrentUserContext } from '../../app/components/current-user/CurrentUserContext';
 import HelpTooltip from '../../components/controls/HelpTooltip';
 import { translate } from '../../helpers/l10n';
 import { IdentityProvider } from '../../types/types';
-import { isLoggedIn, User } from '../../types/users';
+import { isLoggedIn } from '../../types/users';
 import UserListItem from './components/UserListItem';
 
 interface Props {
   identityProviders: IdentityProvider[];
-  users: User[];
+  users: RestUser<'admin'>[];
   manageProvider: string | undefined;
 }
 
index e455cc52725ea05ea0b79b12f95f50b1cde7594c..0fa5d92e89ec05d53c267fb8b3c3068efd0dfc1e 100644 (file)
@@ -78,7 +78,9 @@ const ui = {
   }),
   aliceRowWithLocalBadge: byRole('row', {
     name: (accessibleName) =>
-      accessibleName.startsWith('AM Alice Merveille alice.merveille local '),
+      accessibleName.startsWith(
+        'AM Alice Merveille alice.merveille alice.merveille@wonderland.com local '
+      ),
   }),
   bobRow: byRole('row', {
     name: (accessibleName) => accessibleName.startsWith('BM Bob Marley bob.marley '),
@@ -336,8 +338,9 @@ describe('in non managed mode', () => {
     expect(await ui.dialogUpdateUser.find()).toBeInTheDocument();
 
     expect(ui.userNameInput.get()).toHaveValue('Alice Merveille');
-    expect(ui.emailInput.get()).toHaveValue('');
+    expect(ui.emailInput.get()).toHaveValue('alice.merveille@wonderland.com');
     await user.type(ui.userNameInput.get(), '1');
+    await user.clear(ui.emailInput.get());
     await user.type(ui.emailInput.get(), 'test@test.com');
     await act(() => user.click(ui.updateButton.get()));
     expect(ui.dialogUpdateUser.query()).not.toBeInTheDocument();
@@ -594,6 +597,7 @@ it('should render external identity Providers', async () => {
   renderUsersApp();
 
   await act(async () => expect(await ui.charlieRow.find()).toHaveTextContent(/ExternalTest/));
+  // logRoles(document.body);
   expect(await ui.denisRow.find()).toHaveTextContent(/test2: UnknownExternalProvider/);
 });
 
index 478b9b733766572cbb7a67f27e5bd855638d599f..fda16770551735648055d0f78a201aa969c56f6c 100644 (file)
@@ -21,7 +21,7 @@ import { find, without } from 'lodash';
 import * as React from 'react';
 import { useInvalidateUsersList } from '../../../api/queries/users';
 import { addUserToGroup, removeUserFromGroup } from '../../../api/user_groups';
-import { UserGroup, getUserGroups } from '../../../api/users';
+import { RestUser, UserGroup, getUserGroups } from '../../../api/users';
 import Modal from '../../../components/controls/Modal';
 import SelectList, {
   SelectListFilter,
@@ -29,11 +29,10 @@ import SelectList, {
 } from '../../../components/controls/SelectList';
 import { ResetButtonLink } from '../../../components/controls/buttons';
 import { translate } from '../../../helpers/l10n';
-import { User } from '../../../types/users';
 
 interface Props {
   onClose: () => void;
-  user: User;
+  user: RestUser<'admin'>;
 }
 
 export default function GroupsForm(props: Props) {
index d13ff24e7fbbb052133b167b3e84d4eada4784b4..0387363c3c6849c86311ab29c8e93b43bba9f970 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { changePassword } from '../../../api/users';
+import { RestUser, changePassword } from '../../../api/users';
 import Modal from '../../../components/controls/Modal';
 import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import { Alert } from '../../../components/ui/Alert';
@@ -26,12 +26,12 @@ import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
 import { addGlobalSuccessMessage } from '../../../helpers/globalMessages';
 import { translate } from '../../../helpers/l10n';
-import { ChangePasswordResults, User } from '../../../types/users';
+import { ChangePasswordResults } from '../../../types/users';
 
 interface Props {
   isCurrentUser: boolean;
   onClose: () => void;
-  user: User;
+  user: RestUser<'admin'>;
 }
 
 interface State {
index 6d6f79f267e5c7588533d2cf10cfcb881350234f..87c219710f76a65c49d993e6c3383eda59b5470a 100644 (file)
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { useInvalidateUsersList } from '../../../api/queries/users';
+import { RestUser } from '../../../api/users';
 import Modal from '../../../components/controls/Modal';
 import { ResetButtonLink } from '../../../components/controls/buttons';
 import { translate } from '../../../helpers/l10n';
-import { User } from '../../../types/users';
 import TokensForm from './TokensForm';
 
 interface Props {
-  user: User;
+  user: RestUser<'admin'>;
   onClose: () => void;
 }
 
index e1872825607996008bca0dea3db86fedfd2005e6..aa61b5501949b49c0cc88a23bbe7a6948f84acb3 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { RestUser } from '../../../api/users';
 import ActionsDropdown, {
   ActionsDropdownDivider,
   ActionsDropdownItem,
 } from '../../../components/controls/ActionsDropdown';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { User, isUserActive } from '../../../types/users';
+import { isUserActive } from '../../../types/users';
 import DeactivateForm from './DeactivateForm';
 import PasswordForm from './PasswordForm';
 import UserForm from './UserForm';
 
 interface Props {
   isCurrentUser: boolean;
-  user: User;
+  user: RestUser<'admin'>;
   manageProvider: string | undefined;
 }
 
index a1a4c11d7d4f1208884c0b0cd1d2421ddc60f887..fc18984a583ccc4bbc699edeb3696b6f38212527 100644 (file)
@@ -19,6 +19,7 @@
  */
 import * as React from 'react';
 import { useCreateUserMutation, useUpdateUserMutation } from '../../../api/queries/users';
+import { RestUser } from '../../../api/users';
 import SimpleModal from '../../../components/controls/SimpleModal';
 import { Button, ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import { Alert } from '../../../components/ui/Alert';
@@ -27,12 +28,11 @@ import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsEx
 import { throwGlobalError } from '../../../helpers/error';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { parseError } from '../../../helpers/request';
-import { User } from '../../../types/users';
 import UserScmAccountInput from './UserScmAccountInput';
 
 export interface Props {
   onClose: () => void;
-  user?: User;
+  user?: RestUser<'admin'>;
 }
 
 export default function UserForm(props: Props) {
@@ -50,9 +50,10 @@ export default function UserForm(props: Props) {
 
   const handleError = (response: Response) => {
     if (![400, 500].includes(response.status)) {
-      return throwGlobalError(response);
+      throwGlobalError(response);
+    } else {
+      parseError(response).then((errorMsg) => setError(errorMsg), throwGlobalError);
     }
-    return parseError(response).then((errorMsg) => setError(errorMsg), throwGlobalError);
   };
 
   const handleCreateUser = () => {
index 1b56b5e976470c34043c05a9144bc4fe4b7bd592..d384ffba0dfc329a920669cceffa67034b06c66f 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { RestUser } from '../../../api/users';
 import { ButtonIcon, ButtonLink } from '../../../components/controls/buttons';
 import BulletListIcon from '../../../components/icons/BulletListIcon';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { User } from '../../../types/users';
 import GroupsForm from './GroupsForm';
 
 interface Props {
   groups: string[];
-  user: User;
+  user: RestUser<'admin'>;
   manageProvider: string | undefined;
 }
 
index d298d81b5301eb749551365e2078039ae7027b98..5147301baeb66beedb5067c95a631ab4698a1794 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { RestUser } from '../../../api/users';
 import { ButtonIcon } from '../../../components/controls/buttons';
 import BulletListIcon from '../../../components/icons/BulletListIcon';
 import DateFromNow from '../../../components/intl/DateFromNow';
 import LegacyAvatar from '../../../components/ui/LegacyAvatar';
 import { translateWithParameters } from '../../../helpers/l10n';
 import { IdentityProvider } from '../../../types/types';
-import { User } from '../../../types/users';
 import TokensFormModal from './TokensFormModal';
 import UserActions from './UserActions';
-import UserGroups from './UserGroups';
 import UserListItemIdentity from './UserListItemIdentity';
 import UserScmAccounts from './UserScmAccounts';
 
 export interface UserListItemProps {
   identityProvider?: IdentityProvider;
   isCurrentUser: boolean;
-  user: User;
+  user: RestUser<'admin'>;
   manageProvider: string | undefined;
 }
 
 export default function UserListItem(props: UserListItemProps) {
-  const [openTokenForm, setOpenTokenForm] = React.useState(false);
-
   const { identityProvider, user, manageProvider, isCurrentUser } = props;
+  const {
+    name,
+    login,
+    managed,
+    tokensCount,
+    avatar,
+    sonarQubeLastConnectionDate,
+    sonarLintLastConnectionDate,
+    scmAccounts,
+  } = user;
+
+  const [openTokenForm, setOpenTokenForm] = React.useState(false);
 
   return (
     <tr>
       <td className="thin text-middle">
         <div className="sw-flex sw-items-center">
-          <LegacyAvatar
-            className="sw-shrink-0 sw-mr-4"
-            hash={user.avatar}
-            name={user.name}
-            size={36}
-          />
+          <LegacyAvatar className="sw-shrink-0 sw-mr-4" hash={avatar} name={name} size={36} />
           <UserListItemIdentity
             identityProvider={identityProvider}
             user={user}
@@ -61,30 +65,31 @@ export default function UserListItem(props: UserListItemProps) {
         </div>
       </td>
       <td className="thin text-middle">
-        <UserScmAccounts scmAccounts={user.scmAccounts || []} />
+        <UserScmAccounts scmAccounts={scmAccounts || []} />
       </td>
       <td className="thin nowrap text-middle">
-        <DateFromNow date={user.lastConnectionDate} hourPrecision />
+        <DateFromNow date={sonarQubeLastConnectionDate ?? ''} hourPrecision />
       </td>
       <td className="thin nowrap text-middle">
-        <DateFromNow date={user.sonarLintLastConnectionDate} hourPrecision />
+        <DateFromNow date={sonarLintLastConnectionDate ?? ''} hourPrecision />
       </td>
       <td className="thin nowrap text-middle">
-        <UserGroups groups={user.groups ?? []} manageProvider={manageProvider} user={user} />
+        {user.groupsCount}
+        {/* <UserGroups groups={user.groupsCount} manageProvider={manageProvider} user={user} /> */}
       </td>
       <td className="thin nowrap text-middle">
-        {user.tokensCount}
+        {tokensCount}
         <ButtonIcon
           className="js-user-tokens spacer-left button-small"
           onClick={() => setOpenTokenForm(true)}
           tooltip={translateWithParameters('users.update_tokens')}
-          aria-label={translateWithParameters('users.update_tokens_for_x', user.name ?? user.login)}
+          aria-label={translateWithParameters('users.update_tokens_for_x', name ?? login)}
         >
           <BulletListIcon />
         </ButtonIcon>
       </td>
 
-      {(manageProvider === undefined || !user.managed) && (
+      {(manageProvider === undefined || !managed) && (
         <td className="thin nowrap text-right text-middle">
           <UserActions isCurrentUser={isCurrentUser} user={user} manageProvider={manageProvider} />
         </td>
index 6438cc6225ad7fc416087f24351f84bf4b9f40b7..e21060242ebe8c765bf93403a351c869f6f4f725 100644 (file)
 
 import { getTextColor } from 'design-system';
 import * as React from 'react';
+import { RestUser } from '../../../api/users';
 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';
 
 export interface Props {
   identityProvider?: IdentityProvider;
-  user: User;
+  user: RestUser<'admin'>;
   manageProvider?: string;
 }
 
@@ -55,7 +55,7 @@ export function ExternalProvider({ identityProvider, user }: Omit<Props, 'manage
     return (
       <div className="js-user-identity-provider little-spacer-top">
         <span>
-          {user.externalProvider}: {user.externalIdentity}
+          {user.externalProvider}: {user.externalLogin}
         </span>
       </div>
     );
@@ -77,7 +77,7 @@ export function ExternalProvider({ identityProvider, user }: Omit<Props, 'manage
           src={getBaseUrl() + identityProvider.iconPath}
           width="14"
         />
-        {user.externalIdentity}
+        {user.externalLogin}
       </div>
     </div>
   );
index 735d6da13f8933c14cbe7ff708e81f67eef00645..c3ee78cb3a4228d40f197a1385e3f540fd706755 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { To } from 'react-router-dom';
 import { CompareResponse } from '../api/quality-profiles';
+import { RestUser } from '../api/users';
 import { RuleDescriptionSections } from '../apps/coding-rules/rule';
 import { Exporter, Profile, ProfileChangelogEvent } from '../apps/quality-profiles/types';
 import { LogsLevels } from '../apps/system/utils';
@@ -680,6 +681,27 @@ export function mockUser(overrides: Partial<User> = {}): User {
   };
 }
 
+export function mockRestUser(overrides: Partial<RestUser<'admin'>> = {}): RestUser<'admin'> {
+  return {
+    id: Math.random().toString(),
+    login: 'buzz.aldrin',
+    name: 'Buzz Aldrin',
+    email: 'buzz.aldrin@nasa.com',
+    active: true,
+    local: true,
+    managed: false,
+    externalProvider: '',
+    externalLogin: '',
+    sonarQubeLastConnectionDate: null,
+    sonarLintLastConnectionDate: null,
+    scmAccounts: [],
+    tokensCount: 0,
+    groupsCount: 0,
+    avatar: 'buzzonthemoon',
+    ...overrides,
+  };
+}
+
 export function mockUserSelected(overrides: Partial<UserSelected> = {}): UserSelected {
   return {
     active: true,