From 621544af2b072e21e42b8d4c1c2bd800cbfdd696 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Thu, 8 Dec 2016 17:35:26 +0100 Subject: [PATCH] SONAR-8505 Implement smooth transition between pages (#1440) --- it/it-tests/src/test/java/it/ui/UiTest.java | 62 +++++++ .../src/test/java/pageobjects/LoginPage.java | 2 +- .../test/java/pageobjects/ServerIdPage.java | 2 +- .../main/js/app/components/GlobalContainer.js | 4 +- .../main/js/app/components/GlobalFooter.js | 5 +- .../app/components/GlobalMessagesContainer.js | 5 +- .../src/main/js/app/components/NotFound.js | 3 +- .../js/app/components/ProjectContainer.js | 9 +- .../main/js/app/components/SimpleContainer.js | 5 +- .../components/nav/component/ComponentNav.js | 2 +- .../nav/component/ComponentNavMenu.js | 151 +++++++++++++----- .../nav/global/GlobalNavBranding.js | 4 +- .../components/nav/global/GlobalNavMenu.js | 46 +++--- .../components/nav/global/GlobalNavUser.js | 3 +- .../components/nav/settings/SettingsNav.js | 87 ++++++++-- .../js/apps/about/components/AboutProjects.js | 5 +- .../apps/about/components/AboutStandards.js | 23 +-- .../apps/about/components/EntryIssueTypes.js | 19 +-- .../js/apps/account/projects/ProjectCard.js | 6 +- .../projects/__tests__/ProjectCard-test.js | 3 +- .../components/TaskComponent.js | 7 +- .../sonar-web/src/main/js/apps/code/bucket.js | 12 +- .../src/main/js/apps/code/components/App.js | 3 +- .../apps/code/components/ComponentDetach.js | 10 +- .../js/apps/code/components/ComponentName.js | 6 +- .../components/CodingRulesAppContainer.js | 16 +- .../src/main/js/apps/coding-rules/init.js | 6 + .../components/ComponentIssuesAppContainer.js | 16 +- .../src/main/js/apps/component-issues/init.js | 6 + .../issues/components/IssuesAppContainer.js | 16 +- .../sonar-web/src/main/js/apps/issues/init.js | 6 + .../overview/main/BugsAndVulnerabilities.js | 22 +-- .../main/js/apps/overview/main/CodeSmells.js | 5 +- .../src/main/js/apps/overview/main/enhance.js | 15 +- .../js/apps/overview/meta/MetaQualityGate.js | 5 +- .../apps/overview/meta/MetaQualityProfiles.js | 5 +- .../components/GlobalMessagesContainer.js | 28 ---- .../src/main/js/apps/project-admin/key/Key.js | 4 - .../project-admin/quality-gate/QualityGate.js | 2 - .../quality-profiles/QualityProfiles.js | 3 - .../main/js/apps/projects-admin/projects.js | 17 +- .../main/js/apps/projects/components/App.js | 5 - .../apps/projects/components/ProjectCard.js | 6 +- .../quality-profiles/changelog/Changelog.js | 5 +- .../changelog/__tests__/Changelog-test.js | 4 +- .../compare/ComparisonResults.js | 5 +- .../__tests__/ComparisonResults-test.js | 21 ++- .../components/ProfileActions.js | 4 +- .../details/ProfileProjects.js | 11 +- .../quality-profiles/details/ProfileRules.js | 31 ++-- .../home/EvolutionDeprecated.js | 6 +- .../quality-profiles/home/EvolutionRules.js | 11 +- .../quality-profiles/home/ProfilesListRow.js | 15 +- .../main/js/apps/quality-profiles/routes.js | 2 +- .../main/js/apps/settings/components/App.js | 2 - .../components/GlobalMessagesContainer.js | 29 ---- .../apps/settings/encryption/EncryptionApp.js | 3 - .../js/apps/settings/licenses/LicensesApp.js | 2 - .../licenses/__tests__/LicensesApp-test.js | 2 - .../js/apps/settings/serverId/ServerIdApp.js | 3 - .../js/apps/settings/store/rootReducer.js | 3 +- .../components/UpdateCenterAppContainer.js | 16 +- .../src/main/js/apps/update-center/init.js | 8 +- .../js/apps/web-api/components/WebApiApp.js | 2 +- .../js/components/controls/GlobalMessages.js | 26 ++- .../js/components/shared/drilldown-link.js | 10 +- .../js/components/store/globalMessages.js | 105 +++++++----- .../main/js/helpers/__tests__/urls-test.js | 24 +-- server/sonar-web/src/main/js/helpers/urls.js | 42 +++-- .../src/main/less/components/navbar.less | 9 +- .../src/main/less/components/ui.less | 7 + 71 files changed, 673 insertions(+), 402 deletions(-) delete mode 100644 server/sonar-web/src/main/js/apps/project-admin/components/GlobalMessagesContainer.js delete mode 100644 server/sonar-web/src/main/js/apps/settings/components/GlobalMessagesContainer.js diff --git a/it/it-tests/src/test/java/it/ui/UiTest.java b/it/it-tests/src/test/java/it/ui/UiTest.java index c4960f9192e..a2e71ebc4ef 100644 --- a/it/it-tests/src/test/java/it/ui/UiTest.java +++ b/it/it-tests/src/test/java/it/ui/UiTest.java @@ -20,6 +20,7 @@ package it.ui; import com.sonar.orchestrator.Orchestrator; +import com.sonar.orchestrator.build.SonarScanner; import it.Category4Suite; import java.util.Map; import org.junit.ClassRule; @@ -31,6 +32,12 @@ import pageobjects.Navigation; import util.ItUtils; import static com.codeborne.selenide.Condition.hasText; +import static com.codeborne.selenide.Condition.text; +import static com.codeborne.selenide.Condition.visible; +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.WebDriverRunner.url; +import static org.assertj.core.api.Assertions.assertThat; +import static util.ItUtils.projectDir; public class UiTest { @@ -54,4 +61,59 @@ public class UiTest { nav.getFooter().should(hasText((String) statusMap.get("version"))); } + + @Test + public void many_page_transitions() { + analyzeSampleProject(); + + nav.open("/about"); + + // on about page + $(".about-page-projects-link") + .shouldBe(visible) + .shouldHave(text("1")) + .click(); + + // on projects page + assertThat(url()).contains("/projects"); + $(".project-card-name") + .shouldBe(visible) + .shouldHave(text("Sample")) + .find("a") + .click(); + + // on project dashboard + assertThat(url()).contains("/dashboard?id=sample"); + $(".overview-quality-gate") + .shouldBe(visible) + .shouldHave(text("Passed")); + $("a[href=\"/component_issues?id=sample#resolved=false|types=CODE_SMELL\"]") + .shouldBe(visible) + .shouldHave(text("0")) + .click(); + + // on project issues page + assertThat(url()).contains("/component_issues?id=sample#resolved=false|types=CODE_SMELL"); + $(".facet.active[data-unresolved]").shouldBe(visible); + + $("#global-navigation").find("a[href=\"/profiles\"]").click(); + + // on quality profiles page + assertThat(url()).contains("/profiles"); + $("table[data-language=xoo]").find("tr[data-name=Basic]").find(".quality-profiles-table-name") + .shouldBe(visible) + .shouldHave(text("Basic")) + .find("a") + .click(); + + // on profile page + assertThat(url()).contains("/profiles/show?key=xoo-basic"); + $(".quality-profile-inheritance") + .shouldBe(visible) + .shouldHave(text("1 active rules")); + } + + private static void analyzeSampleProject() { + ORCHESTRATOR.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample"))); + } } diff --git a/it/it-tests/src/test/java/pageobjects/LoginPage.java b/it/it-tests/src/test/java/pageobjects/LoginPage.java index 98e95b01df8..74d311409ab 100644 --- a/it/it-tests/src/test/java/pageobjects/LoginPage.java +++ b/it/it-tests/src/test/java/pageobjects/LoginPage.java @@ -48,7 +48,7 @@ public class LoginPage { } public SelenideElement getErrorMessage() { - return $(By.cssSelector("#login_form .alert")); + return $(".process-spinner-failed"); } private T submitCredentials(String login, String password, Class expectedResultPage) { diff --git a/it/it-tests/src/test/java/pageobjects/ServerIdPage.java b/it/it-tests/src/test/java/pageobjects/ServerIdPage.java index b7a9990a306..4b7db4c87b6 100644 --- a/it/it-tests/src/test/java/pageobjects/ServerIdPage.java +++ b/it/it-tests/src/test/java/pageobjects/ServerIdPage.java @@ -43,7 +43,7 @@ public class ServerIdPage { } public ServerIdPage assertError() { - $(".alert-danger").shouldBe(visible); + $(".process-spinner-failed").shouldBe(visible); return this; } diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.js b/server/sonar-web/src/main/js/app/components/GlobalContainer.js index da833189128..32b5f620145 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.js +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.js @@ -25,11 +25,13 @@ import GlobalMessagesContainer from './GlobalMessagesContainer'; export default class GlobalContainer extends React.Component { render () { + // it is important to pass `location` down to `GlobalNav` to trigger render on url change + return (
- + {this.props.children}
diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooter.js b/server/sonar-web/src/main/js/app/components/GlobalFooter.js index e128246b69b..8d0e5472014 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalFooter.js +++ b/server/sonar-web/src/main/js/app/components/GlobalFooter.js @@ -19,6 +19,7 @@ */ // @flow import React from 'react'; +import { Link } from 'react-router'; import { connect } from 'react-redux'; import { getAppState } from '../store/rootReducer'; @@ -64,9 +65,9 @@ class GlobalFooter extends React.Component { {' - '} Plugins {' - '} - Web API + Web API {' - '} - About + About
); diff --git a/server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.js b/server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.js index 0ee6c22edb5..6faf954d495 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.js +++ b/server/sonar-web/src/main/js/app/components/GlobalMessagesContainer.js @@ -20,9 +20,12 @@ import { connect } from 'react-redux'; import GlobalMessages from '../../components/controls/GlobalMessages'; import { getGlobalMessages } from '../store/rootReducer'; +import { closeGlobalMessage } from '../../components/store/globalMessages'; const mapStateToProps = state => ({ messages: getGlobalMessages(state) }); -export default connect(mapStateToProps)(GlobalMessages); +const mapDispatchToProps = { closeGlobalMessage }; + +export default connect(mapStateToProps, mapDispatchToProps)(GlobalMessages); diff --git a/server/sonar-web/src/main/js/app/components/NotFound.js b/server/sonar-web/src/main/js/app/components/NotFound.js index 16b40224cef..85279ed1a14 100644 --- a/server/sonar-web/src/main/js/app/components/NotFound.js +++ b/server/sonar-web/src/main/js/app/components/NotFound.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import SimpleContainer from './SimpleContainer'; export default class NotFound extends React.Component { @@ -26,7 +27,7 @@ export default class NotFound extends React.Component {

The page you were looking for does not exist.

You may have mistyped the address or the page may have moved.

-

Go back to the homepage

+

Go back to the homepage

); } diff --git a/server/sonar-web/src/main/js/app/components/ProjectContainer.js b/server/sonar-web/src/main/js/app/components/ProjectContainer.js index 1a83c88e618..44d64f0cd9e 100644 --- a/server/sonar-web/src/main/js/app/components/ProjectContainer.js +++ b/server/sonar-web/src/main/js/app/components/ProjectContainer.js @@ -33,8 +33,15 @@ class ProjectContainer extends React.Component { this.props.fetchProject(); } + componentDidUpdate (prevProps) { + if (prevProps.location.query.id !== this.props.location.query.id) { + this.props.fetchProject(); + } + } + render () { - if (!this.props.project) { + // check `canBeFavorite` to be sure that /api/navigation/component has been already called + if (!this.props.project || this.props.project.canBeFavorite == null) { return null; } diff --git a/server/sonar-web/src/main/js/app/components/SimpleContainer.js b/server/sonar-web/src/main/js/app/components/SimpleContainer.js index 17d9c64cb69..342bffa2bc3 100644 --- a/server/sonar-web/src/main/js/app/components/SimpleContainer.js +++ b/server/sonar-web/src/main/js/app/components/SimpleContainer.js @@ -23,7 +23,10 @@ import GlobalFooter from './GlobalFooter'; export default class SimpleContainer extends React.Component { static propTypes = { - children: React.PropTypes.element.isRequired + children: React.PropTypes.oneOfType([ + React.PropTypes.element, + React.PropTypes.arrayOf(React.PropTypes.element) + ]) }; componentDidMount () { diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js index b6dcd1c7359..5e3057b65ff 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js @@ -37,7 +37,7 @@ export default React.createClass({ }, loadStatus() { - getTasksForComponent(this.props.component.uuid).then(r => { + getTasksForComponent(this.props.component.id).then(r => { this.setState({ isPending: !!_.findWhere(r.queue, { status: STATUSES.PENDING }), isInProgress: !!_.findWhere(r.queue, { status: STATUSES.IN_PROGRESS }), diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js index 52ed1e348a5..c348e186615 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js @@ -19,8 +19,8 @@ */ import classNames from 'classnames'; import React from 'react'; +import { Link } from 'react-router'; import { translate } from '../../../../helpers/l10n'; -import { getComponentUrl } from '../../../../helpers/urls'; const SETTINGS_URLS = [ '/project/settings', @@ -50,11 +50,6 @@ export default class ComponentNavMenu extends React.Component { return qualifier === 'VW' || qualifier === 'SVW'; } - isFixedDashboardActive () { - const path = window.location.pathname; - return path.indexOf(window.baseUrl + '/dashboard') === 0 || path.indexOf(window.baseUrl + '/governance') === 0; - } - shouldShowAdministration () { return Object.keys(this.props.conf).some(key => this.props.conf[key]); } @@ -73,12 +68,13 @@ export default class ComponentNavMenu extends React.Component { } renderDashboardLink () { - const url = getComponentUrl(this.props.component.key); - const name = ; - const className = classNames({ active: this.isFixedDashboardActive() }); return ( -
  • - {name} +
  • + + +
  • ); } @@ -88,19 +84,39 @@ export default class ComponentNavMenu extends React.Component { return null; } - const url = `/code/?id=${encodeURIComponent(this.props.component.key)}`; - const header = this.isView() ? translate('view_projects.page') : translate('code.page'); - return this.renderLink(url, header, '/code'); + return ( +
  • + + {this.isView() ? translate('view_projects.page') : translate('code.page')} + +
  • + ); } renderComponentIssuesLink () { - const url = `/component_issues?id=${encodeURIComponent(this.props.component.key)}`; - return this.renderLink(url, translate('issues.page'), '/component_issues'); + return ( +
  • + + {translate('issues.page')} + +
  • + ); } renderComponentMeasuresLink () { - const url = `/component_measures/?id=${encodeURIComponent(this.props.component.key)}`; - return this.renderLink(url, translate('layout.measures'), '/component_measures'); + return ( +
  • + + {translate('layout.measures')} + +
  • + ); } renderAdministration () { @@ -136,48 +152,90 @@ export default class ComponentNavMenu extends React.Component { if (!this.props.conf.showSettings) { return null; } - const url = `/project/settings?id=${encodeURIComponent(this.props.component.key)}`; - return this.renderLink(url, translate('project_settings.page'), '/project/settings'); + return ( +
  • + + {translate('project_settings.page')} + +
  • + ); } renderProfilesLink () { if (!this.props.conf.showQualityProfiles) { return null; } - const url = `/project/quality_profiles?id=${encodeURIComponent(this.props.component.key)}`; - return this.renderLink(url, translate('project_quality_profiles.page'), '/project/quality_profiles'); + return ( +
  • + + {translate('project_quality_profiles.page')} + +
  • + ); } renderQualityGateLink () { if (!this.props.conf.showQualityGates) { return null; } - const url = `/project/quality_gate?id=${encodeURIComponent(this.props.component.key)}`; - return this.renderLink(url, translate('project_quality_gate.page'), '/project/quality_gate'); + return ( +
  • + + {translate('project_quality_gate.page')} + +
  • + ); } renderCustomMeasuresLink () { if (!this.props.conf.showManualMeasures) { return null; } - const url = `/custom_measures?id=${encodeURIComponent(this.props.component.key)}`; - return this.renderLink(url, translate('custom_measures.page'), '/custom_measures'); + return ( +
  • + + {translate('custom_measures.page')} + +
  • + ); } renderLinksLink () { if (!this.props.conf.showLinks) { return null; } - const url = `/project/links?id=${encodeURIComponent(this.props.component.key)}`; - return this.renderLink(url, translate('project_links.page'), '/project/links'); + return ( +
  • + + {translate('project_links.page')} + +
  • + ); } renderPermissionsLink () { if (!this.props.conf.showPermissions) { return null; } - const url = `/project_roles?id=${encodeURIComponent(this.props.component.key)}`; - return this.renderLink(url, translate('permissions.page'), '/project_roles'); + return ( +
  • + + {translate('permissions.page')} + +
  • + ); } renderHistoryLink () { @@ -199,16 +257,30 @@ export default class ComponentNavMenu extends React.Component { if (!this.props.conf.showBackgroundTasks) { return null; } - const url = `/project/background_tasks?id=${encodeURIComponent(this.props.component.key)}`; - return this.renderLink(url, translate('background_tasks.page'), '/project/background_tasks'); + return ( +
  • + + {translate('background_tasks.page')} + +
  • + ); } renderUpdateKeyLink () { if (!this.props.conf.showUpdateKey) { return null; } - const url = `/project/key?id=${encodeURIComponent(this.props.component.key)}`; - return this.renderLink(url, translate('update_key.page'), '/project/key'); + return ( +
  • + + {translate('update_key.page')} + +
  • + ); } renderDeletionLink () { @@ -218,8 +290,15 @@ export default class ComponentNavMenu extends React.Component { return null; } - const url = `/project/deletion?id=${encodeURIComponent(this.props.component.key)}`; - return this.renderLink(url, translate('deletion.page'), '/project/deletion'); + return ( +
  • + + {translate('deletion.page')} + +
  • + ); } renderExtensions () { diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.js index 6542a9fb20b..b845eedfe58 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavBranding.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import { connect } from 'react-redux'; import { getSettingValue, getCurrentUser } from '../../../store/rootReducer'; import { translate } from '../../../../helpers/l10n'; @@ -40,11 +41,10 @@ class GlobalNavBranding extends React.Component { render () { const homeController = this.props.currentUser.isLoggedIn ? '/projects/favorite' : '/about'; - const homeUrl = window.baseUrl + homeController; const homeLinkClassName = 'navbar-brand' + (this.props.customLogoUrl ? ' navbar-brand-custom' : ''); return (
    - {this.renderLogo()} + {this.renderLogo()}
    ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js index 424ee074cef..d5c2f6402a8 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.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import { translate } from '../../../../helpers/l10n'; import { isUserAdmin } from '../../../../helpers/users'; @@ -36,48 +37,54 @@ export default class GlobalNavMenu extends React.Component { } renderProjects () { - const controller = this.props.currentUser.isLoggedIn ? '/projects/favorite' : '/projects'; - const url = window.baseUrl + controller; + const pathname = this.props.currentUser.isLoggedIn ? '/projects/favorite' : '/projects'; return ( -
  • - {translate('projects.page')} +
  • + + {translate('projects.page')} +
  • ); } renderIssuesLink () { const query = this.props.currentUser.isLoggedIn ? '#resolved=false|assigned_to_me=true' : '#resolved=false'; - const url = window.baseUrl + '/issues' + query; + const url = '/issues' + query; return ( -
  • - {translate('issues.page')} +
  • + + {translate('issues.page')} +
  • ); } renderRulesLink () { - const url = window.baseUrl + '/coding_rules'; return ( -
  • - {translate('coding_rules.page')} +
  • + + {translate('coding_rules.page')} +
  • ); } renderProfilesLink () { - const url = window.baseUrl + '/profiles'; return ( -
  • - {translate('quality_profiles.page')} +
  • + + {translate('quality_profiles.page')} +
  • ); } renderQualityGatesLink () { - const url = window.baseUrl + '/quality_gates'; return ( -
  • - {translate('quality_gates.page')} +
  • + + {translate('quality_gates.page')} +
  • ); } @@ -86,10 +93,11 @@ export default class GlobalNavMenu extends React.Component { if (!isUserAdmin(this.props.currentUser)) { return null; } - const url = window.baseUrl + '/settings'; return ( -
  • - {translate('layout.settings')} +
  • + + {translate('layout.settings')} +
  • ); } diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js index 11f633320c2..47eeb101a9b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import Avatar from '../../../../components/ui/Avatar'; import RecentHistory from '../component/RecentHistory'; import { translate } from '../../../../helpers/l10n'; @@ -45,7 +46,7 @@ export default class GlobalNavUser extends React.Component {
    • - {translate('my_account.page')} + {translate('my_account.page')}
    • {translate('layout.logout')} diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js index afe8b25f1f2..6560e94f27c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js +++ b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js @@ -19,6 +19,7 @@ */ import React from 'react'; import classNames from 'classnames'; +import { IndexLink } from 'react-router'; import { translate } from '../../../../helpers/l10n'; export default class SettingsNav extends React.Component { @@ -46,7 +47,7 @@ export default class SettingsNav extends React.Component { return this.isSomethingActive(urls); } - renderLink(url, title, highlightUrl = url) { + renderLink (url, title, highlightUrl = url) { const fullUrl = window.baseUrl + url; const isActive = typeof highlightUrl === 'string' ? window.location.pathname.indexOf(window.baseUrl + highlightUrl) === 0 : @@ -76,7 +77,11 @@ export default class SettingsNav extends React.Component {
        - {this.renderLink('/settings', translate('layout.settings'))} +
      • + + {translate('layout.settings')} + +
        @@ -85,11 +90,31 @@ export default class SettingsNav extends React.Component { {translate('sidebar.project_settings')}
          - {this.renderLink('/settings', translate('settings.page'), url => window.location.pathname === url)} - {this.renderLink('/settings/licenses', translate('property.category.licenses'))} - {this.renderLink('/settings/encryption', translate('property.category.security.encryption'))} - {this.renderLink('/settings/server_id', translate('property.category.server_id'))} - {this.renderLink('/metrics', 'Custom Metrics')} +
        • + + {translate('settings.page')} + +
        • +
        • + + {translate('property.category.licenses')} + +
        • +
        • + + {translate('property.category.security.encryption')} + +
        • +
        • + + {translate('property.category.server_id')} + +
        • +
        • + + Custom Metrics + +
        • {this.props.extensions.map(e => this.renderLink(e.url, e.name))}
        @@ -99,10 +124,26 @@ export default class SettingsNav extends React.Component { {translate('sidebar.security')}
          - {this.renderLink('/users', translate('users.page'))} - {this.renderLink('/groups', translate('user_groups.page'))} - {this.renderLink('/roles/global', translate('global_permissions.page'))} - {this.renderLink('/permission_templates', translate('permission_templates'))} +
        • + + {translate('users.page')} + +
        • +
        • + + {translate('user_groups.page')} + +
        • +
        • + + {translate('global_permissions.page')} + +
        • +
        • + + {translate('permission_templates')} + +
        @@ -111,8 +152,16 @@ export default class SettingsNav extends React.Component { {translate('sidebar.projects')}
          - {this.renderLink('/projects_admin', 'Management')} - {this.renderLink('/background_tasks', translate('background_tasks.page'))} +
        • + + Management + +
        • +
        • + + {translate('background_tasks.page')} + +
        @@ -121,8 +170,16 @@ export default class SettingsNav extends React.Component { {translate('sidebar.system')}
          - {this.renderLink('/updatecenter', translate('update_center.page'))} - {this.renderLink('/system', translate('system_info.page'))} +
        • + + {translate('update_center.page')} + +
        • +
        • + + {translate('system_info.page')} + +
      diff --git a/server/sonar-web/src/main/js/apps/about/components/AboutProjects.js b/server/sonar-web/src/main/js/apps/about/components/AboutProjects.js index 0c407f7b063..190f41f6704 100644 --- a/server/sonar-web/src/main/js/apps/about/components/AboutProjects.js +++ b/server/sonar-web/src/main/js/apps/about/components/AboutProjects.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import { formatMeasure } from '../../../helpers/measures'; import { translate } from '../../../helpers/l10n'; @@ -30,9 +31,9 @@ export default class AboutProjects extends React.Component { return (
      {translate('about_page.projects_analyzed')} diff --git a/server/sonar-web/src/main/js/apps/about/components/AboutStandards.js b/server/sonar-web/src/main/js/apps/about/components/AboutStandards.js index 437c7b5e021..a0a2b8fb0b7 100644 --- a/server/sonar-web/src/main/js/apps/about/components/AboutStandards.js +++ b/server/sonar-web/src/main/js/apps/about/components/AboutStandards.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import ReadMore from './ReadMore'; import { translate } from '../../../helpers/l10n'; import { getRulesUrl } from '../../../helpers/urls'; @@ -37,35 +38,35 @@ export default class AboutStandards extends React.Component { diff --git a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js index 4ebae3adc72..28b3d1ae8bb 100644 --- a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js +++ b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import { formatMeasure } from '../../../helpers/measures'; import { translate } from '../../../helpers/l10n'; import { getIssuesUrl } from '../../../helpers/urls'; @@ -37,28 +38,28 @@ export default class EntryIssueTypes extends React.Component {
      • {translate('issue.type.BUG.plural')}
      • {translate('issue.type.VULNERABILITY.plural')}
      • {translate('issue.type.CODE_SMELL.plural')}
      • diff --git a/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.js b/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.js index f7d7556da81..f459e61c20d 100644 --- a/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.js +++ b/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.js @@ -20,8 +20,8 @@ import React from 'react'; import moment from 'moment'; import sortBy from 'lodash/sortBy'; +import { Link } from 'react-router'; import Level from '../../../components/ui/Level'; -import { getComponentUrl } from '../../../helpers/urls'; import { projectType } from './propTypes'; import { translateWithParameters, translate } from '../../../helpers/l10n'; @@ -61,9 +61,9 @@ export default class ProjectCard extends React.Component {

        - + {project.name} - +

        {links.length > 0 && ( diff --git a/server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.js b/server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.js index 8f85a86d722..2fd284f0c01 100644 --- a/server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.js +++ b/server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.js @@ -19,6 +19,7 @@ */ import React from 'react'; import { shallow } from 'enzyme'; +import { Link } from 'react-router'; import ProjectCard from '../ProjectCard'; import Level from '../../../../components/ui/Level'; @@ -28,7 +29,7 @@ it('should render key and name', () => { const project = { ...BASE }; const output = shallow(); expect(output.find('.account-project-key').text()).toBe('key'); - expect(output.find('.account-project-name').text()).toBe('name'); + expect(output.find('.account-project-name').find(Link).prop('children')).toBe('name'); }); it('should render description', () => { diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js index cdbf6650c7e..628f5f2f644 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js @@ -19,9 +19,8 @@ */ /* @flow */ import React from 'react'; - +import { Link } from 'react-router'; import TaskType from './TaskType'; -import { getComponentUrl } from '../../../helpers/urls'; import QualifierIcon from '../../../components/shared/qualifier-icon'; import { Task } from '../types'; @@ -39,12 +38,12 @@ const TaskComponent = ({ task, types }: { task: Task, types: string[] }) => { return ( - + {task.componentName} - + {types.length > 1 && ( )} diff --git a/server/sonar-web/src/main/js/apps/code/bucket.js b/server/sonar-web/src/main/js/apps/code/bucket.js index ced8587df24..66989f22cbf 100644 --- a/server/sonar-web/src/main/js/apps/code/bucket.js +++ b/server/sonar-web/src/main/js/apps/code/bucket.js @@ -17,9 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const bucket = {}; -const childrenBucket = {}; -const breadcrumbsBucket = {}; +let bucket = {}; +let childrenBucket = {}; +let breadcrumbsBucket = {}; export function addComponent (component) { bucket[component.key] = component; @@ -44,3 +44,9 @@ export function addComponentBreadcrumbs (componentKey, breadcrumbs) { export function getComponentBreadcrumbs (componentKey) { return breadcrumbsBucket[componentKey]; } + +export function clearBucket () { + bucket = {}; + childrenBucket = {}; + breadcrumbsBucket = {}; +} diff --git a/server/sonar-web/src/main/js/apps/code/components/App.js b/server/sonar-web/src/main/js/apps/code/components/App.js index fadc222005c..4e27e90e015 100644 --- a/server/sonar-web/src/main/js/apps/code/components/App.js +++ b/server/sonar-web/src/main/js/apps/code/components/App.js @@ -26,7 +26,7 @@ import SourceViewer from './../../../components/source-viewer/SourceViewer'; import Search from './Search'; import ListFooter from '../../../components/controls/ListFooter'; import { retrieveComponentChildren, retrieveComponent, loadMoreChildren, parseError } from '../utils'; -import { addComponent, addComponentBreadcrumbs } from '../bucket'; +import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket'; import { getComponent } from '../../../app/store/rootReducer'; import '../code.css'; @@ -56,6 +56,7 @@ class App extends React.Component { } componentWillUnmount () { + clearBucket(); this.mounted = false; } diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js b/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js index 7583fcbbe92..0b764f0aaad 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js @@ -18,15 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; - -import { getComponentUrl } from '../../../helpers/urls'; +import { Link } from 'react-router'; import { translate } from '../../../helpers/l10n'; const ComponentDetach = ({ component }) => ( - + ); export default ComponentDetach; diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js index ae166bbaa02..3ac38eff1b5 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js @@ -20,10 +20,8 @@ import _ from 'underscore'; import React from 'react'; import { Link } from 'react-router'; - import Truncated from './Truncated'; import QualifierIcon from '../../../components/shared/qualifier-icon'; -import { getComponentUrl } from '../../../helpers/urls'; function getTooltip (component) { const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS'; @@ -62,11 +60,11 @@ const ComponentName = ({ component, rootComponent, previous, canBrowse }) => { if (component.refKey) { inner = ( - + {' '} {name} - + ); } else { if (canBrowse) { diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesAppContainer.js b/server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesAppContainer.js index 97491378c7f..a28ff4373ce 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesAppContainer.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesAppContainer.js @@ -22,10 +22,22 @@ import init from '../init'; export default class CodingRulesAppContainer extends React.Component { componentDidMount () { - init(this.refs.container); + this.stop = init(this.refs.container); + } + + componentWillUnmount () { + this.stop(); } render () { - return
        ; + // placing container inside div is required, + // because when backbone.marionette's layout is destroyed, + // it also destroys the root element, + // but react wants it to be there to unmount it + return ( +
        +
        +
        + ); } } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/init.js b/server/sonar-web/src/main/js/apps/coding-rules/init.js index aa7c9644ede..37470e65465 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/init.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/init.js @@ -91,4 +91,10 @@ App.on('start', function (el) { export default function (el) { App.start(el); + + return () => { + Backbone.history.stop(); + App.layout.destroy(); + $('#footer').removeClass('search-navigator-footer'); + }; } diff --git a/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js b/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js index 0b3278c596e..4ca3376c3b0 100644 --- a/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js +++ b/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js @@ -24,11 +24,23 @@ import { getComponent, getCurrentUser } from '../../../app/store/rootReducer'; class ComponentIssuesAppContainer extends React.Component { componentDidMount () { - init(this.refs.container, this.props.component, this.props.currentUser); + this.stop = init(this.refs.container, this.props.component, this.props.currentUser); + } + + componentWillUnmount () { + this.stop(); } render () { - return
        ; + // placing container inside div is required, + // because when backbone.marionette's layout is destroyed, + // it also destroys the root element, + // but react wants it to be there to unmount it + return ( +
        +
        +
        + ); } } diff --git a/server/sonar-web/src/main/js/apps/component-issues/init.js b/server/sonar-web/src/main/js/apps/component-issues/init.js index 9fd72073beb..3727a08c044 100644 --- a/server/sonar-web/src/main/js/apps/component-issues/init.js +++ b/server/sonar-web/src/main/js/apps/component-issues/init.js @@ -117,4 +117,10 @@ App.on('start', function (options) { export default function (el, component, currentUser) { App.start({ el, component, currentUser }); + + return () => { + Backbone.history.stop(); + App.layout.destroy(); + $('#footer').removeClass('search-navigator-footer'); + }; } diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js index 08cf0b30f15..4d5c56374ef 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js @@ -28,11 +28,23 @@ class IssuesAppContainer extends React.Component { }; componentDidMount () { - init(this.refs.container, this.props.currentUser); + this.stop = init(this.refs.container, this.props.currentUser); + } + + componentWillUnmount () { + this.stop(); } render () { - return
        ; + // placing container inside div is required, + // because when backbone.marionette's layout is destroyed, + // it also destroys the root element, + // but react wants it to be there to unmount it + return ( +
        +
        +
        + ); } } diff --git a/server/sonar-web/src/main/js/apps/issues/init.js b/server/sonar-web/src/main/js/apps/issues/init.js index da9b2134c07..158189b2686 100644 --- a/server/sonar-web/src/main/js/apps/issues/init.js +++ b/server/sonar-web/src/main/js/apps/issues/init.js @@ -78,5 +78,11 @@ App.on('start', function (el) { export default function (el, user) { App.start({ el, user }); + + return () => { + Backbone.history.stop(); + App.layout.destroy(); + $('#footer').removeClass('search-navigator-footer'); + }; } diff --git a/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js index c78181b7af0..7270fb22500 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js +++ b/server/sonar-web/src/main/js/apps/overview/main/BugsAndVulnerabilities.js @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; - +import { Link } from 'react-router'; import enhance from './enhance'; import LeakPeriodLegend from '../components/LeakPeriodLegend'; import { getMetricName } from '../helpers/metrics'; @@ -27,21 +27,21 @@ import { translate } from '../../../helpers/l10n'; class BugsAndVulnerabilities extends React.Component { renderHeader () { const { component } = this.props; - const bugsDomainUrl = window.baseUrl + '/component_measures/domain/Reliability?id=' + - encodeURIComponent(component.key); - const vulnerabilitiesDomainUrl = window.baseUrl + '/component_measures/domain/Security?id=' + - encodeURIComponent(component.key); + const bugsDomainUrl = { + pathname: '/component_measures/domain/Reliability', + query: { id: component.key } + }; + const vulnerabilitiesDomainUrl = { + pathname: '/component_measures/domain/Security', + query: { id: component.key } + }; return (
        - - {translate('metric.bugs.name')} - + {translate('metric.bugs.name')} {' & '} - - {translate('metric.vulnerabilities.name')} - + {translate('metric.vulnerabilities.name')}
        ); diff --git a/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js b/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js index 9d98f14d001..9abf8e0826e 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js +++ b/server/sonar-web/src/main/js/apps/overview/main/CodeSmells.js @@ -19,6 +19,7 @@ */ import moment from 'moment'; import React from 'react'; +import { Link } from 'react-router'; import enhance from './enhance'; import { getMetricName } from '../helpers/metrics'; import { translate, translateWithParameters } from '../../../helpers/l10n'; @@ -47,11 +48,11 @@ class CodeSmells extends React.Component { formattedSnapshotDate); return ( - + {formatMeasure(value, 'SHORT_WORK_DUR')} - + ); } diff --git a/server/sonar-web/src/main/js/apps/overview/main/enhance.js b/server/sonar-web/src/main/js/apps/overview/main/enhance.js index 1b0b294d046..cedb89d9cf9 100644 --- a/server/sonar-web/src/main/js/apps/overview/main/enhance.js +++ b/server/sonar-web/src/main/js/apps/overview/main/enhance.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import moment from 'moment'; import shallowCompare from 'react-addons-shallow-compare'; import { DrilldownLink } from '../../../components/shared/drilldown-link'; @@ -58,15 +59,15 @@ export default function enhance (ComposedComponent) { renderHeader (domain, label) { const { component } = this.props; - const domainUrl = - window.baseUrl + - `/component_measures/domain/${domain}` + - `?id=${encodeURIComponent(component.key)}`; + const domainUrl = { + pathname: `/component_measures/domain/${domain}`, + query: { id: component.key } + }; return (
        - {label} + {label}
        ); @@ -167,11 +168,11 @@ export default function enhance (ComposedComponent) { const tooltip = translateWithParameters('widget.as_calculated_on_x', formattedSnapshotDate); return ( - + {formatMeasure(value, 'SHORT_INT')} - + ); } diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityGate.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityGate.js index 300faaf761f..745cf3cfbad 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityGate.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityGate.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import { translate } from '../../../helpers/l10n'; import { getQualityGateUrl } from '../../../helpers/urls'; @@ -35,9 +36,9 @@ const MetaQualityGate = ({ gate }) => { {'(' + translate('default') + ')'} )} - + {gate.name} - +
      diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js index 5709d077ec1..0530d58e218 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaQualityProfiles.js @@ -19,6 +19,7 @@ */ import React from 'react'; import { connect } from 'react-redux'; +import { Link } from 'react-router'; import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getQualityProfileUrl } from '../../../helpers/urls'; @@ -79,9 +80,9 @@ class MetaQualityProfiles extends React.Component { {'(' + languageName + ')'} - + {profile.name} - +
      ); diff --git a/server/sonar-web/src/main/js/apps/project-admin/components/GlobalMessagesContainer.js b/server/sonar-web/src/main/js/apps/project-admin/components/GlobalMessagesContainer.js deleted file mode 100644 index 130192047b7..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/components/GlobalMessagesContainer.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 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 { connect } from 'react-redux'; -import GlobalMessages from '../../../components/controls/GlobalMessages'; -import { getProjectAdminGlobalMessages } from '../../../app/store/rootReducer'; - -const mapStateToProps = state => ({ - messages: getProjectAdminGlobalMessages(state) -}); - -export default connect(mapStateToProps)(GlobalMessages); diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/Key.js b/server/sonar-web/src/main/js/apps/project-admin/key/Key.js index ff039076dd1..7b4c04846bb 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/key/Key.js +++ b/server/sonar-web/src/main/js/apps/project-admin/key/Key.js @@ -24,7 +24,6 @@ import Header from './Header'; import UpdateForm from './UpdateForm'; import BulkUpdate from './BulkUpdate'; import FineGrainedUpdate from './FineGrainedUpdate'; -import GlobalMessagesContainer from '../components/GlobalMessagesContainer'; import { fetchProjectModules, changeKey } from '../store/actions'; import { translate } from '../../../helpers/l10n'; import { @@ -100,7 +99,6 @@ class Key extends React.Component { {noModules && (
      - @@ -130,8 +128,6 @@ class Key extends React.Component {
    - - {tab === 'bulk' && ( )} diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js b/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js index 7c75ab9479d..50eabdecb47 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js +++ b/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js @@ -22,7 +22,6 @@ import { connect } from 'react-redux'; import shallowCompare from 'react-addons-shallow-compare'; import Header from './Header'; import Form from './Form'; -import GlobalMessagesContainer from '../components/GlobalMessagesContainer'; import { fetchProjectGate, setProjectGate } from '../store/actions'; import { getProjectAdminAllGates, getProjectAdminProjectGate, getComponent } from '../../../app/store/rootReducer'; @@ -49,7 +48,6 @@ class QualityGate extends React.Component { return (
    -
    - - {profiles.length > 0 ? (
    - + {' '} {project.name} - + {project.key} @@ -86,13 +84,12 @@ export default class Projects extends React.Component {
    • - + {translate('edit_permissions')} - +
    • - + {translate('projects_role.apply_template')}
    • diff --git a/server/sonar-web/src/main/js/apps/projects/components/App.js b/server/sonar-web/src/main/js/apps/projects/components/App.js index 594055dadfb..94224009cd5 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/App.js +++ b/server/sonar-web/src/main/js/apps/projects/components/App.js @@ -20,7 +20,6 @@ import React from 'react'; import Helmet from 'react-helmet'; import PageHeaderContainer from './PageHeaderContainer'; -import GlobalMessagesContainer from '../../../app/components/GlobalMessagesContainer'; import { translate } from '../../../helpers/l10n'; import '../styles.css'; @@ -37,11 +36,7 @@ export default class App extends React.Component { return (
      - - - - {this.props.children}
      ); diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js index 2af19bd183f..58f93c39935 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js +++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js @@ -19,10 +19,10 @@ */ import React from 'react'; import classNames from 'classnames'; +import { Link } from 'react-router'; import ProjectCardQualityGate from './ProjectCardQualityGate'; import ProjectCardMeasures from './ProjectCardMeasures'; import FavoriteContainer from '../../../components/controls/FavoriteContainer'; -import { getComponentUrl } from '../../../helpers/urls'; import { translate } from '../../../helpers/l10n'; export default class ProjectCard extends React.Component { @@ -54,7 +54,9 @@ export default class ProjectCard extends React.Component { )}

      - {project.name} + + {project.name} +

      {isProjectAnalyzed ? ( diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.js index b7c8184d488..42f375b5a95 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import moment from 'moment'; import ChangesList from './ChangesList'; import { translate } from '../../../helpers/l10n'; @@ -48,9 +49,9 @@ export default class Changelog extends React.Component {
    - + {event.ruleName} - + diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.js index b17994f46fd..35759789ac4 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.js @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import React from 'react'; +import { Link } from 'react-router'; import Changelog from '../Changelog'; import ChangesList from '../ChangesList'; @@ -67,8 +68,7 @@ it('should render action', () => { it('should render rule', () => { const events = [createEvent()]; const changelog = shallow(); - expect(changelog.text()).toContain('Do not do this'); - expect(changelog.find('a').prop('href')).toContain('rule_key=squid1234'); + expect(changelog.find('Link').prop('to')).toContain('rule_key=squid1234'); }); it('should render ChangesList', () => { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js index 31bed46d757..3d95efe7f88 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import ComparisonEmpty from './ComparisonEmpty'; import SeverityIcon from '../../../components/shared/severity-icon'; import { translateWithParameters } from '../../../helpers/l10n'; @@ -41,9 +42,9 @@ export default class ComparisonResults extends React.Component { ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js index ed7110ee6a8..ebd2bb0b053 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import React from 'react'; +import { Link } from 'react-router'; import ComparisonResults from '../ComparisonResults'; import ComparisonEmpty from '../ComparisonEmpty'; import SeverityIcon from '../../../../components/shared/severity-icon'; @@ -69,26 +70,24 @@ it('should compare', () => { const leftDiffs = output.find('.js-comparison-in-left'); expect(leftDiffs.length).toBe(1); - expect(leftDiffs.find('a').length).toBe(1); - expect(leftDiffs.find('a').prop('href')).toContain('rule_key=rule1'); - expect(leftDiffs.find('a').text()).toContain('rule1'); + expect(leftDiffs.find(Link).length).toBe(1); + expect(leftDiffs.find(Link).prop('to')).toContain('rule_key=rule1'); + expect(leftDiffs.find(Link).prop('children')).toContain('rule1'); expect(leftDiffs.find(SeverityIcon).length).toBe(1); expect(leftDiffs.find(SeverityIcon).prop('severity')).toBe('BLOCKER'); const rightDiffs = output.find('.js-comparison-in-right'); expect(rightDiffs.length).toBe(2); - expect(rightDiffs.at(0).find('a').length).toBe(1); - expect(rightDiffs.at(0).find('a').prop('href')) - .toContain('rule_key=rule2'); - expect(rightDiffs.at(0).find('a').text()).toContain('rule2'); + expect(rightDiffs.at(0).find(Link).length).toBe(1); + expect(rightDiffs.at(0).find(Link).prop('to')).toContain('rule_key=rule2'); + expect(rightDiffs.at(0).find(Link).prop('children')).toContain('rule2'); expect(rightDiffs.at(0).find(SeverityIcon).length).toBe(1); - expect(rightDiffs.at(0).find(SeverityIcon).prop('severity')) - .toBe('CRITICAL'); + expect(rightDiffs.at(0).find(SeverityIcon).prop('severity')).toBe('CRITICAL'); const modifiedDiffs = output.find('.js-comparison-modified'); expect(modifiedDiffs.length).toBe(1); - expect(modifiedDiffs.find('a').at(0).prop('href')).toContain('rule_key=rule4'); - expect(modifiedDiffs.find('a').at(0).text()).toContain('rule4'); + expect(modifiedDiffs.find(Link).at(0).prop('to')).toContain('rule_key=rule4'); + expect(modifiedDiffs.find(Link).at(0).prop('children')).toContain('rule4'); expect(modifiedDiffs.find(SeverityIcon).length).toBe(2); expect(modifiedDiffs.text()).toContain('bar'); expect(modifiedDiffs.text()).toContain('qwe'); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.js index 6e4a662b271..d095e49be93 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.js @@ -93,9 +93,9 @@ export default class ProfileActions extends React.Component {
      {canAdmin && (
    • - + {translate('quality_profiles.activate_more_rules')} - +
    • )}
    • diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js index 6bdf18fca16..7b3e5db2e4f 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js @@ -18,12 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import ChangeProjectsView from '../views/ChangeProjectsView'; import QualifierIcon from '../../../components/shared/qualifier-icon'; import { ProfileType } from '../propTypes'; import { getProfileProjects } from '../../../api/quality-profiles'; import { translate } from '../../../helpers/l10n'; -import { getComponentUrl } from '../../../helpers/urls'; export default class ProfileProjects extends React.Component { static propTypes = { @@ -109,15 +109,12 @@ export default class ProfileProjects extends React.Component { return ( diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.js index fd6e98d6f72..3284ed59aad 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.js @@ -18,16 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import keyBy from 'lodash/keyBy'; import ProfileRulesRow from './ProfileRulesRow'; import { ProfileType } from '../propTypes'; import { searchRules, takeFacet } from '../../../api/rules'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; -import { - getRulesUrl, - getDeprecatedActiveRulesUrl -} from '../../../helpers/urls'; +import { getRulesUrl, getDeprecatedActiveRulesUrl } from '../../../helpers/urls'; const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL']; @@ -123,11 +121,11 @@ export default class ProfileRules extends React.Component { } return ( - + {formatMeasure(this.state.activatedTotal, 'SHORT_INT')} - + ); } @@ -146,14 +144,14 @@ export default class ProfileRules extends React.Component { } return ( - + {formatMeasure( this.state.total - this.state.activatedTotal, 'SHORT_INT' )} - + ); } @@ -181,9 +179,9 @@ export default class ProfileRules extends React.Component { } return ( - + {formatMeasure(count, 'SHORT_INT')} - + ); } @@ -206,9 +204,9 @@ export default class ProfileRules extends React.Component { } return ( - + {formatMeasure(total - count, 'SHORT_INT')} - + ); } @@ -227,9 +225,9 @@ export default class ProfileRules extends React.Component { {translate('quality_profiles.deprecated_rules')} ); @@ -272,10 +270,9 @@ export default class ProfileRules extends React.Component { {this.props.canAdmin && ( )} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.js index 8239e571ca6..56740c68ab1 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import sortBy from 'lodash/sortBy'; import ProfileLink from '../components/ProfileLink'; import { getDeprecatedActiveRulesUrl } from '../../../helpers/urls'; @@ -65,13 +66,12 @@ export default class EvolutionDeprecated extends React.Component {
    • ))} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.js index 8d31c3e16c9..6e9ddc1c51f 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import moment from 'moment'; import sortBy from 'lodash/sortBy'; import { searchRules } from '../../../api/rules'; @@ -88,11 +89,10 @@ export default class EvolutionRules extends React.Component { {this.state.latestRules.map(rule => (
    • - + {' '} {rule.name} - +
      {rule.activations ? ( translateWithParameters( @@ -113,12 +113,11 @@ export default class EvolutionRules extends React.Component {
    {this.state.latestRulesTotal > RULES_LIMIT && ( )} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js index 255b2bb1e9e..1c0ee07050f 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; +import { Link } from 'react-router'; import shallowCompare from 'react-addons-shallow-compare'; import ProfileLink from '../components/ProfileLink'; import ProfileDate from '../components/ProfileDate'; @@ -86,18 +87,18 @@ export default class ProfilesListRow extends React.Component {
    {profile.activeDeprecatedRuleCount > 0 && ( - + {profile.activeDeprecatedRuleCount} - + )} - + {profile.activeRuleCount} - +
    ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/routes.js b/server/sonar-web/src/main/js/apps/quality-profiles/routes.js index 3530edc7a91..50b808c1a6a 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/routes.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/routes.js @@ -28,7 +28,7 @@ import ComparisonContainer from './compare/ComparisonContainer'; export default ( - + diff --git a/server/sonar-web/src/main/js/apps/settings/components/App.js b/server/sonar-web/src/main/js/apps/settings/components/App.js index 902e4bf4602..1ff0e46f97b 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/App.js +++ b/server/sonar-web/src/main/js/apps/settings/components/App.js @@ -24,7 +24,6 @@ import { connect } from 'react-redux'; import PageHeader from './PageHeader'; import CategoryDefinitionsList from './CategoryDefinitionsList'; import AllCategoriesList from './AllCategoriesList'; -import GlobalMessagesContainer from './GlobalMessagesContainer'; import WildcardsHelp from './WildcardsHelp'; import { fetchSettings } from '../store/actions'; import { getSettingsAppDefaultCategory } from '../../../app/store/rootReducer'; @@ -79,7 +78,6 @@ class App extends React.Component { return (
    -
    ({ - messages: getSettingsAppGlobalMessages(state) -}); - -export default connect(mapStateToProps)(GlobalMessages); diff --git a/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionApp.js b/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionApp.js index fd356a3dd7f..b7a085ad0d0 100644 --- a/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionApp.js +++ b/server/sonar-web/src/main/js/apps/settings/encryption/EncryptionApp.js @@ -20,7 +20,6 @@ import React from 'react'; import GenerateSecretKeyForm from './GenerateSecretKeyForm'; import EncryptionForm from './EncryptionForm'; -import GlobalMessagesContainer from '../components/GlobalMessagesContainer'; import { translate } from '../../../helpers/l10n'; export default class EncryptionApp extends React.Component { @@ -48,8 +47,6 @@ export default class EncryptionApp extends React.Component { {this.props.loading && } - - {!this.props.loading && !this.props.secretKeyAvailable && ( -
    ); diff --git a/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicensesApp-test.js b/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicensesApp-test.js index ab23f0f8ed0..372f5947e3d 100644 --- a/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicensesApp-test.js +++ b/server/sonar-web/src/main/js/apps/settings/licenses/__tests__/LicensesApp-test.js @@ -20,13 +20,11 @@ import React from 'react'; import { shallow } from 'enzyme'; import LicensesApp from '../LicensesApp'; -import GlobalMessagesContainer from '../../components/GlobalMessagesContainer'; import LicensesAppHeader from '../LicensesAppHeader'; import LicensesListContainer from '../LicensesListContainer'; it('should render', () => { const app = shallow(); - expect(app.find(GlobalMessagesContainer).length).toBe(1); expect(app.find(LicensesAppHeader).length).toBe(1); expect(app.find(LicensesListContainer).length).toBe(1); }); diff --git a/server/sonar-web/src/main/js/apps/settings/serverId/ServerIdApp.js b/server/sonar-web/src/main/js/apps/settings/serverId/ServerIdApp.js index db845226e90..8474bbaa581 100644 --- a/server/sonar-web/src/main/js/apps/settings/serverId/ServerIdApp.js +++ b/server/sonar-web/src/main/js/apps/settings/serverId/ServerIdApp.js @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import React from 'react'; -import GlobalMessagesContainer from '../components/GlobalMessagesContainer'; import { translate } from '../../../helpers/l10n'; import { getServerId, generateServerId } from '../../../api/settings'; import { parseError } from '../../code/utils'; @@ -79,8 +78,6 @@ export default class ServerIdApp extends React.Component {
    {translate('server_id_configuration.information')}
    - - {this.state.serverId != null && (
    Server ID: diff --git a/server/sonar-web/src/main/js/apps/settings/store/rootReducer.js b/server/sonar-web/src/main/js/apps/settings/store/rootReducer.js index 90658fcb3db..92b15323978 100644 --- a/server/sonar-web/src/main/js/apps/settings/store/rootReducer.js +++ b/server/sonar-web/src/main/js/apps/settings/store/rootReducer.js @@ -24,12 +24,13 @@ import values, * as fromValues from './values/reducer'; import settingsPage, * as fromSettingsPage from './settingsPage/reducer'; import licenses, * as fromLicenses from './licenses/reducer'; import globalMessages, * as fromGlobalMessages from '../../../components/store/globalMessages'; +import type { State as GlobalMessagesState } from '../../../components/store/globalMessages'; import encryptionPage from './encryptionPage/reducer'; type State = { definitions: {}, encryptionPage: {}, - globalMessages: {}, + globalMessages: GlobalMessagesState, licenses: {}, settingsPage: {}, values: {} diff --git a/server/sonar-web/src/main/js/apps/update-center/components/UpdateCenterAppContainer.js b/server/sonar-web/src/main/js/apps/update-center/components/UpdateCenterAppContainer.js index 500e4c052fa..31a8392fb67 100644 --- a/server/sonar-web/src/main/js/apps/update-center/components/UpdateCenterAppContainer.js +++ b/server/sonar-web/src/main/js/apps/update-center/components/UpdateCenterAppContainer.js @@ -24,11 +24,23 @@ import { getSettingValue } from '../../../app/store/rootReducer'; class UpdateCenterAppContainer extends React.Component { componentDidMount () { - init(this.refs.container, this.props.updateCenterActive); + this.stop = init(this.refs.container, this.props.updateCenterActive); + } + + componentWillUnmount () { + this.stop(); } render () { - return
    ; + // placing container inside div is required, + // because when backbone.marionette's layout is destroyed, + // it also destroys the root element, + // but react wants it to be there to unmount it + return ( +
    +
    +
    + ); } } diff --git a/server/sonar-web/src/main/js/apps/update-center/init.js b/server/sonar-web/src/main/js/apps/update-center/init.js index 830a3555822..0c7fe76fe83 100644 --- a/server/sonar-web/src/main/js/apps/update-center/init.js +++ b/server/sonar-web/src/main/js/apps/update-center/init.js @@ -29,6 +29,7 @@ import Router from './router'; import Plugins from './plugins'; const App = new Marionette.Application(); + const init = function ({ el, updateCenterActive }) { // State this.state = new Backbone.Model({ updateCenterActive }); @@ -71,9 +72,14 @@ const init = function ({ el, updateCenterActive }) { }; App.on('start', function (options) { - init.call(App, options); + return init.call(App, options); }); export default function (el, updateCenterActive) { App.start({ el, updateCenterActive }); + + return () => { + Backbone.history.stop(); + App.layout.destroy(); + }; } diff --git a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.js b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.js index ab5dfebbf23..2a9a016ce1f 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.js +++ b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.js @@ -48,7 +48,7 @@ export default class WebApiApp extends React.Component { componentWillUnmount () { this.mounted = false; - document.getElementById('footer').classList.delete('search-navigator-footer'); + document.getElementById('footer').classList.remove('search-navigator-footer'); } fetchList (cb) { diff --git a/server/sonar-web/src/main/js/components/controls/GlobalMessages.js b/server/sonar-web/src/main/js/components/controls/GlobalMessages.js index 775ec6f2014..4d95243cfec 100644 --- a/server/sonar-web/src/main/js/components/controls/GlobalMessages.js +++ b/server/sonar-web/src/main/js/components/controls/GlobalMessages.js @@ -27,16 +27,24 @@ export default class GlobalMessages extends React.Component { id: React.PropTypes.string.isRequired, message: React.PropTypes.string.isRequired, level: React.PropTypes.oneOf([ERROR, SUCCESS]) - })) + })), + closeGlobalMessage: React.PropTypes.func.isRequired }; - renderMessage (message) { - const className = classNames('alert', { - 'alert-danger': message.level === ERROR, - 'alert-success': message.level === SUCCESS + renderMessage = message => { + const className = classNames('process-spinner', 'shown', { + 'process-spinner-failed': message.level === ERROR, + 'process-spinner-success': message.level === SUCCESS }); - return
    {message.message}
    ; - } + return ( +
    + {message.message} + +
    + ); + }; render () { const { messages } = this.props; @@ -46,7 +54,9 @@ export default class GlobalMessages extends React.Component { } return ( -
    {messages.map(this.renderMessage)}
    +
    + {messages.map(this.renderMessage)} +
    ); } } diff --git a/server/sonar-web/src/main/js/components/shared/drilldown-link.js b/server/sonar-web/src/main/js/components/shared/drilldown-link.js index b6a2c289cfd..086c3091af4 100644 --- a/server/sonar-web/src/main/js/components/shared/drilldown-link.js +++ b/server/sonar-web/src/main/js/components/shared/drilldown-link.js @@ -20,10 +20,8 @@ import _ from 'underscore'; import moment from 'moment'; import React from 'react'; -import { - getComponentDrilldownUrl, - getComponentIssuesUrl -} from '../../helpers/urls'; +import { Link } from 'react-router'; +import { getComponentDrilldownUrl, getComponentIssuesUrl } from '../../helpers/urls'; const ISSUE_MEASURES = [ 'violations', @@ -119,7 +117,7 @@ export const DrilldownLink = React.createClass({ this.propsToIssueParams()); return ( - {this.props.children} + {this.props.children} ); }, @@ -129,6 +127,6 @@ export const DrilldownLink = React.createClass({ } const url = getComponentDrilldownUrl(this.props.component, this.props.metric); - return {this.props.children}; + return {this.props.children}; } }); diff --git a/server/sonar-web/src/main/js/components/store/globalMessages.js b/server/sonar-web/src/main/js/components/store/globalMessages.js index e813f6f59b8..b5e11e1f5df 100644 --- a/server/sonar-web/src/main/js/components/store/globalMessages.js +++ b/server/sonar-web/src/main/js/components/store/globalMessages.js @@ -17,70 +17,97 @@ * 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 uniqueId from 'lodash/uniqueId'; import { actions } from '../../app/store/appState/duck'; +type Level = 'ERROR' | 'SUCCESS'; + +type Message = { + id: string, + message: string, + level: Level +}; + +export type State = Array; + +type Action = Object; + export const ERROR = 'ERROR'; export const SUCCESS = 'SUCCESS'; /* Actions */ const ADD_GLOBAL_MESSAGE = 'ADD_GLOBAL_MESSAGE'; +const CLOSE_GLOBAL_MESSAGE = 'CLOSE_GLOBAL_MESSAGE'; +const CLOSE_ALL_GLOBAL_MESSAGES = 'CLOSE_ALL_GLOBAL_MESSAGES'; -const addGlobalMessage = (message, level) => ({ +const addGlobalMessageActionCreator = (id: string, message: string, level: Level) => ({ type: ADD_GLOBAL_MESSAGE, message, - level + level, + id }); -export const addGlobalErrorMessage = message => - addGlobalMessage(message, ERROR); - -export const addGlobalSuccessMessage = message => - addGlobalMessage(message, SUCCESS); - -const CLOSE_ALL_GLOBAL_MESSAGES = 'CLOSE_ALL_GLOBAL_MESSAGES'; +export const closeGlobalMessage = (id: string) => ({ + type: CLOSE_GLOBAL_MESSAGE, + id +}); -export const closeAllGlobalMessages = id => ({ +export const closeAllGlobalMessages = (id: string) => ({ type: CLOSE_ALL_GLOBAL_MESSAGES, id }); +const addGlobalMessage = (message: string, level: Level) => (dispatch: Function) => { + const id = uniqueId('global-message-'); + dispatch(addGlobalMessageActionCreator(id, message, level)); + setTimeout(() => dispatch(closeGlobalMessage(id)), 5000); +}; + +export const addGlobalErrorMessage = (message: string) => + addGlobalMessage(message, ERROR); + +export const addGlobalSuccessMessage = (message: string) => + addGlobalMessage(message, SUCCESS); + /* Reducer */ -const globalMessages = (state = [], action = {}) => { - if (action.type === ADD_GLOBAL_MESSAGE) { - return [{ - id: uniqueId('global-message-'), - message: action.message, - level: action.level - }]; - } +const globalMessages = (state: State = [], action: Action = {}) => { + switch (action.type) { + case ADD_GLOBAL_MESSAGE: + return [{ + id: action.id, + message: action.message, + level: action.level + }]; - if (action.type === actions.REQUIRE_AUTHENTICATION) { - // FIXME l10n - return [{ - id: uniqueId('global-message-'), - message: 'Authentication required to see this page.', - level: ERROR - }]; - } + case actions.REQUIRE_AUTHENTICATION: + // FIXME l10n + return [{ + id: uniqueId('global-message-'), + message: 'Authentication required to see this page.', + level: ERROR + }]; - if (action.type === actions.REQUIRE_AUTHORIZATION) { - // FIXME l10n - return [{ - id: uniqueId('global-message-'), - message: 'You are not authorized to access this page. Please log in with more privileges and try again.', - level: ERROR - }]; - } + case actions.REQUIRE_AUTHORIZATION: + // FIXME l10n + return [{ + id: uniqueId('global-message-'), + message: 'You are not authorized to access this page. Please log in with more privileges and try again.', + level: ERROR + }]; + + case CLOSE_GLOBAL_MESSAGE: + return state.filter(message => message.id !== action.id); - if (action.type === CLOSE_ALL_GLOBAL_MESSAGES) { - return []; - } - return state; + case CLOSE_ALL_GLOBAL_MESSAGES: + return []; + default: + return state; + } }; export default globalMessages; /* Selectors */ -export const getGlobalMessages = state => state; +export const getGlobalMessages = (state: State) => state; diff --git a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js index a129d3abfc3..ba69a1bde67 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js +++ b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.js @@ -69,28 +69,20 @@ describe('#getComponentIssuesUrl', function () { expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, { componentUuids: COMPLEX_COMPONENT_KEY })).toBe( '/component_issues?id=' + SIMPLE_COMPONENT_KEY + '#componentUuids=' + COMPLEX_COMPONENT_KEY_ENCODED); }); - - it('should take baseUrl into account', function () { - window.baseUrl = '/context'; - expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, {})).toBe( - '/context/component_issues?id=' + SIMPLE_COMPONENT_KEY + '#'); - }); }); describe('#getComponentDrilldownUrl', function () { it('should return component drilldown url', function () { - expect(getComponentDrilldownUrl(SIMPLE_COMPONENT_KEY, METRIC)).toBe( - '/component_measures/metric/' + METRIC + '?id=' + SIMPLE_COMPONENT_KEY); + expect(getComponentDrilldownUrl(SIMPLE_COMPONENT_KEY, METRIC)).toEqual({ + pathname: '/component_measures/metric/' + METRIC, + query: { id: SIMPLE_COMPONENT_KEY } + }); }); it('should encode component key', function () { - expect(getComponentDrilldownUrl(COMPLEX_COMPONENT_KEY, METRIC)).toBe( - '/component_measures/metric/' + METRIC + '?id=' + COMPLEX_COMPONENT_KEY_ENCODED); - }); - - it('should take baseUrl into account', function () { - window.baseUrl = '/context'; - expect(getComponentDrilldownUrl(SIMPLE_COMPONENT_KEY, METRIC)).toBe( - '/context/component_measures/metric/' + METRIC + '?id=' + SIMPLE_COMPONENT_KEY); + expect(getComponentDrilldownUrl(COMPLEX_COMPONENT_KEY, METRIC)).toEqual({ + pathname: '/component_measures/metric/' + METRIC, + query: { id: COMPLEX_COMPONENT_KEY } + }); }); }); diff --git a/server/sonar-web/src/main/js/helpers/urls.js b/server/sonar-web/src/main/js/helpers/urls.js index 16da020a96b..6dddb630c09 100644 --- a/server/sonar-web/src/main/js/helpers/urls.js +++ b/server/sonar-web/src/main/js/helpers/urls.js @@ -35,7 +35,9 @@ export function getIssuesUrl (query) { const serializedQuery = Object.keys(query).map(criterion => ( `${encodeURIComponent(criterion)}=${encodeURIComponent(query[criterion])}` )).join('|'); - return window.baseUrl + '/issues#' + serializedQuery; + + // return a string (not { pathname }) to help react-router's Link handle this properly + return '/issues#' + serializedQuery; } /** @@ -48,44 +50,57 @@ export function getComponentIssuesUrl (componentKey, query) { const serializedQuery = Object.keys(query).map(criterion => ( `${encodeURIComponent(criterion)}=${encodeURIComponent(query[criterion])}` )).join('|'); - return window.baseUrl + '/component_issues?id=' + encodeURIComponent(componentKey) + '#' + serializedQuery; + + // return a string (not { pathname }) to help react-router's Link handle this properly + return '/component_issues?id=' + encodeURIComponent(componentKey) + '#' + serializedQuery; } /** * Generate URL for a component's drilldown page * @param {string} componentKey * @param {string} metric - * @returns {string} + * @returns {Object} */ export function getComponentDrilldownUrl (componentKey, metric) { - return `${window.baseUrl}/component_measures/metric/${metric}?id=${encodeURIComponent(componentKey)}`; + return { + pathname: `/component_measures/metric/${metric}`, + query: { id: componentKey } + }; } /** * Generate URL for a component's permissions page * @param {string} componentKey - * @returns {string} + * @returns {Object} */ export function getComponentPermissionsUrl (componentKey) { - return window.baseUrl + '/project_roles?id=' + encodeURIComponent(componentKey); + return { + pathname: '/project_roles', + query: { id: componentKey } + }; } /** * Generate URL for a quality profile * @param {string} key - * @returns {string} + * @returns {Object} */ export function getQualityProfileUrl (key) { - return window.baseUrl + '/profiles/show?key=' + encodeURIComponent(key); + return { + pathname: '/profiles/show', + query: { key } + }; } /** * Generate URL for a quality gate * @param {string} key - * @returns {string} + * @returns {Object} */ export function getQualityGateUrl (key) { - return window.baseUrl + '/quality_gates/show/' + encodeURIComponent(key); + return { + pathname: '/quality_gates/show/' + encodeURIComponent(key) + }; } /** @@ -99,9 +114,12 @@ export function getRulesUrl (query) { `${encodeURIComponent(criterion)}=${encodeURIComponent( query[criterion])}` )).join('|'); - return window.baseUrl + '/coding_rules#' + serializedQuery; + + // return a string (not { pathname }) to help react-router's Link handle this properly + return '/coding_rules#' + serializedQuery; } - return window.baseUrl + '/coding_rules'; + + return '/coding_rules'; } /** diff --git a/server/sonar-web/src/main/less/components/navbar.less b/server/sonar-web/src/main/less/components/navbar.less index 72baf5cb991..7d2c9a712bb 100644 --- a/server/sonar-web/src/main/less/components/navbar.less +++ b/server/sonar-web/src/main/less/components/navbar.less @@ -170,12 +170,13 @@ font-size: 12px; letter-spacing: 0.05em; - &:hover, &:focus { + &:hover { color: #fff; } } .navbar-nav > .active > a, + .navbar-nav > li > a.active, .navbar-nav > .dropdown.open > a { color: #fff; } @@ -186,8 +187,8 @@ } .navbar-nav > li > a:hover, - .navbar-nav > li > a:focus, .navbar-nav > .active > a, + .navbar-nav > li> a.active, .navbar-nav > .dropdown.open > a { background-color: @blue; } @@ -197,8 +198,8 @@ } .navbar-admin-link:hover, - .navbar-admin-link:focus, - .active > .navbar-admin-link { + .active > .navbar-admin-link, + .active.navbar-admin-link { background-color: @orange !important; } } diff --git a/server/sonar-web/src/main/less/components/ui.less b/server/sonar-web/src/main/less/components/ui.less index 5515bc26485..1a749540cab 100644 --- a/server/sonar-web/src/main/less/components/ui.less +++ b/server/sonar-web/src/main/less/components/ui.less @@ -54,6 +54,12 @@ color: @white; } +.process-spinner-success { + padding-right: 30px; + background-color: @green; + color: @white; +} + .process-spinner-close { position: absolute; top: 0; @@ -62,6 +68,7 @@ background: none !important; border: none !important; line-height: 1; + color: #fff; } -- 2.39.5