aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2016-08-10 14:04:38 +0200
committerGitHub <noreply@github.com>2016-08-10 14:04:38 +0200
commit143f3c560bcf7ceac9585c1dfea7311f037834cb (patch)
tree6d8c5b4884c699ca6f996c0eb9639db7bcfa9fc7
parent04c13b353fcb16dc53b1287f301301dad8d79abf (diff)
downloadsonarqube-143f3c560bcf7ceac9585c1dfea7311f037834cb.tar.gz
sonarqube-143f3c560bcf7ceac9585c1dfea7311f037834cb.zip
SONAR-7921 Rewrite "Quality Gate" project page (#1138)
-rw-r--r--it/it-tests/src/test/java/it/projectAdministration/ProjectQualityGatePageTest.java149
-rw-r--r--it/it-tests/src/test/java/pageobjects/Navigation.java6
-rw-r--r--it/it-tests/src/test/java/pageobjects/ProjectQualityGatePage.java45
-rw-r--r--server/sonar-web/src/main/js/api/quality-gates.js18
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/app.js4
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/components/GlobalMessagesContainer.js28
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/quality-gate/Form.js123
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/quality-gate/Header.js36
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js70
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js3
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/actions.js55
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/gateByProject.js38
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/gates.js39
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js22
-rw-r--r--server/sonar-web/src/main/js/components/controls/GlobalMessages.js52
-rw-r--r--server/sonar-web/src/main/js/components/store/globalMessages.js56
-rw-r--r--server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js10
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb34
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/project/quality_gate.html.erb3
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/project/qualitygate.html.erb25
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties2
21 files changed, 759 insertions, 59 deletions
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';
@@ -50,6 +51,9 @@ window.sonarqube.appStarted.then(options => {
path="/quality_profiles"
component={withComponent(QualityProfiles)}/>
<Route
+ path="/quality_gate"
+ component={withComponent(QualityGate)}/>
+ <Route
path="/links"
component={withComponent(Links)}/>
</Router>
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 (
+ <span>
+ <strong>{translate('default')}</strong>
+ {': '}
+ {gateOption.label}
+ </span>
+ );
+ }
+
+ 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 (
+ <Select
+ options={options}
+ valueRenderer={this.renderGateName}
+ optionRenderer={this.renderGateName}
+ value={gate && gate.id}
+ clearable={false}
+ placeholder={translate('none')}
+ style={{ width: 300 }}
+ disabled={this.state.loading}
+ onChange={this.handleChange.bind(this)}/>
+ );
+ }
+
+ render () {
+ return (
+ <div>
+ {this.renderSelect()}
+ {this.state.loading && <i className="spinner spacer-left"/>}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Header.js b/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Header.js
new file mode 100644
index 00000000000..3cab5373077
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/quality-gate/Header.js
@@ -0,0 +1,36 @@
+/*
+ * 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 { translate } from '../../../helpers/l10n';
+
+export default class Header extends React.Component {
+ render () {
+ return (
+ <header className="page-header">
+ <h1 className="page-title">
+ {translate('project_quality_gate.page')}
+ </h1>
+ <div className="page-description">
+ {translate('project_quality_gate.page.description')}
+ </div>
+ </header>
+ );
+ }
+}
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
new file mode 100644
index 00000000000..e80c5b72284
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js
@@ -0,0 +1,70 @@
+/*
+ * 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 { 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 { getAllGates, getProjectGate } from '../store/rootReducer';
+import { fetchProjectGate, setProjectGate } from '../store/actions';
+
+class QualityGate extends React.Component {
+ static propTypes = {
+ component: React.PropTypes.object.isRequired,
+ allGates: React.PropTypes.array,
+ gate: React.PropTypes.object
+ };
+
+ componentDidMount () {
+ this.props.fetchProjectGate(this.props.component.key);
+ }
+
+ shouldComponentUpdate (nextProps, nextState) {
+ return shallowCompare(this, nextProps, nextState);
+ }
+
+ handleChangeGate (oldId, newId) {
+ this.props.setProjectGate(this.props.component.key, oldId, newId);
+ }
+
+ render () {
+ return (
+ <div id="project-quality-gate" className="page page-limited">
+ <Header/>
+ <GlobalMessagesContainer/>
+ <Form
+ allGates={this.props.allGates}
+ gate={this.props.gate}
+ onChange={this.handleChangeGate.bind(this)}/>
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = (state, ownProps) => ({
+ allGates: getAllGates(state),
+ gate: getProjectGate(state, ownProps.component.key)
+});
+
+export default connect(
+ mapStateToProps,
+ { fetchProjectGate, setProjectGate }
+)(QualityGate);
diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js
index 766e4bf5dae..de20aa1ad8d 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js
@@ -22,6 +22,7 @@ import { connect } from 'react-redux';
import shallowCompare from 'react-addons-shallow-compare';
import Header from './Header';
import Table from './Table';
+import GlobalMessagesContainer from '../components/GlobalMessagesContainer';
import { fetchProjectProfiles, setProjectProfile } from '../store/actions';
import { getProjectProfiles, getAllProfiles } from '../store/rootReducer';
@@ -51,6 +52,8 @@ class QualityProfiles extends React.Component {
<div className="page page-limited">
<Header/>
+ <GlobalMessagesContainer/>
+
{profiles.length > 0 ? (
<Table
allProfiles={allProfiles}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js
index 083a28bfa92..29a627e9d54 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js
@@ -23,7 +23,15 @@ import {
dissociateProject
} from '../../../api/quality-profiles';
import { getProfileByKey } from './rootReducer';
+import {
+ fetchQualityGates,
+ getGateForProject,
+ associateGateWithProject,
+ dissociateGateWithProject
+} from '../../../api/quality-gates';
import { getProjectLinks, createLink } from '../../../api/projectLinks';
+import { addGlobalSuccessMessage } from '../../../components/store/globalMessages';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
export const RECEIVE_PROFILES = 'RECEIVE_PROFILES';
export const receiveProfiles = profiles => ({
@@ -67,9 +75,56 @@ export const setProjectProfile = (projectKey, oldKey, newKey) =>
request.then(() => {
dispatch(setProjectProfileAction(projectKey, oldKey, newKey));
+ dispatch(addGlobalSuccessMessage(
+ translateWithParameters(
+ 'project_quality_profile.successfully_updated',
+ newProfile.languageName)));
});
};
+export const RECEIVE_GATES = 'RECEIVE_GATES';
+export const receiveGates = gates => ({
+ type: RECEIVE_GATES,
+ gates
+});
+
+export const RECEIVE_PROJECT_GATE = 'RECEIVE_PROJECT_GATE';
+export const receiveProjectGate = (projectKey, gate) => ({
+ type: RECEIVE_PROJECT_GATE,
+ projectKey,
+ gate
+});
+
+export const fetchProjectGate = projectKey => dispatch => {
+ Promise.all([
+ fetchQualityGates(),
+ getGateForProject(projectKey)
+ ]).then(responses => {
+ const [allGates, projectGate] = responses;
+ dispatch(receiveGates(allGates));
+ dispatch(receiveProjectGate(projectKey, projectGate));
+ });
+};
+
+export const SET_PROJECT_GATE = 'SET_PROJECT_GATE';
+const setProjectGateAction = (projectKey, gateId) => ({
+ type: SET_PROJECT_GATE,
+ projectKey,
+ gateId
+});
+
+export const setProjectGate = (projectKey, oldId, newId) => dispatch => {
+ const request = newId != null ?
+ associateGateWithProject(newId, projectKey) :
+ dissociateGateWithProject(oldId, projectKey);
+
+ request.then(() => {
+ dispatch(setProjectGateAction(projectKey, newId));
+ dispatch(addGlobalSuccessMessage(
+ translate('project_quality_gate.successfully_updated')));
+ });
+};
+
export const RECEIVE_PROJECT_LINKS = 'RECEIVE_PROJECT_LINKS';
export const receiveProjectLinks = (projectKey, links) => ({
type: RECEIVE_PROJECT_LINKS,
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/gateByProject.js b/server/sonar-web/src/main/js/apps/project-admin/store/gateByProject.js
new file mode 100644
index 00000000000..1218554e576
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/store/gateByProject.js
@@ -0,0 +1,38 @@
+/*
+ * 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 { RECEIVE_PROJECT_GATE, SET_PROJECT_GATE } from './actions';
+
+const gateByProject = (state = {}, action = {}) => {
+ if (action.type === RECEIVE_PROJECT_GATE) {
+ const gateId = action.gate ? action.gate.id : null;
+ return { ...state, [action.projectKey]: gateId };
+ }
+
+ if (action.type === SET_PROJECT_GATE) {
+ return { ...state, [action.projectKey]: action.gateId };
+ }
+
+ return state;
+};
+
+export default gateByProject;
+
+export const getProjectGate = (state, projectKey) =>
+ state[projectKey];
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/gates.js b/server/sonar-web/src/main/js/apps/project-admin/store/gates.js
new file mode 100644
index 00000000000..87fa9d28591
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/store/gates.js
@@ -0,0 +1,39 @@
+/*
+ * 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 keyBy from 'lodash/keyBy';
+import values from 'lodash/values';
+import { RECEIVE_GATES } from './actions';
+
+const gates = (state = {}, action = {}) => {
+ if (action.type === RECEIVE_GATES) {
+ const newGatesById = keyBy(action.gates, 'id');
+ return { ...state, ...newGatesById };
+ }
+
+ return state;
+};
+
+export default gates;
+
+export const getAllGates = state =>
+ values(state);
+
+export const getGate = (state, id) =>
+ state[id];
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
index e8174f53aa0..513e39dc67d 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
@@ -23,14 +23,22 @@ import profiles, {
getAllProfiles as nextGetAllProfiles
} from './profiles';
import profilesByProject, { getProfiles } from './profilesByProject';
+import gates, { getAllGates as nextGetAllGates, getGate } from './gates';
+import gateByProject, { getProjectGate as nextGetProjectGate } from './gateByProject';
import links, { getLink } from './links';
import linksByProject, { getLinks } from './linksByProject';
+import globalMessages, {
+ getGlobalMessages as nextGetGlobalMessages
+} from '../../../components/store/globalMessages';
const rootReducer = combineReducers({
profiles,
profilesByProject,
+ gates,
+ gateByProject,
links,
- linksByProject
+ linksByProject,
+ globalMessages
});
export default rootReducer;
@@ -45,9 +53,21 @@ export const getProjectProfiles = (state, projectKey) =>
getProfiles(state.profilesByProject, projectKey)
.map(profileKey => getProfileByKey(state, profileKey));
+export const getGateById = (state, gateId) =>
+ getGate(state.gates, gateId);
+
+export const getAllGates = state =>
+ nextGetAllGates(state.gates);
+
+export const getProjectGate = (state, projectKey) =>
+ getGateById(state, nextGetProjectGate(state.gateByProject, projectKey));
+
export const getLinkById = (state, linkId) =>
getLink(state.links, linkId);
export const getProjectLinks = (state, projectKey) =>
getLinks(state.linksByProject, projectKey)
.map(linkId => getLinkById(state, linkId));
+
+export const getGlobalMessages = state =>
+ nextGetGlobalMessages(state.globalMessages);
diff --git a/server/sonar-web/src/main/js/components/controls/GlobalMessages.js b/server/sonar-web/src/main/js/components/controls/GlobalMessages.js
new file mode 100644
index 00000000000..775ec6f2014
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/GlobalMessages.js
@@ -0,0 +1,52 @@
+/*
+ * 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 classNames from 'classnames';
+import { ERROR, SUCCESS } from '../store/globalMessages';
+
+export default class GlobalMessages extends React.Component {
+ static propTypes = {
+ messages: React.PropTypes.arrayOf(React.PropTypes.shape({
+ id: React.PropTypes.string.isRequired,
+ message: React.PropTypes.string.isRequired,
+ level: React.PropTypes.oneOf([ERROR, SUCCESS])
+ }))
+ };
+
+ renderMessage (message) {
+ const className = classNames('alert', {
+ 'alert-danger': message.level === ERROR,
+ 'alert-success': message.level === SUCCESS
+ });
+ return <div key={message.id} className={className}>{message.message}</div>;
+ }
+
+ render () {
+ const { messages } = this.props;
+
+ if (messages.length === 0) {
+ return null;
+ }
+
+ return (
+ <div>{messages.map(this.renderMessage)}</div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/store/globalMessages.js b/server/sonar-web/src/main/js/components/store/globalMessages.js
new file mode 100644
index 00000000000..b2860e09a22
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/store/globalMessages.js
@@ -0,0 +1,56 @@
+/*
+ * 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 uniqueId from 'lodash/uniqueId';
+
+export const ERROR = 'ERROR';
+export const SUCCESS = 'SUCCESS';
+
+/* Actions */
+const ADD_GLOBAL_MESSAGE = 'ADD_GLOBAL_MESSAGE';
+
+const addGlobalMessage = (message, level) => ({
+ type: ADD_GLOBAL_MESSAGE,
+ message,
+ level
+});
+
+export const addGlobalErrorMessage = message =>
+ addGlobalMessage(message, ERROR);
+
+export const addGlobalSuccessMessage = message =>
+ 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
+ }];
+ }
+
+ return state;
+};
+
+export default globalMessages;
+
+/* Selectors */
+export const getGlobalMessages = state => state;
diff --git a/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js b/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
index e93564944d6..24a8d5f0a25 100644
--- a/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
+++ b/server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
@@ -28,7 +28,7 @@ import { getComponentUrl } from '../../../helpers/urls';
const SETTINGS_URLS = [
'/project/settings',
'/project/quality_profiles',
- '/project/qualitygate',
+ '/project/quality_gate',
'/custom_measures',
'/project/links',
'/project_roles',
@@ -123,7 +123,7 @@ export default React.createClass({
<ul className="dropdown-menu">
{this.renderSettingsLink()}
{this.renderProfilesLink()}
- {this.renderQualityGatesLink()}
+ {this.renderQualityGateLink()}
{this.renderCustomMeasuresLink()}
{this.renderLinksLink()}
{this.renderPermissionsLink()}
@@ -153,12 +153,12 @@ export default React.createClass({
return this.renderLink(url, translate('project_quality_profiles.page'), '/project/quality_profiles');
},
- renderQualityGatesLink() {
+ renderQualityGateLink() {
if (!this.props.conf.showQualityGates) {
return null;
}
- const url = `/project/qualitygate?id=${encodeURIComponent(this.props.component.key)}`;
- return this.renderLink(url, translate('project_quality_gate.page'), '/project/qualitygate');
+ const url = `/project/quality_gate?id=${encodeURIComponent(this.props.component.key)}`;
+ return this.renderLink(url, translate('project_quality_gate.page'), '/project/quality_gate');
},
renderCustomMeasuresLink() {
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb
index 9fc1a288f7b..d4d40889d91 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb
@@ -48,40 +48,18 @@ class ProjectController < ApplicationController
@project = get_current_project(params[:id])
end
- def links
+ def quality_gate
+ # since 6.1
@project = get_current_project(params[:id])
end
- # GET /project/qualitygate?id=<project id>
def qualitygate
- require_parameters :id
- @project_id = Api::Utils.project_id(params[:id])
- @project = Project.by_key(@project_id)
- access_denied unless (is_admin?(@project.uuid) || has_role?(:gateadmin))
-
- call_backend do
- @all_quality_gates = Internal.quality_gates.list().to_a
- @selected_qgate = Property.value('sonar.qualitygate', @project, '').to_i
- end
+ # redirect to another url since 6.1
+ redirect_to(url_for({:action => 'quality_gate'}) + '?id=' + url_encode(params[:id]))
end
- # POST /project/set_qualitygate?id=<project id>[&qgate_id=<qgate id>]
- def set_qualitygate
- verify_post_request
-
- project_id = params[:id].to_i
- qgate_id = params[:qgate_id].to_i
- previous_qgate_id = params[:previous_qgate_id].to_i
-
- call_backend do
- if qgate_id == 0
- Internal.quality_gates.dissociateProject(previous_qgate_id, project_id)
- else
- Internal.quality_gates.associateProject(qgate_id, project_id)
- end
- end
-
- redirect_to :action => 'qualitygate', :id => project_id
+ def links
+ @project = get_current_project(params[:id])
end
def key
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/quality_gate.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/quality_gate.html.erb
new file mode 100644
index 00000000000..e9dd9ae3410
--- /dev/null
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/quality_gate.html.erb
@@ -0,0 +1,3 @@
+<% content_for :extra_script do %>
+ <script src="<%= ApplicationController.root_context -%>/js/bundles/project-admin.js?v=<%= sonar_version -%>"></script>
+<% end %>
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/qualitygate.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/qualitygate.html.erb
deleted file mode 100644
index cd6e5a57567..00000000000
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/qualitygate.html.erb
+++ /dev/null
@@ -1,25 +0,0 @@
-<div class="page">
- <header class="page-header">
- <h1 class="page-title"><%= message('project_quality_gate.page') -%></h1>
- <p class="page-description"><%= message('project_quality_gate.page.description') -%></p>
- </header>
-
- <form id="select-quality-gate" method="POST" action="<%= ApplicationController.root_context -%>/project/set_qualitygate">
- <input type="hidden" name="id" value="<%= @project_id -%>"/>
- <input type="hidden" name="previous_qgate_id" value="<%= @selected_qgate -%>"/>
-
- <select id="select-qgate" name="qgate_id">
- <option value="" <%= "selected='selected'" unless @selected_qgate -%>><%= message 'project_quality_gate.default_qgate' -%></option>
- <optgroup>
- <%
- qgates = Api::Utils.insensitive_sort(@all_quality_gates) { |qgate| qgate.name }
- qgates.each do |qgate|
- %>
- <option value="<%= qgate.id -%>" <%= "selected='selected'" if @selected_qgate && (@selected_qgate == qgate.id) -%>><%= h qgate.name -%></option>
- <% end %>
- </optgroup>
- </select>
-
- <%= submit_tag message('update_verb'), :id => "submit-qgate", :disable_with => message('updating') %>
- </form>
-</div>
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.
#------------------------------------------------------------------------------
#