]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8829 move organization avatar to the left
authorStas Vilchik <stas.vilchik@sonarsource.com>
Thu, 30 Nov 2017 12:41:38 +0000 (13:41 +0100)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 11 Dec 2017 17:00:33 +0000 (18:00 +0100)
33 files changed:
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js [deleted file]
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.js [deleted file]
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.js [deleted file]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.js.snap [deleted file]
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
server/sonar-web/src/main/js/app/theme.js
server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.js [deleted file]
server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/organizations/actions.js [deleted file]
server/sonar-web/src/main/js/apps/account/organizations/actions.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/explore/Explore.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap
server/sonar-web/src/main/js/components/common/OrganizationAvatar.css [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/OrganizationAvatar.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/icons-components/OrganizationIcon.tsx [deleted file]
server/sonar-web/src/main/js/components/icons-components/icons.ts
server/sonar-web/src/main/js/components/nav/ContextNavBar.css
server/sonar-web/src/main/js/components/nav/NavBarTabs.css
server/sonar-web/src/main/js/components/ui/Avatar.tsx
server/sonar-web/src/main/js/components/ui/GenericAvatar.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/Avatar-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 72b8cbb1c2798390c9b08741ebd536e5d597ce99..ccfd5d9c63e497e7e61e06dc5d04de81e19a9a4b 100644 (file)
@@ -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 {
 }
 
 .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;
 }
 
index 110c8b2a2efa3ea3024251c403ea78e1e67e7124..8f13a285693d71c1d2a3319d67e4c168c4f4d7f4 100644 (file)
@@ -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}
index 8129eeb98fdf12c8817affb9dd57e73704a31e25..23138af7f826565551a7fbeb622540328ee48e6a 100644 (file)
@@ -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" />
index f92f0e066eaf9be2d1da39d2f57bad1552dc5ebc..1baaad7dc1fdb98820f2214b38695f71a38a6ea1 100644 (file)
@@ -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 {
index 259c976fd1111feefe138f2db9822a486d55fc7d..99cf4b7c00f73d4e416bf27df20553fa08524e1b 100644 (file)
@@ -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.js
deleted file mode 100644 (file)
index 3b57755..0000000
+++ /dev/null
@@ -1,204 +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.
- */
-// @flow
-import React from 'react';
-import classNames from 'classnames';
-import { sortBy } from 'lodash';
-import { Link } from 'react-router';
-import * as theme from '../../../theme';
-import Avatar from '../../../../components/ui/Avatar';
-import OrganizationIcon from '../../../../components/icons-components/OrganizationIcon';
-import OrganizationLink from '../../../../components/ui/OrganizationLink';
-import { translate } from '../../../../helpers/l10n';
-
-/*::
-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 };
-
-  componentWillUnmount() {
-    window.removeEventListener('click', this.handleClickOutside);
-  }
-
-  handleClickOutside = (event /*: { target: HTMLElement } */) => {
-    if (!this.node || !this.node.contains(event.target)) {
-      this.closeDropdown();
-    }
-  };
-
-  handleLogin = (e /*: Event */) => {
-    e.preventDefault();
-    const shouldReturnToCurrentPage = window.location.pathname !== `${window.baseUrl}/about`;
-    if (shouldReturnToCurrentPage) {
-      const returnTo = encodeURIComponent(window.location.pathname + window.location.search);
-      window.location =
-        window.baseUrl + `/sessions/new?return_to=${returnTo}${window.location.hash}`;
-    } else {
-      window.location = `${window.baseUrl}/sessions/new`;
-    }
-  };
-
-  handleLogout = (e /*: Event */) => {
-    e.preventDefault();
-    this.closeDropdown();
-    this.props.router.push('/sessions/logout');
-  };
-
-  toggleDropdown = (evt /*: Event */) => {
-    evt.preventDefault();
-    if (this.state.open) {
-      this.closeDropdown();
-    } else {
-      this.openDropdown();
-    }
-  };
-
-  openDropdown = () => {
-    this.fetchMyOrganizations().then(() => {
-      window.addEventListener('click', this.handleClickOutside, true);
-      this.setState({ open: true });
-    });
-  };
-
-  closeDropdown = () => {
-    window.removeEventListener('click', this.handleClickOutside);
-    this.setState({ open: false });
-  };
-
-  fetchMyOrganizations = () => {
-    if (this.props.appState.organizationsEnabled) {
-      return this.props.fetchMyOrganizations();
-    }
-    return Promise.resolve();
-  };
-
-  renderAuthenticated() {
-    const { currentUser, organizations } = this.props;
-    const hasOrganizations = this.props.appState.organizationsEnabled && organizations.length > 0;
-    return (
-      <li
-        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
-            hash={currentUser.avatar}
-            name={currentUser.name}
-            size={theme.globalNavContentHeightRaw}
-          />
-        </a>
-        {this.state.open && (
-          <ul className="dropdown-menu dropdown-menu-right">
-            <li className="dropdown-item">
-              <div className="text-ellipsis text-muted" title={currentUser.name}>
-                <strong>{currentUser.name}</strong>
-              </div>
-              {currentUser.email != null && (
-                <div
-                  className="little-spacer-top text-ellipsis text-muted"
-                  title={currentUser.email}>
-                  {currentUser.email}
-                </div>
-              )}
-            </li>
-            <li className="divider" />
-            <li>
-              <Link to="/account" onClick={this.closeDropdown}>
-                {translate('my_account.page')}
-              </Link>
-            </li>
-            {hasOrganizations && <li role="separator" className="divider" />}
-            {hasOrganizations && (
-              <li>
-                <Link to="/account/organizations" onClick={this.closeDropdown}>
-                  {translate('my_organizations')}
-                </Link>
-              </li>
-            )}
-            {hasOrganizations &&
-              sortBy(organizations, org => org.name.toLowerCase()).map(organization => (
-                <li key={organization.key}>
-                  <OrganizationLink
-                    className="dropdown-item-flex"
-                    organization={organization}
-                    onClick={this.closeDropdown}>
-                    <div>
-                      <OrganizationIcon />
-                      <span className="spacer-left">{organization.name}</span>
-                    </div>
-                    {organization.isAdmin && (
-                      <span className="outline-badge spacer-left">{translate('admin')}</span>
-                    )}
-                  </OrganizationLink>
-                </li>
-              ))}
-            {hasOrganizations && <li role="separator" className="divider" />}
-            <li>
-              <a onClick={this.handleLogout} href="#">
-                {translate('layout.logout')}
-              </a>
-            </li>
-          </ul>
-        )}
-      </li>
-    );
-  }
-
-  renderAnonymous() {
-    return (
-      <li>
-        <a className="navbar-login" onClick={this.handleLogin} href="#">
-          {translate('layout.login')}
-        </a>
-      </li>
-    );
-  }
-
-  render() {
-    return this.props.currentUser.isLoggedIn ? this.renderAuthenticated() : this.renderAnonymous();
-  }
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
new file mode 100644 (file)
index 0000000..3a5a143
--- /dev/null
@@ -0,0 +1,197 @@
+/*
+ * 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';
+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 OrganizationLink from '../../../../components/ui/OrganizationLink';
+import { translate } from '../../../../helpers/l10n';
+import { getBaseUrl } from '../../../../helpers/urls';
+import OrganizationAvatar from '../../../../components/common/OrganizationAvatar';
+
+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: MouseEvent) => {
+    if (!this.node || !this.node.contains(event.target as Node)) {
+      this.closeDropdown();
+    }
+  };
+
+  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.href =
+        getBaseUrl() + `/sessions/new?return_to=${returnTo}${window.location.hash}`;
+    } else {
+      window.location.href = `${getBaseUrl()}/sessions/new`;
+    }
+  };
+
+  handleLogout = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    this.closeDropdown();
+    this.context.router.push('/sessions/logout');
+  };
+
+  toggleDropdown = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    if (this.state.open) {
+      this.closeDropdown();
+    } else {
+      this.openDropdown();
+    }
+  };
+
+  openDropdown = () => {
+    this.fetchMyOrganizations().then(() => {
+      window.addEventListener('click', this.handleClickOutside, true);
+      this.setState({ open: true });
+    });
+  };
+
+  closeDropdown = () => {
+    window.removeEventListener('click', this.handleClickOutside);
+    this.setState({ open: false });
+  };
+
+  fetchMyOrganizations = () => {
+    if (this.props.appState.organizationsEnabled) {
+      return this.props.fetchMyOrganizations();
+    }
+    return Promise.resolve();
+  };
+
+  renderAuthenticated() {
+    const { organizations } = this.props;
+    const currentUser = this.props.currentUser as LoggedInUser;
+    const hasOrganizations = this.props.appState.organizationsEnabled && organizations.length > 0;
+    return (
+      <li
+        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
+            hash={currentUser.avatar}
+            name={currentUser.name}
+            size={theme.globalNavContentHeightRaw}
+          />
+        </a>
+        {this.state.open && (
+          <ul className="dropdown-menu dropdown-menu-right">
+            <li className="dropdown-item">
+              <div className="text-ellipsis text-muted" title={currentUser.name}>
+                <strong>{currentUser.name}</strong>
+              </div>
+              {currentUser.email != null && (
+                <div
+                  className="little-spacer-top text-ellipsis text-muted"
+                  title={currentUser.email}>
+                  {currentUser.email}
+                </div>
+              )}
+            </li>
+            <li className="divider" />
+            <li>
+              <Link to="/account" onClick={this.closeDropdown}>
+                {translate('my_account.page')}
+              </Link>
+            </li>
+            {hasOrganizations && <li role="separator" className="divider" />}
+            {hasOrganizations && (
+              <li>
+                <Link to="/account/organizations" onClick={this.closeDropdown}>
+                  {translate('my_organizations')}
+                </Link>
+              </li>
+            )}
+            {hasOrganizations &&
+              sortBy(organizations, org => org.name.toLowerCase()).map(organization => (
+                <li key={organization.key}>
+                  <OrganizationLink
+                    className="dropdown-item-flex"
+                    organization={organization}
+                    onClick={this.closeDropdown}>
+                    <div>
+                      <OrganizationAvatar organization={organization} small={true} />
+                      <span className="spacer-left">{organization.name}</span>
+                    </div>
+                    {organization.canAdmin && (
+                      <span className="outline-badge spacer-left">{translate('admin')}</span>
+                    )}
+                  </OrganizationLink>
+                </li>
+              ))}
+            {hasOrganizations && <li role="separator" className="divider" />}
+            <li>
+              <a onClick={this.handleLogout} href="#">
+                {translate('layout.logout')}
+              </a>
+            </li>
+          </ul>
+        )}
+      </li>
+    );
+  }
+
+  renderAnonymous() {
+    return (
+      <li>
+        <a className="navbar-login" onClick={this.handleLogin} href="#">
+          {translate('layout.login')}
+        </a>
+      </li>
+    );
+  }
+
+  render() {
+    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.js
deleted file mode 100644 (file)
index c84bdff..0000000
+++ /dev/null
@@ -1,35 +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.
- */
-// @flow
-import { withRouter } from 'react-router';
-import { connect } from 'react-redux';
-import GlobalNavUser from './GlobalNavUser';
-import { fetchMyOrganizations } from '../../../../apps/account/organizations/actions';
-import { getMyOrganizations } from '../../../../store/rootReducer';
-
-const mapStateToProps = state => ({
-  organizations: getMyOrganizations(state)
-});
-
-const mapDispatchToProps = {
-  fetchMyOrganizations
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(withRouter(GlobalNavUser));
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUserContainer.tsx
new file mode 100644 (file)
index 0000000..f732007
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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 { connect } from 'react-redux';
+import GlobalNavUser from './GlobalNavUser';
+import { Organization } from '../../../types';
+import { fetchMyOrganizations } from '../../../../apps/account/organizations/actions';
+import { getMyOrganizations } from '../../../../store/rootReducer';
+
+interface StateProps {
+  organizations: Organization[];
+}
+
+const mapStateToProps = (state: any): StateProps => ({
+  organizations: getMyOrganizations(state)
+});
+
+interface DispatchProps {
+  fetchMyOrganizations: () => Promise<void>;
+}
+
+const mapDispatchToProps = {
+  fetchMyOrganizations: fetchMyOrganizations as any
+} as DispatchProps;
+
+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.js
deleted file mode 100644 (file)
index 4fb4f28..0000000
+++ /dev/null
@@ -1,135 +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 { 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' }
-];
-const appState = { organizationsEnabled: true };
-
-it('should render the right interface for anonymous user', () => {
-  const currentUser = { isLoggedIn: false };
-  const wrapper = shallow(
-    <GlobalNavUser
-      appState={appState}
-      currentUser={currentUser}
-      fetchMyOrganizations={() => {}}
-      organizations={[]}
-    />
-  );
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should render the right interface for logged in user', () => {
-  const wrapper = shallow(
-    <GlobalNavUser
-      appState={appState}
-      currentUser={currentUser}
-      fetchMyOrganizations={() => {}}
-      organizations={[]}
-    />
-  );
-  wrapper.setState({ open: true });
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should render the users organizations', () => {
-  const wrapper = shallow(
-    <GlobalNavUser
-      appState={appState}
-      currentUser={currentUser}
-      fetchMyOrganizations={() => {}}
-      organizations={organizations}
-    />
-  );
-  wrapper.setState({ open: true });
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should not render the users organizations when they are not activated', () => {
-  const wrapper = shallow(
-    <GlobalNavUser
-      appState={{ organizationsEnabled: false }}
-      currentUser={currentUser}
-      fetchMyOrganizations={() => {}}
-      organizations={organizations}
-    />
-  );
-  wrapper.setState({ open: true });
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should update the component correctly when the user changes to anonymous', () => {
-  const fetchMyOrganizations = jest.fn();
-  const wrapper = shallow(
-    <GlobalNavUser
-      appState={appState}
-      currentUser={currentUser}
-      fetchMyOrganizations={fetchMyOrganizations}
-      organizations={[]}
-    />
-  );
-  wrapper.setState({ open: true });
-  expect(wrapper).toMatchSnapshot();
-  wrapper.setProps({ currentUser: { isLoggedIn: false } });
-  expect(fetchMyOrganizations.mock.calls.length).toBe(0);
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should lazyload the organizations when opening the dropdown', () => {
-  const fetchMyOrganizations = jest.fn(() => Promise.resolve());
-  const wrapper = shallow(
-    <GlobalNavUser
-      appState={appState}
-      currentUser={currentUser}
-      fetchMyOrganizations={fetchMyOrganizations}
-      organizations={organizations}
-    />
-  );
-  expect(fetchMyOrganizations.mock.calls.length).toBe(0);
-  wrapper.instance().openDropdown();
-  expect(fetchMyOrganizations.mock.calls.length).toBe(1);
-  wrapper.instance().openDropdown();
-  expect(fetchMyOrganizations.mock.calls.length).toBe(2);
-});
-
-it('should update the organizations when the user changes', () => {
-  const fetchMyOrganizations = jest.fn(() => Promise.resolve());
-  const wrapper = shallow(
-    <GlobalNavUser
-      appState={appState}
-      currentUser={currentUser}
-      fetchMyOrganizations={fetchMyOrganizations}
-      organizations={organizations}
-    />
-  );
-  wrapper.instance().openDropdown();
-  expect(fetchMyOrganizations.mock.calls.length).toBe(1);
-  wrapper.setProps({
-    currentUser: { isLoggedIn: true, name: 'test', email: 'test@sonarsource.com' }
-  });
-  wrapper.instance().openDropdown();
-  expect(fetchMyOrganizations.mock.calls.length).toBe(2);
-});
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx
new file mode 100644 (file)
index 0000000..1ffec34
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * 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 GlobalNavUser from '../GlobalNavUser';
+
+const currentUser = { avatar: 'abcd1234', isLoggedIn: true, name: 'foo', email: 'foo@bar.baz' };
+const organizations = [
+  { key: 'myorg', name: 'MyOrg', projectVisibility: 'public' },
+  { key: 'foo', name: 'Foo', projectVisibility: 'public' },
+  { key: 'bar', name: 'bar', projectVisibility: 'public' }
+];
+const appState = { organizationsEnabled: true };
+
+it('should render the right interface for anonymous user', () => {
+  const currentUser = { isLoggedIn: false };
+  const wrapper = shallow(
+    <GlobalNavUser
+      appState={appState}
+      currentUser={currentUser}
+      fetchMyOrganizations={jest.fn()}
+      organizations={[]}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render the right interface for logged in user', () => {
+  const wrapper = shallow(
+    <GlobalNavUser
+      appState={appState}
+      currentUser={currentUser}
+      fetchMyOrganizations={jest.fn()}
+      organizations={[]}
+    />
+  );
+  wrapper.setState({ open: true });
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should render the users organizations', () => {
+  const wrapper = shallow(
+    <GlobalNavUser
+      appState={appState}
+      currentUser={currentUser}
+      fetchMyOrganizations={jest.fn()}
+      organizations={organizations}
+    />
+  );
+  wrapper.setState({ open: true });
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should not render the users organizations when they are not activated', () => {
+  const wrapper = shallow(
+    <GlobalNavUser
+      appState={{ organizationsEnabled: false }}
+      currentUser={currentUser}
+      fetchMyOrganizations={jest.fn()}
+      organizations={organizations}
+    />
+  );
+  wrapper.setState({ open: true });
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should update the component correctly when the user changes to anonymous', () => {
+  const fetchMyOrganizations = jest.fn();
+  const wrapper = shallow(
+    <GlobalNavUser
+      appState={appState}
+      currentUser={currentUser}
+      fetchMyOrganizations={fetchMyOrganizations}
+      organizations={[]}
+    />
+  );
+  wrapper.setState({ open: true });
+  expect(wrapper).toMatchSnapshot();
+  wrapper.setProps({ currentUser: { isLoggedIn: false } });
+  expect(fetchMyOrganizations.mock.calls.length).toBe(0);
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should lazyload the organizations when opening the dropdown', () => {
+  const fetchMyOrganizations = jest.fn(() => Promise.resolve());
+  const wrapper = shallow(
+    <GlobalNavUser
+      appState={appState}
+      currentUser={currentUser}
+      fetchMyOrganizations={fetchMyOrganizations}
+      organizations={organizations}
+    />
+  );
+  expect(fetchMyOrganizations.mock.calls.length).toBe(0);
+  (wrapper.instance() as GlobalNavUser).openDropdown();
+  expect(fetchMyOrganizations.mock.calls.length).toBe(1);
+  (wrapper.instance() as GlobalNavUser).openDropdown();
+  expect(fetchMyOrganizations.mock.calls.length).toBe(2);
+});
+
+it('should update the organizations when the user changes', () => {
+  const fetchMyOrganizations = jest.fn(() => Promise.resolve());
+  const wrapper = shallow(
+    <GlobalNavUser
+      appState={appState}
+      currentUser={currentUser}
+      fetchMyOrganizations={fetchMyOrganizations}
+      organizations={organizations}
+    />
+  );
+  (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() 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.js.snap
deleted file mode 100644 (file)
index 85ca26d..0000000
+++ /dev/null
@@ -1,356 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should not render the users organizations when they are not activated 1`] = `
-<li
-  className="dropdown js-user-authenticated open"
->
-  <a
-    className="dropdown-toggle navbar-avatar"
-    href="#"
-    onClick={[Function]}
-  >
-    <Connect(Avatar)
-      hash="abcd1234"
-      name="foo"
-      size={32}
-    />
-  </a>
-  <ul
-    className="dropdown-menu dropdown-menu-right"
-  >
-    <li
-      className="dropdown-item"
-    >
-      <div
-        className="text-ellipsis text-muted"
-        title="foo"
-      >
-        <strong>
-          foo
-        </strong>
-      </div>
-      <div
-        className="little-spacer-top text-ellipsis text-muted"
-        title="foo@bar.baz"
-      >
-        foo@bar.baz
-      </div>
-    </li>
-    <li
-      className="divider"
-    />
-    <li>
-      <Link
-        onClick={[Function]}
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/account"
-      >
-        my_account.page
-      </Link>
-    </li>
-    <li>
-      <a
-        href="#"
-        onClick={[Function]}
-      >
-        layout.logout
-      </a>
-    </li>
-  </ul>
-</li>
-`;
-
-exports[`should render the right interface for anonymous user 1`] = `
-<li>
-  <a
-    className="navbar-login"
-    href="#"
-    onClick={[Function]}
-  >
-    layout.login
-  </a>
-</li>
-`;
-
-exports[`should render the right interface for logged in user 1`] = `
-<li
-  className="dropdown js-user-authenticated open"
->
-  <a
-    className="dropdown-toggle navbar-avatar"
-    href="#"
-    onClick={[Function]}
-  >
-    <Connect(Avatar)
-      hash="abcd1234"
-      name="foo"
-      size={32}
-    />
-  </a>
-  <ul
-    className="dropdown-menu dropdown-menu-right"
-  >
-    <li
-      className="dropdown-item"
-    >
-      <div
-        className="text-ellipsis text-muted"
-        title="foo"
-      >
-        <strong>
-          foo
-        </strong>
-      </div>
-      <div
-        className="little-spacer-top text-ellipsis text-muted"
-        title="foo@bar.baz"
-      >
-        foo@bar.baz
-      </div>
-    </li>
-    <li
-      className="divider"
-    />
-    <li>
-      <Link
-        onClick={[Function]}
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/account"
-      >
-        my_account.page
-      </Link>
-    </li>
-    <li>
-      <a
-        href="#"
-        onClick={[Function]}
-      >
-        layout.logout
-      </a>
-    </li>
-  </ul>
-</li>
-`;
-
-exports[`should render the users organizations 1`] = `
-<li
-  className="dropdown js-user-authenticated open"
->
-  <a
-    className="dropdown-toggle navbar-avatar"
-    href="#"
-    onClick={[Function]}
-  >
-    <Connect(Avatar)
-      hash="abcd1234"
-      name="foo"
-      size={32}
-    />
-  </a>
-  <ul
-    className="dropdown-menu dropdown-menu-right"
-  >
-    <li
-      className="dropdown-item"
-    >
-      <div
-        className="text-ellipsis text-muted"
-        title="foo"
-      >
-        <strong>
-          foo
-        </strong>
-      </div>
-      <div
-        className="little-spacer-top text-ellipsis text-muted"
-        title="foo@bar.baz"
-      >
-        foo@bar.baz
-      </div>
-    </li>
-    <li
-      className="divider"
-    />
-    <li>
-      <Link
-        onClick={[Function]}
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/account"
-      >
-        my_account.page
-      </Link>
-    </li>
-    <li
-      className="divider"
-      role="separator"
-    />
-    <li>
-      <Link
-        onClick={[Function]}
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/account/organizations"
-      >
-        my_organizations
-      </Link>
-    </li>
-    <li
-      key="bar"
-    >
-      <OrganizationLink
-        className="dropdown-item-flex"
-        onClick={[Function]}
-        organization={
-          Object {
-            "key": "bar",
-            "name": "bar",
-          }
-        }
-      >
-        <div>
-          <OrganizationIcon />
-          <span
-            className="spacer-left"
-          >
-            bar
-          </span>
-        </div>
-      </OrganizationLink>
-    </li>
-    <li
-      key="foo"
-    >
-      <OrganizationLink
-        className="dropdown-item-flex"
-        onClick={[Function]}
-        organization={
-          Object {
-            "key": "foo",
-            "name": "Foo",
-          }
-        }
-      >
-        <div>
-          <OrganizationIcon />
-          <span
-            className="spacer-left"
-          >
-            Foo
-          </span>
-        </div>
-      </OrganizationLink>
-    </li>
-    <li
-      key="myorg"
-    >
-      <OrganizationLink
-        className="dropdown-item-flex"
-        onClick={[Function]}
-        organization={
-          Object {
-            "key": "myorg",
-            "name": "MyOrg",
-          }
-        }
-      >
-        <div>
-          <OrganizationIcon />
-          <span
-            className="spacer-left"
-          >
-            MyOrg
-          </span>
-        </div>
-      </OrganizationLink>
-    </li>
-    <li
-      className="divider"
-      role="separator"
-    />
-    <li>
-      <a
-        href="#"
-        onClick={[Function]}
-      >
-        layout.logout
-      </a>
-    </li>
-  </ul>
-</li>
-`;
-
-exports[`should update the component correctly when the user changes to anonymous 1`] = `
-<li
-  className="dropdown js-user-authenticated open"
->
-  <a
-    className="dropdown-toggle navbar-avatar"
-    href="#"
-    onClick={[Function]}
-  >
-    <Connect(Avatar)
-      hash="abcd1234"
-      name="foo"
-      size={32}
-    />
-  </a>
-  <ul
-    className="dropdown-menu dropdown-menu-right"
-  >
-    <li
-      className="dropdown-item"
-    >
-      <div
-        className="text-ellipsis text-muted"
-        title="foo"
-      >
-        <strong>
-          foo
-        </strong>
-      </div>
-      <div
-        className="little-spacer-top text-ellipsis text-muted"
-        title="foo@bar.baz"
-      >
-        foo@bar.baz
-      </div>
-    </li>
-    <li
-      className="divider"
-    />
-    <li>
-      <Link
-        onClick={[Function]}
-        onlyActiveOnIndex={false}
-        style={Object {}}
-        to="/account"
-      >
-        my_account.page
-      </Link>
-    </li>
-    <li>
-      <a
-        href="#"
-        onClick={[Function]}
-      >
-        layout.logout
-      </a>
-    </li>
-  </ul>
-</li>
-`;
-
-exports[`should update the component correctly when the user changes to anonymous 2`] = `
-<li>
-  <a
-    className="navbar-login"
-    href="#"
-    onClick={[Function]}
-  >
-    layout.login
-  </a>
-</li>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap
new file mode 100644 (file)
index 0000000..3bb56aa
--- /dev/null
@@ -0,0 +1,386 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should not render the users organizations when they are not activated 1`] = `
+<li
+  className="dropdown js-user-authenticated open"
+>
+  <a
+    className="dropdown-toggle navbar-avatar"
+    href="#"
+    onClick={[Function]}
+  >
+    <Connect(Avatar)
+      hash="abcd1234"
+      name="foo"
+      size={32}
+    />
+  </a>
+  <ul
+    className="dropdown-menu dropdown-menu-right"
+  >
+    <li
+      className="dropdown-item"
+    >
+      <div
+        className="text-ellipsis text-muted"
+        title="foo"
+      >
+        <strong>
+          foo
+        </strong>
+      </div>
+      <div
+        className="little-spacer-top text-ellipsis text-muted"
+        title="foo@bar.baz"
+      >
+        foo@bar.baz
+      </div>
+    </li>
+    <li
+      className="divider"
+    />
+    <li>
+      <Link
+        onClick={[Function]}
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/account"
+      >
+        my_account.page
+      </Link>
+    </li>
+    <li>
+      <a
+        href="#"
+        onClick={[Function]}
+      >
+        layout.logout
+      </a>
+    </li>
+  </ul>
+</li>
+`;
+
+exports[`should render the right interface for anonymous user 1`] = `
+<li>
+  <a
+    className="navbar-login"
+    href="#"
+    onClick={[Function]}
+  >
+    layout.login
+  </a>
+</li>
+`;
+
+exports[`should render the right interface for logged in user 1`] = `
+<li
+  className="dropdown js-user-authenticated open"
+>
+  <a
+    className="dropdown-toggle navbar-avatar"
+    href="#"
+    onClick={[Function]}
+  >
+    <Connect(Avatar)
+      hash="abcd1234"
+      name="foo"
+      size={32}
+    />
+  </a>
+  <ul
+    className="dropdown-menu dropdown-menu-right"
+  >
+    <li
+      className="dropdown-item"
+    >
+      <div
+        className="text-ellipsis text-muted"
+        title="foo"
+      >
+        <strong>
+          foo
+        </strong>
+      </div>
+      <div
+        className="little-spacer-top text-ellipsis text-muted"
+        title="foo@bar.baz"
+      >
+        foo@bar.baz
+      </div>
+    </li>
+    <li
+      className="divider"
+    />
+    <li>
+      <Link
+        onClick={[Function]}
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/account"
+      >
+        my_account.page
+      </Link>
+    </li>
+    <li>
+      <a
+        href="#"
+        onClick={[Function]}
+      >
+        layout.logout
+      </a>
+    </li>
+  </ul>
+</li>
+`;
+
+exports[`should render the users organizations 1`] = `
+<li
+  className="dropdown js-user-authenticated open"
+>
+  <a
+    className="dropdown-toggle navbar-avatar"
+    href="#"
+    onClick={[Function]}
+  >
+    <Connect(Avatar)
+      hash="abcd1234"
+      name="foo"
+      size={32}
+    />
+  </a>
+  <ul
+    className="dropdown-menu dropdown-menu-right"
+  >
+    <li
+      className="dropdown-item"
+    >
+      <div
+        className="text-ellipsis text-muted"
+        title="foo"
+      >
+        <strong>
+          foo
+        </strong>
+      </div>
+      <div
+        className="little-spacer-top text-ellipsis text-muted"
+        title="foo@bar.baz"
+      >
+        foo@bar.baz
+      </div>
+    </li>
+    <li
+      className="divider"
+    />
+    <li>
+      <Link
+        onClick={[Function]}
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/account"
+      >
+        my_account.page
+      </Link>
+    </li>
+    <li
+      className="divider"
+      role="separator"
+    />
+    <li>
+      <Link
+        onClick={[Function]}
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/account/organizations"
+      >
+        my_organizations
+      </Link>
+    </li>
+    <li
+      key="bar"
+    >
+      <OrganizationLink
+        className="dropdown-item-flex"
+        onClick={[Function]}
+        organization={
+          Object {
+            "key": "bar",
+            "name": "bar",
+            "projectVisibility": "public",
+          }
+        }
+      >
+        <div>
+          <OrganizationAvatar
+            organization={
+              Object {
+                "key": "bar",
+                "name": "bar",
+                "projectVisibility": "public",
+              }
+            }
+            small={true}
+          />
+          <span
+            className="spacer-left"
+          >
+            bar
+          </span>
+        </div>
+      </OrganizationLink>
+    </li>
+    <li
+      key="foo"
+    >
+      <OrganizationLink
+        className="dropdown-item-flex"
+        onClick={[Function]}
+        organization={
+          Object {
+            "key": "foo",
+            "name": "Foo",
+            "projectVisibility": "public",
+          }
+        }
+      >
+        <div>
+          <OrganizationAvatar
+            organization={
+              Object {
+                "key": "foo",
+                "name": "Foo",
+                "projectVisibility": "public",
+              }
+            }
+            small={true}
+          />
+          <span
+            className="spacer-left"
+          >
+            Foo
+          </span>
+        </div>
+      </OrganizationLink>
+    </li>
+    <li
+      key="myorg"
+    >
+      <OrganizationLink
+        className="dropdown-item-flex"
+        onClick={[Function]}
+        organization={
+          Object {
+            "key": "myorg",
+            "name": "MyOrg",
+            "projectVisibility": "public",
+          }
+        }
+      >
+        <div>
+          <OrganizationAvatar
+            organization={
+              Object {
+                "key": "myorg",
+                "name": "MyOrg",
+                "projectVisibility": "public",
+              }
+            }
+            small={true}
+          />
+          <span
+            className="spacer-left"
+          >
+            MyOrg
+          </span>
+        </div>
+      </OrganizationLink>
+    </li>
+    <li
+      className="divider"
+      role="separator"
+    />
+    <li>
+      <a
+        href="#"
+        onClick={[Function]}
+      >
+        layout.logout
+      </a>
+    </li>
+  </ul>
+</li>
+`;
+
+exports[`should update the component correctly when the user changes to anonymous 1`] = `
+<li
+  className="dropdown js-user-authenticated open"
+>
+  <a
+    className="dropdown-toggle navbar-avatar"
+    href="#"
+    onClick={[Function]}
+  >
+    <Connect(Avatar)
+      hash="abcd1234"
+      name="foo"
+      size={32}
+    />
+  </a>
+  <ul
+    className="dropdown-menu dropdown-menu-right"
+  >
+    <li
+      className="dropdown-item"
+    >
+      <div
+        className="text-ellipsis text-muted"
+        title="foo"
+      >
+        <strong>
+          foo
+        </strong>
+      </div>
+      <div
+        className="little-spacer-top text-ellipsis text-muted"
+        title="foo@bar.baz"
+      >
+        foo@bar.baz
+      </div>
+    </li>
+    <li
+      className="divider"
+    />
+    <li>
+      <Link
+        onClick={[Function]}
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/account"
+      >
+        my_account.page
+      </Link>
+    </li>
+    <li>
+      <a
+        href="#"
+        onClick={[Function]}
+      >
+        layout.logout
+      </a>
+    </li>
+  </ul>
+</li>
+`;
+
+exports[`should update the component correctly when the user changes to anonymous 2`] = `
+<li>
+  <a
+    className="navbar-login"
+    href="#"
+    onClick={[Function]}
+  >
+    layout.login
+  </a>
+</li>
+`;
index db5f9efcf953cfa8f2332d6cddc12e9dff1f9c73..54d91d77c75ea0f13551e3b7d6814c960c9feb77 100644 (file)
@@ -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()}
index e33f869028629541dd3932a949b9d2b00b6bb77c..3208552f512b664ef8ef5e5ef94d345be1bf8312 100644 (file)
@@ -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
index 6e3f0fc5c51e4d90af645dcf85534736d897ac7e..743dae7e7a93105583a87b3227dfaa139bfc436b 100644 (file)
@@ -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.js
deleted file mode 100644 (file)
index 95794c9..0000000
+++ /dev/null
@@ -1,117 +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.
- */
-// @flow
-import React from 'react';
-import Helmet from 'react-helmet';
-import { connect } from 'react-redux';
-import { Link } from 'react-router';
-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<*>
-  };
-*/
-
-  state /*: { loading: boolean } */ = {
-    loading: true
-  };
-
-  componentDidMount() {
-    this.mounted = true;
-    Promise.all([
-      this.props.fetchMyOrganizations(),
-      this.props.fetchIfAnyoneCanCreateOrganizations()
-    ]).then(() => {
-      if (this.mounted) {
-        this.setState({ loading: false });
-      }
-    });
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  render() {
-    const anyoneCanCreate =
-      this.props.anyoneCanCreate != null && this.props.anyoneCanCreate.value === 'true';
-
-    const canCreateOrganizations = !this.state.loading && (anyoneCanCreate || this.props.canAdmin);
-
-    return (
-      <div className="account-body account-container">
-        <Helmet title={translate('my_account.organizations')} />
-
-        <header className="page-header">
-          <h2 className="page-title">{translate('my_account.organizations')}</h2>
-          {canCreateOrganizations && (
-            <div className="page-actions">
-              <Link to="/account/organizations/create" className="button">
-                {translate('create')}
-              </Link>
-            </div>
-          )}
-          {this.props.organizations.length > 0 ? (
-            <div className="page-description">
-              {translate('my_account.organizations.description')}
-            </div>
-          ) : (
-            <div className="page-description">
-              {translate('my_account.organizations.no_results')}
-            </div>
-          )}
-        </header>
-
-        {this.state.loading ? (
-          <i className="spinner" />
-        ) : (
-          <OrganizationsList organizations={this.props.organizations} />
-        )}
-
-        {this.props.children}
-      </div>
-    );
-  }
-}
-
-const mapStateToProps = state => ({
-  anyoneCanCreate: getGlobalSettingValue(state, 'sonar.organizations.anyoneCanCreate'),
-  canAdmin: getAppState(state).canAdmin,
-  organizations: getMyOrganizations(state)
-});
-
-const mapDispatchToProps = {
-  fetchMyOrganizations,
-  fetchIfAnyoneCanCreateOrganizations
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(UserOrganizations);
diff --git a/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx b/server/sonar-web/src/main/js/apps/account/organizations/UserOrganizations.tsx
new file mode 100644 (file)
index 0000000..75a48b2
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * 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 Helmet from 'react-helmet';
+import { connect } from 'react-redux';
+import { Link } from 'react-router';
+import OrganizationsList from './OrganizationsList';
+import { translate } from '../../../helpers/l10n';
+import { fetchIfAnyoneCanCreateOrganizations, fetchMyOrganizations } from './actions';
+import { getAppState, getMyOrganizations, getGlobalSettingValue } from '../../../store/rootReducer';
+import { Organization } from '../../../app/types';
+
+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(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';
+
+    const canCreateOrganizations = !this.state.loading && (anyoneCanCreate || this.props.canAdmin);
+
+    return (
+      <div className="account-body account-container">
+        <Helmet title={translate('my_account.organizations')} />
+
+        <header className="page-header">
+          <h2 className="page-title">{translate('my_account.organizations')}</h2>
+          {canCreateOrganizations && (
+            <div className="page-actions">
+              <Link to="/account/organizations/create" className="button">
+                {translate('create')}
+              </Link>
+            </div>
+          )}
+          {this.props.organizations.length > 0 ? (
+            <div className="page-description">
+              {translate('my_account.organizations.description')}
+            </div>
+          ) : (
+            <div className="page-description">
+              {translate('my_account.organizations.no_results')}
+            </div>
+          )}
+        </header>
+
+        {this.state.loading ? (
+          <i className="spinner" />
+        ) : (
+          <OrganizationsList organizations={this.props.organizations} />
+        )}
+
+        {this.props.children}
+      </div>
+    );
+  }
+}
+
+const mapStateToProps = (state: any): StateProps => ({
+  anyoneCanCreate: getGlobalSettingValue(state, 'sonar.organizations.anyoneCanCreate'),
+  canAdmin: getAppState(state).canAdmin,
+  organizations: getMyOrganizations(state)
+});
+
+const mapDispatchToProps = {
+  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.js
deleted file mode 100644 (file)
index 38f30b1..0000000
+++ /dev/null
@@ -1,35 +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 { 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 => {
-  return getOrganizations({ member: true }).then(({ organizations }) => {
-    return dispatch(receiveMyOrganizations(organizations));
-  });
-};
-
-export const fetchIfAnyoneCanCreateOrganizations = () => dispatch => {
-  return getValues('sonar.organizations.anyoneCanCreate').then(values => {
-    dispatch(receiveValues(values));
-  });
-};
diff --git a/server/sonar-web/src/main/js/apps/account/organizations/actions.ts b/server/sonar-web/src/main/js/apps/account/organizations/actions.ts
new file mode 100644 (file)
index 0000000..f93bcdd
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * 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 { 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: Dispatch<any>) => {
+  return getOrganizations({ member: true }).then(({ organizations }) => {
+    return dispatch(receiveMyOrganizations(organizations));
+  });
+};
+
+export const fetchIfAnyoneCanCreateOrganizations = () => (dispatch: Dispatch<any>) => {
+  return getValues('sonar.organizations.anyoneCanCreate').then(values => {
+    dispatch(receiveValues(values, undefined));
+  });
+};
index 7cb0c776b2bfc667207d7915d808df2b35865f6d..a1ed154d36af16d54184e463e79bc7b97d829648 100644 (file)
@@ -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>
index 43728684b84082e03c25aa33206e8b9f4d69c8cf..507d43bbfb0b74b621154f53fad2dc8c311414e0 100644 (file)
 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`}
index c4dcad9c707f1698f0ef610e44e5347fb7ca3dde..66184bbc5d4022a9940329d9dd6d5b4ab10db5d0 100644 (file)
@@ -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 (file)
index 0000000..9099fba
--- /dev/null
@@ -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 (file)
index 0000000..6c9ed12
--- /dev/null
@@ -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 (file)
index dc89017..0000000
+++ /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>
-  );
-}
index d84d2ac30c4ced8c4a5c24196f255c3b4385be9f..fad7ac6b86c5a79a7a2f3c76246ca53519b8a133 100644 (file)
@@ -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';
index 4aa4bd60b1606b481aa91d524c9ebb4588ccb91c..489747900bf669b7f8dedab272d0abcf4fa12fd5 100644 (file)
@@ -5,7 +5,7 @@
 }
 
 .navbar-context .navbar-inner {
-  padding-top: 5px;
+  padding-top: var(--gridSize);
   border-bottom: 1px solid var(--barBorderColor);
 }
 
 }
 
 .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);
index d08552ddf87d9804037fcb822c6d5f7269fa0cee..288ce5fd3151c7de495114e72cda6f6a7515f963 100644 (file)
@@ -2,6 +2,8 @@
   display: flex;
   align-items: center;
   clear: left;
+  height: var(--controlHeight);
+  margin-top: var(--gridSize);
 }
 
 .navbar-tabs > li + li {
 
 .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;
 }
index d9f09b240764db30e2aa985f4ea51f3928a9171c..feb5253d1cb7b2171dbac2013cec01548123c64e 100644 (file)
@@ -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 (file)
index 0000000..36498b6
--- /dev/null
@@ -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';
+}
index 6dd7bc59142b531a3d8401251d2dfa752401de34..0083612ce2f7a433105d809207ba0290a83edfc2 100644 (file)
@@ -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`] = `
index 8b3a473149e2410f92b9d5aea8ec46208512fa9a..c055ad8d0a5b612a54ac62ec81ac2e1af84124ea 100644 (file)
@@ -1400,8 +1400,8 @@ my_account.projects.no_results=You are not administering any project yet.
 my_account.projects.analyzed_x=Analyzed {0}
 my_account.projects.never_analyzed=Never analyzed
 my_account.organizations=Organizations
-my_account.organizations.description=Those organizations are the ones you are administering.
-my_account.organizations.no_results=You are not administering any organizations yet.
+my_account.organizations.description=Those organizations are the ones you are member of.
+my_account.organizations.no_results=You are not a member of any organizations yet.
 my_account.create_organization=Create Organization
 my_account.search_project=Search Project
 my_account.set_notifications_for=Set notifications for