From: Stas Vilchik Date: Fri, 27 Oct 2017 15:05:43 +0000 (+0200) Subject: SONAR-10031 Stop computing avatar hash in web app (#2769) X-Git-Tag: 7.0-RC1~396 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=6daddd1d06a834867faccbbb6a0e60f448f2c6d4;p=sonarqube.git SONAR-10031 Stop computing avatar hash in web app (#2769) --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/ws/UsersAction.java b/server/sonar-server/src/main/java/org/sonar/server/permission/ws/UsersAction.java index 8c6f146ca52..f7538691c96 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/permission/ws/UsersAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/ws/UsersAction.java @@ -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 users, List userPermissions, Paging paging) { + private UsersWsResponse buildResponse(List users, List userPermissions, Paging paging) { Multimap 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); }); diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/TemplateUsersAction.java b/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/TemplateUsersAction.java index aadaaae16b2..0c37f59f1c0 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/TemplateUsersAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/TemplateUsersAction.java @@ -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 users, List permissionTemplateUsers, Paging paging) { + private WsPermissions.UsersWsResponse buildResponse(List users, List permissionTemplateUsers, Paging paging) { Multimap 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()) diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ws/CurrentAction.java b/server/sonar-server/src/main/java/org/sonar/server/user/ws/CurrentAction.java index 2529b920e63..9ddfdc9190b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/ws/CurrentAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/ws/CurrentAction.java @@ -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(); diff --git a/server/sonar-server/src/main/resources/org/sonar/server/permission/ws/template/template_users-example.json b/server/sonar-server/src/main/resources/org/sonar/server/permission/ws/template/template_users-example.json index 2e21d9a21f8..f33861b4444 100644 --- a/server/sonar-server/src/main/resources/org/sonar/server/permission/ws/template/template_users-example.json +++ b/server/sonar-server/src/main/resources/org/sonar/server/permission/ws/template/template_users-example.json @@ -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"] } ] } diff --git a/server/sonar-server/src/main/resources/org/sonar/server/permission/ws/users-example.json b/server/sonar-server/src/main/resources/org/sonar/server/permission/ws/users-example.json index eef7bf0f1fe..1f3e6ae34cb 100644 --- a/server/sonar-server/src/main/resources/org/sonar/server/permission/ws/users-example.json +++ b/server/sonar-server/src/main/resources/org/sonar/server/permission/ws/users-example.json @@ -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"] } ] } - diff --git a/server/sonar-server/src/main/resources/org/sonar/server/user/ws/current-example.json b/server/sonar-server/src/main/resources/org/sonar/server/user/ws/current-example.json index 77420e5fe9c..359d2f79961 100644 --- a/server/sonar-server/src/main/resources/org/sonar/server/user/ws/current-example.json +++ b/server/sonar-server/src/main/resources/org/sonar/server/user/ws/current-example.json @@ -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"] } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/ws/PermissionsWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/permission/ws/PermissionsWsTest.java index 39071061930..e3675357ce8 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/permission/ws/PermissionsWsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/ws/PermissionsWsTest.java @@ -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))); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/ws/UsersActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/permission/ws/UsersActionTest.java index 3227f86cf66..c424b43fc99 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/permission/ws/UsersActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/ws/UsersActionTest.java @@ -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 { @Override protected UsersAction buildWsAction() { - return new UsersAction(db.getDbClient(), userSession, newPermissionWsSupport()); + return new UsersAction(db.getDbClient(), userSession, newPermissionWsSupport(), new AvatarResolverImpl()); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/TemplateUsersActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/TemplateUsersActionTest.java index 69c2eadd95e..959d68b549a 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/TemplateUsersActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/TemplateUsersActionTest.java @@ -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 (this.node = node)}> - + {this.state.open && (
    diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.js b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.js index 54f0f55ab25..4fb4f28f2d2 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.js @@ -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' }, diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.js.snap index e77bc5d2d8d..d603a20a60f 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.js.snap @@ -10,7 +10,7 @@ exports[`should not render the users organizations when they are not activated 1 onClick={[Function]} > @@ -83,7 +83,7 @@ exports[`should render the right interface for logged in user 1`] = ` onClick={[Function]} > @@ -144,7 +144,7 @@ exports[`should render the users organizations 1`] = ` onClick={[Function]} > @@ -283,7 +283,7 @@ exports[`should update the component correctly when the user changes to anonymou onClick={[Function]} > diff --git a/server/sonar-web/src/main/js/apps/account/components/UserCard.js b/server/sonar-web/src/main/js/apps/account/components/UserCard.js index f6d071696a6..26dbe6e1f96 100644 --- a/server/sonar-web/src/main/js/apps/account/components/UserCard.js +++ b/server/sonar-web/src/main/js/apps/account/components/UserCard.js @@ -32,7 +32,7 @@ export default class UserCard extends React.PureComponent { return (
    - +

    {user.name} diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js index 025d92df51b..a8c14f3c0c0 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js +++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js @@ -272,10 +272,9 @@ export default class BulkChangeModal extends React.PureComponent { renderAssigneeOption = (option /*: { avatar?: string, email?: string, label: string } */) => ( - {(option.avatar != null || option.email != null) && ( + {option.avatar != null && ( - + {member.name} diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js index 0b0bf92735a..d0b97ae588b 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js @@ -63,7 +63,7 @@ export default class UserHolder extends React.PureComponent { {!isCreator && ( - + {this.props.children} {user.login}

    diff --git a/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchValue.js b/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchValue.js index 4354b75b1e0..347d9359b14 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchValue.js +++ b/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchValue.js @@ -41,7 +41,7 @@ export default class UsersSelectSearchValue extends React.PureComponent { {user && user.login && (
    - + {this.props.children} {user.login}
    diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchOption-test.js.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchOption-test.js.snap index b3e752014e7..e75246d2acb 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchOption-test.js.snap +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchOption-test.js.snap @@ -8,7 +8,6 @@ exports[`should render correctly with email instead of hash 1`] = ` title="Administrator" > diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchValue-test.js.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchValue-test.js.snap index 1387a28aec2..1aad7300b2a 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchValue-test.js.snap +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchValue-test.js.snap @@ -36,7 +36,6 @@ exports[`should render correctly with email instead of hash 1`] = ` className="Select-value-label" > diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs index 44c303a7db1..63136b96402 100644 --- a/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs +++ b/server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs @@ -1,5 +1,5 @@ -
    {{avatarHelper email name 36}}
    +
    {{avatarHelper avatar name 36}}
    diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js index 9c6b6187c65..365ed241de1 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js @@ -148,13 +148,7 @@ export default class SetAssigneePopup extends React.PureComponent { {this.state.users.map(user => ( {!!user.login && ( - + )} > (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 ( -
    - {text.toUpperCase()} -
    - ); - } - - 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 ( - {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 index 00000000000..d9f09b24076 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/Avatar.tsx @@ -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 { + 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 ( +
    + {text.toUpperCase()} +
    + ); + } + + 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 ( + {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 index 044e07240b1..00000000000 --- a/server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.js +++ /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( - - ); - expect(avatar).toMatchSnapshot(); -}); - -it('should be able to render with hash only', () => { - const avatar = shallow( - - ); - expect(avatar).toMatchSnapshot(); -}); - -it('falls back to dummy avatar', () => { - const avatar = shallow( - - ); - 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 index 00000000000..e42d8c0d454 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/__tests__/Avatar-test.tsx @@ -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( + + ); + expect(avatar).toMatchSnapshot(); +}); + +it('falls back to dummy avatar', () => { + const avatar = shallow( + + ); + 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 index d51b5ecabcb..00000000000 --- a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Avatar-test.js.snap +++ /dev/null @@ -1,42 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`falls back to dummy avatar 1`] = ` -
    - FB -
    -`; - -exports[`should be able to render with hash only 1`] = ` - -`; - -exports[`should render 1`] = ` -mail@example.com -`; 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 index 00000000000..6dd7bc59142 --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Avatar-test.tsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`falls back to dummy avatar 1`] = ` +
    + FB +
    +`; + +exports[`should be able to render with hash only 1`] = ` +Foo +`; diff --git a/server/sonar-web/src/main/js/helpers/handlebars/avatarHelper.js b/server/sonar-web/src/main/js/helpers/handlebars/avatarHelper.js index 03e2d4190a1..1d975f433cf 100644 --- a/server/sonar-web/src/main/js/helpers/handlebars/avatarHelper.js +++ b/server/sonar-web/src/main/js/helpers/handlebars/avatarHelper.js @@ -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( - + ) ); diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index 8bf05a358c7..d7a273aa92a 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -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" diff --git a/sonar-ws/src/main/protobuf/ws-permissions.proto b/sonar-ws/src/main/protobuf/ws-permissions.proto index 2ed5c93bb22..6b6b1e2e501 100644 --- a/sonar-ws/src/main/protobuf/ws-permissions.proto +++ b/sonar-ws/src/main/protobuf/ws-permissions.proto @@ -120,6 +120,7 @@ message User { optional string name = 2; optional string email = 3; repeated string permissions = 4; + optional string avatar = 5; } message OldGroup { diff --git a/sonar-ws/src/main/protobuf/ws-users.proto b/sonar-ws/src/main/protobuf/ws-users.proto index c71a3395f2d..cf5a9d96456 100644 --- a/sonar-ws/src/main/protobuf/ws-users.proto +++ b/sonar-ws/src/main/protobuf/ws-users.proto @@ -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;