From 143f3c560bcf7ceac9585c1dfea7311f037834cb Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Wed, 10 Aug 2016 14:04:38 +0200 Subject: [PATCH] SONAR-7921 Rewrite "Quality Gate" project page (#1138) --- .../ProjectQualityGatePageTest.java | 149 ++++++++++++++++++ .../src/test/java/pageobjects/Navigation.java | 6 + .../pageobjects/ProjectQualityGatePage.java | 45 ++++++ .../src/main/js/api/quality-gates.js | 18 +++ .../src/main/js/apps/project-admin/app.js | 4 + .../components/GlobalMessagesContainer.js | 28 ++++ .../apps/project-admin/quality-gate/Form.js | 123 +++++++++++++++ .../apps/project-admin/quality-gate/Header.js | 36 +++++ .../project-admin/quality-gate/QualityGate.js | 70 ++++++++ .../quality-profiles/QualityProfiles.js | 3 + .../js/apps/project-admin/store/actions.js | 55 +++++++ .../apps/project-admin/store/gateByProject.js | 38 +++++ .../main/js/apps/project-admin/store/gates.js | 39 +++++ .../apps/project-admin/store/rootReducer.js | 22 ++- .../js/components/controls/GlobalMessages.js | 52 ++++++ .../js/components/store/globalMessages.js | 56 +++++++ .../main/nav/component/component-nav-menu.js | 10 +- .../app/controllers/project_controller.rb | 34 +--- .../app/views/project/quality_gate.html.erb | 3 + .../app/views/project/qualitygate.html.erb | 25 --- .../resources/org/sonar/l10n/core.properties | 2 + 21 files changed, 759 insertions(+), 59 deletions(-) create mode 100644 it/it-tests/src/test/java/it/projectAdministration/ProjectQualityGatePageTest.java create mode 100644 it/it-tests/src/test/java/pageobjects/ProjectQualityGatePage.java create mode 100644 server/sonar-web/src/main/js/apps/project-admin/components/GlobalMessagesContainer.js create mode 100644 server/sonar-web/src/main/js/apps/project-admin/quality-gate/Form.js create mode 100644 server/sonar-web/src/main/js/apps/project-admin/quality-gate/Header.js create mode 100644 server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js create mode 100644 server/sonar-web/src/main/js/apps/project-admin/store/gateByProject.js create mode 100644 server/sonar-web/src/main/js/apps/project-admin/store/gates.js create mode 100644 server/sonar-web/src/main/js/components/controls/GlobalMessages.js create mode 100644 server/sonar-web/src/main/js/components/store/globalMessages.js create mode 100644 server/sonar-web/src/main/webapp/WEB-INF/app/views/project/quality_gate.html.erb delete mode 100644 server/sonar-web/src/main/webapp/WEB-INF/app/views/project/qualitygate.html.erb diff --git a/it/it-tests/src/test/java/it/projectAdministration/ProjectQualityGatePageTest.java b/it/it-tests/src/test/java/it/projectAdministration/ProjectQualityGatePageTest.java new file mode 100644 index 00000000000..401f5a87766 --- /dev/null +++ b/it/it-tests/src/test/java/it/projectAdministration/ProjectQualityGatePageTest.java @@ -0,0 +1,149 @@ +/* + * 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. + */ +package it.projectAdministration; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.sonar.orchestrator.Orchestrator; +import it.Category1Suite; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.wsclient.qualitygate.QualityGate; +import org.sonar.wsclient.qualitygate.QualityGateClient; +import org.sonarqube.ws.client.PostRequest; +import org.sonarqube.ws.client.WsClient; +import org.sonarqube.ws.client.qualitygate.SelectWsRequest; +import pageobjects.Navigation; +import pageobjects.ProjectQualityGatePage; + +import static util.ItUtils.newAdminWsClient; + +public class ProjectQualityGatePageTest { + + @ClassRule + public static Orchestrator ORCHESTRATOR = Category1Suite.ORCHESTRATOR; + + @Rule + public Navigation nav = Navigation.get(ORCHESTRATOR); + + private static WsClient wsClient; + + @BeforeClass + public static void prepare() { + wsClient = newAdminWsClient(ORCHESTRATOR); + } + + @Before + public void setUp() { + ORCHESTRATOR.resetData(); + + wsClient.wsConnector().call(new PostRequest("api/projects/create") + .setParam("name", "Sample") + .setParam("key", "sample")); + } + + @Test + public void should_display_default() { + QualityGate customQualityGate = createCustomQualityGate("should_display_default"); + qualityGateClient().setDefault(customQualityGate.id()); + + ProjectQualityGatePage page = openPage(); + SelenideElement selectedQualityGate = page.getSelectedQualityGate(); + selectedQualityGate.should(Condition.hasText("Default")); + selectedQualityGate.should(Condition.hasText(customQualityGate.name())); + } + + @Test + public void should_display_custom() { + QualityGate customQualityGate = createCustomQualityGate("should_display_custom"); + associateWithQualityGate(customQualityGate); + + ProjectQualityGatePage page = openPage(); + SelenideElement selectedQualityGate = page.getSelectedQualityGate(); + selectedQualityGate.shouldNot(Condition.hasText("Default")); + selectedQualityGate.should(Condition.hasText(customQualityGate.name())); + } + + @Test + public void should_display_none() { + qualityGateClient().unsetDefault(); + + ProjectQualityGatePage page = openPage(); + page.assertNotSelected(); + } + + @Test + public void should_set_custom() { + QualityGate customQualityGate = createCustomQualityGate("should_set_custom"); + + ProjectQualityGatePage page = openPage(); + page.setQualityGate(customQualityGate.name()); + + SelenideElement selectedQualityGate = page.getSelectedQualityGate(); + selectedQualityGate.should(Condition.hasText(customQualityGate.name())); + } + + @Test + public void should_set_default() { + QualityGate customQualityGate = createCustomQualityGate("should_set_default"); + qualityGateClient().setDefault(customQualityGate.id()); + + ProjectQualityGatePage page = openPage(); + page.setQualityGate(customQualityGate.name()); + + SelenideElement selectedQualityGate = page.getSelectedQualityGate(); + selectedQualityGate.should(Condition.hasText("Default")); + selectedQualityGate.should(Condition.hasText(customQualityGate.name())); + } + + @Test + @Ignore("find a way to select None") + public void should_set_none() { + qualityGateClient().unsetDefault(); + QualityGate customQualityGate = createCustomQualityGate("should_set_none"); + associateWithQualityGate(customQualityGate); + + ProjectQualityGatePage page = openPage(); + page.setQualityGate(""); + + page.assertNotSelected(); + } + + private ProjectQualityGatePage openPage() { + nav.logIn().submitCredentials("admin", "admin"); + return nav.openProjectQualityGate("sample"); + } + + private static QualityGate createCustomQualityGate(String name) { + return qualityGateClient().create(name); + } + + private void associateWithQualityGate(QualityGate qualityGate) { + wsClient.qualityGates().associateProject(new SelectWsRequest().setProjectKey("sample").setGateId(qualityGate.id())); + } + + private static QualityGateClient qualityGateClient() { + return ORCHESTRATOR.getServer().adminWsClient().qualityGateClient(); + } +} diff --git a/it/it-tests/src/test/java/pageobjects/Navigation.java b/it/it-tests/src/test/java/pageobjects/Navigation.java index e615acdcded..d7d4a794614 100644 --- a/it/it-tests/src/test/java/pageobjects/Navigation.java +++ b/it/it-tests/src/test/java/pageobjects/Navigation.java @@ -56,6 +56,12 @@ public class Navigation extends ExternalResource { return open(url, ProjectLinksPage.class); } + public ProjectQualityGatePage openProjectQualityGate(String projectKey) { + // TODO encode projectKey + String url = "/project/quality_gate?id=" + projectKey; + return open(url, ProjectQualityGatePage.class); + } + public ProjectHistoryPage openProjectHistory(String projectKey) { // TODO encode projectKey String url = "/project/history?id=" + projectKey; diff --git a/it/it-tests/src/test/java/pageobjects/ProjectQualityGatePage.java b/it/it-tests/src/test/java/pageobjects/ProjectQualityGatePage.java new file mode 100644 index 00000000000..952f9640319 --- /dev/null +++ b/it/it-tests/src/test/java/pageobjects/ProjectQualityGatePage.java @@ -0,0 +1,45 @@ +/* + * 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. + */ +package pageobjects; + +import com.codeborne.selenide.SelenideElement; + +import static com.codeborne.selenide.Condition.exist; +import static com.codeborne.selenide.Selenide.$; + +public class ProjectQualityGatePage { + + public ProjectQualityGatePage() { + $("#project-quality-gate").should(exist); + } + + public SelenideElement getSelectedQualityGate() { + return $(".Select-value-label"); + } + + public void assertNotSelected() { + $(".Select-placeholder").should(exist); + $(".Select-value-label").shouldNot(exist); + } + + public void setQualityGate(String name) { + $(".Select-input input").val(name).pressEnter(); + } +} diff --git a/server/sonar-web/src/main/js/api/quality-gates.js b/server/sonar-web/src/main/js/api/quality-gates.js index a32b0295d1d..edab4f545ea 100644 --- a/server/sonar-web/src/main/js/api/quality-gates.js +++ b/server/sonar-web/src/main/js/api/quality-gates.js @@ -85,3 +85,21 @@ export function deleteCondition (id) { const url = '/api/qualitygates/delete_condition'; return post(url, { id }); } + +export function getGateForProject (projectKey) { + const url = '/api/qualitygates/get_by_project'; + const data = { projectKey }; + return getJSON(url, data).then(r => r.qualityGate); +} + +export function associateGateWithProject(gateId, projectKey) { + const url = '/api/qualitygates/select'; + const data = { gateId, projectKey }; + return post(url, data); +} + +export function dissociateGateWithProject(gateId, projectKey) { + const url = '/api/qualitygates/deselect'; + const data = { gateId, projectKey }; + return post(url, data); +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/app.js b/server/sonar-web/src/main/js/apps/project-admin/app.js index 85fb770962f..7981c936e87 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/app.js +++ b/server/sonar-web/src/main/js/apps/project-admin/app.js @@ -24,6 +24,7 @@ import { Router, Route, useRouterHistory } from 'react-router'; import { createHistory } from 'history'; import Deletion from './deletion/Deletion'; import QualityProfiles from './quality-profiles/QualityProfiles'; +import QualityGate from './quality-gate/QualityGate'; import Links from './links/Links'; import rootReducer from './store/rootReducer'; import configureStore from '../../components/store/configureStore'; @@ -49,6 +50,9 @@ window.sonarqube.appStarted.then(options => { + 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 new file mode 100644 index 00000000000..f64ef91664b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/components/GlobalMessagesContainer.js @@ -0,0 +1,28 @@ +/* + * 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 { getGlobalMessages } from '../store/rootReducer'; + +const mapStateToProps = state => ({ + messages: getGlobalMessages(state) +}); + +export default connect(mapStateToProps)(GlobalMessages); diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Form.js b/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Form.js new file mode 100644 index 00000000000..e35dd5c3e64 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Form.js @@ -0,0 +1,123 @@ +/* + * 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 React from 'react'; +import Select from 'react-select'; +import shallowCompare from 'react-addons-shallow-compare'; +import some from 'lodash/some'; +import { translate } from '../../../helpers/l10n'; + +export default class Form extends React.Component { + static propTypes = { + allGates: React.PropTypes.array.isRequired, + gate: React.PropTypes.object, + onChange: React.PropTypes.func.isRequired + }; + + state = { + loading: false + }; + + componentWillMount () { + this.handleChange = this.handleChange.bind(this); + this.renderGateName = this.renderGateName.bind(this); + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + componentDidUpdate (prevProps) { + if (prevProps.gate !== this.props.gate) { + this.stopLoading(); + } + } + + stopLoading () { + this.setState({ loading: false }); + } + + handleChange (option) { + const { gate } = this.props; + + const isSet = gate == null && option.value != null; + const isUnset = gate != null && option.value == null; + const isChanged = gate != null && gate.id !== option.value; + const hasChanged = isSet || isUnset || isChanged; + + if (hasChanged) { + this.setState({ loading: true }); + this.props.onChange(gate && gate.id, option.value); + } + } + + renderGateName (gateOption) { + if (gateOption.isDefault) { + return ( + + {translate('default')} + {': '} + {gateOption.label} + + ); + } + + return gateOption.label; + } + + renderSelect () { + const { gate, allGates } = this.props; + + const options = allGates.map(gate => ({ + value: gate.id, + label: gate.name, + isDefault: gate.isDefault + })); + + const hasDefault = some(allGates, gate => gate.isDefault); + if (!hasDefault) { + options.unshift({ + value: null, + label: translate('none') + }); + } + + return ( + - - - - - <%= submit_tag message('update_verb'), :id => "submit-qgate", :disable_with => message('updating') %> - - diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 7878ece8129..930442a42cf 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1633,6 +1633,7 @@ update_key.no_key_to_update=No key contains the string to replace ("{0}"). # #------------------------------------------------------------------------------ project_quality_profile.default_profile=Default +project_quality_profile.successfully_updated={0} quality profile has been successfully updated. #------------------------------------------------------------------------------ # @@ -1640,6 +1641,7 @@ project_quality_profile.default_profile=Default # #------------------------------------------------------------------------------ project_quality_gate.default_qgate=Default +project_quality_gate.successfully_updated=Quality gate has been successfully updated. #------------------------------------------------------------------------------ # -- 2.39.5