aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2017-12-13 13:44:12 +0100
committerStas Vilchik <stas.vilchik@sonarsource.com>2018-01-02 10:38:10 +0100
commit027514e6f94607fcd7df8e69e668fe32aeb2873e (patch)
treeb73db22c100713df85f1a790c4a772300b41a7f5 /server/sonar-web/src
parent492cd3de03d14aaf91b54e96531d77079c0db7f1 (diff)
downloadsonarqube-027514e6f94607fcd7df8e69e668fe32aeb2873e.tar.gz
sonarqube-027514e6f94607fcd7df8e69e668fe32aeb2873e.zip
SONAR-10182 Users should be able to choose their homepage
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/api/users.ts6
-rw-r--r--server/sonar-web/src/main/js/app/components/Landing.tsx10
-rw-r--r--server/sonar-web/src/main/js/app/components/help/GlobalHelp.js2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css19
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx10
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js107
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.tsx111
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js48
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx67
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js)30
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx14
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap108
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.tsx.snap98
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap84
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.tsx23
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.tsx2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js)2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.tsx.snap (renamed from server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavMenu-test.js.snap)0
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx4
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap8
-rw-r--r--server/sonar-web/src/main/js/app/components/search/Search.css2
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/forms.css2
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/icons.css10
-rw-r--r--server/sonar-web/src/main/js/app/types.ts17
-rw-r--r--server/sonar-web/src/main/js/apps/explore/Explore.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/App.js8
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/PageActions.js8
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css4
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationHeader.tsx46
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationHeader-test.tsx.snap34
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMeta-test.tsx.snap12
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js6
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js2
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js4
-rw-r--r--server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap6
-rw-r--r--server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx19
-rw-r--r--server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx90
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap42
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/FavoriteIcon.tsx31
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/HomeIcon.tsx50
-rw-r--r--server/sonar-web/src/main/js/components/nav/ContextNavBar.css25
-rw-r--r--server/sonar-web/src/main/js/helpers/urls.ts22
-rw-r--r--server/sonar-web/src/main/js/store/users/actions.ts (renamed from server/sonar-web/src/main/js/store/users/actions.js)23
-rw-r--r--server/sonar-web/src/main/js/store/users/reducer.ts (renamed from server/sonar-web/src/main/js/store/users/reducer.js)36
52 files changed, 770 insertions, 550 deletions
diff --git a/server/sonar-web/src/main/js/api/users.ts b/server/sonar-web/src/main/js/api/users.ts
index fa1b73f80fa..7b3f4de1b77 100644
--- a/server/sonar-web/src/main/js/api/users.ts
+++ b/server/sonar-web/src/main/js/api/users.ts
@@ -19,7 +19,7 @@
*/
import { getJSON, post, postJSON, RequestData } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
-import { CurrentUser, Paging } from '../app/types';
+import { Paging, HomePage, CurrentUser } from '../app/types';
export interface IdentityProvider {
backgroundColor: string;
@@ -102,3 +102,7 @@ export function deactivateUser(data: { login: string }): Promise<User> {
export function skipOnboarding(): Promise<void | Response> {
return post('/api/users/skip_onboarding_tutorial').catch(throwGlobalError);
}
+
+export function setHomePage(homepage: HomePage): Promise<void | Response> {
+ return post('/api/users/set_homepage', homepage).catch(throwGlobalError);
+}
diff --git a/server/sonar-web/src/main/js/app/components/Landing.tsx b/server/sonar-web/src/main/js/app/components/Landing.tsx
index 792826f1091..cdf7a236ad2 100644
--- a/server/sonar-web/src/main/js/app/components/Landing.tsx
+++ b/server/sonar-web/src/main/js/app/components/Landing.tsx
@@ -20,8 +20,9 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import { getCurrentUser, getGlobalSettingValue } from '../../store/rootReducer';
import { CurrentUser, isLoggedIn } from '../types';
+import { getCurrentUser, getGlobalSettingValue } from '../../store/rootReducer';
+import { getHomePageUrl } from '../../helpers/urls';
interface Props {
currentUser: CurrentUser;
@@ -36,7 +37,12 @@ class Landing extends React.PureComponent<Props> {
componentDidMount() {
const { currentUser, onSonarCloud } = this.props;
if (isLoggedIn(currentUser)) {
- this.context.router.replace('/projects');
+ if (onSonarCloud && currentUser.homepage) {
+ const homepage = getHomePageUrl(currentUser.homepage);
+ this.context.router.replace(homepage);
+ } else {
+ this.context.router.replace('/projects');
+ }
} else if (onSonarCloud) {
window.location.href = 'https://about.sonarcloud.io';
} else {
diff --git a/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js b/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js
index a4ec0a4c324..a20b7e80dd2 100644
--- a/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js
+++ b/server/sonar-web/src/main/js/app/components/help/GlobalHelp.js
@@ -88,7 +88,7 @@ export default class GlobalHelp extends React.PureComponent {
renderMenu = () => (
<ul className="side-tabs-menu">
- {(this.props.currentUser.isLoggedIn
+ {(this.props.currentUser.isLoggedIn && !this.props.onSonarCloud
? ['shortcuts', 'tutorials', 'links']
: ['shortcuts', 'links']
).map(this.renderMenuItem)}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
index ccfd5d9c63e..fd0f3a46df1 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
@@ -1,17 +1,3 @@
-.navbar-context-favorite {
- display: inline-block;
- vertical-align: top;
- padding-top: var(--gridSize);
- padding-left: calc(1.5 * var(--gridSize));
-}
-
-.navbar-context-title-qualifier {
- display: inline-block;
- line-height: 16px;
- padding-top: 5px;
- box-sizing: border-box;
-}
-
.navbar-context-branches {
display: inline-block;
vertical-align: top;
@@ -20,11 +6,6 @@
line-height: 16px;
}
-.navbar-context-meta-branch {
- margin-top: 3px;
- line-height: 16px;
-}
-
.navbar-context-meta-branch-menu-item {
display: flex !important;
justify-content: space-between;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
index 2dbc6fe9ea4..ff2205adbb6 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
@@ -18,7 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import ComponentNavFavorite from './ComponentNavFavorite';
import ComponentNavBranch from './ComponentNavBranch';
import ComponentNavBreadcrumbs from './ComponentNavBreadcrumbs';
import ComponentNavMeta from './ComponentNavMeta';
@@ -112,14 +111,7 @@ export default class ComponentNav extends React.PureComponent<Props, State> {
id="context-navigation"
height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw}
notif={notifComponent}>
- <ComponentNavBreadcrumbs
- component={this.props.component}
- breadcrumbs={this.props.component.breadcrumbs}
- />
- <ComponentNavFavorite
- component={this.props.component.key}
- favorite={this.props.component.isFavorite}
- />
+ <ComponentNavBreadcrumbs component={this.props.component} />
{this.props.currentBranch && (
<ComponentNavBranch
branches={this.props.branches}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js
deleted file mode 100644
index 23138af7f82..00000000000
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js
+++ /dev/null
@@ -1,107 +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 { connect } from 'react-redux';
-import { Link } from 'react-router';
-import QualifierIcon from '../../../../components/shared/QualifierIcon';
-import { getOrganizationByKey, areThereCustomOrganizations } from '../../../../store/rootReducer';
-import OrganizationAvatar from '../../../../components/common/OrganizationAvatar';
-import OrganizationHelmet from '../../../../components/common/OrganizationHelmet';
-import OrganizationLink from '../../../../components/ui/OrganizationLink';
-import PrivateBadge from '../../../../components/common/PrivateBadge';
-import { collapsePath, limitComponentName } from '../../../../helpers/path';
-import { getProjectUrl } from '../../../../helpers/urls';
-
-class ComponentNavBreadcrumbs extends React.PureComponent {
- static propTypes = {
- breadcrumbs: PropTypes.array,
- component: PropTypes.shape({
- visibility: PropTypes.string
- }).isRequired
- };
-
- render() {
- const { breadcrumbs, component, organization, shouldOrganizationBeDisplayed } = this.props;
-
- if (!breadcrumbs) {
- return null;
- }
-
- const displayOrganization = organization != null && shouldOrganizationBeDisplayed;
-
- const lastItem = breadcrumbs[breadcrumbs.length - 1];
-
- const items = breadcrumbs.map((item, index) => {
- const isPath = item.qualifier === 'DIR';
- const itemName = isPath ? collapsePath(item.name, 15) : limitComponentName(item.name);
- return (
- <span key={item.key}>
- {index === 0 && (
- <span className="navbar-context-title-qualifier spacer-right">
- <QualifierIcon qualifier={lastItem.qualifier} />
- </span>
- )}
- <Link
- className="link-base-color link-no-underline"
- title={item.name}
- to={getProjectUrl(item.key)}>
- {itemName}
- </Link>
- {index < breadcrumbs.length - 1 && <span className="slash-separator" />}
- </span>
- );
- });
-
- return (
- <h1 className="navbar-context-header">
- <OrganizationHelmet
- title={component.name}
- organization={displayOrganization ? organization : null}
- />
- {displayOrganization && (
- <span>
- <OrganizationAvatar organization={organization} />
- <OrganizationLink
- organization={organization}
- className="link-base-color link-no-underline spacer-left">
- {organization.name}
- </OrganizationLink>
- <span className="slash-separator" />
- </span>
- )}
- {items}
- {component.visibility === 'private' && (
- <PrivateBadge className="spacer-left" qualifier={component.qualifier} />
- )}
- </h1>
- );
- }
-}
-
-const mapStateToProps = (state, ownProps) => ({
- organization:
- ownProps.component.organization && getOrganizationByKey(state, ownProps.component.organization),
- shouldOrganizationBeDisplayed: areThereCustomOrganizations(state)
-});
-
-export default connect(mapStateToProps)(ComponentNavBreadcrumbs);
-
-export const Unconnected = ComponentNavBreadcrumbs;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.tsx
new file mode 100644
index 00000000000..c8a75e8a105
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.tsx
@@ -0,0 +1,111 @@
+/*
+ * 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 { Link } from 'react-router';
+import { Component, Organization } from '../../../types';
+import QualifierIcon from '../../../../components/shared/QualifierIcon';
+import { getOrganizationByKey, areThereCustomOrganizations } from '../../../../store/rootReducer';
+import OrganizationAvatar from '../../../../components/common/OrganizationAvatar';
+import OrganizationHelmet from '../../../../components/common/OrganizationHelmet';
+import OrganizationLink from '../../../../components/ui/OrganizationLink';
+import PrivateBadge from '../../../../components/common/PrivateBadge';
+import { collapsePath, limitComponentName } from '../../../../helpers/path';
+import { getProjectUrl } from '../../../../helpers/urls';
+
+interface StateProps {
+ organization?: Organization;
+ shouldOrganizationBeDisplayed: boolean;
+}
+
+interface OwnProps {
+ component: Component;
+}
+
+interface Props extends StateProps, OwnProps {}
+
+export function ComponentNavBreadcrumbs(props: Props) {
+ const { component, organization, shouldOrganizationBeDisplayed } = props;
+ const { breadcrumbs } = component;
+
+ const lastItem = breadcrumbs[breadcrumbs.length - 1];
+
+ const items: JSX.Element[] = [];
+ breadcrumbs.forEach((item, index) => {
+ const isPath = item.qualifier === 'DIR';
+ const itemName = isPath ? collapsePath(item.name, 15) : limitComponentName(item.name);
+
+ if (index === 0) {
+ items.push(
+ <QualifierIcon
+ className="spacer-right"
+ key={`qualifier-${item.key}`}
+ qualifier={lastItem.qualifier}
+ />
+ );
+ }
+
+ items.push(
+ <Link
+ className="link-base-color link-no-underline"
+ key={`name-${item.key}`}
+ title={item.name}
+ to={getProjectUrl(item.key)}>
+ {itemName}
+ </Link>
+ );
+
+ if (index < breadcrumbs.length - 1) {
+ items.push(<span className="slash-separator" key={`separator-${item.key}`} />);
+ }
+ });
+
+ return (
+ <header className="navbar-context-header">
+ <OrganizationHelmet
+ title={component.name}
+ organization={organization && shouldOrganizationBeDisplayed ? organization : undefined}
+ />
+ {organization &&
+ shouldOrganizationBeDisplayed && <OrganizationAvatar organization={organization} />}
+ {organization &&
+ shouldOrganizationBeDisplayed && (
+ <OrganizationLink
+ organization={organization}
+ className="link-base-color link-no-underline spacer-left">
+ {organization.name}
+ </OrganizationLink>
+ )}
+ {organization && shouldOrganizationBeDisplayed && <span className="slash-separator" />}
+ {items}
+ {component.visibility === 'private' && (
+ <PrivateBadge className="spacer-left" qualifier={component.qualifier} />
+ )}
+ </header>
+ );
+}
+
+const mapStateToProps = (state: any, ownProps: OwnProps): StateProps => ({
+ organization:
+ ownProps.component.organization && getOrganizationByKey(state, ownProps.component.organization),
+ shouldOrganizationBeDisplayed: areThereCustomOrganizations(state)
+});
+
+export default connect(mapStateToProps)(ComponentNavBreadcrumbs);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js
deleted file mode 100644
index 9d6c59c000b..00000000000
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavFavorite.js
+++ /dev/null
@@ -1,48 +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 { connect } from 'react-redux';
-import Favorite from '../../../../components/controls/Favorite';
-import { getCurrentUser } from '../../../../store/rootReducer';
-
-class ComponentNavFavorite extends React.PureComponent {
- static propTypes = {
- currentUser: PropTypes.object.isRequired
- };
-
- render() {
- if (!this.props.currentUser.isLoggedIn) {
- return null;
- }
-
- return (
- <div className="navbar-context-favorite">
- <Favorite component={this.props.component} favorite={this.props.favorite} />
- </div>
- );
- }
-}
-
-const mapStateToProps = state => ({
- currentUser: getCurrentUser(state)
-});
-
-export default connect(mapStateToProps)(ComponentNavFavorite);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
index 0e157ebe219..01be74777a6 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
@@ -18,47 +18,62 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { connect } from 'react-redux';
+import { Branch, Component, CurrentUser, isLoggedIn } from '../../../types';
import BranchStatus from '../../../../components/common/BranchStatus';
-import { Branch, Component } from '../../../types';
import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter';
+import Favorite from '../../../../components/controls/Favorite';
+import HomePageSelect from '../../../../components/controls/HomePageSelect';
import Tooltip from '../../../../components/controls/Tooltip';
import { isShortLivingBranch } from '../../../../helpers/branches';
import { translate } from '../../../../helpers/l10n';
+import { getCurrentUser } from '../../../../store/rootReducer';
-interface Props {
+interface StateProps {
+ currentUser: CurrentUser;
+}
+
+interface Props extends StateProps {
branch?: Branch;
component: Component;
}
-export default function ComponentNavMeta(props: Props) {
- const shortBranch = props.branch && isShortLivingBranch(props.branch);
- const showVersion = props.component.version && !shortBranch;
+export function ComponentNavMeta({ branch, component, currentUser }: Props) {
+ const shortBranch = branch && isShortLivingBranch(branch);
+ const mainBranch = !branch || branch.isMain;
return (
<div className="navbar-context-meta">
- <ul className="list-inline">
- {props.component.analysisDate && (
- <li>
- <DateTimeFormatter date={props.component.analysisDate} />
- </li>
- )}
- {showVersion && (
- <li>
- <Tooltip
- overlay={`${translate('version')} ${props.component.version}`}
- mouseEnterDelay={0.5}>
- <span className="text-limited">
- {translate('version')} {props.component.version}
- </span>
- </Tooltip>
- </li>
- )}
- </ul>
- {shortBranch && (
- <div className="navbar-context-meta-branch">
- <BranchStatus branch={props.branch!} />
+ {component.analysisDate && (
+ <div className="spacer-left">
+ <DateTimeFormatter date={component.analysisDate} />
</div>
)}
+ {component.version &&
+ !shortBranch && (
+ <Tooltip overlay={`${translate('version')} ${component.version}`} mouseEnterDelay={0.5}>
+ <div className="spacer-left text-limited">
+ {translate('version')} {component.version}
+ </div>
+ </Tooltip>
+ )}
+ {isLoggedIn(currentUser) &&
+ mainBranch && (
+ <div className="navbar-context-meta-secondary">
+ <Favorite component={component.key} favorite={Boolean(component.isFavorite)} />
+ <HomePageSelect
+ className="spacer-left"
+ currentPage={{ type: 'project', key: component.key }}
+ />
+ </div>
+ )}
+ {shortBranch && <BranchStatus branch={branch!} />}
</div>
);
}
+
+const mapStateToProps = (state: any): StateProps => ({
+ currentUser: getCurrentUser(state)
+});
+
+export default connect(mapStateToProps)(ComponentNavMeta);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
index 170bce7eb27..26f876d34f0 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
@@ -22,9 +22,9 @@ import * as React from 'react';
import { mount, shallow } from 'enzyme';
import ComponentNav from '../ComponentNav';
-jest.mock('../ComponentNavFavorite', () => ({
+jest.mock('../ComponentNavMeta', () => ({
// eslint-disable-next-line
- default: function ComponentNavFavorite() {
+ default: function ComponentNavMeta() {
return null;
}
}));
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.tsx
index 3a524a9ee61..78a2eed1234 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.tsx
@@ -17,35 +17,42 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
+import * as React from 'react';
import { shallow } from 'enzyme';
-import { Unconnected } from '../ComponentNavBreadcrumbs';
+import { ComponentNavBreadcrumbs } from '../ComponentNavBreadcrumbs';
+import { Visibility } from '../../../../types';
it('should not render breadcrumbs with one element', () => {
const component = {
+ breadcrumbs: [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }],
key: 'my-project',
name: 'My Project',
+ organization: 'org',
qualifier: 'TRK',
visibility: 'public'
};
- const breadcrumbs = [component];
- const result = shallow(<Unconnected breadcrumbs={breadcrumbs} component={component} />);
+ const result = shallow(
+ <ComponentNavBreadcrumbs component={component} shouldOrganizationBeDisplayed={false} />
+ );
expect(result).toMatchSnapshot();
});
it('should render organization', () => {
const component = {
+ breadcrumbs: [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }],
key: 'my-project',
name: 'My Project',
organization: 'foo',
qualifier: 'TRK',
visibility: 'public'
};
- const breadcrumbs = [component];
- const organization = { key: 'foo', name: 'The Foo Organization' };
+ const organization = {
+ key: 'foo',
+ name: 'The Foo Organization',
+ projectVisibility: Visibility.Public
+ };
const result = shallow(
- <Unconnected
- breadcrumbs={breadcrumbs}
+ <ComponentNavBreadcrumbs
component={component}
organization={organization}
shouldOrganizationBeDisplayed={true}
@@ -56,12 +63,15 @@ it('should render organization', () => {
it('renders private badge', () => {
const component = {
+ breadcrumbs: [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }],
key: 'my-project',
name: 'My Project',
+ organization: 'org',
qualifier: 'TRK',
visibility: 'private'
};
- const breadcrumbs = [component];
- const result = shallow(<Unconnected breadcrumbs={breadcrumbs} component={component} />);
+ const result = shallow(
+ <ComponentNavBreadcrumbs component={component} shouldOrganizationBeDisplayed={false} />
+ );
expect(result.find('PrivateBadge')).toHaveLength(1);
});
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx
index 331c7c034e7..b989df12142 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx
@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
-import ComponentNavMeta from '../ComponentNavMeta';
+import { ComponentNavMeta } from '../ComponentNavMeta';
import { BranchType, ShortLivingBranch, LongLivingBranch } from '../../../../types';
const component = {
@@ -40,7 +40,11 @@ it('renders status of short-living branch', () => {
status: { bugs: 0, codeSmells: 2, vulnerabilities: 3 },
type: BranchType.SHORT
};
- expect(shallow(<ComponentNavMeta branch={branch} component={component} />)).toMatchSnapshot();
+ expect(
+ shallow(
+ <ComponentNavMeta branch={branch} component={component} currentUser={{ isLoggedIn: false }} />
+ )
+ ).toMatchSnapshot();
});
it('renders meta for long-living branch', () => {
@@ -50,5 +54,9 @@ it('renders meta for long-living branch', () => {
status: { qualityGateStatus: 'OK' },
type: BranchType.LONG
};
- expect(shallow(<ComponentNavMeta branch={branch} component={component} />)).toMatchSnapshot();
+ expect(
+ shallow(
+ <ComponentNavMeta branch={branch} component={component} currentUser={{ isLoggedIn: false }} />
+ )
+ ).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
index 1baaad7dc1f..6fea2eff38e 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNav-test.tsx.snap
@@ -28,15 +28,6 @@ exports[`renders 1`] = `
}
>
<ComponentNavBreadcrumbs
- breadcrumbs={
- Array [
- Object {
- "key": "component",
- "name": "component",
- "qualifier": "TRK",
- },
- ]
- }
component={
Object {
"breadcrumbs": Array [
@@ -53,9 +44,6 @@ exports[`renders 1`] = `
}
}
/>
- <ComponentNavFavorite
- component="component"
- />
<ComponentNavMeta
component={
Object {
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap
deleted file mode 100644
index 99cf4b7c00f..00000000000
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.js.snap
+++ /dev/null
@@ -1,108 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should not render breadcrumbs with one element 1`] = `
-<h1
- className="navbar-context-header"
->
- <OrganizationHelmet
- organization={null}
- title="My Project"
- />
- <span
- key="my-project"
- >
- <span
- className="navbar-context-title-qualifier spacer-right"
- >
- <QualifierIcon
- qualifier="TRK"
- />
- </span>
- <Link
- className="link-base-color link-no-underline"
- onlyActiveOnIndex={false}
- style={Object {}}
- title="My Project"
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "branch": undefined,
- "id": "my-project",
- },
- }
- }
- >
- My Project
- </Link>
- </span>
-</h1>
-`;
-
-exports[`should render organization 1`] = `
-<h1
- className="navbar-context-header"
->
- <OrganizationHelmet
- organization={
- Object {
- "key": "foo",
- "name": "The Foo Organization",
- }
- }
- title="My Project"
- />
- <span>
- <OrganizationAvatar
- organization={
- Object {
- "key": "foo",
- "name": "The Foo Organization",
- }
- }
- />
- <OrganizationLink
- className="link-base-color link-no-underline spacer-left"
- organization={
- Object {
- "key": "foo",
- "name": "The Foo Organization",
- }
- }
- >
- The Foo Organization
- </OrganizationLink>
- <span
- className="slash-separator"
- />
- </span>
- <span
- key="my-project"
- >
- <span
- className="navbar-context-title-qualifier spacer-right"
- >
- <QualifierIcon
- qualifier="TRK"
- />
- </span>
- <Link
- className="link-base-color link-no-underline"
- onlyActiveOnIndex={false}
- style={Object {}}
- title="My Project"
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "branch": undefined,
- "id": "my-project",
- },
- }
- }
- >
- My Project
- </Link>
- </span>
-</h1>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.tsx.snap
new file mode 100644
index 00000000000..e907a97d1b5
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBreadcrumbs-test.tsx.snap
@@ -0,0 +1,98 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should not render breadcrumbs with one element 1`] = `
+<header
+ className="navbar-context-header"
+>
+ <OrganizationHelmet
+ title="My Project"
+ />
+ <QualifierIcon
+ className="spacer-right"
+ key="qualifier-my-project"
+ qualifier="TRK"
+ />
+ <Link
+ className="link-base-color link-no-underline"
+ key="name-my-project"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ title="My Project"
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": undefined,
+ "id": "my-project",
+ },
+ }
+ }
+ >
+ My Project
+ </Link>
+</header>
+`;
+
+exports[`should render organization 1`] = `
+<header
+ className="navbar-context-header"
+>
+ <OrganizationHelmet
+ organization={
+ Object {
+ "key": "foo",
+ "name": "The Foo Organization",
+ "projectVisibility": "public",
+ }
+ }
+ title="My Project"
+ />
+ <OrganizationAvatar
+ organization={
+ Object {
+ "key": "foo",
+ "name": "The Foo Organization",
+ "projectVisibility": "public",
+ }
+ }
+ />
+ <OrganizationLink
+ className="link-base-color link-no-underline spacer-left"
+ organization={
+ Object {
+ "key": "foo",
+ "name": "The Foo Organization",
+ "projectVisibility": "public",
+ }
+ }
+ >
+ The Foo Organization
+ </OrganizationLink>
+ <span
+ className="slash-separator"
+ />
+ <QualifierIcon
+ className="spacer-right"
+ key="qualifier-my-project"
+ qualifier="TRK"
+ />
+ <Link
+ className="link-base-color link-no-underline"
+ key="name-my-project"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ title="My Project"
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": undefined,
+ "id": "my-project",
+ },
+ }
+ }
+ >
+ My Project
+ </Link>
+</header>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap
index a1dd339dfce..726f3168f9f 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMeta-test.tsx.snap
@@ -4,30 +4,26 @@ exports[`renders meta for long-living branch 1`] = `
<div
className="navbar-context-meta"
>
- <ul
- className="list-inline"
+ <div
+ className="spacer-left"
+ >
+ <DateTimeFormatter
+ date="2017-01-02T00:00:00.000Z"
+ />
+ </div>
+ <Tooltip
+ mouseEnterDelay={0.5}
+ overlay="version 0.0.1"
+ placement="bottom"
>
- <li>
- <DateTimeFormatter
- date="2017-01-02T00:00:00.000Z"
- />
- </li>
- <li>
- <Tooltip
- mouseEnterDelay={0.5}
- overlay="version 0.0.1"
- placement="bottom"
- >
- <span
- className="text-limited"
- >
- version
-
- 0.0.1
- </span>
- </Tooltip>
- </li>
- </ul>
+ <div
+ className="spacer-left text-limited"
+ >
+ version
+
+ 0.0.1
+ </div>
+ </Tooltip>
</div>
`;
@@ -35,33 +31,27 @@ exports[`renders status of short-living branch 1`] = `
<div
className="navbar-context-meta"
>
- <ul
- className="list-inline"
- >
- <li>
- <DateTimeFormatter
- date="2017-01-02T00:00:00.000Z"
- />
- </li>
- </ul>
<div
- className="navbar-context-meta-branch"
+ className="spacer-left"
>
- <BranchStatus
- branch={
- Object {
- "isMain": false,
- "mergeBranch": "master",
- "name": "feature",
- "status": Object {
- "bugs": 0,
- "codeSmells": 2,
- "vulnerabilities": 3,
- },
- "type": "SHORT",
- }
- }
+ <DateTimeFormatter
+ date="2017-01-02T00:00:00.000Z"
/>
</div>
+ <BranchStatus
+ branch={
+ Object {
+ "isMain": false,
+ "mergeBranch": "master",
+ "name": "feature",
+ "status": Object {
+ "bugs": 0,
+ "codeSmells": 2,
+ "vulnerabilities": 3,
+ },
+ "type": "SHORT",
+ }
+ }
+ />
</div>
`;
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
index 04404a1fe1e..107251d3629 100644
--- 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
@@ -110,6 +110,15 @@ class GlobalNav extends React.PureComponent<Props, State> {
}, 3000);
};
+ withTutorialTooltip = (element: React.ReactNode) =>
+ this.state.onboardingTutorialTooltip ? (
+ <Tooltip defaultVisible={true} overlay={translate('tutorials.follow_later')} trigger="manual">
+ {element}
+ </Tooltip>
+ ) : (
+ element
+ );
+
render() {
return (
<NavBar className="navbar-global" id="global-navigation" height={theme.globalNavHeightRaw}>
@@ -121,21 +130,13 @@ class GlobalNav extends React.PureComponent<Props, State> {
<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 />
- )}
+ {this.props.onSonarCloud ? <HelpIcon /> : this.withTutorialTooltip(<HelpIcon />)}
</a>
</li>
<Search appState={this.props.appState} currentUser={this.props.currentUser} />
{isLoggedIn(this.props.currentUser) &&
- this.props.onSonarCloud && (
+ this.props.onSonarCloud &&
+ this.withTutorialTooltip(
<GlobalNavPlus openOnboardingTutorial={this.openOnboardingTutorial} />
)}
<GlobalNavUserContainer {...this.props} />
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
index 50277e52187..ba7dfcdbf64 100644
--- 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
@@ -28,7 +28,7 @@ interface Props {
appState: AppState;
currentUser: CurrentUser;
location: { pathname: string };
- onSonarCloud: boolean;
+ onSonarCloud?: boolean;
}
export default class GlobalNavMenu extends React.PureComponent<Props> {
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 fb19ba06d40..b2e8119ec2a 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
@@ -31,7 +31,7 @@ import { getBaseUrl } from '../../../../helpers/urls';
import Dropdown from '../../../../components/controls/Dropdown';
interface Props {
- appState: { organizationsEnabled: boolean };
+ appState: { organizationsEnabled?: boolean };
currentUser: CurrentUser;
organizations: Organization[];
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx
index 33546be3731..7b3afe86905 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.js
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavMenu-test.tsx
@@ -17,7 +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.
*/
-import React from 'react';
+import * as React from 'react';
import { shallow } from 'enzyme';
import GlobalNavMenu from '../GlobalNavMenu';
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.tsx.snap
index 34dd8f91396..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.tsx.snap
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
index 54d91d77c75..f45d95441dc 100644
--- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
@@ -206,7 +206,9 @@ export default class SettingsNav extends React.PureComponent<Props> {
id="context-navigation"
height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw}
notif={notifComponent}>
- <h1 className="navbar-context-header">{translate('layout.settings')}</h1>
+ <header className="navbar-context-header">
+ <h1>{translate('layout.settings')}</h1>
+ </header>
<NavBarTabs>
{this.renderConfigurationTab()}
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
index 3208552f512..64822b179f6 100644
--- a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
@@ -5,11 +5,13 @@ exports[`should work with extensions 1`] = `
height={72}
id="context-navigation"
>
- <h1
+ <header
className="navbar-context-header"
>
- layout.settings
- </h1>
+ <h1>
+ layout.settings
+ </h1>
+ </header>
<NavBarTabs>
<li
className="dropdown"
diff --git a/server/sonar-web/src/main/js/app/components/search/Search.css b/server/sonar-web/src/main/js/app/components/search/Search.css
index 028707705cf..033eda0c07a 100644
--- a/server/sonar-web/src/main/js/app/components/search/Search.css
+++ b/server/sonar-web/src/main/js/app/components/search/Search.css
@@ -81,7 +81,7 @@
left: 0;
}
-.navbar-search-item-icons > .icon-star,
+.navbar-search-item-icons > .icon-outline,
.navbar-search-item-icons > .icon-clock {
z-index: 6;
top: -4px;
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 97287e21ac0..d63fe1e3b15 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
@@ -304,7 +304,7 @@ input[type='submit'].button-grey.button-active {
}
.button-small > svg {
- margin-top: 2px;
+ padding-top: 2px;
}
.button-group {
diff --git a/server/sonar-web/src/main/js/app/styles/init/icons.css b/server/sonar-web/src/main/js/app/styles/init/icons.css
index 315803eb185..2033873f784 100644
--- a/server/sonar-web/src/main/js/app/styles/init/icons.css
+++ b/server/sonar-web/src/main/js/app/styles/init/icons.css
@@ -476,11 +476,11 @@ a:hover > .icon-radio {
background-image: url('data:image/svg+xml,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cpath%20d%3D%22M15.428%205.777c0%20.13-.078.274-.233.428l-3.24%203.16.767%204.465c.006.042.01.102.01.18%200%20.124-.032.23-.095.316-.062.086-.153.13-.272.13-.113%200-.232-.036-.357-.108l-4.01-2.107L3.99%2014.35c-.13.072-.25.107-.357.107-.125%200-.22-.043-.28-.13-.064-.085-.095-.19-.095-.316%200-.037.006-.096.018-.18l.768-4.464-3.25-3.16C.644%206.045.57%205.9.57%205.775c0-.22.167-.356.5-.41l4.482-.652L7.562.652c.112-.244.258-.366.437-.366.177%200%20.323.122.436.366l2.01%204.062%204.48.652c.335.054.5.19.5.41h.002z%22%20fill%3D%22%23CDCDCD%22%20fill-rule%3D%22nonzero%22%2F%3E%3C%2Fsvg%3E');
}
-.icon-star {
+.icon-outline {
transition: all 0.2s ease !important;
}
-.icon-star path {
+.icon-outline path {
stroke: var(--secondFontColor);
stroke-width: 1.41421356;
stroke-opacity: 1;
@@ -488,9 +488,9 @@ a:hover > .icon-radio {
transition: all 0.2s ease;
}
-.icon-star-favorite path {
- fill: #ff9900;
- stroke-opacity: 0;
+.icon-outline.is-filled path {
+ fill: currentColor;
+ stroke: currentColor;
fill-opacity: 1;
}
diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts
index 2b2bc7a64a8..70df316dea2 100644
--- a/server/sonar-web/src/main/js/app/types.ts
+++ b/server/sonar-web/src/main/js/app/types.ts
@@ -80,6 +80,7 @@ export interface Component {
qualifier: string;
refKey?: string;
version?: string;
+ visibility?: string;
}
interface ComponentConfiguration {
@@ -140,9 +141,19 @@ export interface CurrentUser {
showOnboardingTutorial?: boolean;
}
+export interface HomePage {
+ key?: string;
+ type: string;
+}
+
+export function isSameHomePage(a: HomePage, b: HomePage) {
+ return a.type === b.type && a.key === b.key;
+}
+
export interface LoggedInUser extends CurrentUser {
avatar?: string;
email?: string;
+ homepage?: HomePage;
isLoggedIn: true;
name: string;
}
@@ -153,10 +164,10 @@ export function isLoggedIn(user: CurrentUser): user is LoggedInUser {
export interface AppState {
adminPages?: Extension[];
- authenticationError: boolean;
- authorizationError: boolean;
+ authenticationError?: boolean;
+ authorizationError?: boolean;
canAdmin?: boolean;
globalPages?: Extension[];
- organizationsEnabled: boolean;
+ organizationsEnabled?: boolean;
qualifiers: string[];
}
diff --git a/server/sonar-web/src/main/js/apps/explore/Explore.tsx b/server/sonar-web/src/main/js/apps/explore/Explore.tsx
index a1ed154d36a..62510555da8 100644
--- a/server/sonar-web/src/main/js/apps/explore/Explore.tsx
+++ b/server/sonar-web/src/main/js/apps/explore/Explore.tsx
@@ -32,9 +32,9 @@ export default function Explore(props: Props) {
return (
<div id="explore">
<ContextNavBar id="explore-navigation" height={theme.contextNavHeightRaw}>
- <div className="navbar-context-header">
- <h1 className="display-inline-block">{translate('explore')}</h1>
- </div>
+ <header className="navbar-context-header">
+ <h1>{translate('explore')}</h1>
+ </header>
<NavBarTabs>
<li>
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js
index a7121ced22d..cc68fbb41c9 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/App.js
+++ b/server/sonar-web/src/main/js/apps/issues/components/App.js
@@ -56,6 +56,7 @@ import {
CurrentUser
} from '../utils'; */
import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
+import { isLoggedIn } from '../../../app/types';
import ListFooter from '../../../components/controls/ListFooter';
import EmptySearch from '../../../components/common/EmptySearch';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
@@ -923,6 +924,13 @@ export default class App extends React.PureComponent {
</div>
) : (
<PageActions
+ canSetHome={
+ this.props.onSonarCloud &&
+ isLoggedIn(this.props.currentUser) &&
+ this.props.myIssues &&
+ !this.props.organization &&
+ !this.props.component
+ }
loading={this.state.loading}
onReload={this.handleReload}
paging={paging}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/PageActions.js b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js
index e939e60869f..2aaca8517e8 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/PageActions.js
+++ b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js
@@ -19,14 +19,16 @@
*/
// @flow
import React from 'react';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
import IssuesCounter from './IssuesCounter';
import ReloadButton from './ReloadButton';
/*:: import type { Paging } from '../utils'; */
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import HomePageSelect from '../../../components/controls/HomePageSelect';
import { translate } from '../../../helpers/l10n';
/*::
type Props = {|
+ canSetHome: bool,
loading: boolean,
onReload: () => void,
paging: ?Paging,
@@ -70,6 +72,10 @@ export default class PageActions extends React.PureComponent {
<IssuesCounter className="spacer-left" current={selectedIndex} total={paging.total} />
)}
</div>
+
+ {this.props.canSetHome && (
+ <HomePageSelect className="huge-spacer-left" currentPage={{ type: 'my-issues' }} />
+ )}
</div>
);
}
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css
index f61bbd33bd2..05af0bd0bbe 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.css
@@ -22,9 +22,9 @@
}
.organization-switch .dropdown-toggle {
- display: block;
+ display: flex;
+ align-items: center;
height: calc(4 * var(--gridSize));
- line-height: calc(4 * var(--gridSize) - 2px);
padding: 0 var(--gridSize);
border: 1px solid transparent;
border-radius: 2px;
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 84f0256e579..22e7d9e480d 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
@@ -35,29 +35,27 @@ export default function OrganizationNavigationHeader({ organization, organizatio
const other = organizations.filter(o => o.key !== organization.key);
return (
- <div className="navbar-context-header">
- <h1 className="display-inline-block">
- <OrganizationAvatar organization={organization} />
- {other.length ? (
- <Dropdown>
- {({ onToggleClick, open }) => (
- <div className={classNames('organization-switch', 'dropdown', { open })}>
- <a className="dropdown-toggle" href="#" onClick={onToggleClick}>
- {organization.name}
- <DropdownIcon className="little-spacer-left" />
- </a>
- <ul className="dropdown-menu">
- {sortBy(other, org => org.name.toLowerCase()).map(organization => (
- <OrganizationListItem key={organization.key} organization={organization} />
- ))}
- </ul>
- </div>
- )}
- </Dropdown>
- ) : (
- <span className="spacer-left">{organization.name}</span>
- )}
- </h1>
+ <header className="navbar-context-header">
+ <OrganizationAvatar organization={organization} />
+ {other.length ? (
+ <Dropdown>
+ {({ onToggleClick, open }) => (
+ <div className={classNames('organization-switch', 'dropdown', { open })}>
+ <a className="dropdown-toggle" href="#" onClick={onToggleClick}>
+ {organization.name}
+ <DropdownIcon className="little-spacer-left" />
+ </a>
+ <ul className="dropdown-menu">
+ {sortBy(other, org => org.name.toLowerCase()).map(organization => (
+ <OrganizationListItem key={organization.key} organization={organization} />
+ ))}
+ </ul>
+ </div>
+ )}
+ </Dropdown>
+ ) : (
+ <span className="spacer-left">{organization.name}</span>
+ )}
{organization.description != null && (
<div className="navbar-context-description">
<p className="text-limited text-top" title={organization.description}>
@@ -65,6 +63,6 @@ export default function OrganizationNavigationHeader({ organization, organizatio
</p>
</div>
)}
- </div>
+ </header>
);
}
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx
index b720223abe1..077cc4a67ee 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMeta.tsx
@@ -19,6 +19,7 @@
*/
import * as React from 'react';
import { Organization } from '../../../app/types';
+import HomePageSelect from '../../../components/controls/HomePageSelect';
import { translate } from '../../../helpers/l10n';
interface Props {
@@ -28,22 +29,21 @@ interface Props {
export default function OrganizationNavigationMeta({ organization }: Props) {
return (
<div className="navbar-context-meta">
+ {organization.url != null && (
+ <a
+ className="spacer-right text-limited"
+ href={organization.url}
+ title={organization.url}
+ rel="nofollow">
+ {organization.url}
+ </a>
+ )}
<div className="text-muted">
<strong>{translate('organization.key')}:</strong> {organization.key}
</div>
- {organization.url != null && (
- <div>
- <p className="text-limited text-top">
- <a
- className="link-underline"
- href={organization.url}
- title={organization.url}
- rel="nofollow">
- {organization.url}
- </a>
- </p>
- </div>
- )}
+ <div className="navbar-context-meta-secondary">
+ <HomePageSelect currentPage={{ type: 'organization', key: organization.key }} />
+ </div>
</div>
);
}
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 78f6ea2ae7c..949bb39ff04 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
@@ -1,28 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders 1`] = `
-<div
+<header
className="navbar-context-header"
>
- <h1
- className="display-inline-block"
- >
- <OrganizationAvatar
- organization={
- Object {
- "key": "foo",
- "name": "Foo",
- "projectVisibility": "public",
- }
+ <OrganizationAvatar
+ organization={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ "projectVisibility": "public",
}
- />
- <span
- className="spacer-left"
- >
- Foo
- </span>
- </h1>
-</div>
+ }
+ />
+ <span
+ className="spacer-left"
+ >
+ Foo
+ </span>
+</header>
`;
exports[`renders dropdown 1`] = `
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMeta-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMeta-test.tsx.snap
index 3da71c25930..bdd7594127b 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMeta-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationMeta-test.tsx.snap
@@ -14,5 +14,17 @@ exports[`renders 1`] = `
foo
</div>
+ <div
+ className="navbar-context-meta-secondary"
+ >
+ <Connect(HomePageSelect)
+ currentPage={
+ Object {
+ "key": "foo",
+ "type": "organization",
+ }
+ }
+ />
+ </div>
</div>
`;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
index daa3ffa6fa4..15dc8f4e7ca 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
@@ -263,9 +263,11 @@ export default class AllProjects extends React.PureComponent<Props, State> {
<div className="layout-page-main-inner">
<PageHeader
currentUser={this.props.currentUser}
+ isFavorite={this.props.isFavorite}
loading={this.state.loading}
onPerspectiveChange={this.handlePerspectiveChange}
onQueryChange={this.updateLocationQuery}
+ onSonarCloud={this.props.onSonarCloud}
onSortChange={this.handleSortChange}
organization={this.props.organization}
projects={this.state.projects}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
index d8de26017fe..3f347339ad0 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
@@ -24,15 +24,18 @@ import Tooltip from '../../../components/controls/Tooltip';
import PerspectiveSelect from './PerspectiveSelect';
import ProjectsSortingSelect from './ProjectsSortingSelect';
import { CurrentUser, isLoggedIn } from '../../../app/types';
+import HomePageSelect from '../../../components/controls/HomePageSelect';
import { translate } from '../../../helpers/l10n';
import { RawQuery } from '../../../helpers/query';
import { Project } from '../types';
interface Props {
currentUser: CurrentUser;
+ isFavorite: boolean;
loading: boolean;
onPerspectiveChange: (x: { view: string; visualization?: string }) => void;
onQueryChange: (change: RawQuery) => void;
+ onSonarCloud: boolean;
onSortChange: (sort: string, desc: boolean) => void;
organization?: { key: string };
projects?: Project[];
@@ -97,6 +100,13 @@ export default function PageHeader(props: Props) {
</span>
)}
</div>
+
+ {props.onSonarCloud &&
+ isLoggedIn(currentUser) &&
+ props.isFavorite &&
+ !props.organization && (
+ <HomePageSelect className="huge-spacer-left" currentPage={{ type: 'my-projects' }} />
+ )}
</header>
);
}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx
index ec11f741410..2c2f8ab5333 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx
@@ -71,9 +71,11 @@ function shallowRender(props?: {}) {
return shallow(
<PageHeader
currentUser={{ isLoggedIn: false }}
+ isFavorite={false}
loading={false}
onPerspectiveChange={jest.fn()}
onQueryChange={jest.fn()}
+ onSonarCloud={false}
onSortChange={jest.fn()}
projects={[]}
query={{ search: 'test' }}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
index 40a5ea5b647..3dd798e4094 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
@@ -31,9 +31,11 @@ exports[`renders 1`] = `
"isLoggedIn": true,
}
}
+ isFavorite={false}
loading={false}
onPerspectiveChange={[Function]}
onQueryChange={[Function]}
+ onSonarCloud={false}
onSortChange={[Function]}
projects={
Array [
@@ -158,9 +160,11 @@ exports[`renders 2`] = `
"isLoggedIn": true,
}
}
+ isFavorite={false}
loading={false}
onPerspectiveChange={[Function]}
onQueryChange={[Function]}
+ onSonarCloud={false}
onSortChange={[Function]}
projects={
Array [
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js
index 5a1ea7dcaa6..c3c52bb74cb 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js
@@ -165,7 +165,11 @@ export default class Onboarding extends React.PureComponent {
{translate('tutorials.skip')}
</a>
)}
- <p className="note">{translate('tutorials.find_it_back_in_help')}</p>
+ <p className="note">
+ {translate(
+ sonarCloud ? 'tutorials.find_it_back_in_plus' : 'tutorials.find_it_back_in_help'
+ )}
+ </p>
</div>
<div className="page-description">
{translateWithParameters(
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js
index 4db0fec476c..3ff62c4f7d8 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/OrganizationStep.js
@@ -71,7 +71,7 @@ export default class OrganizationStep extends React.PureComponent {
getOrganizations({ member: true }).then(
({ organizations }) => {
if (this.mounted) {
- const organizationKeys = organizations.map(o => o.key);
+ const organizationKeys = organizations.filter(o => o.isAdmin).map(o => o.key);
// best guess: if there is only one organization, then it is personal
// otherwise, we can't guess, let's display them all as just "existing organizations"
const personalOrganization =
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js
index be9c2542a87..dda967a28bf 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/OrganizationStep-test.js
@@ -26,7 +26,9 @@ import { getOrganizations } from '../../../../api/organizations';
jest.mock('../../../../api/organizations', () => ({
getOrganizations: jest.fn(() =>
- Promise.resolve({ organizations: [{ key: 'user' }, { key: 'another' }] })
+ Promise.resolve({
+ organizations: [{ isAdmin: true, key: 'user' }, { isAdmin: true, key: 'another' }]
+ })
)
}));
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap
index 9c24b5b42a4..57a706a0d5d 100644
--- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap
@@ -169,7 +169,7 @@ exports[`guides for sonarcloud 1`] = `
<p
className="note"
>
- tutorials.find_it_back_in_help
+ tutorials.find_it_back_in_plus
</p>
</div>
<div
@@ -249,7 +249,7 @@ exports[`guides for sonarcloud 2`] = `
<p
className="note"
>
- tutorials.find_it_back_in_help
+ tutorials.find_it_back_in_plus
</p>
</div>
<div
@@ -330,7 +330,7 @@ exports[`guides for sonarcloud 3`] = `
<p
className="note"
>
- tutorials.find_it_back_in_help
+ tutorials.find_it_back_in_plus
</p>
</div>
<div
diff --git a/server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx b/server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx
index fc76186117b..f802e0885f7 100644
--- a/server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx
+++ b/server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx
@@ -19,7 +19,9 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
+import Tooltip from './Tooltip';
import FavoriteIcon from '../icons-components/FavoriteIcon';
+import { translate } from '../../helpers/l10n';
interface Props {
addFavorite: () => Promise<void>;
@@ -80,13 +82,18 @@ export default class FavoriteBase extends React.PureComponent<Props, State> {
}
render() {
+ const tooltip = this.state.favorite
+ ? translate('favorite.current')
+ : translate('favorite.check');
return (
- <a
- className={classNames('link-no-underline', this.props.className)}
- href="#"
- onClick={this.toggleFavorite}>
- <FavoriteIcon favorite={this.state.favorite} />
- </a>
+ <Tooltip overlay={tooltip}>
+ <a
+ className={classNames('display-inline-block', 'link-no-underline', this.props.className)}
+ href="#"
+ onClick={this.toggleFavorite}>
+ <FavoriteIcon favorite={this.state.favorite} />
+ </a>
+ </Tooltip>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx b/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx
new file mode 100644
index 00000000000..9d68ef7e1fc
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/HomePageSelect.tsx
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { connect } from 'react-redux';
+import Tooltip from './Tooltip';
+import HomeIcon from '../icons-components/HomeIcon';
+import { CurrentUser, isLoggedIn, HomePage, isSameHomePage } from '../../app/types';
+import { translate } from '../../helpers/l10n';
+import { getCurrentUser } from '../../store/rootReducer';
+import { setHomePage } from '../../store/users/actions';
+
+interface StateProps {
+ currentUser: CurrentUser;
+}
+
+interface DispatchProps {
+ setHomePage: (homepage: HomePage) => void;
+}
+
+interface Props extends StateProps, DispatchProps {
+ className?: string;
+ currentPage: HomePage;
+}
+
+class HomePageSelect extends React.PureComponent<Props> {
+ handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.props.setHomePage(this.props.currentPage);
+ };
+
+ render() {
+ const { currentPage, currentUser } = this.props;
+
+ if (!isLoggedIn(currentUser)) {
+ return null;
+ }
+
+ const { homepage } = currentUser;
+ const checked = homepage !== undefined && isSameHomePage(homepage, currentPage);
+ const tooltip = checked ? translate('homepage.current') : translate('homepage.check');
+
+ return (
+ <Tooltip overlay={tooltip}>
+ {checked ? (
+ <span className={classNames('display-inline-block', this.props.className)}>
+ <HomeIcon filled={checked} />
+ </span>
+ ) : (
+ <a
+ className={classNames(
+ 'link-no-underline',
+ 'display-inline-block',
+ this.props.className
+ )}
+ href="#"
+ onClick={this.handleClick}>
+ <HomeIcon filled={checked} />
+ </a>
+ )}
+ </Tooltip>
+ );
+ }
+}
+
+const mapStateToProps = (state: any): StateProps => ({
+ currentUser: getCurrentUser(state)
+});
+
+const mapDispatchToProps: DispatchProps = { setHomePage };
+
+export default connect(mapStateToProps, mapDispatchToProps)(HomePageSelect);
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap
index 98dc0d71b5b..bb842b986b0 100644
--- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/FavoriteBase-test.tsx.snap
@@ -1,25 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render favorite 1`] = `
-<a
- className="link-no-underline"
- href="#"
- onClick={[Function]}
+<Tooltip
+ overlay="favorite.current"
+ placement="bottom"
>
- <FavoriteIcon
- favorite={true}
- />
-</a>
+ <a
+ className="display-inline-block link-no-underline"
+ href="#"
+ onClick={[Function]}
+ >
+ <FavoriteIcon
+ favorite={true}
+ />
+ </a>
+</Tooltip>
`;
exports[`should render not favorite 1`] = `
-<a
- className="link-no-underline"
- href="#"
- onClick={[Function]}
+<Tooltip
+ overlay="favorite.check"
+ placement="bottom"
>
- <FavoriteIcon
- favorite={false}
- />
-</a>
+ <a
+ className="display-inline-block link-no-underline"
+ href="#"
+ onClick={[Function]}
+ >
+ <FavoriteIcon
+ favorite={false}
+ />
+ </a>
+</Tooltip>
`;
diff --git a/server/sonar-web/src/main/js/components/icons-components/FavoriteIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/FavoriteIcon.tsx
index a07533ae13f..abf8844a6cf 100644
--- a/server/sonar-web/src/main/js/components/icons-components/FavoriteIcon.tsx
+++ b/server/sonar-web/src/main/js/components/icons-components/FavoriteIcon.tsx
@@ -19,19 +19,32 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
+import { IconProps } from './types';
+import * as theme from '../../app/theme';
-interface Props {
- className?: string;
+export interface Props extends IconProps {
favorite: boolean;
- size?: number;
}
-export default function FavoriteIcon({ className, favorite, size = 16 }: Props) {
+export default function FavoriteIcon({
+ className,
+ favorite,
+ fill = theme.orange,
+ size = 16
+}: Props) {
return (
- <span className={classNames('icon-star', { 'icon-star-favorite': favorite }, className)}>
- <svg width={size} height={size} viewBox="0 0 16 16">
- <path d="M15.4275,5.77678C15.4275,5.90773 15.3501,6.05059 15.1953,6.20536L11.9542,9.36608L12.7221,13.8304C12.728,13.872 12.731,13.9316 12.731,14.0089C12.731,14.1339 12.6998,14.2396 12.6373,14.3259C12.5748,14.4122 12.484,14.4554 12.3649,14.4554C12.2518,14.4554 12.1328,14.4197 12.0078,14.3482L7.99888,12.2411L3.98995,14.3482C3.85901,14.4197 3.73996,14.4554 3.63281,14.4554C3.50781,14.4554 3.41406,14.4122 3.35156,14.3259C3.28906,14.2396 3.25781,14.1339 3.25781,14.0089C3.25781,13.9732 3.26377,13.9137 3.27567,13.8304L4.04353,9.36608L0.793531,6.20536C0.644719,6.04464 0.570313,5.90178 0.570313,5.77678C0.570313,5.55654 0.736979,5.41964 1.07031,5.36606L5.55245,4.71428L7.56138,0.651781C7.67447,0.407729 7.8203,0.285703 7.99888,0.285703C8.17745,0.285703 8.32328,0.407729 8.43638,0.651781L10.4453,4.71428L14.9274,5.36606C15.2608,5.41964 15.4274,5.55654 15.4274,5.77678L15.4275,5.77678Z" />
- </svg>
- </span>
+ <svg
+ className={classNames('icon-outline', { 'is-filled': favorite }, className)}
+ style={{ color: fill }}
+ width={size}
+ height={size}
+ viewBox="0 0 16 16"
+ version="1.1"
+ xmlnsXlink="http://www.w3.org/1999/xlink"
+ xmlSpace="preserve">
+ <g transform="matrix(0.988024,0,0,0.988024,0.0957953,0.717719)">
+ <path d="M15.428,5.777C15.428,5.908 15.35,6.051 15.195,6.205L11.954,9.366L12.722,13.83C12.728,13.872 12.731,13.932 12.731,14.009C12.731,14.134 12.7,14.24 12.637,14.326C12.575,14.412 12.484,14.455 12.365,14.455C12.252,14.455 12.133,14.42 12.008,14.348L7.999,12.241L3.99,14.348C3.859,14.42 3.74,14.455 3.633,14.455C3.508,14.455 3.414,14.412 3.352,14.326C3.289,14.24 3.258,14.134 3.258,14.009C3.258,13.973 3.264,13.914 3.276,13.83L4.044,9.366L0.794,6.205C0.645,6.045 0.57,5.902 0.57,5.777C0.57,5.557 0.737,5.42 1.07,5.366L5.552,4.714L7.561,0.652C7.674,0.408 7.82,0.286 7.999,0.286C8.177,0.286 8.323,0.408 8.436,0.652L10.445,4.714L14.927,5.366C15.261,5.42 15.427,5.557 15.427,5.777L15.428,5.777Z" />
+ </g>
+ </svg>
);
}
diff --git a/server/sonar-web/src/main/js/components/icons-components/HomeIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/HomeIcon.tsx
new file mode 100644
index 00000000000..c63d84f20b0
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/icons-components/HomeIcon.tsx
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { IconProps } from './types';
+import * as theme from '../../app/theme';
+
+export interface Props extends IconProps {
+ filled?: boolean;
+}
+
+export default function HomeIcon({
+ className,
+ fill = theme.orange,
+ filled = false,
+ size = 16
+}: Props) {
+ return (
+ <svg
+ className={classNames(className, 'icon-outline', { 'is-filled': filled })}
+ style={{ color: fill }}
+ width={size}
+ height={size}
+ viewBox="0 0 16 16"
+ version="1.1"
+ xmlnsXlink="http://www.w3.org/1999/xlink"
+ xmlSpace="preserve">
+ <g transform="matrix(0.870918,0,0,0.870918,0.978227,0.978227)">
+ <path d="M15.9,7.8L8.2,0.1C8.1,0 7.9,0 7.8,0.1L0.1,7.8C0,7.9 0,8.1 0.1,8.2C0.2,8.3 0.2,8.3 0.3,8.3L2.2,8.3L2.2,15.8C2.2,15.9 2.2,15.9 2.3,16C2.3,16 2.4,16.1 2.5,16.1L6.2,16.1C6.3,16.1 6.5,16 6.5,15.8L6.5,10.5L9.7,10.5L9.7,15.8C9.7,15.9 9.8,16.1 10,16.1L13.7,16.1C13.8,16.1 14,16 14,15.8L14,8.2L15.9,8.2C16,8.2 16,8.2 16.1,8.1C16,8 16.1,7.9 15.9,7.8Z" />
+ </g>
+ </svg>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/nav/ContextNavBar.css b/server/sonar-web/src/main/js/components/nav/ContextNavBar.css
index 489747900bf..81a469b239b 100644
--- a/server/sonar-web/src/main/js/components/nav/ContextNavBar.css
+++ b/server/sonar-web/src/main/js/components/nav/ContextNavBar.css
@@ -14,21 +14,13 @@
}
.navbar-context-header {
- display: inline-block;
+ display: inline-flex;
+ align-items: center;
height: calc(4 * var(--gridSize));
- line-height: calc(4 * var(--gridSize));
font-size: var(--bigFontSize);
}
-.navbar-context-header h1 {
- vertical-align: top;
- line-height: calc(4 * var(--gridSize));
-}
-
.navbar-context-header .slash-separator {
- display: inline-block;
- vertical-align: top;
- height: calc(4 * var(--gridSize));
margin-left: var(--gridSize);
margin-right: var(--gridSize);
font-size: 24px;
@@ -42,13 +34,22 @@
position: absolute;
top: 0;
right: 0;
- line-height: calc(4 * var(--gridSize));
- padding: 0 10px;
+ display: flex;
+ align-items: center;
+ height: calc(4 * var(--gridSize));
+ padding: 0 20px;
color: var(--secondFontColor);
font-size: var(--smallFontSize);
text-align: right;
}
+.navbar-context-meta-secondary {
+ position: absolute;
+ top: 36px;
+ right: 0;
+ padding: 0 20px;
+}
+
.navbar-context-description {
display: inline-block;
line-height: var(--controlHeight);
diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts
index ffe6fa17794..10ab02703cb 100644
--- a/server/sonar-web/src/main/js/helpers/urls.ts
+++ b/server/sonar-web/src/main/js/helpers/urls.ts
@@ -21,7 +21,7 @@ import { stringify } from 'querystring';
import { omitBy, isNil } from 'lodash';
import { isShortLivingBranch } from './branches';
import { getProfilePath } from '../apps/quality-profiles/utils';
-import { Branch } from '../app/types';
+import { Branch, HomePage } from '../app/types';
interface Query {
[x: string]: string | undefined;
@@ -167,3 +167,23 @@ export function getMarkdownHelpUrl(): string {
export function getCodeUrl(project: string, branch?: string, selected?: string) {
return { pathname: '/code', query: { id: project, branch, selected } };
}
+
+export function getOrganizationUrl(organization: string) {
+ return `/organizations/${organization}`;
+}
+
+export function getHomePageUrl(homepage: HomePage) {
+ switch (homepage.type) {
+ case 'project':
+ return getProjectUrl(homepage.key!);
+ case 'organization':
+ return getOrganizationUrl(homepage.key!);
+ case 'my-projects':
+ return '/projects';
+ case 'my-issues':
+ return { pathname: '/issues', query: { resolved: 'false' } };
+ }
+
+ // should never happen, but just in case...
+ return '/projects';
+}
diff --git a/server/sonar-web/src/main/js/store/users/actions.js b/server/sonar-web/src/main/js/store/users/actions.ts
index aaee4498138..c55fff019c9 100644
--- a/server/sonar-web/src/main/js/store/users/actions.js
+++ b/server/sonar-web/src/main/js/store/users/actions.ts
@@ -17,23 +17,36 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { getCurrentUser } from '../../api/users';
+import { Dispatch } from 'redux';
+import * as api from '../../api/users';
+import { CurrentUser, HomePage } from '../../app/types';
export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER';
export const RECEIVE_USER = 'RECEIVE_USER';
export const SKIP_ONBOARDING = 'SKIP_ONBOARDING';
+export const SET_HOMEPAGE = 'SET_HOMEPAGE';
-export const receiveCurrentUser = user => ({
+export const receiveCurrentUser = (user: CurrentUser) => ({
type: RECEIVE_CURRENT_USER,
user
});
-export const receiveUser = user => ({
+export const receiveUser = (user: any) => ({
type: RECEIVE_USER,
user
});
export const skipOnboarding = () => ({ type: SKIP_ONBOARDING });
-export const fetchCurrentUser = () => dispatch =>
- getCurrentUser().then(user => dispatch(receiveCurrentUser(user)));
+export const fetchCurrentUser = () => (dispatch: Dispatch<any>) => {
+ return api.getCurrentUser().then(user => dispatch(receiveCurrentUser(user)));
+};
+
+export const setHomePage = (homepage: HomePage) => (dispatch: Dispatch<any>) => {
+ api.setHomePage(homepage).then(
+ () => {
+ dispatch({ type: SET_HOMEPAGE, homepage });
+ },
+ () => {}
+ );
+};
diff --git a/server/sonar-web/src/main/js/store/users/reducer.js b/server/sonar-web/src/main/js/store/users/reducer.ts
index 79472f1f8d3..1cee5bae5e9 100644
--- a/server/sonar-web/src/main/js/store/users/reducer.js
+++ b/server/sonar-web/src/main/js/store/users/reducer.ts
@@ -19,10 +19,15 @@
*/
import { combineReducers } from 'redux';
import { uniq, keyBy } from 'lodash';
-import { RECEIVE_CURRENT_USER, RECEIVE_USER, SKIP_ONBOARDING } from './actions';
+import { RECEIVE_CURRENT_USER, RECEIVE_USER, SKIP_ONBOARDING, SET_HOMEPAGE } from './actions';
import { actions as membersActions } from '../organizationsMembers/actions';
+import { CurrentUser } from '../../app/types';
-const usersByLogin = (state = {}, action = {}) => {
+interface UsersByLogin {
+ [login: string]: any;
+}
+
+const usersByLogin = (state: UsersByLogin = {}, action: any = {}) => {
switch (action.type) {
case RECEIVE_CURRENT_USER:
case RECEIVE_USER:
@@ -37,14 +42,16 @@ const usersByLogin = (state = {}, action = {}) => {
}
};
-const userLogins = (state = [], action = {}) => {
+type UserLogins = string[];
+
+const userLogins = (state: UserLogins = [], action: any = {}) => {
switch (action.type) {
case RECEIVE_CURRENT_USER:
case RECEIVE_USER:
return uniq([...state, action.user.login]);
case membersActions.RECEIVE_MEMBERS:
case membersActions.RECEIVE_MORE_MEMBERS:
- return uniq([...state, action.members.map(member => member.login)]);
+ return uniq([...state, action.members.map((member: any) => member.login)]);
case membersActions.ADD_MEMBER: {
return uniq([...state, action.member.login]).sort();
}
@@ -53,21 +60,30 @@ const userLogins = (state = [], action = {}) => {
}
};
-const currentUser = (state = null, action = {}) => {
+const currentUser = (state: CurrentUser | null = null, action: any = {}) => {
if (action.type === RECEIVE_CURRENT_USER) {
return action.user;
}
if (action.type === SKIP_ONBOARDING) {
return state ? { ...state, showOnboardingTutorial: false } : null;
}
+ if (action.type === SET_HOMEPAGE) {
+ return state && { ...state, homepage: action.homepage };
+ }
return state;
};
+interface State {
+ usersByLogin: UsersByLogin;
+ userLogins: UserLogins;
+ currentUser: CurrentUser | null;
+}
+
export default combineReducers({ usersByLogin, userLogins, currentUser });
-export const getCurrentUser = state => state.currentUser;
-export const getUserLogins = state => state.userLogins;
-export const getUserByLogin = (state, login) => state.usersByLogin[login];
-export const getUsersByLogins = (state, logins) =>
+export const getCurrentUser = (state: State) => state.currentUser!;
+export const getUserLogins = (state: State) => state.userLogins;
+export const getUserByLogin = (state: State, login: string) => state.usersByLogin[login];
+export const getUsersByLogins = (state: State, logins: string[]) =>
logins.map(login => getUserByLogin(state, login));
-export const getUsers = state => getUsersByLogins(state, getUserLogins(state));
+export const getUsers = (state: State) => getUsersByLogins(state, getUserLogins(state));