]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10187 Provide more options to populate empty "My Projects" page
authorStas Vilchik <stas.vilchik@sonarsource.com>
Tue, 12 Dec 2017 13:37:19 +0000 (14:37 +0100)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Tue, 2 Jan 2018 09:38:10 +0000 (10:38 +0100)
50 files changed:
server/sonar-web/src/main/js/app/components/GlobalContainer.js [deleted file]
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/GlobalFooter.js [deleted file]
server/sonar-web/src/main/js/app/components/GlobalFooter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx
server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.js [deleted file]
server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.js [deleted file]
server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/__tests__/GlobalFooterSonarCloud-test.js [deleted file]
server/sonar-web/src/main/js/app/components/__tests__/GlobalFooterSonarCloud-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.js.snap [deleted file]
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.js.snap [deleted file]
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/help/GlobalHelp.d.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js [deleted file]
server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js [deleted file]
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap
server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap
server/sonar-web/src/main/js/app/components/search/Search.d.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/styles/components/page.css
server/sonar-web/src/main/js/app/styles/init/forms.css
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.js [deleted file]
server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.js [deleted file]
server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/styles.css
server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.d.ts [new file with mode: 0644]
server/sonar-web/src/main/js/components/icons-components/DropdownIcon.tsx
server/sonar-web/src/main/js/components/nav/NavBar.tsx
server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/__tests__/OrganizationListItem-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/store/appState/duck.ts
server/sonar-web/src/main/js/store/organizations/duck.js
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.js b/server/sonar-web/src/main/js/app/components/GlobalContainer.js
deleted file mode 100644 (file)
index 43f8b05..0000000
+++ /dev/null
@@ -1,41 +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 GlobalNav from './nav/global/GlobalNav';
-import GlobalFooterContainer from './GlobalFooterContainer';
-import GlobalMessagesContainer from './GlobalMessagesContainer';
-
-export default function GlobalContainer(props /*: Object */) {
-  // it is important to pass `location` down to `GlobalNav` to trigger render on url change
-
-  return (
-    <div className="global-container">
-      <div className="page-wrapper" id="container">
-        <div className="page-container">
-          <GlobalNav location={props.location} />
-          <GlobalMessagesContainer />
-          {props.children}
-        </div>
-      </div>
-      <GlobalFooterContainer />
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
new file mode 100644 (file)
index 0000000..cbab467
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * 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 PropTypes from 'prop-types';
+import GlobalNav from './nav/global/GlobalNav';
+import GlobalFooterContainer from './GlobalFooterContainer';
+import GlobalMessagesContainer from './GlobalMessagesContainer';
+
+interface Props {
+  children: React.ReactNode;
+  location: { pathname: string };
+}
+
+interface State {
+  isOnboardingTutorialOpen: boolean;
+}
+
+export default class GlobalContainer extends React.PureComponent<Props, State> {
+  static childContextTypes = {
+    closeOnboardingTutorial: PropTypes.func,
+    openOnboardingTutorial: PropTypes.func
+  };
+
+  constructor(props: Props) {
+    super(props);
+    this.state = { isOnboardingTutorialOpen: false };
+  }
+
+  getChildContext() {
+    return {
+      closeOnboardingTutorial: this.closeOnboardingTutorial,
+      openOnboardingTutorial: this.openOnboardingTutorial
+    };
+  }
+
+  openOnboardingTutorial = () => this.setState({ isOnboardingTutorialOpen: true });
+
+  closeOnboardingTutorial = () => this.setState({ isOnboardingTutorialOpen: false });
+
+  render() {
+    // it is important to pass `location` down to `GlobalNav` to trigger render on url change
+
+    return (
+      <div className="global-container">
+        <div className="page-wrapper" id="container">
+          <div className="page-container">
+            <GlobalNav
+              closeOnboardingTutorial={this.closeOnboardingTutorial}
+              isOnboardingTutorialOpen={this.state.isOnboardingTutorialOpen}
+              location={this.props.location}
+              openOnboardingTutorial={this.openOnboardingTutorial}
+            />
+            <GlobalMessagesContainer />
+            {this.props.children}
+          </div>
+        </div>
+        <GlobalFooterContainer />
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooter.js b/server/sonar-web/src/main/js/app/components/GlobalFooter.js
deleted file mode 100644 (file)
index 6cd0088..0000000
+++ /dev/null
@@ -1,83 +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 { Link } from 'react-router';
-import GlobalFooterSonarCloud from './GlobalFooterSonarCloud';
-import GlobalFooterBranding from './GlobalFooterBranding';
-import { translate, translateWithParameters } from '../../helpers/l10n';
-
-/*::
-type Props = {
-  hideLoggedInInfo?: boolean,
-  productionDatabase: boolean,
-  onSonarCloud?: { value: string },
-  sonarqubeVersion?: string
-};
-*/
-
-export default function GlobalFooter(
-  { hideLoggedInInfo, productionDatabase, onSonarCloud, sonarqubeVersion } /*: Props */
-) {
-  if (onSonarCloud && onSonarCloud.value === 'true') {
-    return <GlobalFooterSonarCloud hideLoggedInInfo={hideLoggedInInfo} />;
-  }
-
-  return (
-    <div id="footer" className="page-footer page-container">
-      {productionDatabase === false && (
-        <div className="alert alert-danger">
-          <p className="big" id="evaluation_warning">
-            {translate('footer.production_database_warning')}
-          </p>
-          <p>{translate('footer.production_database_explanation')}</p>
-        </div>
-      )}
-
-      <GlobalFooterBranding />
-
-      <div>
-        {!hideLoggedInInfo &&
-          sonarqubeVersion &&
-          translateWithParameters('footer.version_x', sonarqubeVersion)}
-        {!hideLoggedInInfo && sonarqubeVersion && ' - '}
-        <a href="http://www.gnu.org/licenses/lgpl-3.0.txt">{translate('footer.licence')}</a>
-        {' - '}
-        <a href="http://www.sonarqube.org">{translate('footer.community')}</a>
-        {' - '}
-        <a href="https://redirect.sonarsource.com/doc/home.html">
-          {translate('footer.documentation')}
-        </a>
-        {' - '}
-        <a href="https://redirect.sonarsource.com/doc/community.html">
-          {translate('footer.support')}
-        </a>
-        {' - '}
-        <a href="https://redirect.sonarsource.com/doc/plugin-library.html">
-          {translate('footer.plugins')}
-        </a>
-        {!hideLoggedInInfo && ' - '}
-        {!hideLoggedInInfo && <Link to="/web_api">{translate('footer.web_api')}</Link>}
-        {!hideLoggedInInfo && ' - '}
-        {!hideLoggedInInfo && <Link to="/about">{translate('footer.about')}</Link>}
-      </div>
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx b/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx
new file mode 100644 (file)
index 0000000..39422c1
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * 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 { Link } from 'react-router';
+import GlobalFooterSonarCloud from './GlobalFooterSonarCloud';
+import GlobalFooterBranding from './GlobalFooterBranding';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+
+interface Props {
+  hideLoggedInInfo?: boolean;
+  productionDatabase: boolean;
+  onSonarCloud?: { value: string };
+  sonarqubeVersion?: string;
+}
+
+export default function GlobalFooter({
+  hideLoggedInInfo,
+  productionDatabase,
+  onSonarCloud,
+  sonarqubeVersion
+}: Props) {
+  if (onSonarCloud && onSonarCloud.value === 'true') {
+    return <GlobalFooterSonarCloud />;
+  }
+
+  return (
+    <div id="footer" className="page-footer page-container">
+      {productionDatabase === false && (
+        <div className="alert alert-danger">
+          <p className="big" id="evaluation_warning">
+            {translate('footer.production_database_warning')}
+          </p>
+          <p>{translate('footer.production_database_explanation')}</p>
+        </div>
+      )}
+
+      <GlobalFooterBranding />
+
+      <ul className="page-footer-menu">
+        {!hideLoggedInInfo &&
+          sonarqubeVersion && (
+            <li className="page-footer-menu-item">
+              {translateWithParameters('footer.version_x', sonarqubeVersion)}
+            </li>
+          )}
+        <li className="page-footer-menu-item">
+          <a href="http://www.gnu.org/licenses/lgpl-3.0.txt">{translate('footer.license')}</a>
+        </li>
+        <li className="page-footer-menu-item">
+          <a href="http://www.sonarqube.org">{translate('footer.community')}</a>
+        </li>
+        <li className="page-footer-menu-item">
+          <a href="https://redirect.sonarsource.com/doc/home.html">
+            {translate('footer.documentation')}
+          </a>
+        </li>
+        <li className="page-footer-menu-item">
+          <a href="https://redirect.sonarsource.com/doc/community.html">
+            {translate('footer.support')}
+          </a>
+        </li>
+        <li className="page-footer-menu-item">
+          <a href="https://redirect.sonarsource.com/doc/plugin-library.html">
+            {translate('footer.plugins')}
+          </a>
+        </li>
+        {!hideLoggedInInfo && (
+          <li className="page-footer-menu-item">
+            <Link to="/web_api">{translate('footer.web_api')}</Link>
+          </li>
+        )}
+        {!hideLoggedInInfo && (
+          <li className="page-footer-menu-item">
+            <Link to="/about">{translate('footer.about')}</Link>
+          </li>
+        )}
+      </ul>
+    </div>
+  );
+}
index e9b31253cddd7c96affbb3a6af94855f797f18da..1e85753db0690b7b3d473723adbf5921c0318e55 100644 (file)
@@ -21,7 +21,13 @@ import { connect } from 'react-redux';
 import { getAppState, getGlobalSettingValue } from '../../store/rootReducer';
 import GlobalFooter from './GlobalFooter';
 
-const mapStateToProps = (state: any) => ({
+interface StateProps {
+  onSonarCloud?: { value: string };
+  productionDatabase: boolean;
+  sonarqubeVersion?: string;
+}
+
+const mapStateToProps = (state: any): StateProps => ({
   sonarqubeVersion: getAppState(state).version,
   productionDatabase: getAppState(state).productionDatabase,
   onSonarCloud: getGlobalSettingValue(state, 'sonar.sonarcloud.enabled')
diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.js b/server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.js
deleted file mode 100644 (file)
index 9c60185..0000000
+++ /dev/null
@@ -1,50 +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 { translate } from '../../helpers/l10n';
-
-export default function GlobalFooterSonarCloud() {
-  return (
-    <div id="footer" className="page-footer page-container">
-      <div>
-        Â© 2008-2017, SonarCloud.io by{' '}
-        <a href="http://www.sonarsource.com" title="SonarSource SA">
-          SonarSource SA
-        </a>
-        . All rights reserved.
-      </div>
-
-      <div>
-        <a href="https://about.sonarcloud.io/news/">{translate('footer.news')}</a>
-        {' - '}
-        <a href="https://about.sonarcloud.io/terms.pdf">{translate('footer.terms')}</a>
-        {' - '}
-        <a href="https://twitter.com/sonarqube">{translate('footer.twitter')}</a>
-        {' - '}
-        <a href="https://about.sonarcloud.io/get-started/">{translate('footer.get_started')}</a>
-        {' - '}
-        <a href="https://about.sonarcloud.io/contact/">{translate('footer.help')}</a>
-        {' - '}
-        <a href="https://about.sonarcloud.io/">{translate('footer.about')}</a>
-      </div>
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.tsx b/server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.tsx
new file mode 100644 (file)
index 0000000..1ccd6d5
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * 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 { translate } from '../../helpers/l10n';
+
+export default function GlobalFooterSonarCloud() {
+  return (
+    <div id="footer" className="page-footer page-container">
+      <div>
+        Â© 2008-2017, SonarCloud.io by{' '}
+        <a href="http://www.sonarsource.com" title="SonarSource SA">
+          SonarSource SA
+        </a>
+        . All rights reserved.
+      </div>
+
+      <ul className="page-footer-menu">
+        <li className="page-footer-menu-item">
+          <a href="https://about.sonarcloud.io/news/">{translate('footer.news')}</a>
+        </li>
+        <li className="page-footer-menu-item">
+          <a href="https://about.sonarcloud.io/terms.pdf">{translate('footer.terms')}</a>
+        </li>
+        <li className="page-footer-menu-item">
+          <a href="https://twitter.com/sonarqube">{translate('footer.twitter')}</a>
+        </li>
+        <li className="page-footer-menu-item">
+          <a href="https://about.sonarcloud.io/get-started/">{translate('footer.get_started')}</a>
+        </li>
+        <li className="page-footer-menu-item">
+          <a href="https://about.sonarcloud.io/contact/">{translate('footer.help')}</a>
+        </li>
+        <li className="page-footer-menu-item">
+          <a href="https://about.sonarcloud.io/">{translate('footer.about')}</a>
+        </li>
+      </ul>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.js b/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.js
deleted file mode 100644 (file)
index 74ef097..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import React from 'react';
-import GlobalFooter from '../GlobalFooter';
-
-it('should render the only logged in information', () => {
-  expect(shallow(<GlobalFooter productionDatabase={true} />)).toMatchSnapshot();
-});
-
-it('should not render the only logged in information', () => {
-  expect(
-    shallow(
-      <GlobalFooter
-        hideLoggedInInfo={true}
-        productionDatabase={true}
-        onSonarCloud={{ value: 'false' }}
-        sonarqubeVersion="6.4-SNAPSHOT"
-      />
-    )
-  ).toMatchSnapshot();
-});
-
-it('should show the db warning message', () => {
-  expect(shallow(<GlobalFooter productionDatabase={false} />).find('.alert')).toMatchSnapshot();
-});
-
-it('should display the sq version', () => {
-  expect(
-    shallow(<GlobalFooter productionDatabase={true} sonarqubeVersion="6.4-SNAPSHOT" />)
-  ).toMatchSnapshot();
-});
-
-it('should render SonarCloud footer', () => {
-  expect(
-    shallow(<GlobalFooter productionDatabase={true} onSonarCloud={{ value: 'true' }} />)
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx
new file mode 100644 (file)
index 0000000..f832509
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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 GlobalFooter from '../GlobalFooter';
+
+it('should render the only logged in information', () => {
+  expect(shallow(<GlobalFooter productionDatabase={true} />)).toMatchSnapshot();
+});
+
+it('should not render the only logged in information', () => {
+  expect(
+    shallow(
+      <GlobalFooter
+        hideLoggedInInfo={true}
+        productionDatabase={true}
+        onSonarCloud={{ value: 'false' }}
+        sonarqubeVersion="6.4-SNAPSHOT"
+      />
+    )
+  ).toMatchSnapshot();
+});
+
+it('should show the db warning message', () => {
+  expect(shallow(<GlobalFooter productionDatabase={false} />).find('.alert')).toMatchSnapshot();
+});
+
+it('should display the sq version', () => {
+  expect(
+    shallow(<GlobalFooter productionDatabase={true} sonarqubeVersion="6.4-SNAPSHOT" />)
+  ).toMatchSnapshot();
+});
+
+it('should render SonarCloud footer', () => {
+  expect(
+    shallow(<GlobalFooter productionDatabase={true} onSonarCloud={{ value: 'true' }} />)
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooterSonarCloud-test.js b/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooterSonarCloud-test.js
deleted file mode 100644 (file)
index 2f41d40..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import React from 'react';
-import GlobalFooterSonarCloud from '../GlobalFooterSonarCloud';
-
-it('should render correctly', () => {
-  expect(shallow(<GlobalFooterSonarCloud />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooterSonarCloud-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooterSonarCloud-test.tsx
new file mode 100644 (file)
index 0000000..9864455
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * 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 GlobalFooterSonarCloud from '../GlobalFooterSonarCloud';
+
+it('should render correctly', () => {
+  expect(shallow(<GlobalFooterSonarCloud />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.js.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.js.snap
deleted file mode 100644 (file)
index 438127c..0000000
+++ /dev/null
@@ -1,173 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display the sq version 1`] = `
-<div
-  className="page-footer page-container"
-  id="footer"
->
-  <GlobalFooterBranding />
-  <div>
-    footer.version_x.6.4-SNAPSHOT
-     - 
-    <a
-      href="http://www.gnu.org/licenses/lgpl-3.0.txt"
-    >
-      footer.licence
-    </a>
-     - 
-    <a
-      href="http://www.sonarqube.org"
-    >
-      footer.community
-    </a>
-     - 
-    <a
-      href="https://redirect.sonarsource.com/doc/home.html"
-    >
-      footer.documentation
-    </a>
-     - 
-    <a
-      href="https://redirect.sonarsource.com/doc/community.html"
-    >
-      footer.support
-    </a>
-     - 
-    <a
-      href="https://redirect.sonarsource.com/doc/plugin-library.html"
-    >
-      footer.plugins
-    </a>
-     - 
-    <Link
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to="/web_api"
-    >
-      footer.web_api
-    </Link>
-     - 
-    <Link
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to="/about"
-    >
-      footer.about
-    </Link>
-  </div>
-</div>
-`;
-
-exports[`should not render the only logged in information 1`] = `
-<div
-  className="page-footer page-container"
-  id="footer"
->
-  <GlobalFooterBranding />
-  <div>
-    <a
-      href="http://www.gnu.org/licenses/lgpl-3.0.txt"
-    >
-      footer.licence
-    </a>
-     - 
-    <a
-      href="http://www.sonarqube.org"
-    >
-      footer.community
-    </a>
-     - 
-    <a
-      href="https://redirect.sonarsource.com/doc/home.html"
-    >
-      footer.documentation
-    </a>
-     - 
-    <a
-      href="https://redirect.sonarsource.com/doc/community.html"
-    >
-      footer.support
-    </a>
-     - 
-    <a
-      href="https://redirect.sonarsource.com/doc/plugin-library.html"
-    >
-      footer.plugins
-    </a>
-  </div>
-</div>
-`;
-
-exports[`should render SonarCloud footer 1`] = `<GlobalFooterSonarCloud />`;
-
-exports[`should render the only logged in information 1`] = `
-<div
-  className="page-footer page-container"
-  id="footer"
->
-  <GlobalFooterBranding />
-  <div>
-    <a
-      href="http://www.gnu.org/licenses/lgpl-3.0.txt"
-    >
-      footer.licence
-    </a>
-     - 
-    <a
-      href="http://www.sonarqube.org"
-    >
-      footer.community
-    </a>
-     - 
-    <a
-      href="https://redirect.sonarsource.com/doc/home.html"
-    >
-      footer.documentation
-    </a>
-     - 
-    <a
-      href="https://redirect.sonarsource.com/doc/community.html"
-    >
-      footer.support
-    </a>
-     - 
-    <a
-      href="https://redirect.sonarsource.com/doc/plugin-library.html"
-    >
-      footer.plugins
-    </a>
-     - 
-    <Link
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to="/web_api"
-    >
-      footer.web_api
-    </Link>
-     - 
-    <Link
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to="/about"
-    >
-      footer.about
-    </Link>
-  </div>
-</div>
-`;
-
-exports[`should show the db warning message 1`] = `
-<div
-  className="alert alert-danger"
->
-  <p
-    className="big"
-    id="evaluation_warning"
-  >
-    footer.production_database_warning
-  </p>
-  <p>
-    footer.production_database_explanation
-  </p>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap
new file mode 100644 (file)
index 0000000..b7069f5
--- /dev/null
@@ -0,0 +1,242 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display the sq version 1`] = `
+<div
+  className="page-footer page-container"
+  id="footer"
+>
+  <GlobalFooterBranding />
+  <ul
+    className="page-footer-menu"
+  >
+    <li
+      className="page-footer-menu-item"
+    >
+      footer.version_x.6.4-SNAPSHOT
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="http://www.gnu.org/licenses/lgpl-3.0.txt"
+      >
+        footer.license
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="http://www.sonarqube.org"
+      >
+        footer.community
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="https://redirect.sonarsource.com/doc/home.html"
+      >
+        footer.documentation
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="https://redirect.sonarsource.com/doc/community.html"
+      >
+        footer.support
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="https://redirect.sonarsource.com/doc/plugin-library.html"
+      >
+        footer.plugins
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <Link
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/web_api"
+      >
+        footer.web_api
+      </Link>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <Link
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/about"
+      >
+        footer.about
+      </Link>
+    </li>
+  </ul>
+</div>
+`;
+
+exports[`should not render the only logged in information 1`] = `
+<div
+  className="page-footer page-container"
+  id="footer"
+>
+  <GlobalFooterBranding />
+  <ul
+    className="page-footer-menu"
+  >
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="http://www.gnu.org/licenses/lgpl-3.0.txt"
+      >
+        footer.license
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="http://www.sonarqube.org"
+      >
+        footer.community
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="https://redirect.sonarsource.com/doc/home.html"
+      >
+        footer.documentation
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="https://redirect.sonarsource.com/doc/community.html"
+      >
+        footer.support
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="https://redirect.sonarsource.com/doc/plugin-library.html"
+      >
+        footer.plugins
+      </a>
+    </li>
+  </ul>
+</div>
+`;
+
+exports[`should render SonarCloud footer 1`] = `<GlobalFooterSonarCloud />`;
+
+exports[`should render the only logged in information 1`] = `
+<div
+  className="page-footer page-container"
+  id="footer"
+>
+  <GlobalFooterBranding />
+  <ul
+    className="page-footer-menu"
+  >
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="http://www.gnu.org/licenses/lgpl-3.0.txt"
+      >
+        footer.license
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="http://www.sonarqube.org"
+      >
+        footer.community
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="https://redirect.sonarsource.com/doc/home.html"
+      >
+        footer.documentation
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="https://redirect.sonarsource.com/doc/community.html"
+      >
+        footer.support
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="https://redirect.sonarsource.com/doc/plugin-library.html"
+      >
+        footer.plugins
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <Link
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/web_api"
+      >
+        footer.web_api
+      </Link>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <Link
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/about"
+      >
+        footer.about
+      </Link>
+    </li>
+  </ul>
+</div>
+`;
+
+exports[`should show the db warning message 1`] = `
+<div
+  className="alert alert-danger"
+>
+  <p
+    className="big"
+    id="evaluation_warning"
+  >
+    footer.production_database_warning
+  </p>
+  <p>
+    footer.production_database_explanation
+  </p>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.js.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.js.snap
deleted file mode 100644 (file)
index 0a3aa19..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<div
-  className="page-footer page-container"
-  id="footer"
->
-  <div>
-    Â© 2008-2017, SonarCloud.io by
-     
-    <a
-      href="http://www.sonarsource.com"
-      title="SonarSource SA"
-    >
-      SonarSource SA
-    </a>
-    . All rights reserved.
-  </div>
-  <div>
-    <a
-      href="https://about.sonarcloud.io/news/"
-    >
-      footer.news
-    </a>
-     - 
-    <a
-      href="https://about.sonarcloud.io/terms.pdf"
-    >
-      footer.terms
-    </a>
-     - 
-    <a
-      href="https://twitter.com/sonarqube"
-    >
-      footer.twitter
-    </a>
-     - 
-    <a
-      href="https://about.sonarcloud.io/get-started/"
-    >
-      footer.get_started
-    </a>
-     - 
-    <a
-      href="https://about.sonarcloud.io/contact/"
-    >
-      footer.help
-    </a>
-     - 
-    <a
-      href="https://about.sonarcloud.io/"
-    >
-      footer.about
-    </a>
-  </div>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.tsx.snap
new file mode 100644 (file)
index 0000000..f7a6f4b
--- /dev/null
@@ -0,0 +1,78 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="page-footer page-container"
+  id="footer"
+>
+  <div>
+    Â© 2008-2017, SonarCloud.io by
+     
+    <a
+      href="http://www.sonarsource.com"
+      title="SonarSource SA"
+    >
+      SonarSource SA
+    </a>
+    . All rights reserved.
+  </div>
+  <ul
+    className="page-footer-menu"
+  >
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="https://about.sonarcloud.io/news/"
+      >
+        footer.news
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="https://about.sonarcloud.io/terms.pdf"
+      >
+        footer.terms
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="https://twitter.com/sonarqube"
+      >
+        footer.twitter
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="https://about.sonarcloud.io/get-started/"
+      >
+        footer.get_started
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="https://about.sonarcloud.io/contact/"
+      >
+        footer.help
+      </a>
+    </li>
+    <li
+      className="page-footer-menu-item"
+    >
+      <a
+        href="https://about.sonarcloud.io/"
+      >
+        footer.about
+      </a>
+    </li>
+  </ul>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/help/GlobalHelp.d.ts b/server/sonar-web/src/main/js/app/components/help/GlobalHelp.d.ts
new file mode 100644 (file)
index 0000000..ea8fb50
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 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 { CurrentUser, AppState } from '../../types';
+
+export interface Props {
+  currentUser: CurrentUser;
+  onClose: () => void;
+  onSonarCloud?: boolean;
+  onTutorialSelect: () => void;
+}
+
+export default class GlobalHelp extends React.PureComponent<Props> {}
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js
deleted file mode 100644 (file)
index 564775c..0000000
+++ /dev/null
@@ -1,171 +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 { connect } from 'react-redux';
-import GlobalNavBranding from './GlobalNavBranding';
-import GlobalNavMenu from './GlobalNavMenu';
-import GlobalNavExplore from './GlobalNavExplore';
-import GlobalNavUserContainer from './GlobalNavUserContainer';
-import GlobalNavPlus from './GlobalNavPlus';
-import Search from '../../search/Search';
-import GlobalHelp from '../../help/GlobalHelp';
-import * as theme from '../../../theme';
-import { isLoggedIn } from '../../../types';
-import NavBar from '../../../../components/nav/NavBar';
-import Tooltip from '../../../../components/controls/Tooltip';
-import HelpIcon from '../../../../components/icons-components/HelpIcon';
-import OnboardingModal from '../../../../apps/tutorials/onboarding/OnboardingModal';
-import { getCurrentUser, getAppState, getGlobalSettingValue } from '../../../../store/rootReducer';
-import { skipOnboarding } from '../../../../store/users/actions';
-import { translate } from '../../../../helpers/l10n';
-import './GlobalNav.css';
-
-/*::
-type Props = {
-  appState: { organizationsEnabled: boolean },
-  currentUser: { isLoggedIn: boolean, showOnboardingTutorial: boolean },
-  location: { pathname: string },
-  skipOnboarding: () => void,
-  onSonarCloud: boolean
-};
-*/
-
-/*::
-type State = {
-  helpOpen: boolean,
-  onboardingTutorialOpen: boolean,
-  onboardingTutorialTooltip: boolean
-};
-*/
-
-class GlobalNav extends React.PureComponent {
-  /*:: interval: ?number; */
-  /*:: props: Props; */
-  state /*: State */ = {
-    helpOpen: false,
-    onboardingTutorialOpen: false,
-    onboardingTutorialTooltip: false
-  };
-
-  componentDidMount() {
-    window.addEventListener('keypress', this.onKeyPress);
-    if (this.props.currentUser.showOnboardingTutorial) {
-      this.openOnboardingTutorial();
-    }
-  }
-
-  componentWillUnmount() {
-    if (this.interval) {
-      clearInterval(this.interval);
-    }
-    window.removeEventListener('keypress', this.onKeyPress);
-  }
-
-  onKeyPress = e => {
-    const tagName = e.target.tagName;
-    const code = e.keyCode || e.which;
-    const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA';
-    const isTriggerKey = code === 63;
-    if (!isInput && isTriggerKey) {
-      this.openHelp();
-    }
-  };
-
-  handleHelpClick = event => {
-    event.preventDefault();
-    this.openHelp();
-  };
-
-  openHelp = () => this.setState({ helpOpen: true });
-
-  closeHelp = () => this.setState({ helpOpen: false });
-
-  openOnboardingTutorial = () => this.setState({ helpOpen: false, onboardingTutorialOpen: true });
-
-  closeOnboardingTutorial = () => {
-    this.setState({ onboardingTutorialOpen: false, onboardingTutorialTooltip: true });
-    this.props.skipOnboarding();
-    this.interval = setInterval(() => {
-      this.setState({ onboardingTutorialTooltip: false });
-    }, 3000);
-  };
-
-  render() {
-    return (
-      <NavBar className="navbar-global" id="global-navigation" height={theme.globalNavHeightRaw}>
-        <GlobalNavBranding />
-
-        <GlobalNavMenu {...this.props} />
-
-        <ul className="global-navbar-menu pull-right">
-          <GlobalNavExplore location={this.props.location} onSonarCloud={this.props.onSonarCloud} />
-          <li>
-            <a className="navbar-help" onClick={this.handleHelpClick} href="#">
-              {this.state.onboardingTutorialTooltip ? (
-                <Tooltip
-                  defaultVisible={true}
-                  overlay={translate('tutorials.follow_later')}
-                  trigger="manual">
-                  <HelpIcon />
-                </Tooltip>
-              ) : (
-                <HelpIcon />
-              )}
-            </a>
-          </li>
-          <Search appState={this.props.appState} currentUser={this.props.currentUser} />
-          {isLoggedIn(this.props.currentUser) &&
-            this.props.onSonarCloud && (
-              <GlobalNavPlus openOnboardingTutorial={this.openOnboardingTutorial} />
-            )}
-          <GlobalNavUserContainer {...this.props} />
-        </ul>
-
-        {this.state.helpOpen && (
-          <GlobalHelp
-            currentUser={this.props.currentUser}
-            onClose={this.closeHelp}
-            onTutorialSelect={this.openOnboardingTutorial}
-            onSonarCloud={this.props.onSonarCloud}
-          />
-        )}
-
-        {this.state.onboardingTutorialOpen && (
-          <OnboardingModal onFinish={this.closeOnboardingTutorial} />
-        )}
-      </NavBar>
-    );
-  }
-}
-
-const mapStateToProps = state => {
-  const sonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled');
-
-  return {
-    currentUser: getCurrentUser(state),
-    appState: getAppState(state),
-    onSonarCloud: Boolean(sonarCloudSetting && sonarCloudSetting.value === 'true')
-  };
-};
-
-const mapDispatchToProps = { skipOnboarding };
-
-export default connect(mapStateToProps, mapDispatchToProps)(GlobalNav);
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx
new file mode 100644 (file)
index 0000000..04404a1
--- /dev/null
@@ -0,0 +1,173 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { connect } from 'react-redux';
+import GlobalNavBranding from './GlobalNavBranding';
+import GlobalNavMenu from './GlobalNavMenu';
+import GlobalNavExplore from './GlobalNavExplore';
+import GlobalNavUserContainer from './GlobalNavUserContainer';
+import GlobalNavPlus from './GlobalNavPlus';
+import Search from '../../search/Search';
+import GlobalHelp from '../../help/GlobalHelp';
+import * as theme from '../../../theme';
+import { isLoggedIn, CurrentUser, AppState } from '../../../types';
+import OnboardingModal from '../../../../apps/tutorials/onboarding/OnboardingModal';
+import NavBar from '../../../../components/nav/NavBar';
+import Tooltip from '../../../../components/controls/Tooltip';
+import HelpIcon from '../../../../components/icons-components/HelpIcon';
+import { translate } from '../../../../helpers/l10n';
+import { getCurrentUser, getAppState, getGlobalSettingValue } from '../../../../store/rootReducer';
+import { skipOnboarding } from '../../../../store/users/actions';
+import './GlobalNav.css';
+
+interface StateProps {
+  appState: AppState;
+  currentUser: CurrentUser;
+  onSonarCloud: boolean;
+}
+
+interface DispatchProps {
+  skipOnboarding: () => void;
+}
+
+interface Props extends StateProps, DispatchProps {
+  closeOnboardingTutorial: () => void;
+  isOnboardingTutorialOpen: boolean;
+  location: { pathname: string };
+  openOnboardingTutorial: () => void;
+}
+
+interface State {
+  helpOpen: boolean;
+  onboardingTutorialTooltip: boolean;
+}
+
+class GlobalNav extends React.PureComponent<Props, State> {
+  interval?: number;
+  state: State = { helpOpen: false, onboardingTutorialTooltip: false };
+
+  componentDidMount() {
+    window.addEventListener('keypress', this.onKeyPress);
+    if (this.props.currentUser.showOnboardingTutorial) {
+      this.openOnboardingTutorial();
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.interval) {
+      clearInterval(this.interval);
+    }
+    window.removeEventListener('keypress', this.onKeyPress);
+  }
+
+  onKeyPress = (event: KeyboardEvent) => {
+    const { tagName } = event.target as HTMLElement;
+    const code = event.keyCode || event.which;
+    const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA';
+    const isTriggerKey = code === 63;
+    if (!isInput && isTriggerKey) {
+      this.openHelp();
+    }
+  };
+
+  handleHelpClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    this.openHelp();
+  };
+
+  openHelp = () => this.setState({ helpOpen: true });
+
+  closeHelp = () => this.setState({ helpOpen: false });
+
+  openOnboardingTutorial = () => {
+    this.setState({ helpOpen: false });
+    this.props.openOnboardingTutorial();
+  };
+
+  closeOnboardingTutorial = () => {
+    this.setState({ onboardingTutorialTooltip: true });
+    this.props.skipOnboarding();
+    this.props.closeOnboardingTutorial();
+    this.interval = window.setInterval(() => {
+      this.setState({ onboardingTutorialTooltip: false });
+    }, 3000);
+  };
+
+  render() {
+    return (
+      <NavBar className="navbar-global" id="global-navigation" height={theme.globalNavHeightRaw}>
+        <GlobalNavBranding />
+
+        <GlobalNavMenu {...this.props} />
+
+        <ul className="global-navbar-menu pull-right">
+          <GlobalNavExplore location={this.props.location} onSonarCloud={this.props.onSonarCloud} />
+          <li>
+            <a className="navbar-help" onClick={this.handleHelpClick} href="#">
+              {this.state.onboardingTutorialTooltip ? (
+                <Tooltip
+                  defaultVisible={true}
+                  overlay={translate('tutorials.follow_later')}
+                  trigger="manual">
+                  <HelpIcon />
+                </Tooltip>
+              ) : (
+                <HelpIcon />
+              )}
+            </a>
+          </li>
+          <Search appState={this.props.appState} currentUser={this.props.currentUser} />
+          {isLoggedIn(this.props.currentUser) &&
+            this.props.onSonarCloud && (
+              <GlobalNavPlus openOnboardingTutorial={this.openOnboardingTutorial} />
+            )}
+          <GlobalNavUserContainer {...this.props} />
+        </ul>
+
+        {this.state.helpOpen && (
+          <GlobalHelp
+            currentUser={this.props.currentUser}
+            onClose={this.closeHelp}
+            onTutorialSelect={this.openOnboardingTutorial}
+            onSonarCloud={this.props.onSonarCloud}
+          />
+        )}
+
+        {this.props.isOnboardingTutorialOpen && (
+          <OnboardingModal onFinish={this.closeOnboardingTutorial} />
+        )}
+      </NavBar>
+    );
+  }
+}
+
+const mapStateToProps = (state: any): StateProps => {
+  const sonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled');
+
+  return {
+    currentUser: getCurrentUser(state),
+    appState: getAppState(state),
+    onSonarCloud: Boolean(sonarCloudSetting && sonarCloudSetting.value === 'true')
+  };
+};
+
+const mapDispatchToProps: DispatchProps = { skipOnboarding };
+
+export default connect(mapStateToProps, mapDispatchToProps)(GlobalNav);
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js
deleted file mode 100644 (file)
index 7cea4c6..0000000
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import PropTypes from 'prop-types';
-import { Link } from 'react-router';
-import { isLoggedIn } from '../../../../app/types';
-import { translate } from '../../../../helpers/l10n';
-import { getQualityGatesUrl } from '../../../../helpers/urls';
-import { isMySet } from '../../../../apps/issues/utils';
-
-export default class GlobalNavMenu extends React.PureComponent {
-  static propTypes = {
-    appState: PropTypes.object.isRequired,
-    currentUser: PropTypes.object.isRequired,
-    location: PropTypes.shape({
-      pathname: PropTypes.string.isRequired
-    }).isRequired,
-    onSonarCloud: PropTypes.bool
-  };
-
-  static defaultProps = {
-    globalDashboards: [],
-    globalPages: []
-  };
-
-  activeLink(url) {
-    return window.location.pathname.indexOf(window.baseUrl + url) === 0 ? 'active' : null;
-  }
-
-  renderProjects() {
-    if (this.props.onSonarCloud && !isLoggedIn(this.props.currentUser)) {
-      return null;
-    }
-
-    return (
-      <li>
-        <Link to="/projects" activeClassName="active">
-          {this.props.onSonarCloud ? translate('my_projects') : translate('projects.page')}
-        </Link>
-      </li>
-    );
-  }
-
-  renderPortfolios() {
-    return (
-      <li>
-        <Link to="/portfolios" activeClassName="active">
-          {translate('portfolios.page')}
-        </Link>
-      </li>
-    );
-  }
-
-  renderIssuesLink() {
-    if (this.props.onSonarCloud && !isLoggedIn(this.props.currentUser)) {
-      return null;
-    }
-
-    const active = this.props.location.pathname === 'issues';
-
-    if (this.props.onSonarCloud) {
-      return (
-        <li>
-          <Link
-            to={{ pathname: '/issues', query: { resolved: 'false' } }}
-            className={active ? 'active' : undefined}>
-            {translate('my_issues')}
-          </Link>
-        </li>
-      );
-    }
-
-    const query =
-      this.props.currentUser.isLoggedIn && isMySet()
-        ? { resolved: 'false', myIssues: 'true' }
-        : { resolved: 'false' };
-    return (
-      <li>
-        <Link to={{ pathname: '/issues', query }} className={active ? 'active' : undefined}>
-          {translate('issues.page')}
-        </Link>
-      </li>
-    );
-  }
-
-  renderRulesLink() {
-    return (
-      <li>
-        <Link to="/coding_rules" className={this.activeLink('/coding_rules')}>
-          {translate('coding_rules.page')}
-        </Link>
-      </li>
-    );
-  }
-
-  renderProfilesLink() {
-    return (
-      <li>
-        <Link to="/profiles" activeClassName="active">
-          {translate('quality_profiles.page')}
-        </Link>
-      </li>
-    );
-  }
-
-  renderQualityGatesLink() {
-    return (
-      <li>
-        <Link to={getQualityGatesUrl()} activeClassName="active">
-          {translate('quality_gates.page')}
-        </Link>
-      </li>
-    );
-  }
-
-  renderAdministrationLink() {
-    if (!this.props.appState.canAdmin) {
-      return null;
-    }
-
-    return (
-      <li>
-        <Link to="/admin" activeClassName="active">
-          {translate('layout.settings')}
-        </Link>
-      </li>
-    );
-  }
-
-  renderGlobalPageLink = ({ key, name }) => {
-    return (
-      <li key={key}>
-        <Link to={`/extension/${key}`}>{name}</Link>
-      </li>
-    );
-  };
-
-  renderMore() {
-    const { globalPages } = this.props.appState;
-    const withoutPortfolios = globalPages.filter(page => page.key !== 'governance/portfolios');
-    if (withoutPortfolios.length === 0) {
-      return null;
-    }
-    return (
-      <li className="dropdown">
-        <a className="dropdown-toggle" id="global-navigation-more" data-toggle="dropdown" href="#">
-          {translate('more')}&nbsp;
-          <span className="icon-dropdown" />
-        </a>
-        <ul className="dropdown-menu">{withoutPortfolios.map(this.renderGlobalPageLink)}</ul>
-      </li>
-    );
-  }
-
-  render() {
-    const governanceInstalled = this.props.appState.qualifiers.includes('VW');
-    const { organizationsEnabled } = this.props.appState;
-
-    return (
-      <ul className="global-navbar-menu pull-left">
-        {this.renderProjects()}
-        {governanceInstalled && this.renderPortfolios()}
-        {this.renderIssuesLink()}
-        {!organizationsEnabled && this.renderRulesLink()}
-        {!organizationsEnabled && this.renderProfilesLink()}
-        {!organizationsEnabled && this.renderQualityGatesLink()}
-        {this.renderAdministrationLink()}
-        {this.renderMore()}
-      </ul>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx
new file mode 100644 (file)
index 0000000..50277e5
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * 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 { Link } from 'react-router';
+import { isLoggedIn, CurrentUser, AppState, Extension } from '../../../../app/types';
+import { translate } from '../../../../helpers/l10n';
+import { getQualityGatesUrl, getBaseUrl } from '../../../../helpers/urls';
+import { isMySet } from '../../../../apps/issues/utils';
+
+interface Props {
+  appState: AppState;
+  currentUser: CurrentUser;
+  location: { pathname: string };
+  onSonarCloud: boolean;
+}
+
+export default class GlobalNavMenu extends React.PureComponent<Props> {
+  activeLink(url: string) {
+    return window.location.pathname.indexOf(getBaseUrl() + url) === 0 ? 'active' : undefined;
+  }
+
+  renderProjects() {
+    if (this.props.onSonarCloud && !isLoggedIn(this.props.currentUser)) {
+      return null;
+    }
+
+    return (
+      <li>
+        <Link to="/projects" activeClassName="active">
+          {this.props.onSonarCloud ? translate('my_projects') : translate('projects.page')}
+        </Link>
+      </li>
+    );
+  }
+
+  renderPortfolios() {
+    return (
+      <li>
+        <Link to="/portfolios" activeClassName="active">
+          {translate('portfolios.page')}
+        </Link>
+      </li>
+    );
+  }
+
+  renderIssuesLink() {
+    if (this.props.onSonarCloud && !isLoggedIn(this.props.currentUser)) {
+      return null;
+    }
+
+    const active = this.props.location.pathname === 'issues';
+
+    if (this.props.onSonarCloud) {
+      return (
+        <li>
+          <Link
+            to={{ pathname: '/issues', query: { resolved: 'false' } }}
+            className={active ? 'active' : undefined}>
+            {translate('my_issues')}
+          </Link>
+        </li>
+      );
+    }
+
+    const query =
+      this.props.currentUser.isLoggedIn && isMySet()
+        ? { resolved: 'false', myIssues: 'true' }
+        : { resolved: 'false' };
+    return (
+      <li>
+        <Link to={{ pathname: '/issues', query }} className={active ? 'active' : undefined}>
+          {translate('issues.page')}
+        </Link>
+      </li>
+    );
+  }
+
+  renderRulesLink() {
+    return (
+      <li>
+        <Link to="/coding_rules" className={this.activeLink('/coding_rules')}>
+          {translate('coding_rules.page')}
+        </Link>
+      </li>
+    );
+  }
+
+  renderProfilesLink() {
+    return (
+      <li>
+        <Link to="/profiles" activeClassName="active">
+          {translate('quality_profiles.page')}
+        </Link>
+      </li>
+    );
+  }
+
+  renderQualityGatesLink() {
+    return (
+      <li>
+        <Link to={getQualityGatesUrl()} activeClassName="active">
+          {translate('quality_gates.page')}
+        </Link>
+      </li>
+    );
+  }
+
+  renderAdministrationLink() {
+    if (!this.props.appState.canAdmin) {
+      return null;
+    }
+
+    return (
+      <li>
+        <Link to="/admin" activeClassName="active">
+          {translate('layout.settings')}
+        </Link>
+      </li>
+    );
+  }
+
+  renderGlobalPageLink = ({ key, name }: Extension) => {
+    return (
+      <li key={key}>
+        <Link to={`/extension/${key}`}>{name}</Link>
+      </li>
+    );
+  };
+
+  renderMore() {
+    const { globalPages = [] } = this.props.appState;
+    const withoutPortfolios = globalPages.filter(page => page.key !== 'governance/portfolios');
+    if (withoutPortfolios.length === 0) {
+      return null;
+    }
+    return (
+      <li className="dropdown">
+        <a className="dropdown-toggle" id="global-navigation-more" data-toggle="dropdown" href="#">
+          {translate('more')}&nbsp;
+          <span className="icon-dropdown" />
+        </a>
+        <ul className="dropdown-menu">{withoutPortfolios.map(this.renderGlobalPageLink)}</ul>
+      </li>
+    );
+  }
+
+  render() {
+    const governanceInstalled = this.props.appState.qualifiers.includes('VW');
+    const { organizationsEnabled } = this.props.appState;
+
+    return (
+      <ul className="global-navbar-menu pull-left">
+        {this.renderProjects()}
+        {governanceInstalled && this.renderPortfolios()}
+        {this.renderIssuesLink()}
+        {!organizationsEnabled && this.renderRulesLink()}
+        {!organizationsEnabled && this.renderProfilesLink()}
+        {!organizationsEnabled && this.renderQualityGatesLink()}
+        {this.renderAdministrationLink()}
+        {this.renderMore()}
+      </ul>
+    );
+  }
+}
index 1649a5e370e80ae251c3eb8983f49cf90e113cff..fb19ba06d40435a20e41363dbfa3cb41b07f7aca 100644 (file)
@@ -25,10 +25,10 @@ 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 OrganizationListItem from '../../../../components/ui/OrganizationListItem';
 import { translate } from '../../../../helpers/l10n';
 import { getBaseUrl } from '../../../../helpers/urls';
-import OrganizationAvatar from '../../../../components/common/OrganizationAvatar';
+import Dropdown from '../../../../components/controls/Dropdown';
 
 interface Props {
   appState: { organizationsEnabled: boolean };
@@ -36,32 +36,11 @@ interface Props {
   organizations: Organization[];
 }
 
-interface State {
-  open: boolean;
-}
-
-export default class GlobalNavUser extends React.PureComponent<Props, State> {
-  node?: HTMLElement | null;
-
+export default class GlobalNavUser extends React.PureComponent<Props> {
   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`;
@@ -76,98 +55,61 @@ export default class GlobalNavUser extends React.PureComponent<Props, State> {
 
   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 = () => {
-    window.addEventListener('click', this.handleClickOutside, true);
-    this.setState({ open: true });
-  };
-
-  closeDropdown = () => {
-    window.removeEventListener('click', this.handleClickOutside);
-    this.setState({ open: false });
-  };
-
   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}
+      <Dropdown>
+        {({ onToggleClick, open }) => (
+          <li className={classNames('dropdown', 'js-user-authenticated', { open })}>
+            <a className="dropdown-toggle navbar-avatar" href="#" onClick={onToggleClick}>
+              <Avatar
+                hash={currentUser.avatar}
+                name={currentUser.name}
+                size={theme.globalNavContentHeightRaw}
+              />
+            </a>
+            <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>
-              )}
-            </li>
-            <li className="divider" />
-            <li>
-              <Link to="/account" onClick={this.closeDropdown}>
-                {translate('my_account.page')}
-              </Link>
-            </li>
-            {hasOrganizations && <li role="separator" className="divider" />}
-            {hasOrganizations && (
+                {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/organizations" onClick={this.closeDropdown}>
-                  {translate('my_organizations')}
-                </Link>
+                <Link to="/account">{translate('my_account.page')}</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.isAdmin && (
-                      <span className="outline-badge spacer-left">{translate('admin')}</span>
-                    )}
-                  </OrganizationLink>
+              {hasOrganizations && <li role="separator" className="divider" />}
+              {hasOrganizations && (
+                <li>
+                  <Link to="/account/organizations">{translate('my_organizations')}</Link>
                 </li>
-              ))}
-            {hasOrganizations && <li role="separator" className="divider" />}
-            <li>
-              <a onClick={this.handleLogout} href="#">
-                {translate('layout.logout')}
-              </a>
-            </li>
-          </ul>
+              )}
+              {hasOrganizations &&
+                sortBy(organizations, org => org.name.toLowerCase()).map(organization => (
+                  <OrganizationListItem key={organization.key} organization={organization} />
+                ))}
+              {hasOrganizations && <li role="separator" className="divider" />}
+              <li>
+                <a onClick={this.handleLogout} href="#">
+                  {translate('layout.logout')}
+                </a>
+              </li>
+            </ul>
+          </li>
         )}
-      </li>
+      </Dropdown>
     );
   }
 
index 659fe6def1659fb20a28dbfb16ebe440d7d9a93e..dbec4b979920c89cc43cbc20131a42bba05ca100 100644 (file)
@@ -43,7 +43,7 @@ it('should render the right interface for logged in user', () => {
     <GlobalNavUser appState={appState} currentUser={currentUser} organizations={[]} />
   );
   wrapper.setState({ open: true });
-  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find('Dropdown').dive()).toMatchSnapshot();
 });
 
 it('should render user organizations', () => {
@@ -51,7 +51,7 @@ it('should render user organizations', () => {
     <GlobalNavUser appState={appState} currentUser={currentUser} organizations={organizations} />
   );
   wrapper.setState({ open: true });
-  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find('Dropdown').dive()).toMatchSnapshot();
 });
 
 it('should not render user organizations when they are not activated', () => {
@@ -63,5 +63,5 @@ it('should not render user organizations when they are not activated', () => {
     />
   );
   wrapper.setState({ open: true });
-  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find('Dropdown').dive()).toMatchSnapshot();
 });
index 673cc003e3ceeb5b6e71fe53f8628f229d63bb35..34dd8f91396541c9905e15e9ca2fe94d0de6b1e5 100644 (file)
@@ -32,7 +32,6 @@ exports[`should show administration menu if the user has the rights 1`] = `
   </li>
   <li>
     <Link
-      className={null}
       onlyActiveOnIndex={false}
       style={Object {}}
       to="/coding_rules"
@@ -109,7 +108,6 @@ exports[`should work with extensions 1`] = `
   </li>
   <li>
     <Link
-      className={null}
       onlyActiveOnIndex={false}
       style={Object {}}
       to="/coding_rules"
index 23fef6f2d99da12580e6da8771fd6ae7cc78030c..378367bce1571e337eeac4a7246a02f3c794b30e 100644 (file)
@@ -2,7 +2,7 @@
 
 exports[`should not render user organizations when they are not activated 1`] = `
 <li
-  className="dropdown js-user-authenticated open"
+  className="dropdown js-user-authenticated"
 >
   <a
     className="dropdown-toggle navbar-avatar"
@@ -41,7 +41,6 @@ exports[`should not render user organizations when they are not activated 1`] =
     />
     <li>
       <Link
-        onClick={[Function]}
         onlyActiveOnIndex={false}
         style={Object {}}
         to="/account"
@@ -75,7 +74,7 @@ exports[`should render the right interface for anonymous user 1`] = `
 
 exports[`should render the right interface for logged in user 1`] = `
 <li
-  className="dropdown js-user-authenticated open"
+  className="dropdown js-user-authenticated"
 >
   <a
     className="dropdown-toggle navbar-avatar"
@@ -114,7 +113,6 @@ exports[`should render the right interface for logged in user 1`] = `
     />
     <li>
       <Link
-        onClick={[Function]}
         onlyActiveOnIndex={false}
         style={Object {}}
         to="/account"
@@ -136,7 +134,7 @@ exports[`should render the right interface for logged in user 1`] = `
 
 exports[`should render user organizations 1`] = `
 <li
-  className="dropdown js-user-authenticated open"
+  className="dropdown js-user-authenticated"
 >
   <a
     className="dropdown-toggle navbar-avatar"
@@ -175,7 +173,6 @@ exports[`should render user organizations 1`] = `
     />
     <li>
       <Link
-        onClick={[Function]}
         onlyActiveOnIndex={false}
         style={Object {}}
         to="/account"
@@ -189,7 +186,6 @@ exports[`should render user organizations 1`] = `
     />
     <li>
       <Link
-        onClick={[Function]}
         onlyActiveOnIndex={false}
         style={Object {}}
         to="/account/organizations"
@@ -197,105 +193,36 @@ exports[`should render user organizations 1`] = `
         my_organizations
       </Link>
     </li>
-    <li
+    <OrganizationListItem
       key="bar"
-    >
-      <OrganizationLink
-        className="dropdown-item-flex"
-        onClick={[Function]}
-        organization={
-          Object {
-            "key": "bar",
-            "name": "bar",
-            "projectVisibility": "public",
-          }
+      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
+      }
+    />
+    <OrganizationListItem
       key="foo"
-    >
-      <OrganizationLink
-        className="dropdown-item-flex"
-        onClick={[Function]}
-        organization={
-          Object {
-            "key": "foo",
-            "name": "Foo",
-            "projectVisibility": "public",
-          }
+      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
+      }
+    />
+    <OrganizationListItem
       key="myorg"
-    >
-      <OrganizationLink
-        className="dropdown-item-flex"
-        onClick={[Function]}
-        organization={
-          Object {
-            "key": "myorg",
-            "name": "MyOrg",
-            "projectVisibility": "public",
-          }
+      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"
diff --git a/server/sonar-web/src/main/js/app/components/search/Search.d.ts b/server/sonar-web/src/main/js/app/components/search/Search.d.ts
new file mode 100644 (file)
index 0000000..4aa6e4e
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * 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 { CurrentUser, AppState } from '../../types';
+
+export interface Props {
+  appState: AppState;
+  currentUser: CurrentUser;
+}
+
+export default class Search extends React.PureComponent<Props> {}
index a1a7d2b861092dcd89cce9d60c5b0fd80a3f27ac..53855c6747f541a0fd6fa2e43160eb4844d5d088 100644 (file)
 }
 
 .page-footer a:hover,
-.page-footer a:active,
-.page-footer a:focus {
-  color: var(--blue);
-}
-
-.page-footer a:hover {
-  border-bottom-color: var(--lightBlue);
-}
-
 .page-footer a:active,
 .page-footer a:focus {
   border-bottom-color: var(--lightBlue);
+  color: var(--blue);
 }
 
 .page-footer-with-sidebar {
   max-width: 980px;
 }
 
+.page-footer-menu-item {
+  display: inline-block;
+}
+
+.page-footer-menu-item + .page-footer-menu-item::before {
+  content: '-';
+  padding: 0 calc(0.5 * var(--gridSize));
+  user-select: none;
+}
+
 .page-with-sidebar {
   display: flex;
 }
index a9a610f66328be607f6732c37bb2eade7f28daaf..97287e21ac06157dd654b13c2fe44861d6a7d51f 100644 (file)
@@ -118,7 +118,7 @@ input[type='button'] {
   display: inline-block;
   vertical-align: baseline;
   height: var(--controlHeight);
-  line-height: 22px;
+  line-height: calc(var(--controlHeight) - 2px);
   padding: 0 12px;
   border: 1px solid var(--darkBlue);
   border-radius: 2px;
@@ -184,6 +184,10 @@ input[type='button']:disabled:focus {
   box-shadow: none;
 }
 
+.button svg {
+  padding-top: calc((var(--controlHeight) - 16px - 2px) / 2);
+}
+
 .button-red,
 input[type='submit'].button-red {
   border-color: var(--red);
index af3c21a26db528689577e5d271f6a3ffd44906c9..2b2bc7a64a8802dfd1a3837bd3756cb5ec87e6e6 100644 (file)
@@ -150,3 +150,13 @@ export interface LoggedInUser extends CurrentUser {
 export function isLoggedIn(user: CurrentUser): user is LoggedInUser {
   return user.isLoggedIn;
 }
+
+export interface AppState {
+  adminPages?: Extension[];
+  authenticationError: boolean;
+  authorizationError: boolean;
+  canAdmin?: boolean;
+  globalPages?: Extension[];
+  organizationsEnabled: boolean;
+  qualifiers: string[];
+}
diff --git a/server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.js b/server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.js
deleted file mode 100644 (file)
index 1bded7b..0000000
+++ /dev/null
@@ -1,77 +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 OrganizationLink from '../../../components/ui/OrganizationLink';
-/*:: import type { Organization } from '../../../store/organizations/duck'; */
-import { translate } from '../../../helpers/l10n';
-
-/*::
-type Props = {
-  organization: Organization
-};
-*/
-
-export default function OrganizationCard(props /*: Props */) {
-  const { organization } = props;
-
-  return (
-    <div className="account-project-card clearfix">
-      <aside className="account-project-side">
-        {!!organization.avatar && (
-          <div className="spacer-bottom">
-            <img src={organization.avatar} height={30} alt={organization.name} />
-          </div>
-        )}
-        {!!organization.url && (
-          <div className="text-limited text-top spacer-bottom">
-            <a className="small" href={organization.url} title={organization.url} rel="nofollow">
-              {organization.url}
-            </a>
-          </div>
-        )}
-      </aside>
-
-      <h3 className="account-project-name">
-        <OrganizationLink organization={organization}>{organization.name}</OrganizationLink>
-        {organization.isAdmin && (
-          <span className="outline-badge spacer-left">{translate('admin')}</span>
-        )}
-      </h3>
-
-      {!!organization.description && (
-        <div className="account-project-description">{organization.description}</div>
-      )}
-
-      <div className="account-project-key">
-        <span className="little-spacer-right">
-          {translate('key')}
-          {':'}
-        </span>
-        <input
-          onClick={event => event.currentTarget.select()}
-          readOnly={true}
-          type="text"
-          value={organization.key}
-        />
-      </div>
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.tsx b/server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.tsx
new file mode 100644 (file)
index 0000000..0f8b571
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * 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 OrganizationAvatar from '../../../components/common/OrganizationAvatar';
+import OrganizationLink from '../../../components/ui/OrganizationLink';
+import { translate } from '../../../helpers/l10n';
+import { Organization } from '../../../app/types';
+
+interface Props {
+  organization: Organization;
+}
+
+export default function OrganizationCard({ organization }: Props) {
+  return (
+    <div className="account-project-card clearfix">
+      <aside className="account-project-side note">
+        <strong>{translate('organization.key')}:</strong> {organization.key}
+      </aside>
+
+      <h3 className="account-project-name">
+        <OrganizationAvatar organization={organization} />
+        <OrganizationLink className="spacer-left text-middle" organization={organization}>
+          {organization.name}
+        </OrganizationLink>
+        {organization.isAdmin && (
+          <span className="outline-badge spacer-left">{translate('admin')}</span>
+        )}
+      </h3>
+
+      {!!organization.description && (
+        <div className="markdown spacer-top">{organization.description}</div>
+      )}
+
+      {!!organization.url && (
+        <div className="markdown spacer-top">
+          <a href={organization.url} title={organization.url} rel="nofollow">
+            {organization.url}
+          </a>
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.js b/server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.js
deleted file mode 100644 (file)
index 921b636..0000000
+++ /dev/null
@@ -1,44 +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 { sortBy } from 'lodash';
-import OrganizationCard from './OrganizationCard';
-/*:: import type { Organization } from '../../../store/organizations/duck'; */
-
-/*::
-type Props = {
-  organizations: Array<Organization>
-};
-*/
-
-export default function OrganizationsList(props /*: Props */) {
-  return (
-    <ul className="account-projects-list">
-      {sortBy(props.organizations, organization => organization.name.toLocaleLowerCase()).map(
-        organization => (
-          <li key={organization.key}>
-            <OrganizationCard organization={organization} />
-          </li>
-        )
-      )}
-    </ul>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.tsx b/server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.tsx
new file mode 100644 (file)
index 0000000..5b0b8cc
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * 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 { sortBy } from 'lodash';
+import OrganizationCard from './OrganizationCard';
+import { Organization } from '../../../app/types';
+
+interface Props {
+  organizations: Organization[];
+}
+
+export default function OrganizationsList({ organizations }: Props) {
+  return (
+    <ul className="account-projects-list">
+      {sortBy(organizations, organization => organization.name.toLocaleLowerCase()).map(
+        organization => (
+          <li key={organization.key}>
+            <OrganizationCard organization={organization} />
+          </li>
+        )
+      )}
+    </ul>
+  );
+}
index 98d50b19d61af89754a237974bd691da0f548a97..accf47cbd704ad58e5ae895107cc0ac87c86ad43 100644 (file)
@@ -56,7 +56,7 @@ export default function OrganizationNavigationAdministration({ location, organiz
             href="#"
             onClick={onToggleClick}>
             {translate('layout.settings')}
-            <DropdownIcon />
+            <DropdownIcon className="little-spacer-left" />
           </a>
           <ul className="dropdown-menu">
             {extensions.map(extension => (
index afa4332544ca59685725f4204f5a87b5243d3d85..84f0256e579342a1efa351a8ba4aa0cb75e8a01a 100644 (file)
@@ -24,8 +24,7 @@ import { Organization } from '../../../app/types';
 import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
 import Dropdown from '../../../components/controls/Dropdown';
 import DropdownIcon from '../../../components/icons-components/DropdownIcon';
-import OrganizationLink from '../../../components/ui/OrganizationLink';
-import { translate } from '../../../helpers/l10n';
+import OrganizationListItem from '../../../components/ui/OrganizationListItem';
 
 interface Props {
   organization: Organization;
@@ -49,17 +48,7 @@ export default function OrganizationNavigationHeader({ organization, organizatio
                 </a>
                 <ul className="dropdown-menu">
                   {sortBy(other, org => org.name.toLowerCase()).map(organization => (
-                    <li key={organization.key}>
-                      <OrganizationLink className="dropdown-item-flex" organization={organization}>
-                        <div>
-                          <OrganizationAvatar organization={organization} small={true} />
-                          <span className="spacer-left">{organization.name}</span>
-                        </div>
-                        {organization.isAdmin && (
-                          <span className="outline-badge spacer-left">{translate('admin')}</span>
-                        )}
-                      </OrganizationLink>
-                    </li>
+                    <OrganizationListItem key={organization.key} organization={organization} />
                   ))}
                 </ul>
               </div>
index 61f766d7cf70308728e3c0b719ef24e973be6680..713261b2290335a412b80a8099382c217028d515 100644 (file)
@@ -11,7 +11,9 @@ exports[`renders 1`] = `
     onClick={[Function]}
   >
     layout.settings
-    <DropdownIcon />
+    <DropdownIcon
+      className="little-spacer-left"
+    />
   </a>
   <ul
     className="dropdown-menu"
index 32683a0f5bedb9926a0ab4a606d1695b32fcc5bc..78f6ea2ae7c4c938c07b4e4f1765819114376be3 100644 (file)
@@ -42,79 +42,28 @@ exports[`renders dropdown 1`] = `
   <ul
     className="dropdown-menu"
   >
-    <li
+    <OrganizationListItem
       key="org1"
-    >
-      <OrganizationLink
-        className="dropdown-item-flex"
-        organization={
-          Object {
-            "isAdmin": true,
-            "key": "org1",
-            "name": "org1",
-            "projectVisibility": "public",
-          }
+      organization={
+        Object {
+          "isAdmin": true,
+          "key": "org1",
+          "name": "org1",
+          "projectVisibility": "public",
         }
-      >
-        <div>
-          <OrganizationAvatar
-            organization={
-              Object {
-                "isAdmin": true,
-                "key": "org1",
-                "name": "org1",
-                "projectVisibility": "public",
-              }
-            }
-            small={true}
-          />
-          <span
-            className="spacer-left"
-          >
-            org1
-          </span>
-        </div>
-        <span
-          className="outline-badge spacer-left"
-        >
-          admin
-        </span>
-      </OrganizationLink>
-    </li>
-    <li
+      }
+    />
+    <OrganizationListItem
       key="org2"
-    >
-      <OrganizationLink
-        className="dropdown-item-flex"
-        organization={
-          Object {
-            "isAdmin": false,
-            "key": "org2",
-            "name": "org2",
-            "projectVisibility": "public",
-          }
+      organization={
+        Object {
+          "isAdmin": false,
+          "key": "org2",
+          "name": "org2",
+          "projectVisibility": "public",
         }
-      >
-        <div>
-          <OrganizationAvatar
-            organization={
-              Object {
-                "isAdmin": false,
-                "key": "org2",
-                "name": "org2",
-                "projectVisibility": "public",
-              }
-            }
-            small={true}
-          />
-          <span
-            className="spacer-left"
-          >
-            org2
-          </span>
-        </div>
-      </OrganizationLink>
-    </li>
+      }
+    />
   </ul>
 </div>
 `;
index b562b7d46bf490722eb98b84d37ef80a52ba3201..d1bb1d60a3422bcba3b4d37adfd37714a361fb24 100644 (file)
  */
 import * as React from 'react';
 import { Link } from 'react-router';
+import * as classNames from 'classnames';
+import { connect } from 'react-redux';
+import * as PropTypes from 'prop-types';
+import { sortBy } from 'lodash';
+import { Organization } from '../../../app/types';
+import DropdownIcon from '../../../components/icons-components/DropdownIcon';
+import Dropdown from '../../../components/controls/Dropdown';
+import { getMyOrganizations } from '../../../store/rootReducer';
+import OrganizationListItem from '../../../components/ui/OrganizationListItem';
 import { translate } from '../../../helpers/l10n';
 
-interface Props {
+interface StateProps {
+  organizations: Organization[];
+}
+
+interface Props extends StateProps {
   onSonarCloud: boolean;
 }
 
-export default function NoFavoriteProjects({ onSonarCloud }: Props) {
-  return (
-    <div className="projects-empty-list">
-      <h3>{translate('projects.no_favorite_projects')}</h3>
-      <p className="big-spacer-top">{translate('projects.no_favorite_projects.engagement')}</p>
-      <p className="big-spacer-top">
-        <Link to={onSonarCloud ? '/explore/projects' : '/projects/all'} className="button">
-          {translate('projects.explore_projects')}
-        </Link>
-      </p>
-    </div>
-  );
+export class NoFavoriteProjects extends React.PureComponent<Props> {
+  static contextTypes = {
+    openOnboardingTutorial: PropTypes.func
+  };
+
+  onAnalyzeProjectClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    this.context.openOnboardingTutorial();
+  };
+
+  render() {
+    const { onSonarCloud, organizations } = this.props;
+    return (
+      <div className="projects-empty-list">
+        <h3>{translate('projects.no_favorite_projects')}</h3>
+        {onSonarCloud ? (
+          <div className="spacer-top">
+            <p>{translate('projects.no_favorite_projects.how_to_add_projects')}</p>
+            <div className="huge-spacer-top">
+              <a className="button" href="#" onClick={this.onAnalyzeProjectClick}>
+                {translate('my_account.analyze_new_project')}
+              </a>
+              <Dropdown>
+                {({ onToggleClick, open }) => (
+                  <div
+                    className={classNames('display-inline-block', 'big-spacer-left', 'dropdown', {
+                      open
+                    })}>
+                    <a className="button" href="#" onClick={onToggleClick}>
+                      {translate('projects.no_favorite_projects.favorite_projects_from_orgs')}
+                      <DropdownIcon className="little-spacer-left" />
+                    </a>
+                    <ul className="dropdown-menu">
+                      {sortBy(organizations, org => org.name.toLowerCase()).map(organization => (
+                        <OrganizationListItem key={organization.key} organization={organization} />
+                      ))}
+                    </ul>
+                  </div>
+                )}
+              </Dropdown>
+              <Link className="button big-spacer-left" to="/explore/projects">
+                {translate('projects.no_favorite_projects.favorite_public_projects')}
+              </Link>
+            </div>
+          </div>
+        ) : (
+          <div>
+            <p className="big-spacer-top">
+              {translate('projects.no_favorite_projects.engagement')}
+            </p>
+            <p className="big-spacer-top">
+              <Link to="/projects/all" className="button">
+                {translate('projects.explore_projects')}
+              </Link>
+            </p>
+          </div>
+        )}
+      </div>
+    );
+  }
 }
+
+const mapStateToProps = (state: any): StateProps => ({
+  organizations: getMyOrganizations(state)
+});
+
+export default connect(mapStateToProps)(NoFavoriteProjects);
index 3635c476f77f293ac18376e93cebd8b4c8f1c76d..8f767812c60b963f1ab01fc203b3f47b89fe8372 100644 (file)
  */
 import * as React from 'react';
 import { shallow } from 'enzyme';
-import NoFavoriteProjects from '../NoFavoriteProjects';
+import { NoFavoriteProjects } from '../NoFavoriteProjects';
+import { Visibility } from '../../../../app/types';
 
 it('renders', () => {
-  expect(shallow(<NoFavoriteProjects onSonarCloud={false} />)).toMatchSnapshot();
+  expect(shallow(<NoFavoriteProjects onSonarCloud={false} organizations={[]} />)).toMatchSnapshot();
+});
+
+it('renders for SonarCloud', () => {
+  const organizations = [
+    { isAdmin: true, key: 'org1', name: 'org1', projectVisibility: Visibility.Public },
+    { isAdmin: false, key: 'org2', name: 'org2', projectVisibility: Visibility.Public }
+  ];
+  expect(
+    shallow(<NoFavoriteProjects onSonarCloud={true} organizations={organizations} />)
+  ).toMatchSnapshot();
 });
index 3b6652f10f8bb3c5f05c885f4b0dced62f82e98c..a7dede573484f1136af3e84b19712f3e31741902 100644 (file)
@@ -7,22 +7,61 @@ exports[`renders 1`] = `
   <h3>
     projects.no_favorite_projects
   </h3>
-  <p
-    className="big-spacer-top"
-  >
-    projects.no_favorite_projects.engagement
-  </p>
-  <p
-    className="big-spacer-top"
+  <div>
+    <p
+      className="big-spacer-top"
+    >
+      projects.no_favorite_projects.engagement
+    </p>
+    <p
+      className="big-spacer-top"
+    >
+      <Link
+        className="button"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/projects/all"
+      >
+        projects.explore_projects
+      </Link>
+    </p>
+  </div>
+</div>
+`;
+
+exports[`renders for SonarCloud 1`] = `
+<div
+  className="projects-empty-list"
+>
+  <h3>
+    projects.no_favorite_projects
+  </h3>
+  <div
+    className="spacer-top"
   >
-    <Link
-      className="button"
-      onlyActiveOnIndex={false}
-      style={Object {}}
-      to="/projects/all"
+    <p>
+      projects.no_favorite_projects.how_to_add_projects
+    </p>
+    <div
+      className="huge-spacer-top"
     >
-      projects.explore_projects
-    </Link>
-  </p>
+      <a
+        className="button"
+        href="#"
+        onClick={[Function]}
+      >
+        my_account.analyze_new_project
+      </a>
+      <Dropdown />
+      <Link
+        className="button big-spacer-left"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/explore/projects"
+      >
+        projects.no_favorite_projects.favorite_public_projects
+      </Link>
+    </div>
+  </div>
 </div>
 `;
index 93fb447b814a7df60ed284fda5c32b627960a78a..b2552033befc3e8b85723f675eb99b04a92deb83 100644 (file)
@@ -47,6 +47,6 @@ exports[`renders different types of "no projects" 3`] = `
 <div
   className="projects-list"
 >
-  <NoFavoriteProjects />
+  <Connect(NoFavoriteProjects) />
 </div>
 `;
index 707f0243994a0fd3b901a27a76d4e8230bda74c8..e1084c7f01f9f24ed89076330652a838deeb9132 100644 (file)
   margin-left: -250px;
   text-align: center;
 }
+
+.projects-empty-list {
+  padding: calc(4 * var(--gridSize)) 0;
+  text-align: center;
+}
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.d.ts b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.d.ts
new file mode 100644 (file)
index 0000000..d5d3100
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * 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';
+
+export interface Props {
+  onFinish: () => void;
+}
+
+export default class OnboardingModal extends React.PureComponent<Props> {}
index 20bc9629f318d140623a17d7f363af14307be475..7cd3226777ac72e1d3915c75474e87cba5d51162 100644 (file)
@@ -24,13 +24,13 @@ export default function DropdownIcon({ className, fill = 'currentColor', size =
   return (
     <svg
       className={className}
-      width={size}
+      width={size / 16 * 7}
       height={size}
-      viewBox="0 0 16 16"
+      viewBox="0 0 7 16"
       version="1.1"
       xmlnsXlink="http://www.w3.org/1999/xlink"
       xmlSpace="preserve">
-      <g transform="matrix(0.0273438,0,0,0.0273438,4.5,2.65625)">
+      <g transform="matrix(0.0273438,0,0,0.0273438,-6.4e-06,2.65625)">
         <path
           style={{ fill }}
           d="M256,176C256,180.333 254.417,184.083 251.25,187.25L139.25,299.25C136.083,302.417 132.333,304 128,304C123.667,304 119.917,302.417 116.75,299.25L4.75,187.25C1.583,184.083 0,180.333 0,176C0,171.667 1.583,167.917 4.75,164.75C7.917,161.583 11.667,160 16,160L240,160C244.333,160 248.083,161.583 251.25,164.75C254.417,167.917 256,171.667 256,176Z"
index 19397032533857416b422b4fc37b5a29365ea6a2..f5647516be833b2b6ae18ee866d7ba1e4c5bf088 100644 (file)
@@ -26,6 +26,7 @@ interface Props {
   className?: string;
   height: number;
   notif?: React.ReactNode;
+  [prop: string]: any;
 }
 
 export default function NavBar({ children, className, height, notif, ...other }: Props) {
diff --git a/server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx b/server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx
new file mode 100644 (file)
index 0000000..384849e
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { Organization } from '../../app/types';
+import OrganizationLink from './OrganizationLink';
+import OrganizationAvatar from '../common/OrganizationAvatar';
+import { translate } from '../../helpers/l10n';
+
+interface Props {
+  organization: Organization;
+}
+
+export default function OrganizationListItem({ organization }: Props) {
+  return (
+    <li>
+      <OrganizationLink className="dropdown-item-flex" organization={organization}>
+        <div>
+          <OrganizationAvatar organization={organization} small={true} />
+          <span className="spacer-left">{organization.name}</span>
+        </div>
+        {organization.isAdmin && (
+          <span className="outline-badge spacer-left">{translate('admin')}</span>
+        )}
+      </OrganizationLink>
+    </li>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/OrganizationListItem-test.tsx b/server/sonar-web/src/main/js/components/ui/__tests__/OrganizationListItem-test.tsx
new file mode 100644 (file)
index 0000000..dd340f5
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import OrganizationListItem from '../OrganizationListItem';
+import { Visibility } from '../../../app/types';
+
+it('renders', () => {
+  expect(
+    shallow(
+      <OrganizationListItem
+        organization={{
+          isAdmin: true,
+          key: 'org',
+          name: 'org',
+          projectVisibility: Visibility.Public
+        }}
+      />
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap
new file mode 100644 (file)
index 0000000..bd3a76a
--- /dev/null
@@ -0,0 +1,41 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<li>
+  <OrganizationLink
+    className="dropdown-item-flex"
+    organization={
+      Object {
+        "isAdmin": true,
+        "key": "org",
+        "name": "org",
+        "projectVisibility": "public",
+      }
+    }
+  >
+    <div>
+      <OrganizationAvatar
+        organization={
+          Object {
+            "isAdmin": true,
+            "key": "org",
+            "name": "org",
+            "projectVisibility": "public",
+          }
+        }
+        small={true}
+      />
+      <span
+        className="spacer-left"
+      >
+        org
+      </span>
+    </div>
+    <span
+      className="outline-badge spacer-left"
+    >
+      admin
+    </span>
+  </OrganizationLink>
+</li>
+`;
index abb05d0b3090b2f5c4e4a6653c90457a0252c991..dd0f2a8c962ba0c22ca1855c6e3dc364a532e7a7 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import { Extension } from '../../app/types';
-
-interface AppState {
-  adminPages?: Extension[];
-  authenticationError: boolean;
-  authorizationError: boolean;
-  organizationsEnabled: boolean;
-  qualifiers?: string[];
-}
+import { Extension, AppState } from '../../app/types';
 
 interface SetAppStateAction {
   type: 'SET_APP_STATE';
@@ -62,7 +54,8 @@ export function requireAuthorization(): RequireAuthorizationAction {
 const defaultValue: AppState = {
   authenticationError: false,
   authorizationError: false,
-  organizationsEnabled: false
+  organizationsEnabled: false,
+  qualifiers: []
 };
 
 export default function(state: AppState = defaultValue, action: Action): AppState {
index 4b4023f97cebc2e5a86ddef9fe39764d6fb2b35a..35578963545cb89a9525eb779ccaa5a9cc7d89eb 100644 (file)
@@ -199,7 +199,7 @@ function byKey(state /*: ByKeyState */ = {}, action /*: Action */) {
     case 'RECEIVE_MY_ORGANIZATIONS':
       return onReceiveOrganizations(state, action);
     case 'CREATE_ORGANIZATION':
-      return { ...state, [action.organization.key]: action.organization };
+      return { ...state, [action.organization.key]: { ...action.organization, isAdmin: true } };
     case 'UPDATE_ORGANIZATION':
       return {
         ...state,
index 030f1b305bf3c63a707d6253e58b712506087b46..1fa402c992e129fb6b61d2d390aeef4e4722dbe4 100644 (file)
@@ -685,6 +685,9 @@ projects._projects=projects
 projects.no_projects.empty_instance=Once you analyze some projects, they will show up here.
 projects.no_favorite_projects=You don't have any favorite projects yet.
 projects.no_favorite_projects.engagement=Discover and mark as favorites projects you are interested in to have a quick access to them.
+projects.no_favorite_projects.how_to_add_projects=Here is how to add projects to this page
+projects.no_favorite_projects.favorite_projects_from_orgs=Favorite projects from your orgs
+projects.no_favorite_projects.favorite_public_projects=Favorite public projects
 projects.explore_projects=Explore Projects
 projects.not_analyzed=Project is not analyzed yet.
 projects.no_leak_period=Project has no leak data yet.
@@ -2472,7 +2475,7 @@ footer.community=Community
 footer.documentation=Documentation
 footer.get_started=Get Started
 footer.help=Help
-footer.licence=LGPL v3
+footer.license=LGPL v3
 footer.news=News
 footer.plugins=Plugins
 footer.production_database_explanation=The embedded database will not scale, it will not support upgrading to newer versions of SonarQube, and there is no support for migrating your data out of it into a different database engine.