aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2017-12-12 14:37:19 +0100
committerStas Vilchik <stas.vilchik@sonarsource.com>2018-01-02 10:38:10 +0100
commit82d363f3bc37245ea92c77b5c1b0ea418f91dbb5 (patch)
tree156a7596df64b9198df7ecc7046b9bf580d3777a /server/sonar-web/src/main
parent0d3c531f4a6ef72a1f0982808543273760ea05ba (diff)
downloadsonarqube-82d363f3bc37245ea92c77b5c1b0ea418f91dbb5.tar.gz
sonarqube-82d363f3bc37245ea92c77b5c1b0ea418f91dbb5.zip
SONAR-10187 Provide more options to populate empty "My Projects" page
Diffstat (limited to 'server/sonar-web/src/main')
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalContainer.tsx78
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalFooter.js83
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalFooter.tsx97
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx8
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.tsx (renamed from server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.js)36
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx (renamed from server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.js)2
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/GlobalFooterSonarCloud-test.tsx (renamed from server/sonar-web/src/main/js/app/components/__tests__/GlobalFooterSonarCloud-test.js)2
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.js.snap173
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap242
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.js.snap57
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.tsx.snap78
-rw-r--r--server/sonar-web/src/main/js/app/components/help/GlobalHelp.d.ts30
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js)82
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js)36
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx148
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx6
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap127
-rw-r--r--server/sonar-web/src/main/js/app/components/search/Search.d.ts28
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/page.css20
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/forms.css6
-rw-r--r--server/sonar-web/src/main/js/app/types.ts10
-rw-r--r--server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.tsx (renamed from server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.js)59
-rw-r--r--server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.tsx (renamed from server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.js)17
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap87
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx94
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap69
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/styles.css5
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.d.ts26
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/DropdownIcon.tsx6
-rw-r--r--server/sonar-web/src/main/js/components/nav/NavBar.tsx1
-rw-r--r--server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx (renamed from server/sonar-web/src/main/js/app/components/GlobalContainer.js)35
-rw-r--r--server/sonar-web/src/main/js/components/ui/__tests__/OrganizationListItem-test.tsx38
-rw-r--r--server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap41
-rw-r--r--server/sonar-web/src/main/js/store/appState/duck.ts13
-rw-r--r--server/sonar-web/src/main/js/store/organizations/duck.js2
41 files changed, 1078 insertions, 804 deletions
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
index 00000000000..cbab4678fd5
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
@@ -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
index 6cd008827fa..00000000000
--- a/server/sonar-web/src/main/js/app/components/GlobalFooter.js
+++ /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
index 00000000000..39422c1e761
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx
index e9b31253cdd..1e85753db06 100644
--- a/server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx
+++ b/server/sonar-web/src/main/js/app/components/GlobalFooterContainer.tsx
@@ -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.tsx
index 9c601855952..1ccd6d5c9ff 100644
--- a/server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.js
+++ b/server/sonar-web/src/main/js/app/components/GlobalFooterSonarCloud.tsx
@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-// @flow
-import React from 'react';
+import * as React from 'react';
import { translate } from '../../helpers/l10n';
export default function GlobalFooterSonarCloud() {
@@ -32,19 +31,26 @@ export default function GlobalFooterSonarCloud() {
. 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>
+ <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.tsx
index 74ef097213f..f8325090a85 100644
--- 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.tsx
@@ -17,8 +17,8 @@
* 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 React from 'react';
import GlobalFooter from '../GlobalFooter';
it('should render the only logged in information', () => {
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.tsx
index 2f41d408c61..98644558082 100644
--- 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.tsx
@@ -17,8 +17,8 @@
* 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 React from 'react';
import GlobalFooterSonarCloud from '../GlobalFooterSonarCloud';
it('should render correctly', () => {
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
index 438127ce0bc..00000000000
--- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.js.snap
+++ /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
index 00000000000..b7069f5b1cb
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooter-test.tsx.snap
@@ -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
index 0a3aa1953b1..00000000000
--- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.js.snap
+++ /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
index 00000000000..f7a6f4b0228
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalFooterSonarCloud-test.tsx.snap
@@ -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
index 00000000000..ea8fb503279
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/help/GlobalHelp.d.ts
@@ -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.tsx
index 564775c0179..04404a1fe1e 100644
--- 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.tsx
@@ -17,8 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-// @flow
-import React from 'react';
+import * as React from 'react';
import { connect } from 'react-redux';
import GlobalNavBranding from './GlobalNavBranding';
import GlobalNavMenu from './GlobalNavMenu';
@@ -28,42 +27,41 @@ import GlobalNavPlus from './GlobalNavPlus';
import Search from '../../search/Search';
import GlobalHelp from '../../help/GlobalHelp';
import * as theme from '../../../theme';
-import { isLoggedIn } from '../../../types';
+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 OnboardingModal from '../../../../apps/tutorials/onboarding/OnboardingModal';
+import { translate } from '../../../../helpers/l10n';
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
-};
-*/
+interface StateProps {
+ appState: AppState;
+ currentUser: CurrentUser;
+ 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
- };
+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);
@@ -79,9 +77,9 @@ class GlobalNav extends React.PureComponent {
window.removeEventListener('keypress', this.onKeyPress);
}
- onKeyPress = e => {
- const tagName = e.target.tagName;
- const code = e.keyCode || e.which;
+ 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) {
@@ -89,7 +87,7 @@ class GlobalNav extends React.PureComponent {
}
};
- handleHelpClick = event => {
+ handleHelpClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.openHelp();
};
@@ -98,12 +96,16 @@ class GlobalNav extends React.PureComponent {
closeHelp = () => this.setState({ helpOpen: false });
- openOnboardingTutorial = () => this.setState({ helpOpen: false, onboardingTutorialOpen: true });
+ openOnboardingTutorial = () => {
+ this.setState({ helpOpen: false });
+ this.props.openOnboardingTutorial();
+ };
closeOnboardingTutorial = () => {
- this.setState({ onboardingTutorialOpen: false, onboardingTutorialTooltip: true });
+ this.setState({ onboardingTutorialTooltip: true });
this.props.skipOnboarding();
- this.interval = setInterval(() => {
+ this.props.closeOnboardingTutorial();
+ this.interval = window.setInterval(() => {
this.setState({ onboardingTutorialTooltip: false });
}, 3000);
};
@@ -148,7 +150,7 @@ class GlobalNav extends React.PureComponent {
/>
)}
- {this.state.onboardingTutorialOpen && (
+ {this.props.isOnboardingTutorialOpen && (
<OnboardingModal onFinish={this.closeOnboardingTutorial} />
)}
</NavBar>
@@ -156,7 +158,7 @@ class GlobalNav extends React.PureComponent {
}
}
-const mapStateToProps = state => {
+const mapStateToProps = (state: any): StateProps => {
const sonarCloudSetting = getGlobalSettingValue(state, 'sonar.sonarcloud.enabled');
return {
@@ -166,6 +168,6 @@ const mapStateToProps = state => {
};
};
-const mapDispatchToProps = { skipOnboarding };
+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.tsx
index 7cea4c635b2..50277e52187 100644
--- 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.tsx
@@ -17,31 +17,23 @@
* 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 * as React from 'react';
import { Link } from 'react-router';
-import { isLoggedIn } from '../../../../app/types';
+import { isLoggedIn, CurrentUser, AppState, Extension } from '../../../../app/types';
import { translate } from '../../../../helpers/l10n';
-import { getQualityGatesUrl } from '../../../../helpers/urls';
+import { getQualityGatesUrl, getBaseUrl } 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: []
- };
+interface Props {
+ appState: AppState;
+ currentUser: CurrentUser;
+ location: { pathname: string };
+ onSonarCloud: boolean;
+}
- activeLink(url) {
- return window.location.pathname.indexOf(window.baseUrl + url) === 0 ? 'active' : null;
+export default class GlobalNavMenu extends React.PureComponent<Props> {
+ activeLink(url: string) {
+ return window.location.pathname.indexOf(getBaseUrl() + url) === 0 ? 'active' : undefined;
}
renderProjects() {
@@ -144,7 +136,7 @@ export default class GlobalNavMenu extends React.PureComponent {
);
}
- renderGlobalPageLink = ({ key, name }) => {
+ renderGlobalPageLink = ({ key, name }: Extension) => {
return (
<li key={key}>
<Link to={`/extension/${key}`}>{name}</Link>
@@ -153,7 +145,7 @@ export default class GlobalNavMenu extends React.PureComponent {
};
renderMore() {
- const { globalPages } = this.props.appState;
+ const { globalPages = [] } = this.props.appState;
const withoutPortfolios = globalPages.filter(page => page.key !== 'governance/portfolios');
if (withoutPortfolios.length === 0) {
return null;
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
index 1649a5e370e..fb19ba06d40 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx
@@ -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>
);
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx
index 659fe6def16..dbec4b97992 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavUser-test.tsx
@@ -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();
});
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap
index 673cc003e3c..34dd8f91396 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap
@@ -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"
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap
index 23fef6f2d99..378367bce15 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavUser-test.tsx.snap
@@ -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
index 00000000000..4aa6e4ea685
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/search/Search.d.ts
@@ -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> {}
diff --git a/server/sonar-web/src/main/js/app/styles/components/page.css b/server/sonar-web/src/main/js/app/styles/components/page.css
index a1a7d2b8610..53855c6747f 100644
--- a/server/sonar-web/src/main/js/app/styles/components/page.css
+++ b/server/sonar-web/src/main/js/app/styles/components/page.css
@@ -142,16 +142,8 @@
.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 {
@@ -162,6 +154,16 @@
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;
}
diff --git a/server/sonar-web/src/main/js/app/styles/init/forms.css b/server/sonar-web/src/main/js/app/styles/init/forms.css
index a9a610f6632..97287e21ac0 100644
--- a/server/sonar-web/src/main/js/app/styles/init/forms.css
+++ b/server/sonar-web/src/main/js/app/styles/init/forms.css
@@ -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);
diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts
index af3c21a26db..2b2bc7a64a8 100644
--- a/server/sonar-web/src/main/js/app/types.ts
+++ b/server/sonar-web/src/main/js/app/types.ts
@@ -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.tsx
index 1bded7bb20f..0f8b57127a7 100644
--- a/server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.js
+++ b/server/sonar-web/src/main/js/apps/account/organizations/OrganizationCard.tsx
@@ -17,61 +17,44 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-// @flow
-import React from 'react';
+import * as React from 'react';
+import OrganizationAvatar from '../../../components/common/OrganizationAvatar';
import OrganizationLink from '../../../components/ui/OrganizationLink';
-/*:: import type { Organization } from '../../../store/organizations/duck'; */
import { translate } from '../../../helpers/l10n';
+import { Organization } from '../../../app/types';
-/*::
-type Props = {
- organization: Organization
-};
-*/
-
-export default function OrganizationCard(props /*: Props */) {
- const { organization } = props;
+interface Props {
+ organization: Organization;
+}
+export default function OrganizationCard({ 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 className="account-project-side note">
+ <strong>{translate('organization.key')}:</strong> {organization.key}
</aside>
<h3 className="account-project-name">
- <OrganizationLink organization={organization}>{organization.name}</OrganizationLink>
+ <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="account-project-description">{organization.description}</div>
+ <div className="markdown spacer-top">{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>
+ {!!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.tsx
index 921b636dd3a..5b0b8ccc367 100644
--- a/server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.js
+++ b/server/sonar-web/src/main/js/apps/account/organizations/OrganizationsList.tsx
@@ -17,22 +17,19 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-// @flow
-import React from 'react';
+import * as React from 'react';
import { sortBy } from 'lodash';
import OrganizationCard from './OrganizationCard';
-/*:: import type { Organization } from '../../../store/organizations/duck'; */
+import { Organization } from '../../../app/types';
-/*::
-type Props = {
- organizations: Array<Organization>
-};
-*/
+interface Props {
+ organizations: Organization[];
+}
-export default function OrganizationsList(props /*: Props */) {
+export default function OrganizationsList({ organizations }: Props) {
return (
<ul className="account-projects-list">
- {sortBy(props.organizations, organization => organization.name.toLocaleLowerCase()).map(
+ {sortBy(organizations, organization => organization.name.toLocaleLowerCase()).map(
organization => (
<li key={organization.key}>
<OrganizationCard organization={organization} />
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
index 98d50b19d61..accf47cbd70 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
@@ -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 => (
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
index afa4332544c..84f0256e579 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap
index 61f766d7cf7..713261b2290 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap
@@ -11,7 +11,9 @@ exports[`renders 1`] = `
onClick={[Function]}
>
layout.settings
- <DropdownIcon />
+ <DropdownIcon
+ className="little-spacer-left"
+ />
</a>
<ul
className="dropdown-menu"
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
index 32683a0f5be..78f6ea2ae7c 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap
@@ -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>
`;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
index b562b7d46bf..d1bb1d60a34 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
@@ -19,22 +19,90 @@
*/
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);
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx
index 3635c476f77..8f767812c60 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx
@@ -19,8 +19,19 @@
*/
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();
});
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
index 3b6652f10f8..a7dede57348 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
@@ -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>
`;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap
index 93fb447b814..b2552033bef 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectsList-test.tsx.snap
@@ -47,6 +47,6 @@ exports[`renders different types of "no projects" 3`] = `
<div
className="projects-list"
>
- <NoFavoriteProjects />
+ <Connect(NoFavoriteProjects) />
</div>
`;
diff --git a/server/sonar-web/src/main/js/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css
index 707f0243994..e1084c7f01f 100644
--- a/server/sonar-web/src/main/js/apps/projects/styles.css
+++ b/server/sonar-web/src/main/js/apps/projects/styles.css
@@ -267,3 +267,8 @@
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
index 00000000000..d5d3100cbfa
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OnboardingModal.d.ts
@@ -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> {}
diff --git a/server/sonar-web/src/main/js/components/icons-components/DropdownIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/DropdownIcon.tsx
index 20bc9629f31..7cd3226777a 100644
--- a/server/sonar-web/src/main/js/components/icons-components/DropdownIcon.tsx
+++ b/server/sonar-web/src/main/js/components/icons-components/DropdownIcon.tsx
@@ -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"
diff --git a/server/sonar-web/src/main/js/components/nav/NavBar.tsx b/server/sonar-web/src/main/js/components/nav/NavBar.tsx
index 19397032533..f5647516be8 100644
--- a/server/sonar-web/src/main/js/components/nav/NavBar.tsx
+++ b/server/sonar-web/src/main/js/components/nav/NavBar.tsx
@@ -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/app/components/GlobalContainer.js b/server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx
index 43f8b058ec3..384849e61c1 100644
--- a/server/sonar-web/src/main/js/app/components/GlobalContainer.js
+++ b/server/sonar-web/src/main/js/components/ui/OrganizationListItem.tsx
@@ -17,25 +17,28 @@
* 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';
+import * as React from 'react';
+import { Organization } from '../../app/types';
+import OrganizationLink from './OrganizationLink';
+import OrganizationAvatar from '../common/OrganizationAvatar';
+import { translate } from '../../helpers/l10n';
-export default function GlobalContainer(props /*: Object */) {
- // it is important to pass `location` down to `GlobalNav` to trigger render on url change
+interface Props {
+ organization: Organization;
+}
+export default function OrganizationListItem({ organization }: Props) {
return (
- <div className="global-container">
- <div className="page-wrapper" id="container">
- <div className="page-container">
- <GlobalNav location={props.location} />
- <GlobalMessagesContainer />
- {props.children}
+ <li>
+ <OrganizationLink className="dropdown-item-flex" organization={organization}>
+ <div>
+ <OrganizationAvatar organization={organization} small={true} />
+ <span className="spacer-left">{organization.name}</span>
</div>
- </div>
- <GlobalFooterContainer />
- </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
index 00000000000..dd340f58686
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/__tests__/OrganizationListItem-test.tsx
@@ -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
index 00000000000..bd3a76ad927
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/OrganizationListItem-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/store/appState/duck.ts b/server/sonar-web/src/main/js/store/appState/duck.ts
index abb05d0b309..dd0f2a8c962 100644
--- a/server/sonar-web/src/main/js/store/appState/duck.ts
+++ b/server/sonar-web/src/main/js/store/appState/duck.ts
@@ -18,15 +18,7 @@
* 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 {
diff --git a/server/sonar-web/src/main/js/store/organizations/duck.js b/server/sonar-web/src/main/js/store/organizations/duck.js
index 4b4023f97ce..35578963545 100644
--- a/server/sonar-web/src/main/js/store/organizations/duck.js
+++ b/server/sonar-web/src/main/js/store/organizations/duck.js
@@ -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,