diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-08-10 14:04:38 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-08-10 14:04:38 +0200 |
commit | 143f3c560bcf7ceac9585c1dfea7311f037834cb (patch) | |
tree | 6d8c5b4884c699ca6f996c0eb9639db7bcfa9fc7 | |
parent | 04c13b353fcb16dc53b1287f301301dad8d79abf (diff) | |
download | sonarqube-143f3c560bcf7ceac9585c1dfea7311f037834cb.tar.gz sonarqube-143f3c560bcf7ceac9585c1dfea7311f037834cb.zip |
SONAR-7921 Rewrite "Quality Gate" project page (#1138)
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. #------------------------------------------------------------------------------ # |