]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7921 Rewrite "Quality Gate" project page (#1138)
authorStas Vilchik <vilchiks@gmail.com>
Wed, 10 Aug 2016 12:04:38 +0000 (14:04 +0200)
committerGitHub <noreply@github.com>
Wed, 10 Aug 2016 12:04:38 +0000 (14:04 +0200)
21 files changed:
it/it-tests/src/test/java/it/projectAdministration/ProjectQualityGatePageTest.java [new file with mode: 0644]
it/it-tests/src/test/java/pageobjects/Navigation.java
it/it-tests/src/test/java/pageobjects/ProjectQualityGatePage.java [new file with mode: 0644]
server/sonar-web/src/main/js/api/quality-gates.js
server/sonar-web/src/main/js/apps/project-admin/app.js
server/sonar-web/src/main/js/apps/project-admin/components/GlobalMessagesContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-admin/quality-gate/Form.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-admin/quality-gate/Header.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js
server/sonar-web/src/main/js/apps/project-admin/store/actions.js
server/sonar-web/src/main/js/apps/project-admin/store/gateByProject.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-admin/store/gates.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
server/sonar-web/src/main/js/components/controls/GlobalMessages.js [new file with mode: 0644]
server/sonar-web/src/main/js/components/store/globalMessages.js [new file with mode: 0644]
server/sonar-web/src/main/js/main/nav/component/component-nav-menu.js
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb
server/sonar-web/src/main/webapp/WEB-INF/app/views/project/quality_gate.html.erb [new file with mode: 0644]
server/sonar-web/src/main/webapp/WEB-INF/app/views/project/qualitygate.html.erb [deleted file]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

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 (file)
index 0000000..401f5a8
--- /dev/null
@@ -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();
+  }
+}
index e615acdcdedec67f993b92eb366645ee4b41d6f5..d7d4a79461429fcbcde2fbb4eb0e4f93b27ba918 100644 (file)
@@ -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 (file)
index 0000000..952f964
--- /dev/null
@@ -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();
+  }
+}
index a32b0295d1dbde84eec9a65ab770db3e0d2eb944..edab4f545ea4315979e360a129800272573381e8 100644 (file)
@@ -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);
+}
index 85fb770962f1be9184341e49b5ede0de269c38a0..7981c936e87530d8ba08211c3140cc2566b7425a 100644 (file)
@@ -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 => {
           <Route
               path="/quality_profiles"
               component={withComponent(QualityProfiles)}/>
+          <Route
+              path="/quality_gate"
+              component={withComponent(QualityGate)}/>
           <Route
               path="/links"
               component={withComponent(Links)}/>
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 (file)
index 0000000..f64ef91
--- /dev/null
@@ -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 (file)
index 0000000..e35dd5c
--- /dev/null
@@ -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 (file)
index 0000000..3cab537
--- /dev/null
@@ -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 (file)
index 0000000..e80c5b7
--- /dev/null
@@ -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);
index 766e4bf5dae7f0a70ba1e4e77c855e7f42d56833..de20aa1ad8d64d0c9c8a29e100e96ebf94fe07cc 100644 (file)
@@ -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}
index 083a28bfa92a7d3797428403ca1ca754a6b6731d..29a627e9d54cd15d358d48a38bf4e53e07cc074c 100644 (file)
@@ -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 (file)
index 0000000..1218554
--- /dev/null
@@ -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 (file)
index 0000000..87fa9d2
--- /dev/null
@@ -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];
index e8174f53aa06d387ffba03dd6202d3e779506e74..513e39dc67dddb9ad3cbc855cc402e3ff092c5d4 100644 (file)
@@ -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 (file)
index 0000000..775ec6f
--- /dev/null
@@ -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 (file)
index 0000000..b2860e0
--- /dev/null
@@ -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;
index e93564944d6961612c0b2aa5b8822de2fc840317..24a8d5f0a25d214bc1f993c647455617268f46f0 100644 (file)
@@ -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() {
index 9fc1a288f7bdd1bd6eb2236e8e84ece904e1dc34..d4d40889d91ddaf97e2af6ec3c487605f5a1122d 100644 (file)
@@ -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 (file)
index 0000000..e9dd9ae
--- /dev/null
@@ -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 (file)
index cd6e5a5..0000000
+++ /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>
index 7878ece812908b77d545a68529ce624e97bc741a..930442a42cfbc9e458761500c9ee95de221f9df3 100644 (file)
@@ -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.
 
 #------------------------------------------------------------------------------
 #