diff options
Diffstat (limited to 'server/sonar-web')
26 files changed, 531 insertions, 342 deletions
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css index 72b8cbb1c27..ccfd5d9c63e 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css @@ -1,6 +1,8 @@ .navbar-context-favorite { - float: left; - padding: 7px 10px 0 0; + display: inline-block; + vertical-align: top; + padding-top: var(--gridSize); + padding-left: calc(1.5 * var(--gridSize)); } .navbar-context-title-qualifier { @@ -11,9 +13,10 @@ } .navbar-context-branches { - float: left; - padding: 8px 0 6px; - margin-left: 16px; + display: inline-block; + vertical-align: top; + padding: var(--gridSize) 0; + margin-left: calc(2 * var(--gridSize)); line-height: 16px; } diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx index 110c8b2a2ef..8f13a285693 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx @@ -25,6 +25,7 @@ import ComponentNavMeta from './ComponentNavMeta'; import ComponentNavMenu from './ComponentNavMenu'; import ComponentNavBgTaskNotif from './ComponentNavBgTaskNotif'; import RecentHistory from '../../RecentHistory'; +import * as theme from '../../../theme'; import { Branch, Component } from '../../../types'; import ContextNavBar from '../../../../components/nav/ContextNavBar'; import { getTasksForComponent, PendingTask, Task } from '../../../../api/ce'; @@ -109,16 +110,16 @@ export default class ComponentNav extends React.PureComponent<Props, State> { return ( <ContextNavBar id="context-navigation" - height={notifComponent ? 95 : 65} + height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw} notif={notifComponent}> - <ComponentNavFavorite - component={this.props.component.key} - favorite={this.props.component.isFavorite} - /> <ComponentNavBreadcrumbs component={this.props.component} breadcrumbs={this.props.component.breadcrumbs} /> + <ComponentNavFavorite + component={this.props.component.key} + favorite={this.props.component.isFavorite} + /> {this.props.currentBranch && ( <ComponentNavBranch branches={this.props.branches} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js index 8129eeb98fd..23138af7f82 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js @@ -23,10 +23,12 @@ import { connect } from 'react-redux'; import { Link } from 'react-router'; import QualifierIcon from '../../../../components/shared/QualifierIcon'; import { getOrganizationByKey, areThereCustomOrganizations } from '../../../../store/rootReducer'; +import OrganizationAvatar from '../../../../components/common/OrganizationAvatar'; import OrganizationHelmet from '../../../../components/common/OrganizationHelmet'; import OrganizationLink from '../../../../components/ui/OrganizationLink'; import PrivateBadge from '../../../../components/common/PrivateBadge'; import { collapsePath, limitComponentName } from '../../../../helpers/path'; +import { getProjectUrl } from '../../../../helpers/urls'; class ComponentNavBreadcrumbs extends React.PureComponent { static propTypes = { @@ -52,21 +54,16 @@ class ComponentNavBreadcrumbs extends React.PureComponent { const itemName = isPath ? collapsePath(item.name, 15) : limitComponentName(item.name); return ( <span key={item.key}> - {!displayOrganization && - index === 0 && ( - <span className="navbar-context-title-qualifier little-spacer-right"> - <QualifierIcon qualifier={lastItem.qualifier} /> - </span> - )} + {index === 0 && ( + <span className="navbar-context-title-qualifier spacer-right"> + <QualifierIcon qualifier={lastItem.qualifier} /> + </span> + )} <Link + className="link-base-color link-no-underline" title={item.name} - to={{ pathname: '/dashboard', query: { id: item.key } }} - className="link-base-color link-no-underline"> - {index === breadcrumbs.length - 1 ? ( - <strong>{itemName}</strong> - ) : ( - <span>{itemName}</span> - )} + to={getProjectUrl(item.key)}> + {itemName} </Link> {index < breadcrumbs.length - 1 && <span className="slash-separator" />} </span> @@ -81,12 +78,10 @@ class ComponentNavBreadcrumbs extends React.PureComponent { /> {displayOrganization && ( <span> - <span className="navbar-context-title-qualifier little-spacer-right"> - <QualifierIcon qualifier={lastItem.qualifier} /> - </span> + <OrganizationAvatar organization={organization} /> <OrganizationLink organization={organization} - className="link-base-color link-no-underline"> + className="link-base-color link-no-underline spacer-left"> {organization.name} </OrganizationLink> <span className="slash-separator" /> diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap index f92f0e066ea..1baaad7dc1f 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap @@ -2,7 +2,7 @@ exports[`renders 1`] = ` <ContextNavBar - height={95} + height={92} id="context-navigation" notif={ <ComponentNavBgTaskNotif @@ -27,9 +27,6 @@ exports[`renders 1`] = ` /> } > - <ComponentNavFavorite - component="component" - /> <ComponentNavBreadcrumbs breadcrumbs={ Array [ @@ -56,6 +53,9 @@ exports[`renders 1`] = ` } } /> + <ComponentNavFavorite + component="component" + /> <ComponentNavMeta component={ Object { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap index 259c976fd11..99cf4b7c00f 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap @@ -12,7 +12,7 @@ exports[`should not render breadcrumbs with one element 1`] = ` key="my-project" > <span - className="navbar-context-title-qualifier little-spacer-right" + className="navbar-context-title-qualifier spacer-right" > <QualifierIcon qualifier="TRK" @@ -27,14 +27,13 @@ exports[`should not render breadcrumbs with one element 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "my-project", }, } } > - <strong> - My Project - </strong> + My Project </Link> </span> </h1> @@ -54,15 +53,16 @@ exports[`should render organization 1`] = ` title="My Project" /> <span> - <span - className="navbar-context-title-qualifier little-spacer-right" - > - <QualifierIcon - qualifier="TRK" - /> - </span> + <OrganizationAvatar + organization={ + Object { + "key": "foo", + "name": "The Foo Organization", + } + } + /> <OrganizationLink - className="link-base-color link-no-underline" + className="link-base-color link-no-underline spacer-left" organization={ Object { "key": "foo", @@ -79,6 +79,13 @@ exports[`should render organization 1`] = ` <span key="my-project" > + <span + className="navbar-context-title-qualifier spacer-right" + > + <QualifierIcon + qualifier="TRK" + /> + </span> <Link className="link-base-color link-no-underline" onlyActiveOnIndex={false} @@ -88,14 +95,13 @@ exports[`should render organization 1`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "my-project", }, } } > - <strong> - My Project - </strong> + My Project </Link> </span> </h1> diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx index 3b577558541..3a5a143f963 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx @@ -17,80 +17,72 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; -import classNames from 'classnames'; +import * as React from 'react'; +import * as classNames from 'classnames'; import { sortBy } from 'lodash'; +import * as PropTypes from 'prop-types'; import { Link } from 'react-router'; import * as theme from '../../../theme'; +import { CurrentUser, LoggedInUser, isLoggedIn, Organization } from '../../../types'; import Avatar from '../../../../components/ui/Avatar'; -import OrganizationIcon from '../../../../components/icons-components/OrganizationIcon'; import OrganizationLink from '../../../../components/ui/OrganizationLink'; import { translate } from '../../../../helpers/l10n'; +import { getBaseUrl } from '../../../../helpers/urls'; +import OrganizationAvatar from '../../../../components/common/OrganizationAvatar'; -/*:: -type CurrentUser = { - avatar?: string, - email?: string, - isLoggedIn: boolean, - name: string -}; -*/ - -/*:: -type Props = { - appState: { - organizationsEnabled: boolean - }, - currentUser: CurrentUser, - fetchMyOrganizations: () => Promise<*>, - location: Object, - organizations: Array<{ isAdmin: bool, key: string, name: string }>, - router: { push: string => void } -}; -*/ - -/*:: -type State = { - open: boolean -}; -*/ - -export default class GlobalNavUser extends React.PureComponent { - /*:: node: HTMLElement; */ - /*:: props: Props; */ - state /*: State */ = { open: false }; +interface Props { + appState: { organizationsEnabled: boolean }; + currentUser: CurrentUser; + fetchMyOrganizations: () => Promise<void>; + organizations: Organization[]; +} + +interface State { + open: boolean; +} + +export default class GlobalNavUser extends React.PureComponent<Props, State> { + node?: HTMLElement | null; + + static contextTypes = { + router: PropTypes.object + }; + + constructor(props: Props) { + super(props); + this.state = { open: false }; + } componentWillUnmount() { window.removeEventListener('click', this.handleClickOutside); } - handleClickOutside = (event /*: { target: HTMLElement } */) => { - if (!this.node || !this.node.contains(event.target)) { + handleClickOutside = (event: MouseEvent) => { + if (!this.node || !this.node.contains(event.target as Node)) { this.closeDropdown(); } }; - handleLogin = (e /*: Event */) => { - e.preventDefault(); - const shouldReturnToCurrentPage = window.location.pathname !== `${window.baseUrl}/about`; + handleLogin = (event: React.SyntheticEvent<HTMLAnchorElement>) => { + event.preventDefault(); + const shouldReturnToCurrentPage = window.location.pathname !== `${getBaseUrl()}/about`; if (shouldReturnToCurrentPage) { const returnTo = encodeURIComponent(window.location.pathname + window.location.search); - window.location = - window.baseUrl + `/sessions/new?return_to=${returnTo}${window.location.hash}`; + window.location.href = + getBaseUrl() + `/sessions/new?return_to=${returnTo}${window.location.hash}`; } else { - window.location = `${window.baseUrl}/sessions/new`; + window.location.href = `${getBaseUrl()}/sessions/new`; } }; - handleLogout = (e /*: Event */) => { - e.preventDefault(); + handleLogout = (event: React.SyntheticEvent<HTMLAnchorElement>) => { + event.preventDefault(); this.closeDropdown(); - this.props.router.push('/sessions/logout'); + this.context.router.push('/sessions/logout'); }; - toggleDropdown = (evt /*: Event */) => { - evt.preventDefault(); + toggleDropdown = (event: React.SyntheticEvent<HTMLAnchorElement>) => { + event.preventDefault(); if (this.state.open) { this.closeDropdown(); } else { @@ -118,7 +110,8 @@ export default class GlobalNavUser extends React.PureComponent { }; renderAuthenticated() { - const { currentUser, organizations } = this.props; + const { organizations } = this.props; + const currentUser = this.props.currentUser as LoggedInUser; const hasOrganizations = this.props.appState.organizationsEnabled && organizations.length > 0; return ( <li @@ -167,10 +160,10 @@ export default class GlobalNavUser extends React.PureComponent { organization={organization} onClick={this.closeDropdown}> <div> - <OrganizationIcon /> + <OrganizationAvatar organization={organization} small={true} /> <span className="spacer-left">{organization.name}</span> </div> - {organization.isAdmin && ( + {organization.canAdmin && ( <span className="outline-badge spacer-left">{translate('admin')}</span> )} </OrganizationLink> @@ -199,6 +192,6 @@ export default class GlobalNavUser extends React.PureComponent { } render() { - return this.props.currentUser.isLoggedIn ? this.renderAuthenticated() : this.renderAnonymous(); + return isLoggedIn(this.props.currentUser) ? this.renderAuthenticated() : this.renderAnonymous(); } } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.tsx index c84bdff2a7a..f73200791a0 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.tsx @@ -17,19 +17,26 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import { withRouter } from 'react-router'; import { connect } from 'react-redux'; import GlobalNavUser from './GlobalNavUser'; +import { Organization } from '../../../types'; import { fetchMyOrganizations } from '../../../../apps/account/organizations/actions'; import { getMyOrganizations } from '../../../../store/rootReducer'; -const mapStateToProps = state => ({ +interface StateProps { + organizations: Organization[]; +} + +const mapStateToProps = (state: any): StateProps => ({ organizations: getMyOrganizations(state) }); +interface DispatchProps { + fetchMyOrganizations: () => Promise<void>; +} + const mapDispatchToProps = { - fetchMyOrganizations -}; + fetchMyOrganizations: fetchMyOrganizations as any +} as DispatchProps; -export default connect(mapStateToProps, mapDispatchToProps)(withRouter(GlobalNavUser)); +export default connect(mapStateToProps, mapDispatchToProps)(GlobalNavUser); 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.tsx index 4fb4f28f2d2..1ffec34be8f 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.tsx @@ -17,15 +17,15 @@ * 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 * as React from 'react'; import { shallow } from 'enzyme'; import GlobalNavUser from '../GlobalNavUser'; const currentUser = { avatar: 'abcd1234', isLoggedIn: true, name: 'foo', email: 'foo@bar.baz' }; const organizations = [ - { key: 'myorg', name: 'MyOrg' }, - { key: 'foo', name: 'Foo' }, - { key: 'bar', name: 'bar' } + { key: 'myorg', name: 'MyOrg', projectVisibility: 'public' }, + { key: 'foo', name: 'Foo', projectVisibility: 'public' }, + { key: 'bar', name: 'bar', projectVisibility: 'public' } ]; const appState = { organizationsEnabled: true }; @@ -35,7 +35,7 @@ it('should render the right interface for anonymous user', () => { <GlobalNavUser appState={appState} currentUser={currentUser} - fetchMyOrganizations={() => {}} + fetchMyOrganizations={jest.fn()} organizations={[]} /> ); @@ -47,7 +47,7 @@ it('should render the right interface for logged in user', () => { <GlobalNavUser appState={appState} currentUser={currentUser} - fetchMyOrganizations={() => {}} + fetchMyOrganizations={jest.fn()} organizations={[]} /> ); @@ -60,7 +60,7 @@ it('should render the users organizations', () => { <GlobalNavUser appState={appState} currentUser={currentUser} - fetchMyOrganizations={() => {}} + fetchMyOrganizations={jest.fn()} organizations={organizations} /> ); @@ -73,7 +73,7 @@ it('should not render the users organizations when they are not activated', () = <GlobalNavUser appState={{ organizationsEnabled: false }} currentUser={currentUser} - fetchMyOrganizations={() => {}} + fetchMyOrganizations={jest.fn()} organizations={organizations} /> ); @@ -109,9 +109,9 @@ it('should lazyload the organizations when opening the dropdown', () => { /> ); expect(fetchMyOrganizations.mock.calls.length).toBe(0); - wrapper.instance().openDropdown(); + (wrapper.instance() as GlobalNavUser).openDropdown(); expect(fetchMyOrganizations.mock.calls.length).toBe(1); - wrapper.instance().openDropdown(); + (wrapper.instance() as GlobalNavUser).openDropdown(); expect(fetchMyOrganizations.mock.calls.length).toBe(2); }); @@ -125,11 +125,11 @@ it('should update the organizations when the user changes', () => { organizations={organizations} /> ); - wrapper.instance().openDropdown(); + (wrapper.instance() as GlobalNavUser).openDropdown(); expect(fetchMyOrganizations.mock.calls.length).toBe(1); wrapper.setProps({ currentUser: { isLoggedIn: true, name: 'test', email: 'test@sonarsource.com' } }); - wrapper.instance().openDropdown(); + (wrapper.instance() as GlobalNavUser).openDropdown(); expect(fetchMyOrganizations.mock.calls.length).toBe(2); }); 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.tsx.snap index 85ca26d59b2..3bb56aac675 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.tsx.snap @@ -207,11 +207,21 @@ exports[`should render the users organizations 1`] = ` Object { "key": "bar", "name": "bar", + "projectVisibility": "public", } } > <div> - <OrganizationIcon /> + <OrganizationAvatar + organization={ + Object { + "key": "bar", + "name": "bar", + "projectVisibility": "public", + } + } + small={true} + /> <span className="spacer-left" > @@ -230,11 +240,21 @@ exports[`should render the users organizations 1`] = ` Object { "key": "foo", "name": "Foo", + "projectVisibility": "public", } } > <div> - <OrganizationIcon /> + <OrganizationAvatar + organization={ + Object { + "key": "foo", + "name": "Foo", + "projectVisibility": "public", + } + } + small={true} + /> <span className="spacer-left" > @@ -253,11 +273,21 @@ exports[`should render the users organizations 1`] = ` Object { "key": "myorg", "name": "MyOrg", + "projectVisibility": "public", } } > <div> - <OrganizationIcon /> + <OrganizationAvatar + organization={ + Object { + "key": "myorg", + "name": "MyOrg", + "projectVisibility": "public", + } + } + small={true} + /> <span className="spacer-left" > diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx index db5f9efcf95..54d91d77c75 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import * as classNames from 'classnames'; import { IndexLink, Link } from 'react-router'; +import * as theme from '../../../../app/theme'; import ContextNavBar from '../../../../components/nav/ContextNavBar'; import SettingsEditionsNotifContainer from './SettingsEditionsNotifContainer'; import NavBarTabs from '../../../../components/nav/NavBarTabs'; @@ -203,11 +204,9 @@ export default class SettingsNav extends React.PureComponent<Props> { return ( <ContextNavBar id="context-navigation" - height={notifComponent ? 95 : 65} + height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw} notif={notifComponent}> - <h1 className="navbar-context-header"> - <strong>{translate('layout.settings')}</strong> - </h1> + <h1 className="navbar-context-header">{translate('layout.settings')}</h1> <NavBarTabs> {this.renderConfigurationTab()} diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap index e33f8690286..3208552f512 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap @@ -2,15 +2,13 @@ exports[`should work with extensions 1`] = ` <ContextNavBar - height={65} + height={72} id="context-navigation" > <h1 className="navbar-context-header" > - <strong> - layout.settings - </strong> + layout.settings </h1> <NavBarTabs> <li diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js index 6e3f0fc5c51..743dae7e7a9 100644 --- a/server/sonar-web/src/main/js/app/theme.js +++ b/server/sonar-web/src/main/js/app/theme.js @@ -20,6 +20,8 @@ // IMPORTANT: any change in this file requires restart of the dev server +const grid = 8; + module.exports = { // colors blue: '#4b9fd5', @@ -48,19 +50,23 @@ module.exports = { leakBorderColor: '#eae3c7', // sizes + gridSize: `${grid}px`, + baseFontSize: '13px', smallFontSize: '12px', mediumFontSize: '14px', bigFontSize: '16px', - controlHeight: '24px', - smallControlHeight: '20px', - tinyControlHeight: '16px', + controlHeight: `${3 * grid}px`, + smallControlHeight: `${2.5 * grid}px`, + tinyControlHeight: `${2 * grid}px`, + + globalNavHeight: `${6 * grid}px`, + globalNavHeightRaw: 6 * grid, + globalNavContentHeight: `${4 * grid}px`, + globalNavContentHeightRaw: 4 * grid, - globalNavHeight: '48px', - globalNavHeightRaw: 48, - globalNavContentHeight: '32px', - globalNavContentHeightRaw: 32, + contextNavHeightRaw: 9 * grid, // different defaultShadow: '0 6px 12px rgba(0, 0, 0, 0.175)', diff --git a/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.js b/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx index 95794c931c5..75a48b2bed9 100644 --- a/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.js +++ b/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx @@ -17,8 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; import Helmet from 'react-helmet'; import { connect } from 'react-redux'; import { Link } from 'react-router'; @@ -26,41 +25,49 @@ import OrganizationsList from './OrganizationsList'; import { translate } from '../../../helpers/l10n'; import { fetchIfAnyoneCanCreateOrganizations, fetchMyOrganizations } from './actions'; import { getAppState, getMyOrganizations, getGlobalSettingValue } from '../../../store/rootReducer'; -/*:: import type { Organization } from '../../../store/organizations/duck'; */ - -class UserOrganizations extends React.PureComponent { - /*:: mounted: boolean; */ - - /*:: props: { - anyoneCanCreate?: { value: string }, - canAdmin: boolean, - children?: React.Element<*>, - organizations: Array<Organization>, - fetchIfAnyoneCanCreateOrganizations: () => Promise<*>, - fetchMyOrganizations: () => Promise<*> - }; -*/ +import { Organization } from '../../../app/types'; - state /*: { loading: boolean } */ = { - loading: true - }; +interface StateProps { + anyoneCanCreate?: { value: string }; + canAdmin: boolean; + organizations: Array<Organization>; +} + +interface DispatchProps { + fetchIfAnyoneCanCreateOrganizations: () => Promise<void>; + fetchMyOrganizations: () => Promise<void>; +} + +interface Props extends StateProps, DispatchProps { + children?: React.ReactNode; +} + +interface State { + loading: boolean; +} + +class UserOrganizations extends React.PureComponent<Props, State> { + mounted: boolean; + state: State = { loading: true }; componentDidMount() { this.mounted = true; Promise.all([ this.props.fetchMyOrganizations(), this.props.fetchIfAnyoneCanCreateOrganizations() - ]).then(() => { - if (this.mounted) { - this.setState({ loading: false }); - } - }); + ]).then(this.stopLoading, this.stopLoading); } componentWillUnmount() { this.mounted = false; } + stopLoading = () => { + if (this.mounted) { + this.setState({ loading: false }); + } + }; + render() { const anyoneCanCreate = this.props.anyoneCanCreate != null && this.props.anyoneCanCreate.value === 'true'; @@ -103,15 +110,15 @@ class UserOrganizations extends React.PureComponent { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any): StateProps => ({ anyoneCanCreate: getGlobalSettingValue(state, 'sonar.organizations.anyoneCanCreate'), canAdmin: getAppState(state).canAdmin, organizations: getMyOrganizations(state) }); const mapDispatchToProps = { - fetchMyOrganizations, - fetchIfAnyoneCanCreateOrganizations -}; + fetchMyOrganizations: fetchMyOrganizations as any, + fetchIfAnyoneCanCreateOrganizations: fetchIfAnyoneCanCreateOrganizations as any +} as DispatchProps; export default connect(mapStateToProps, mapDispatchToProps)(UserOrganizations); diff --git a/server/sonar-web/src/main/js/apps/account/organizations/actions.js b/server/sonar-web/src/main/js/apps/account/organizations/actions.ts index 38f30b159b0..f93bcdd1633 100644 --- a/server/sonar-web/src/main/js/apps/account/organizations/actions.js +++ b/server/sonar-web/src/main/js/apps/account/organizations/actions.ts @@ -17,19 +17,20 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Dispatch } from 'redux'; import { getOrganizations } from '../../../api/organizations'; import { receiveMyOrganizations } from '../../../store/organizations/duck'; import { getValues } from '../../../api/settings'; import { receiveValues } from '../../settings/store/values/actions'; -export const fetchMyOrganizations = () => dispatch => { +export const fetchMyOrganizations = () => (dispatch: Dispatch<any>) => { return getOrganizations({ member: true }).then(({ organizations }) => { return dispatch(receiveMyOrganizations(organizations)); }); }; -export const fetchIfAnyoneCanCreateOrganizations = () => dispatch => { +export const fetchIfAnyoneCanCreateOrganizations = () => (dispatch: Dispatch<any>) => { return getValues('sonar.organizations.anyoneCanCreate').then(values => { - dispatch(receiveValues(values)); + dispatch(receiveValues(values, undefined)); }); }; diff --git a/server/sonar-web/src/main/js/apps/explore/Explore.tsx b/server/sonar-web/src/main/js/apps/explore/Explore.tsx index 7cb0c776b2b..a1ed154d36a 100644 --- a/server/sonar-web/src/main/js/apps/explore/Explore.tsx +++ b/server/sonar-web/src/main/js/apps/explore/Explore.tsx @@ -19,6 +19,7 @@ */ import * as React from 'react'; import { Link } from 'react-router'; +import * as theme from '../../app/theme'; import ContextNavBar from '../../components/nav/ContextNavBar'; import NavBarTabs from '../../components/nav/NavBarTabs'; import { translate } from '../../helpers/l10n'; @@ -30,7 +31,7 @@ interface Props { export default function Explore(props: Props) { return ( <div id="explore"> - <ContextNavBar id="explore-navigation" height={65}> + <ContextNavBar id="explore-navigation" height={theme.contextNavHeightRaw}> <div className="navbar-context-header"> <h1 className="display-inline-block">{translate('explore')}</h1> </div> diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js index 43728684b84..507d43bbfb0 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js @@ -21,10 +21,11 @@ import React from 'react'; import { Link } from 'react-router'; import classNames from 'classnames'; +import * as theme from '../../../app/theme'; import { translate } from '../../../helpers/l10n'; import ContextNavBar from '../../../components/nav/ContextNavBar'; import NavBarTabs from '../../../components/nav/NavBarTabs'; -import OrganizationIcon from '../../../components/icons-components/OrganizationIcon'; +import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; import { getQualityGatesUrl } from '../../../helpers/urls'; /*:: import type { Organization } from '../../../store/organizations/duck'; */ @@ -153,14 +154,14 @@ export default class OrganizationNavigation extends React.PureComponent { const moreActive = !adminActive && location.pathname.includes('/extension/'); return ( - <ContextNavBar id="context-navigation" height={65}> + <ContextNavBar id="context-navigation" height={theme.contextNavHeightRaw}> <div className="navbar-context-header"> <h1 className="display-inline-block"> - <OrganizationIcon className="little-spacer-right" /> + <OrganizationAvatar organization={organization} /> <Link to={`/organizations/${organization.key}`} - className="link-base-color link-no-underline"> - <strong>{organization.name}</strong> + className="link-base-color link-no-underline spacer-left"> + {organization.name} </Link> </h1> {organization.description != null && ( @@ -173,9 +174,9 @@ export default class OrganizationNavigation extends React.PureComponent { </div> <div className="navbar-context-meta"> - {!!organization.avatar && ( - <img src={organization.avatar} height={30} alt={organization.name} /> - )} + <div className="text-muted"> + <strong>{translate('organization.key')}:</strong> {organization.key} + </div> {organization.url != null && ( <div> <p className="text-limited text-top"> @@ -191,7 +192,7 @@ export default class OrganizationNavigation extends React.PureComponent { )} </div> - <NavBarTabs> + <NavBarTabs className="navbar-context-tabs"> <li> <Link to={`/organizations/${organization.key}/projects`} diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap index c4dcad9c707..66184bbc5d4 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap @@ -2,7 +2,7 @@ exports[`admin 1`] = ` <ContextNavBar - height={65} + height={72} id="context-navigation" > <div @@ -11,25 +11,43 @@ exports[`admin 1`] = ` <h1 className="display-inline-block" > - <OrganizationIcon - className="little-spacer-right" + <OrganizationAvatar + organization={ + Object { + "canAdmin": true, + "canDelete": true, + "key": "foo", + "name": "Foo", + } + } /> <Link - className="link-base-color link-no-underline" + className="link-base-color link-no-underline spacer-left" onlyActiveOnIndex={false} style={Object {}} to="/organizations/foo" > - <strong> - Foo - </strong> + Foo </Link> </h1> </div> <div className="navbar-context-meta" - /> - <NavBarTabs> + > + <div + className="text-muted" + > + <strong> + organization.key + : + </strong> + + foo + </div> + </div> + <NavBarTabs + className="navbar-context-tabs" + > <li> <Link className="" @@ -187,7 +205,7 @@ exports[`admin 1`] = ` exports[`regular user 1`] = ` <ContextNavBar - height={65} + height={72} id="context-navigation" > <div @@ -196,25 +214,43 @@ exports[`regular user 1`] = ` <h1 className="display-inline-block" > - <OrganizationIcon - className="little-spacer-right" + <OrganizationAvatar + organization={ + Object { + "canAdmin": false, + "canDelete": false, + "key": "foo", + "name": "Foo", + } + } /> <Link - className="link-base-color link-no-underline" + className="link-base-color link-no-underline spacer-left" onlyActiveOnIndex={false} style={Object {}} to="/organizations/foo" > - <strong> - Foo - </strong> + Foo </Link> </h1> </div> <div className="navbar-context-meta" - /> - <NavBarTabs> + > + <div + className="text-muted" + > + <strong> + organization.key + : + </strong> + + foo + </div> + </div> + <NavBarTabs + className="navbar-context-tabs" + > <li> <Link className="" @@ -292,7 +328,7 @@ exports[`regular user 1`] = ` exports[`undeletable org 1`] = ` <ContextNavBar - height={65} + height={72} id="context-navigation" > <div @@ -301,25 +337,43 @@ exports[`undeletable org 1`] = ` <h1 className="display-inline-block" > - <OrganizationIcon - className="little-spacer-right" + <OrganizationAvatar + organization={ + Object { + "canAdmin": true, + "canDelete": false, + "key": "foo", + "name": "Foo", + } + } /> <Link - className="link-base-color link-no-underline" + className="link-base-color link-no-underline spacer-left" onlyActiveOnIndex={false} style={Object {}} to="/organizations/foo" > - <strong> - Foo - </strong> + Foo </Link> </h1> </div> <div className="navbar-context-meta" - /> - <NavBarTabs> + > + <div + className="text-muted" + > + <strong> + organization.key + : + </strong> + + foo + </div> + </div> + <NavBarTabs + className="navbar-context-tabs" + > <li> <Link className="" diff --git a/server/sonar-web/src/main/js/components/common/OrganizationAvatar.css b/server/sonar-web/src/main/js/components/common/OrganizationAvatar.css new file mode 100644 index 00000000000..9099fba90c2 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/OrganizationAvatar.css @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:contact 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. + */ +.navbar-context-avatar { + display: inline-flex; + vertical-align: top; + justify-content: center; + align-items: center; + width: calc(4 * var(--gridSize)); + height: calc(4 * var(--gridSize)); + border: 1px solid var(--barBorderColor); +} + +.navbar-context-avatar.is-empty { + border: none; +} + +.navbar-context-avatar.is-small { + width: calc(2 * var(--gridSize)); + height: calc(2 * var(--gridSize)); +} + +.navbar-context-avatar img { + vertical-align: top; + max-width: 100%; + max-height: 100%; +} + +.navbar-context-avatar img, +.navbar-context-avatar svg { + transform: none; +} diff --git a/server/sonar-web/src/main/js/components/common/OrganizationAvatar.tsx b/server/sonar-web/src/main/js/components/common/OrganizationAvatar.tsx new file mode 100644 index 00000000000..6c9ed12eca3 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/OrganizationAvatar.tsx @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:contact 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 * as classNames from 'classnames'; +import GenericAvatar from '../ui/GenericAvatar'; +import './OrganizationAvatar.css'; + +interface Props { + organization: { + avatar?: string; + name: string; + }; + small?: boolean; +} + +export default function OrganizationAvatar({ organization, small }: Props) { + return ( + <div + className={classNames('navbar-context-avatar', 'rounded', { + 'is-empty': !organization.avatar, + 'is-small': small + })}> + {organization.avatar ? ( + <img className="rounded" src={organization.avatar} alt={organization.name} /> + ) : ( + <GenericAvatar name={organization.name} size={small ? 15 : 30} /> + )} + </div> + ); +} diff --git a/server/sonar-web/src/main/js/components/icons-components/OrganizationIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/OrganizationIcon.tsx deleted file mode 100644 index dc89017c23d..00000000000 --- a/server/sonar-web/src/main/js/components/icons-components/OrganizationIcon.tsx +++ /dev/null @@ -1,40 +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 * as React from 'react'; -import * as theme from '../../app/theme'; -import { IconProps } from './types'; - -export default function OrganizationIcon({ className, fill = theme.blue, size = 16 }: IconProps) { - return ( - <svg - className={className} - width={size} - height={size} - viewBox="0 0 16 16" - version="1.1" - xmlnsXlink="http://www.w3.org/1999/xlink" - xmlSpace="preserve"> - <path - style={{ fill }} - d="M13.5 6c-.4 0-.7.1-1.1.2L11 4.8v-.3C11 3.1 9.9 2 8.5 2S6 3.1 6 4.5v.2L4.5 6.2c-.3-.1-.7-.2-1-.2C2.1 6 1 7.1 1 8.5S2.1 11 3.5 11 6 9.9 6 8.5c0-.7-.3-1.3-.7-1.7l1-1c.4.6 1 1 1.7 1.1V9c-1.1.2-2 1.2-2 2.4C6 12.9 7.1 14 8.5 14s2.5-1.1 2.5-2.5c0-1.2-.9-2.2-2-2.4V6.9c.7-.1 1.2-.5 1.6-1.1l1 1c-.4.4-.6 1-.6 1.6 0 1.4 1.1 2.5 2.5 2.5s2.5-1 2.5-2.4S14.9 6 13.5 6zm-10 4C2.7 10 2 9.3 2 8.5S2.7 7 3.5 7 5 7.7 5 8.5 4.3 10 3.5 10zm6.5 1.5c0 .8-.7 1.5-1.5 1.5S7 12.3 7 11.5 7.7 10 8.5 10s1.5.7 1.5 1.5zM8.5 6C7.7 6 7 5.3 7 4.5S7.7 3 8.5 3s1.5.7 1.5 1.5S9.3 6 8.5 6zm5 4c-.8 0-1.5-.7-1.5-1.5S12.7 7 13.5 7s1.5.7 1.5 1.5-.7 1.5-1.5 1.5z" - /> - </svg> - ); -} diff --git a/server/sonar-web/src/main/js/components/icons-components/icons.ts b/server/sonar-web/src/main/js/components/icons-components/icons.ts index d84d2ac30c4..fad7ac6b86c 100644 --- a/server/sonar-web/src/main/js/components/icons-components/icons.ts +++ b/server/sonar-web/src/main/js/components/icons-components/icons.ts @@ -40,7 +40,6 @@ export { default as LinkIcon } from './LinkIcon'; export { default as ListIcon } from './ListIcon'; export { default as LongLivingBranchIcon } from './LongLivingBranchIcon'; export { default as OpenCloseIcon } from './OpenCloseIcon'; -export { default as OrganizationIcon } from './OrganizationIcon'; export { default as PendingIcon } from './PendingIcon'; export { default as ProjectEventIcon } from './ProjectEventIcon'; export { default as PullRequestIcon } from './PullRequestIcon'; diff --git a/server/sonar-web/src/main/js/components/nav/ContextNavBar.css b/server/sonar-web/src/main/js/components/nav/ContextNavBar.css index 4aa4bd60b16..489747900bf 100644 --- a/server/sonar-web/src/main/js/components/nav/ContextNavBar.css +++ b/server/sonar-web/src/main/js/components/nav/ContextNavBar.css @@ -5,7 +5,7 @@ } .navbar-context .navbar-inner { - padding-top: 5px; + padding-top: var(--gridSize); border-bottom: 1px solid var(--barBorderColor); } @@ -14,16 +14,35 @@ } .navbar-context-header { - float: left; - line-height: 30px; - font-size: 15px; + display: inline-block; + height: calc(4 * var(--gridSize)); + line-height: calc(4 * var(--gridSize)); + font-size: var(--bigFontSize); +} + +.navbar-context-header h1 { + vertical-align: top; + line-height: calc(4 * var(--gridSize)); +} + +.navbar-context-header .slash-separator { + display: inline-block; + vertical-align: top; + height: calc(4 * var(--gridSize)); + margin-left: var(--gridSize); + margin-right: var(--gridSize); + font-size: 24px; +} + +.navbar-context-header .slash-separator::after { + color: rgba(68, 68, 68, 0.2); } .navbar-context-meta { position: absolute; top: 0; right: 0; - line-height: 30px; + line-height: calc(4 * var(--gridSize)); padding: 0 10px; color: var(--secondFontColor); font-size: var(--smallFontSize); diff --git a/server/sonar-web/src/main/js/components/nav/NavBarTabs.css b/server/sonar-web/src/main/js/components/nav/NavBarTabs.css index d08552ddf87..288ce5fd315 100644 --- a/server/sonar-web/src/main/js/components/nav/NavBarTabs.css +++ b/server/sonar-web/src/main/js/components/nav/NavBarTabs.css @@ -2,6 +2,8 @@ display: flex; align-items: center; clear: left; + height: var(--controlHeight); + margin-top: var(--gridSize); } .navbar-tabs > li + li { @@ -10,8 +12,10 @@ .navbar-tabs > li > a { display: block; - padding: 7px 0 4px; + height: var(--controlHeight); + line-height: calc(var(--controlHeight) - 6px); border-bottom: 3px solid transparent; + box-sizing: border-box; color: var(--baseFontColor); transition: none; } diff --git a/server/sonar-web/src/main/js/components/ui/Avatar.tsx b/server/sonar-web/src/main/js/components/ui/Avatar.tsx index d9f09b24076..feb5253d1cb 100644 --- a/server/sonar-web/src/main/js/components/ui/Avatar.tsx +++ b/server/sonar-web/src/main/js/components/ui/Avatar.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import { connect } from 'react-redux'; import * as classNames from 'classnames'; import { getGlobalSettingValue } from '../../store/rootReducer'; +import GenericAvatar from './GenericAvatar'; interface Props { className?: string; @@ -31,58 +32,24 @@ interface Props { 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> - ); +function Avatar(props: Props) { + if (!props.enableGravatar || !props.hash) { + return <GenericAvatar className={props.className} name={props.name} size={props.size} />; } - 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 url = props.gravatarServerUrl + .replace('{EMAIL_MD5}', props.hash) + .replace('{SIZE}', String(props.size * 2)); + + return ( + <img + className={classNames(props.className, 'rounded')} + src={url} + width={props.size} + height={props.size} + alt={props.name} + /> + ); } const mapStateToProps = (state: any) => ({ @@ -93,26 +60,3 @@ const mapStateToProps = (state: any) => ({ 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/GenericAvatar.tsx b/server/sonar-web/src/main/js/components/ui/GenericAvatar.tsx new file mode 100644 index 00000000000..36498b69b1b --- /dev/null +++ b/server/sonar-web/src/main/js/components/ui/GenericAvatar.tsx @@ -0,0 +1,81 @@ +/* + * 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 * as classNames from 'classnames'; + +interface Props { + className?: string; + name: string; + size: number; +} + +export default function GenericAvatar({ className, name, size }: Props) { + const color = stringToColor(name); + + let text = ''; + const words = name.split(/\s+/).filter(word => word.length > 0); + if (words.length >= 2) { + text = words[0][0] + words[1][0]; + } else if (name.length > 0) { + text = name[0]; + } + + return ( + <div + className={classNames(className, 'rounded')} + style={{ + backgroundColor: color, + color: getTextColor(color), + display: 'inline-block', + fontSize: Math.min(size / 2, 14), + fontWeight: 'normal', + height: size, + lineHeight: `${size}px`, + textAlign: 'center', + verticalAlign: 'top', + width: size + }}> + {text.toUpperCase()} + </div> + ); +} + +/* 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__/__snapshots__/Avatar-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Avatar-test.tsx.snap index 6dd7bc59142..0083612ce2f 100644 --- 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 @@ -1,25 +1,10 @@ // 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> +<GenericAvatar + name="Foo Bar" + size={30} +/> `; exports[`should be able to render with hash only 1`] = ` |