]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10031 Stop computing avatar hash in web app (#2769)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Fri, 27 Oct 2017 15:05:43 +0000 (17:05 +0200)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 30 Oct 2017 08:20:37 +0000 (09:20 +0100)
35 files changed:
server/sonar-server/src/main/java/org/sonar/server/permission/ws/UsersAction.java
server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/TemplateUsersAction.java
server/sonar-server/src/main/java/org/sonar/server/user/ws/CurrentAction.java
server/sonar-server/src/main/resources/org/sonar/server/permission/ws/template/template_users-example.json
server/sonar-server/src/main/resources/org/sonar/server/permission/ws/users-example.json
server/sonar-server/src/main/resources/org/sonar/server/user/ws/current-example.json
server/sonar-server/src/test/java/org/sonar/server/permission/ws/PermissionsWsTest.java
server/sonar-server/src/test/java/org/sonar/server/permission/ws/UsersActionTest.java
server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/TemplateUsersActionTest.java
server/sonar-server/src/test/java/org/sonar/server/user/ws/CurrentActionTest.java
server/sonar-server/src/test/java/org/sonar/server/user/ws/UsersWsTest.java
server/sonar-web/package.json
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.js
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.js.snap
server/sonar-web/src/main/js/apps/account/components/UserCard.js
server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js
server/sonar-web/src/main/js/apps/organizations/components/MembersListItem.js
server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js
server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchOption.js
server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchValue.js
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchOption-test.js.snap
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchValue-test.js.snap
server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs
server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js
server/sonar-web/src/main/js/components/ui/Avatar.js [deleted file]
server/sonar-web/src/main/js/components/ui/Avatar.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.js [deleted file]
server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Avatar-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Avatar-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/handlebars/avatarHelper.js
server/sonar-web/yarn.lock
sonar-ws/src/main/protobuf/ws-permissions.proto
sonar-ws/src/main/protobuf/ws-users.proto

index 8c6f146ca520298bce0a5dc9887454c7e11ab518..f7538691c9620f240257f6e7af6fc0e7916240e4 100644 (file)
@@ -36,11 +36,13 @@ import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.permission.PermissionQuery;
 import org.sonar.db.permission.UserPermissionDto;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.issue.ws.AvatarResolver;
 import org.sonar.server.permission.ProjectId;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.WsPermissions;
 import org.sonarqube.ws.WsPermissions.UsersWsResponse;
 
+import static com.google.common.base.Strings.emptyToNull;
 import static java.util.Collections.emptyList;
 import static org.sonar.core.util.Protobuf.setNullable;
 import static org.sonar.db.permission.PermissionQuery.DEFAULT_PAGE_SIZE;
@@ -62,11 +64,13 @@ public class UsersAction implements PermissionsWsAction {
   private final DbClient dbClient;
   private final UserSession userSession;
   private final PermissionWsSupport support;
+  private final AvatarResolver avatarResolver;
 
-  public UsersAction(DbClient dbClient, UserSession userSession, PermissionWsSupport support) {
+  public UsersAction(DbClient dbClient, UserSession userSession, PermissionWsSupport support, AvatarResolver avatarResolver) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.support = support;
+    this.avatarResolver = avatarResolver;
   }
 
   @Override
@@ -139,7 +143,7 @@ public class UsersAction implements PermissionsWsAction {
     return permissionQuery.build();
   }
 
-  private static UsersWsResponse buildResponse(List<UserDto> users, List<UserPermissionDto> userPermissions, Paging paging) {
+  private UsersWsResponse buildResponse(List<UserDto> users, List<UserPermissionDto> userPermissions, Paging paging) {
     Multimap<Integer, String> permissionsByUserId = TreeMultimap.create();
     userPermissions.forEach(userPermission -> permissionsByUserId.put(userPermission.getUserId(), userPermission.getPermission()));
 
@@ -149,6 +153,7 @@ public class UsersAction implements PermissionsWsAction {
         .setLogin(user.getLogin())
         .addAllPermissions(permissionsByUserId.get(user.getId()));
       setNullable(user.getEmail(), userResponse::setEmail);
+      setNullable(emptyToNull(user.getEmail()), u -> userResponse.setAvatar(avatarResolver.create(user)));
       setNullable(user.getName(), userResponse::setName);
     });
 
index aadaaae16b24b5a4bbde841bdc28888c36648fbc..0c37f59f1c00c776744f29520957bf5bedf2d85b 100644 (file)
@@ -35,12 +35,14 @@ import org.sonar.db.permission.PermissionQuery;
 import org.sonar.db.permission.template.PermissionTemplateDto;
 import org.sonar.db.permission.template.PermissionTemplateUserDto;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.issue.ws.AvatarResolver;
 import org.sonar.server.permission.ws.PermissionWsSupport;
 import org.sonar.server.permission.ws.PermissionsWsAction;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.WsPermissions;
 import org.sonarqube.ws.WsPermissions.UsersWsResponse;
 
+import static com.google.common.base.Strings.emptyToNull;
 import static org.sonar.api.server.ws.WebService.Param.PAGE;
 import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
 import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY;
@@ -60,11 +62,13 @@ public class TemplateUsersAction implements PermissionsWsAction {
   private final DbClient dbClient;
   private final UserSession userSession;
   private final PermissionWsSupport support;
+  private final AvatarResolver avatarResolver;
 
-  public TemplateUsersAction(DbClient dbClient, UserSession userSession, PermissionWsSupport support) {
+  public TemplateUsersAction(DbClient dbClient, UserSession userSession, PermissionWsSupport support, AvatarResolver avatarResolver) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.support = support;
+    this.avatarResolver = avatarResolver;
   }
 
   @Override
@@ -122,7 +126,7 @@ public class TemplateUsersAction implements PermissionsWsAction {
     return query.build();
   }
 
-  private static WsPermissions.UsersWsResponse buildResponse(List<UserDto> users, List<PermissionTemplateUserDto> permissionTemplateUsers, Paging paging) {
+  private WsPermissions.UsersWsResponse buildResponse(List<UserDto> users, List<PermissionTemplateUserDto> permissionTemplateUsers, Paging paging) {
     Multimap<Integer, String> permissionsByUserId = TreeMultimap.create();
     permissionTemplateUsers.forEach(userPermission -> permissionsByUserId.put(userPermission.getUserId(), userPermission.getPermission()));
 
@@ -133,6 +137,7 @@ public class TemplateUsersAction implements PermissionsWsAction {
         .addAllPermissions(permissionsByUserId.get(user.getId()));
       setNullable(user.getEmail(), userResponse::setEmail);
       setNullable(user.getName(), userResponse::setName);
+      setNullable(emptyToNull(user.getEmail()), u -> userResponse.setAvatar(avatarResolver.create(user)));
     });
     responseBuilder.getPagingBuilder()
       .setPageIndex(paging.pageIndex())
index 2529b920e6312c52586ab6a38a75c071f9564f5f..9ddfdc9190bd7d1586d1377db6bde04fea4c6140 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.permission.OrganizationPermission;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.issue.ws.AvatarResolver;
 import org.sonar.server.organization.DefaultOrganizationProvider;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.WsUsers.CurrentWsResponse;
@@ -47,11 +48,13 @@ public class CurrentAction implements UsersWsAction {
   private final UserSession userSession;
   private final DbClient dbClient;
   private final DefaultOrganizationProvider defaultOrganizationProvider;
+  private final AvatarResolver avatarResolver;
 
-  public CurrentAction(UserSession userSession, DbClient dbClient, DefaultOrganizationProvider defaultOrganizationProvider) {
+  public CurrentAction(UserSession userSession, DbClient dbClient, DefaultOrganizationProvider defaultOrganizationProvider, AvatarResolver avatarResolver) {
     this.userSession = userSession;
     this.dbClient = dbClient;
     this.defaultOrganizationProvider = defaultOrganizationProvider;
+    this.avatarResolver = avatarResolver;
   }
 
   @Override
@@ -95,6 +98,7 @@ public class CurrentAction implements UsersWsAction {
       .setPermissions(Permissions.newBuilder().addAllGlobal(getGlobalPermissions()).build())
       .setShowOnboardingTutorial(!user.isOnboarded());
     setNullable(emptyToNull(user.getEmail()), builder::setEmail);
+    setNullable(emptyToNull(user.getEmail()), u -> builder.setAvatar(avatarResolver.create(user)));
     setNullable(user.getExternalIdentity(), builder::setExternalIdentity);
     setNullable(user.getExternalIdentityProvider(), builder::setExternalProvider);
     return builder.build();
index 2e21d9a21f818e076fcb886a8c930011574376fe..f33861b444405e5b68b4a2332ffd6e5f225d643f 100644 (file)
@@ -9,18 +9,15 @@
       "login": "admin",
       "name": "Administrator",
       "email": "admin@admin.com",
-      "permissions": [
-        "codeviewer"
-      ]
+      "avatar": "64e1b8d34f425d19e1ee2ea7236d3028",
+      "permissions": ["codeviewer"]
     },
     {
       "login": "george.orwell",
       "name": "George Orwell",
       "email": "george.orwell@1984.net",
-      "permissions": [
-        "admin",
-        "codeviewer"
-      ]
+      "avatar": "583af86a274c1027ef078cada831babf",
+      "permissions": ["admin", "codeviewer"]
     }
   ]
 }
index eef7bf0f1fe337360a4f0e07d88b2737355aca3d..1f3e6ae34cb9ecafd4161d126ea36d19a6459c4c 100644 (file)
@@ -9,20 +9,15 @@
       "login": "admin",
       "name": "Administrator",
       "email": "admin@admin.com",
-      "permissions": [
-        "admin",
-        "gateadmin",
-        "profileadmin"
-      ]
+      "avatar": "64e1b8d34f425d19e1ee2ea7236d3028",
+      "permissions": ["admin", "gateadmin", "profileadmin"]
     },
     {
       "login": "george.orwell",
       "name": "George Orwell",
       "email": "george.orwell@1984.net",
-      "permissions": [
-        "scan"
-      ]
+      "avatar": "583af86a274c1027ef078cada831babf",
+      "permissions": ["scan"]
     }
   ]
 }
-
index 77420e5fe9c75d741a394f84257cb4f41b4a85a1..359d2f79961d0044d6562936278e374b53b2b345 100644 (file)
@@ -3,21 +3,14 @@
   "login": "obiwan.kenobi",
   "name": "Obiwan Kenobi",
   "email": "obiwan.kenobi@starwars.com",
+  "avatar": "f5aa64437a1821ffe8b563099d506aef",
   "local": true,
   "externalIdentity": "obiwan.kenobi",
   "externalProvider": "sonarqube",
   "showOnboardingTutorial": false,
-  "scmAccounts": [
-    "obiwan:github",
-    "obiwan:bitbucket"
-  ],
-  "groups": [
-    "Jedi", "Rebel"
-  ],
+  "scmAccounts": ["obiwan:github", "obiwan:bitbucket"],
+  "groups": ["Jedi", "Rebel"],
   "permissions": {
-    "global": [
-      "profileadmin",
-      "scan"
-    ]
+    "global": ["profileadmin", "scan"]
   }
 }
index 39071061930ce652c2a2490bbd0521bf0e3a3648..e3675357ce88e3c6455f6d3c04f42224f9818a43 100644 (file)
@@ -23,6 +23,7 @@ import org.junit.Before;
 import org.junit.Test;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.db.DbClient;
+import org.sonar.server.issue.ws.AvatarResolverImpl;
 import org.sonar.server.permission.ws.template.TemplateGroupsAction;
 import org.sonar.server.permission.ws.template.TemplateUsersAction;
 import org.sonar.server.user.UserSession;
@@ -43,7 +44,7 @@ public class PermissionsWsTest {
     PermissionWsSupport permissionWsSupport = mock(PermissionWsSupport.class);
 
     ws = new WsTester(new PermissionsWs(
-      new TemplateUsersAction(dbClient, userSession, permissionWsSupport),
+      new TemplateUsersAction(dbClient, userSession, permissionWsSupport, new AvatarResolverImpl()),
       new TemplateGroupsAction(dbClient, userSession, permissionWsSupport)));
   }
 
index 3227f86cf6649aebd4b41f05402d18d8fe212340..c424b43fc9917bacbfbf085f257356ebe587546e 100644 (file)
@@ -31,6 +31,7 @@ import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.issue.ws.AvatarResolverImpl;
 
 import static java.lang.String.format;
 import static org.apache.commons.lang.StringUtils.countMatches;
@@ -58,7 +59,7 @@ public class UsersActionTest extends BasePermissionWsTest<UsersAction> {
 
   @Override
   protected UsersAction buildWsAction() {
-    return new UsersAction(db.getDbClient(), userSession, newPermissionWsSupport());
+    return new UsersAction(db.getDbClient(), userSession, newPermissionWsSupport(), new AvatarResolverImpl());
   }
 
   @Test
index 69c2eadd95ed2aab3e5b77ebd1cd498d4025b4eb..959d68b549a28564439580ec64d9a9b0ef8fe745 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.issue.ws.AvatarResolverImpl;
 import org.sonar.server.permission.ws.BasePermissionWsTest;
 import org.sonar.server.ws.TestRequest;
 import org.sonarqube.ws.WsPermissions;
@@ -51,7 +52,7 @@ public class TemplateUsersActionTest extends BasePermissionWsTest<TemplateUsersA
 
   @Override
   protected TemplateUsersAction buildWsAction() {
-    return new TemplateUsersAction(db.getDbClient(), userSession, newPermissionWsSupport());
+    return new TemplateUsersAction(db.getDbClient(), userSession, newPermissionWsSupport(), new AvatarResolverImpl());
   }
 
   @Test
index a5b9f4b3f1007fb8356fe92e764c231988b3f83a..c58427b9cf4898173a439635e7f9893d0667181c 100644 (file)
@@ -27,6 +27,7 @@ import org.sonar.api.utils.System2;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbTester;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.issue.ws.AvatarResolverImpl;
 import org.sonar.server.organization.DefaultOrganizationProvider;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.tester.UserSessionRule;
@@ -52,7 +53,7 @@ public class CurrentActionTest {
 
   private DbClient dbClient = db.getDbClient();
   private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
-  private WsActionTester ws = new WsActionTester(new CurrentAction(userSessionRule, dbClient, defaultOrganizationProvider));
+  private WsActionTester ws = new WsActionTester(new CurrentAction(userSessionRule, dbClient, defaultOrganizationProvider, new AvatarResolverImpl()));
 
   @Test
   public void return_user_info() {
@@ -70,9 +71,9 @@ public class CurrentActionTest {
     CurrentWsResponse response = call();
 
     assertThat(response)
-      .extracting(CurrentWsResponse::getIsLoggedIn, CurrentWsResponse::getLogin, CurrentWsResponse::getName, CurrentWsResponse::getEmail, CurrentWsResponse::getLocal,
+      .extracting(CurrentWsResponse::getIsLoggedIn, CurrentWsResponse::getLogin, CurrentWsResponse::getName, CurrentWsResponse::getEmail, CurrentWsResponse::getAvatar, CurrentWsResponse::getLocal,
         CurrentWsResponse::getExternalIdentity, CurrentWsResponse::getExternalProvider, CurrentWsResponse::getScmAccountsList, CurrentWsResponse::getShowOnboardingTutorial)
-      .containsExactly(true, "obiwan.kenobi", "Obiwan Kenobi", "obiwan.kenobi@starwars.com", true, "obiwan", "sonarqube",
+      .containsExactly(true, "obiwan.kenobi", "Obiwan Kenobi", "obiwan.kenobi@starwars.com", "f5aa64437a1821ffe8b563099d506aef", true, "obiwan", "sonarqube",
         newArrayList("obiwan:github", "obiwan:bitbucket"), true);
   }
 
@@ -91,9 +92,9 @@ public class CurrentActionTest {
     CurrentWsResponse response = call();
 
     assertThat(response)
-      .extracting(CurrentWsResponse::getIsLoggedIn, CurrentWsResponse::getLogin, CurrentWsResponse::getName, CurrentWsResponse::getLocal,
+      .extracting(CurrentWsResponse::getIsLoggedIn, CurrentWsResponse::getLogin, CurrentWsResponse::getName, CurrentWsResponse::hasAvatar, CurrentWsResponse::getLocal,
         CurrentWsResponse::getExternalIdentity, CurrentWsResponse::getExternalProvider)
-      .containsExactly(true, "obiwan.kenobi", "Obiwan Kenobi", true, "obiwan", "sonarqube");
+      .containsExactly(true, "obiwan.kenobi", "Obiwan Kenobi", false, true, "obiwan", "sonarqube");
     assertThat(response.hasEmail()).isFalse();
     assertThat(response.getScmAccountsList()).isEmpty();
     assertThat(response.getGroupsList()).isEmpty();
index 24b0d52639448f42d62522a30632a0ed620c98b6..de5e7a08608722dbc28f3bee205eb343f9b6e320 100644 (file)
@@ -45,7 +45,7 @@ public class UsersWsTest {
     WsTester tester = new WsTester(new UsersWs(
       new CreateAction(mock(DbClient.class), mock(UserUpdater.class), userSessionRule),
       new UpdateAction(mock(UserUpdater.class), userSessionRule, mock(UserJsonWriter.class), mock(DbClient.class)),
-      new CurrentAction(userSessionRule, mock(DbClient.class), mock(DefaultOrganizationProvider.class)),
+      new CurrentAction(userSessionRule, mock(DbClient.class), mock(DefaultOrganizationProvider.class), mock(AvatarResolver.class)),
       new ChangePasswordAction(mock(DbClient.class), mock(UserUpdater.class), userSessionRule),
       new SearchAction(userSessionRule, mock(UserIndex.class), mock(DbClient.class), mock(AvatarResolver.class))));
     controller = tester.controller("api/users");
index 070b2310f07619ee8faab850130462179e667a85..67eaef0baae8106fe39eb6a7897260fd58d5c2a2 100644 (file)
@@ -8,7 +8,6 @@
     "babel-polyfill": "6.26.0",
     "backbone": "1.2.3",
     "backbone.marionette": "2.4.3",
-    "blueimp-md5": "1.1.1",
     "classnames": "2.2.5",
     "clipboard": "1.7.1",
     "create-react-class": "15.6.2",
index 68c45c921ec2f170b8607749450bfe3692b92e6f..761cff9cde4cd1b0745d8e167ea0195a768bafb4 100644 (file)
@@ -29,6 +29,7 @@ import { translate } from '../../../../helpers/l10n';
 
 /*::
 type CurrentUser = {
+  avatar?: string,
   email?: string,
   isLoggedIn: boolean,
   name: string
@@ -123,7 +124,7 @@ export default class GlobalNavUser extends React.PureComponent {
         className={classNames('dropdown js-user-authenticated', { open: this.state.open })}
         ref={node => (this.node = node)}>
         <a className="dropdown-toggle navbar-avatar" href="#" onClick={this.toggleDropdown}>
-          <Avatar email={currentUser.email} name={currentUser.name} size={24} />
+          <Avatar hash={currentUser.avatar} name={currentUser.name} size={24} />
         </a>
         {this.state.open && (
           <ul className="dropdown-menu dropdown-menu-right">
index 54f0f55ab25ab592d4c061a1b8024ffb235efa5c..4fb4f28f2d2e6e7bddfaa9978b5a3665c35b6287 100644 (file)
@@ -21,7 +21,7 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import GlobalNavUser from '../GlobalNavUser';
 
-const currentUser = { isLoggedIn: true, name: 'foo', email: 'foo@bar.baz' };
+const currentUser = { avatar: 'abcd1234', isLoggedIn: true, name: 'foo', email: 'foo@bar.baz' };
 const organizations = [
   { key: 'myorg', name: 'MyOrg' },
   { key: 'foo', name: 'Foo' },
index e77bc5d2d8d5ef3e2f07b6105fe458c462569dd1..d603a20a60f11c463b6d5abbd1968b24560b227f 100644 (file)
@@ -10,7 +10,7 @@ exports[`should not render the users organizations when they are not activated 1
     onClick={[Function]}
   >
     <Connect(Avatar)
-      email="foo@bar.baz"
+      hash="abcd1234"
       name="foo"
       size={24}
     />
@@ -83,7 +83,7 @@ exports[`should render the right interface for logged in user 1`] = `
     onClick={[Function]}
   >
     <Connect(Avatar)
-      email="foo@bar.baz"
+      hash="abcd1234"
       name="foo"
       size={24}
     />
@@ -144,7 +144,7 @@ exports[`should render the users organizations 1`] = `
     onClick={[Function]}
   >
     <Connect(Avatar)
-      email="foo@bar.baz"
+      hash="abcd1234"
       name="foo"
       size={24}
     />
@@ -283,7 +283,7 @@ exports[`should update the component correctly when the user changes to anonymou
     onClick={[Function]}
   >
     <Connect(Avatar)
-      email="foo@bar.baz"
+      hash="abcd1234"
       name="foo"
       size={24}
     />
index f6d071696a6a6240c6c0107b030c4482d8688f49..26dbe6e1f9612b50fffe1b949d232ccf4bf79e1f 100644 (file)
@@ -32,7 +32,7 @@ export default class UserCard extends React.PureComponent {
     return (
       <div className="account-user">
         <div id="avatar" className="pull-left account-user-avatar">
-          <Avatar email={user.email} name={user.name} size={60} />
+          <Avatar hash={user.avatar} name={user.name} size={60} />
         </div>
         <h1 id="name" className="pull-left">
           {user.name}
index 025d92df51ba5e870c05a1d0b8bb1f67e74b609c..a8c14f3c0c02876db7e22a62d8b68f5ee261d792 100644 (file)
@@ -272,10 +272,9 @@ export default class BulkChangeModal extends React.PureComponent {
 
   renderAssigneeOption = (option /*: { avatar?: string, email?: string, label: string } */) => (
     <span>
-      {(option.avatar != null || option.email != null) && (
+      {option.avatar != null && (
         <Avatar
           className="little-spacer-right"
-          email={option.email}
           hash={option.avatar}
           name={option.label}
           size={16}
index 832d3050075d7f7f8ca022adfe15c72b95f96d48..7c5c784dfd6396a9e1d19aa6892aa902cb615344 100644 (file)
@@ -48,7 +48,7 @@ export default class MembersListItem extends React.PureComponent {
     return (
       <tr>
         <td className="thin nowrap">
-          <Avatar hash={member.avatar} email={member.email} name={member.name} size={AVATAR_SIZE} />
+          <Avatar hash={member.avatar} name={member.name} size={AVATAR_SIZE} />
         </td>
         <td className="nowrap text-middle">
           <strong>{member.name}</strong>
index 0b0bf92735a0f64035dd3c97fbfa99939c549e20..d0b97ae588b9cb8322c09f0cc6322a9d98895259 100644 (file)
@@ -63,7 +63,7 @@ export default class UserHolder extends React.PureComponent {
         <td className="nowrap">
           {!isCreator && (
             <Avatar
-              email={user.email}
+              hash={user.avatar}
               name={user.name}
               size={36}
               className="text-middle big-spacer-right"
index 46f0794ddac5b3a3b6f2cd7c1ee8905d419ac0a0..3fecdcc4592889c6e297a9d73854a7fd0d4724c8 100644 (file)
@@ -64,7 +64,7 @@ export default class UsersSelectSearchOption extends React.PureComponent {
         onMouseEnter={this.handleMouseEnter}
         onMouseMove={this.handleMouseMove}
         title={user.name}>
-        <Avatar hash={user.avatar} email={user.email} name={user.name} size={AVATAR_SIZE} />
+        <Avatar hash={user.avatar} name={user.name} size={AVATAR_SIZE} />
         <strong className="spacer-left">{this.props.children}</strong>
         <span className="note little-spacer-left">{user.login}</span>
       </div>
index 4354b75b1e06621bb85a0557bc4296d709b541bf..347d9359b14c77415c71eb6b05f27fe502dfe559 100644 (file)
@@ -41,7 +41,7 @@ export default class UsersSelectSearchValue extends React.PureComponent {
         {user &&
           user.login && (
             <div className="Select-value-label">
-              <Avatar hash={user.avatar} email={user.email} name={user.name} size={AVATAR_SIZE} />
+              <Avatar hash={user.avatar} name={user.name} size={AVATAR_SIZE} />
               <strong className="spacer-left">{this.props.children}</strong>
               <span className="note little-spacer-left">{user.login}</span>
             </div>
index b3e752014e7c077541d829b99b30508bc13d22a0..e75246d2acbfe20e703d8bff793d5c8eb8292126 100644 (file)
@@ -8,7 +8,6 @@ exports[`should render correctly with email instead of hash 1`] = `
   title="Administrator"
 >
   <Connect(Avatar)
-    email="admin@admin.ch"
     name="Administrator"
     size={16}
   />
index 1387a28aec296b483b5a26e41e37376a67d0b06e..1aad7300b2a9bf4ce196d788433070e62925602d 100644 (file)
@@ -36,7 +36,6 @@ exports[`should render correctly with email instead of hash 1`] = `
     className="Select-value-label"
   >
     <Connect(Avatar)
-      email="admin@admin.ch"
       name="Administrator"
       size={16}
     />
index 44c303a7db1b9aa77789cb2f7cdf62bfbb6eeb21..63136b96402e31fc1f6ac90fceafa29341ea792e 100644 (file)
@@ -1,5 +1,5 @@
 <td class="thin nowrap">
-  <div>{{avatarHelper email name 36}}</div>
+  <div>{{avatarHelper avatar name 36}}</div>
 </td>
 
 <td>
index 9c6b6187c658c2877011a9d2b9962fda22591231..365ed241de187ad033662c080b89682896382340 100644 (file)
@@ -148,13 +148,7 @@ export default class SetAssigneePopup extends React.PureComponent {
             {this.state.users.map(user => (
               <SelectListItem key={user.login} item={user.login}>
                 {!!user.login && (
-                  <Avatar
-                    className="spacer-right"
-                    email={user.email}
-                    hash={user.avatar}
-                    name={user.name}
-                    size={16}
-                  />
+                  <Avatar className="spacer-right" hash={user.avatar} name={user.name} size={16} />
                 )}
                 <span
                   className="vertical-middle"
diff --git a/server/sonar-web/src/main/js/components/ui/Avatar.js b/server/sonar-web/src/main/js/components/ui/Avatar.js
deleted file mode 100644 (file)
index 448fb78..0000000
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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 React from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import md5 from 'blueimp-md5';
-import classNames from 'classnames';
-import { getGlobalSettingValue } from '../../store/rootReducer';
-
-function stringToColor(str) {
-  let hash = 0;
-  for (let i = 0; i < str.length; i++) {
-    hash = str.charCodeAt(i) + ((hash << 5) - hash);
-  }
-  let color = '#';
-  for (let i = 0; i < 3; i++) {
-    const value = (hash >> (i * 8)) & 0xff;
-    color += ('00' + value.toString(16)).substr(-2);
-  }
-  return color;
-}
-
-function getTextColor(background) {
-  const rgb = parseInt(background.substr(1), 16);
-  const r = (rgb >> 16) & 0xff;
-  const g = (rgb >> 8) & 0xff;
-  const b = (rgb >> 0) & 0xff;
-  const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
-  return luma > 140 ? '#222' : '#fff';
-}
-
-class Avatar extends React.PureComponent {
-  static propTypes = {
-    enableGravatar: PropTypes.bool.isRequired,
-    gravatarServerUrl: PropTypes.string.isRequired,
-    email: PropTypes.string,
-    hash: PropTypes.string,
-    name: PropTypes.string.isRequired,
-    size: PropTypes.number.isRequired,
-    className: PropTypes.string
-  };
-
-  renderFallback() {
-    const className = classNames(this.props.className, 'rounded');
-    const color = stringToColor(this.props.name);
-
-    let text = '';
-    const words = this.props.name.split(/\s+/).filter(word => word.length > 0);
-    if (words.length >= 2) {
-      text = words[0][0] + words[1][0];
-    } else if (this.props.name.length > 0) {
-      text = this.props.name[0];
-    }
-
-    return (
-      <div
-        className={className}
-        style={{
-          backgroundColor: color,
-          color: getTextColor(color),
-          display: 'inline-block',
-          fontSize: Math.min(this.props.size / 2, 14),
-          fontWeight: 'normal',
-          height: this.props.size,
-          lineHeight: `${this.props.size}px`,
-          textAlign: 'center',
-          verticalAlign: 'top',
-          width: this.props.size
-        }}>
-        {text.toUpperCase()}
-      </div>
-    );
-  }
-
-  render() {
-    if (!this.props.enableGravatar) {
-      return this.renderFallback();
-    }
-
-    const emailHash = this.props.hash || md5((this.props.email || '').toLowerCase()).trim();
-    const url = this.props.gravatarServerUrl
-      .replace('{EMAIL_MD5}', emailHash)
-      .replace('{SIZE}', this.props.size * 2);
-
-    const className = classNames(this.props.className, 'rounded');
-
-    return (
-      <img
-        className={className}
-        src={url}
-        width={this.props.size}
-        height={this.props.size}
-        alt={this.props.email}
-      />
-    );
-  }
-}
-
-const mapStateToProps = state => ({
-  enableGravatar: (getGlobalSettingValue(state, 'sonar.lf.enableGravatar') || {}).value === 'true',
-  gravatarServerUrl: (getGlobalSettingValue(state, 'sonar.lf.gravatarServerUrl') || {}).value
-});
-
-export default connect(mapStateToProps)(Avatar);
-
-export const unconnectedAvatar = Avatar;
diff --git a/server/sonar-web/src/main/js/components/ui/Avatar.tsx b/server/sonar-web/src/main/js/components/ui/Avatar.tsx
new file mode 100644 (file)
index 0000000..d9f09b2
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { connect } from 'react-redux';
+import * as classNames from 'classnames';
+import { getGlobalSettingValue } from '../../store/rootReducer';
+
+interface Props {
+  className?: string;
+  enableGravatar: boolean;
+  gravatarServerUrl: string;
+  hash?: string;
+  name: string;
+  size: number;
+}
+
+class Avatar extends React.PureComponent<Props> {
+  renderFallback() {
+    const className = classNames(this.props.className, 'rounded');
+    const color = stringToColor(this.props.name);
+
+    let text = '';
+    const words = this.props.name.split(/\s+/).filter(word => word.length > 0);
+    if (words.length >= 2) {
+      text = words[0][0] + words[1][0];
+    } else if (this.props.name.length > 0) {
+      text = this.props.name[0];
+    }
+
+    return (
+      <div
+        className={className}
+        style={{
+          backgroundColor: color,
+          color: getTextColor(color),
+          display: 'inline-block',
+          fontSize: Math.min(this.props.size / 2, 14),
+          fontWeight: 'normal',
+          height: this.props.size,
+          lineHeight: `${this.props.size}px`,
+          textAlign: 'center',
+          verticalAlign: 'top',
+          width: this.props.size
+        }}>
+        {text.toUpperCase()}
+      </div>
+    );
+  }
+
+  render() {
+    if (!this.props.enableGravatar || !this.props.hash) {
+      return this.renderFallback();
+    }
+
+    const url = this.props.gravatarServerUrl
+      .replace('{EMAIL_MD5}', this.props.hash)
+      .replace('{SIZE}', String(this.props.size * 2));
+
+    return (
+      <img
+        className={classNames(this.props.className, 'rounded')}
+        src={url}
+        width={this.props.size}
+        height={this.props.size}
+        alt={this.props.name}
+      />
+    );
+  }
+}
+
+const mapStateToProps = (state: any) => ({
+  enableGravatar: (getGlobalSettingValue(state, 'sonar.lf.enableGravatar') || {}).value === 'true',
+  gravatarServerUrl: (getGlobalSettingValue(state, 'sonar.lf.gravatarServerUrl') || {}).value
+});
+
+export default connect(mapStateToProps)(Avatar);
+
+export const unconnectedAvatar = Avatar;
+
+/* eslint-disable no-bitwise, no-mixed-operators */
+function stringToColor(str: string) {
+  let hash = 0;
+  for (let i = 0; i < str.length; i++) {
+    hash = str.charCodeAt(i) + ((hash << 5) - hash);
+  }
+  let color = '#';
+  for (let i = 0; i < 3; i++) {
+    const value = (hash >> (i * 8)) & 0xff;
+    color += ('00' + value.toString(16)).substr(-2);
+  }
+  return color;
+}
+
+function getTextColor(background: string) {
+  const rgb = parseInt(background.substr(1), 16);
+  const r = (rgb >> 16) & 0xff;
+  const g = (rgb >> 8) & 0xff;
+  const b = (rgb >> 0) & 0xff;
+  const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+  return luma > 140 ? '#222' : '#fff';
+}
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.js b/server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.js
deleted file mode 100644 (file)
index 044e072..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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 React from 'react';
-import { unconnectedAvatar as Avatar } from '../Avatar';
-
-const gravatarServerUrl = 'http://example.com/{EMAIL_MD5}.jpg?s={SIZE}';
-
-it.skip('should render', () => {
-  const avatar = shallow(
-    <Avatar
-      enableGravatar={true}
-      gravatarServerUrl={gravatarServerUrl}
-      email="mail@example.com"
-      name="Foo"
-      size={20}
-    />
-  );
-  expect(avatar).toMatchSnapshot();
-});
-
-it('should be able to render with hash only', () => {
-  const avatar = shallow(
-    <Avatar
-      enableGravatar={true}
-      gravatarServerUrl={gravatarServerUrl}
-      hash="7daf6c79d4802916d83f6266e24850af"
-      name="Foo"
-      size={30}
-    />
-  );
-  expect(avatar).toMatchSnapshot();
-});
-
-it('falls back to dummy avatar', () => {
-  const avatar = shallow(
-    <Avatar enableGravatar={false} gravatarServerUrl="" name="Foo Bar" size={30} />
-  );
-  expect(avatar).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.tsx b/server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.tsx
new file mode 100644 (file)
index 0000000..e42d8c0
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { shallow } from 'enzyme';
+import { unconnectedAvatar as Avatar } from '../Avatar';
+
+const gravatarServerUrl = 'http://example.com/{EMAIL_MD5}.jpg?s={SIZE}';
+
+it('should be able to render with hash only', () => {
+  const avatar = shallow(
+    <Avatar
+      enableGravatar={true}
+      gravatarServerUrl={gravatarServerUrl}
+      hash="7daf6c79d4802916d83f6266e24850af"
+      name="Foo"
+      size={30}
+    />
+  );
+  expect(avatar).toMatchSnapshot();
+});
+
+it('falls back to dummy avatar', () => {
+  const avatar = shallow(
+    <Avatar enableGravatar={false} gravatarServerUrl="" name="Foo Bar" size={30} />
+  );
+  expect(avatar).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Avatar-test.js.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Avatar-test.js.snap
deleted file mode 100644 (file)
index d51b5ec..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`falls back to dummy avatar 1`] = `
-<div
-  className="rounded"
-  style={
-    Object {
-      "backgroundColor": "#79e189",
-      "color": "#222",
-      "display": "inline-block",
-      "fontSize": 14,
-      "fontWeight": "normal",
-      "height": 30,
-      "lineHeight": "30px",
-      "textAlign": "center",
-      "verticalAlign": "top",
-      "width": 30,
-    }
-  }
->
-  FB
-</div>
-`;
-
-exports[`should be able to render with hash only 1`] = `
-<img
-  className="rounded"
-  height={30}
-  src="http://example.com/7daf6c79d4802916d83f6266e24850af.jpg?s=60"
-  width={30}
-/>
-`;
-
-exports[`should render 1`] = `
-<img
-  alt="mail@example.com"
-  className="rounded"
-  height={20}
-  src="http://example.com/7daf6c79d4802916d83f6266e24850af.jpg?s=40"
-  width={20}
-/>
-`;
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Avatar-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Avatar-test.tsx.snap
new file mode 100644 (file)
index 0000000..6dd7bc5
--- /dev/null
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`falls back to dummy avatar 1`] = `
+<div
+  className="rounded"
+  style={
+    Object {
+      "backgroundColor": "#79e189",
+      "color": "#222",
+      "display": "inline-block",
+      "fontSize": 14,
+      "fontWeight": "normal",
+      "height": 30,
+      "lineHeight": "30px",
+      "textAlign": "center",
+      "verticalAlign": "top",
+      "width": 30,
+    }
+  }
+>
+  FB
+</div>
+`;
+
+exports[`should be able to render with hash only 1`] = `
+<img
+  alt="Foo"
+  className="rounded"
+  height={30}
+  src="http://example.com/7daf6c79d4802916d83f6266e24850af.jpg?s=60"
+  width={30}
+/>
+`;
index 03e2d4190a1db714ce8a1d803fb1a0c66617f12b..1d975f433cf888520699d9fe66d306ee8f6fbb24 100644 (file)
@@ -23,11 +23,11 @@ const Handlebars = require('handlebars/runtime');
 const WithStore = require('../../components/shared/WithStore').default;
 const Avatar = require('../../components/ui/Avatar').default;
 
-module.exports = function(email, name, size) {
+module.exports = function(hash, name, size) {
   return new Handlebars.default.SafeString(
     renderToString(
       <WithStore>
-        <Avatar email={email} name={name} size={size} />
+        <Avatar hash={hash} name={name} size={size} />
       </WithStore>
     )
   );
index 8bf05a358c7ab6bbf020b32b15cfebe73dc1d65c..d7a273aa92a11f3b6f98f10452a75e9b22c72296 100644 (file)
@@ -1296,10 +1296,6 @@ bluebird@^3.4.7:
   version "3.5.0"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
 
-blueimp-md5@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-1.1.1.tgz#cf84ba18285f5c8835dae8ddae5af6468ceace17"
-
 bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
   version "4.11.8"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
index 2ed5c93bb22ada10b32a77aacf88fe4deddff8c1..6b6b1e2e50108f5cc0acb67e95f1054c4ef284f7 100644 (file)
@@ -120,6 +120,7 @@ message User {
   optional string name = 2;
   optional string email = 3;
   repeated string permissions = 4;
+  optional string avatar = 5;
 }
 
 message OldGroup {
index c71a3395f2dad490ed7be3719a89b017bb42e6f1..cf5a9d964565dc0413ab70ffe2a32b99740c5f57 100644 (file)
@@ -106,6 +106,7 @@ message CurrentWsResponse {
   repeated string groups = 9;
   optional Permissions permissions = 10;
   optional bool showOnboardingTutorial = 11;
+  optional string avatar = 12;
 
   message Permissions {
     repeated string global = 1;