aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2016-06-27 16:08:44 +0200
committerGitHub <noreply@github.com>2016-06-27 16:08:44 +0200
commit4ecb6fc3ec08ee37a1bb5c19d8ca44e2b832b5a7 (patch)
tree763ce4b5a9d7b37982ab54fc64a0597167c2bc3d /server/sonar-web/src/main/js/apps
parentc467e84eed1a60bbe8c0fe800e2cada2bb26bb4c (diff)
downloadsonarqube-4ecb6fc3ec08ee37a1bb5c19d8ca44e2b832b5a7.tar.gz
sonarqube-4ecb6fc3ec08ee37a1bb5c19d8ca44e2b832b5a7.zip
refactor quality profiles page (#1056)
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/__tests__/utils-test.js74
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/actions-view.js114
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/app.js97
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.js81
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.js128
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogEmpty.js (renamed from server/sonar-web/src/main/js/apps/quality-profiles/profiles-empty-view.js)18
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogSearch.js62
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangesList.js46
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/ParameterChange.js53
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js38
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.js84
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangelogSearch-test.js68
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangesList-test.js54
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ParameterChange-test.js31
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js34
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.js122
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonEmpty.js (renamed from server/sonar-web/src/main/js/apps/quality-profiles/intro-view.js)17
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonForm.js56
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js176
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonForm-test.js49
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js98
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/App.js97
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.js72
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.js (renamed from server/sonar-web/src/main/js/apps/quality-profiles/router.js)54
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.js (renamed from server/sonar-web/src/main/js/apps/quality-profiles/profiles.js)36
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.js40
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.js89
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/controller.js128
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.js52
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileEvolution.js61
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.js64
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.js183
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.js126
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.js77
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js148
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.js237
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.js54
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProgressBar.js54
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/Evolution.js40
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.js82
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.js123
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.js71
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.js41
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.js114
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.js134
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.js96
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js132
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/layout.js47
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/profile-changelog-view.js55
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/profile-comparison-view.js65
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/profile-details-view.js181
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/profile-header-view.js91
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/profile-view.js60
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/profile.js136
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/profiles-view.js84
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/propTypes.js45
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/restore-built-in-profiles-view.js79
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/styles.css149
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profile-changelog.hbs66
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profile-comparison.hbs89
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-actions.hbs40
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-change-projects.hbs12
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-create-profile.hbs6
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-delete-profile.hbs4
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-empty.hbs1
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-intro.hbs4
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-layout.hbs11
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile-details.hbs134
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile-header.hbs19
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile.hbs21
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profiles-language.hbs1
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profiles.hbs1
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-restore-built-in-profiles-success.hbs2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-restore-built-in-profiles.hbs12
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/utils.js61
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeParentView.js (renamed from server/sonar-web/src/main/js/apps/quality-profiles/change-profile-parent-view.js)53
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeProjectsView.js70
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/views/CopyProfileView.js (renamed from server/sonar-web/src/main/js/apps/quality-profiles/copy-profile-view.js)45
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/views/CreateProfileView.js (renamed from server/sonar-web/src/main/js/apps/quality-profiles/create-profile-view.js)15
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/views/DeleteProfileView.js (renamed from server/sonar-web/src/main/js/apps/quality-profiles/delete-profile-view.js)39
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/views/RenameProfileView.js (renamed from server/sonar-web/src/main/js/apps/quality-profiles/rename-profile-view.js)41
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreBuiltInProfilesView.js56
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreProfileView.js (renamed from server/sonar-web/src/main/js/apps/quality-profiles/restore-profile-view.js)23
83 files changed, 4021 insertions, 1702 deletions
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/utils-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/utils-test.js
new file mode 100644
index 00000000000..97811993e43
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/utils-test.js
@@ -0,0 +1,74 @@
+/*
+ * 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 { expect } from 'chai';
+import { sortProfiles } from '../utils';
+
+function createProfile (key, parentKey) {
+ return { name: key, key, parentKey };
+}
+
+function checkOrder (list, order) {
+ const listKeys = list.map(item => item.key);
+ expect(listKeys).to.deep.equal(order);
+}
+
+describe('Quality Profiles :: Utils', () => {
+ describe('#sortProfiles', () => {
+ it('should sort when no parents', () => {
+ const profile1 = createProfile('profile1');
+ const profile2 = createProfile('profile2');
+ const profile3 = createProfile('profile3');
+ checkOrder(
+ sortProfiles([profile1, profile2, profile3]),
+ ['profile1', 'profile2', 'profile3']
+ );
+ });
+
+ it('should sort by name', () => {
+ const profile1 = createProfile('profile1');
+ const profile2 = createProfile('profile2');
+ const profile3 = createProfile('profile3');
+ checkOrder(
+ sortProfiles([profile3, profile1, profile2]),
+ ['profile1', 'profile2', 'profile3']
+ );
+ });
+
+ it('should sort with children', () => {
+ const child1 = createProfile('child1', 'parent');
+ const child2 = createProfile('child2', 'parent');
+ const parent = createProfile('parent');
+ checkOrder(
+ sortProfiles([child1, child2, parent]),
+ ['parent', 'child1', 'child2']
+ );
+ });
+
+ it('should sort single branch', () => {
+ const profile1 = createProfile('profile1');
+ const profile2 = createProfile('profile2', 'profile3');
+ const profile3 = createProfile('profile3', 'profile1');
+ checkOrder(
+ sortProfiles([profile3, profile2, profile1]),
+ ['profile1', 'profile3', 'profile2']
+ );
+ });
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/actions-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/actions-view.js
deleted file mode 100644
index ae5c603d3cb..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/actions-view.js
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import _ from 'underscore';
-import Marionette from 'backbone.marionette';
-import CreateProfileView from './create-profile-view';
-import RestoreProfileView from './restore-profile-view';
-import RestoreBuiltInProfilesView from './restore-built-in-profiles-view';
-import Template from './templates/quality-profiles-actions.hbs';
-
-export default Marionette.ItemView.extend({
- template: Template,
-
- events: {
- 'click #quality-profiles-create': 'onCreateClick',
- 'click #quality-profiles-restore': 'onRestoreClick',
- 'click #quality-profiles-restore-built-in': 'onRestoreBuiltInClick',
-
- 'click .js-filter-by-language': 'onLanguageClick'
- },
-
- onCreateClick (e) {
- e.preventDefault();
- this.create();
- },
-
- onRestoreClick (e) {
- e.preventDefault();
- this.restore();
- },
-
- onRestoreBuiltInClick (e) {
- e.preventDefault();
- this.restoreBuiltIn();
- },
-
- onLanguageClick (e) {
- e.preventDefault();
- const language = $(e.currentTarget).data('language');
- this.filterByLanguage(language);
- },
-
- create () {
- const that = this;
- this.requestImporters().done(function () {
- new CreateProfileView({
- collection: that.collection,
- languages: that.languages,
- importers: that.importers
- }).render();
- });
- },
-
- restore () {
- new RestoreProfileView({
- collection: this.collection
- }).render();
- },
-
- restoreBuiltIn () {
- new RestoreBuiltInProfilesView({
- collection: this.collection,
- languages: this.languages
- }).render();
- },
-
- requestLanguages () {
- const that = this;
- const url = window.baseUrl + '/api/languages/list';
- return $.get(url).done(function (r) {
- that.languages = r.languages;
- });
- },
-
- requestImporters () {
- const that = this;
- const url = window.baseUrl + '/api/qualityprofiles/importers';
- return $.get(url).done(function (r) {
- that.importers = r.importers;
- });
- },
-
- filterByLanguage (language) {
- this.selectedLanguage = _.findWhere(this.languages, { key: language });
- this.render();
- this.collection.trigger('filter', language);
- },
-
- serializeData () {
- return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
- canWrite: this.options.canWrite,
- languages: this.languages,
- selectedLanguage: this.selectedLanguage
- });
- }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/app.js b/server/sonar-web/src/main/js/apps/quality-profiles/app.js
index 74a8511776c..cb998c8edfc 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/app.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/app.js
@@ -17,66 +17,43 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import $ from 'jquery';
-import Backbone from 'backbone';
-import Marionette from 'backbone.marionette';
-import Router from './router';
-import Controller from './controller';
-import Layout from './layout';
-import Profiles from './profiles';
-import ActionsView from './actions-view';
-import ProfilesView from './profiles-view';
-
-const App = new Marionette.Application();
-const requestUser = $.get(window.baseUrl + '/api/users/current').done(function (r) {
- App.canWrite = r.permissions.global.indexOf('profileadmin') !== -1;
-});
-const requestExporters = $.get(window.baseUrl + '/api/qualityprofiles/exporters').done(function (r) {
- App.exporters = r.exporters;
-});
-const init = function () {
- const options = window.sonarqube;
-
- // Layout
- this.layout = new Layout({ el: options.el });
- this.layout.render();
- $('#footer').addClass('search-navigator-footer');
-
- // Profiles List
- this.profiles = new Profiles();
-
- // Controller
- this.controller = new Controller({ app: this });
-
- // Actions View
- this.actionsView = new ActionsView({
- collection: this.profiles,
- canWrite: this.canWrite
- });
- this.actionsView.requestLanguages().done(function () {
- App.layout.actionsRegion.show(App.actionsView);
+import React from 'react';
+import { render } from 'react-dom';
+import {
+ Router,
+ Route,
+ IndexRoute,
+ Redirect,
+ useRouterHistory
+} from 'react-router';
+import { createHistory } from 'history';
+import App from './components/App';
+import ProfileContainer from './components/ProfileContainer';
+import HomeContainer from './home/HomeContainer';
+import ProfileDetails from './details/ProfileDetails';
+import ChangelogContainer from './changelog/ChangelogContainer';
+import ComparisonContainer from './compare/ComparisonContainer';
+
+window.sonarqube.appStarted.then(options => {
+ const el = document.querySelector(options.el);
+
+ const history = useRouterHistory(createHistory)({
+ basename: window.baseUrl + '/profiles'
});
- // Profiles View
- this.profilesView = new ProfilesView({
- collection: this.profiles,
- canWrite: this.canWrite
- });
- this.layout.resultsRegion.show(this.profilesView);
-
- // Router
- this.router = new Router({ app: this });
- Backbone.history.start({
- pushState: true,
- root: options.urlRoot
- });
-};
-
-App.on('start', function () {
- $.when(requestUser, requestExporters).done(function () {
- init.call(App);
- });
+ render((
+ <Router history={history}>
+ <Route path="/" component={App}>
+ <Redirect from="/index" to="/"/>
+
+ <IndexRoute component={HomeContainer}/>
+
+ <Route component={ProfileContainer}>
+ <Route path="show" component={ProfileDetails}/>
+ <Route path="changelog" component={ChangelogContainer}/>
+ <Route path="compare" component={ComparisonContainer}/>
+ </Route>
+ </Route>
+ </Router>
+ ), el);
});
-
-window.sonarqube.appStarted.then(options => App.start(options));
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.js
new file mode 100644
index 00000000000..b7c8184d488
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/Changelog.js
@@ -0,0 +1,81 @@
+/*
+ * 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 moment from 'moment';
+import ChangesList from './ChangesList';
+import { translate } from '../../../helpers/l10n';
+import { getRulesUrl } from '../../../helpers/urls';
+
+export default class Changelog extends React.Component {
+ static propTypes = {
+ events: React.PropTypes.array.isRequired
+ };
+
+ render () {
+ const rows = this.props.events.map((event, index) => (
+ <tr key={index} className="js-profile-changelog-event">
+ <td className="thin nowrap">
+ {moment(event.date).format('LLL')}
+ </td>
+
+ <td className="thin nowrap">
+ {event.authorName ? (
+ <span>{event.authorName}</span>
+ ) : (
+ <span className="note">System</span>
+ )}
+ </td>
+
+ <td className="thin nowrap">
+ {translate('quality_profiles.changelog', event.action)}
+ </td>
+
+ <td style={{ lineHeight: '1.5' }}>
+ <a href={getRulesUrl({ 'rule_key': event.ruleKey })}>
+ {event.ruleName}
+ </a>
+ </td>
+
+ <td className="thin nowrap">
+ <ChangesList changes={event.params}/>
+ </td>
+ </tr>
+ ));
+
+ return (
+ <table className="data zebra zebra-hover">
+ <thead>
+ <tr>
+ <th className="thin nowrap">
+ {translate('date')}
+ {' '}
+ <i className="icon-sort-desc"/>
+ </th>
+ <th className="thin nowrap">{translate('user')}</th>
+ <th className="thin nowrap">{translate('action')}</th>
+ <th>{translate('rule')}</th>
+ <th className="thin nowrap">{translate('parameters')}</th>
+ </tr>
+ </thead>
+ <tbody>{rows}</tbody>
+ </table>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.js
new file mode 100644
index 00000000000..247b3a6f402
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogContainer.js
@@ -0,0 +1,128 @@
+/*
+ * 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 Changelog from './Changelog';
+import ChangelogSearch from './ChangelogSearch';
+import ChangelogEmpty from './ChangelogEmpty';
+import { getProfileChangelog } from '../../../api/quality-profiles';
+import { ProfileType } from '../propTypes';
+
+export default class ChangelogContainer extends React.Component {
+ static propTypes = {
+ location: React.PropTypes.object.isRequired,
+ profile: ProfileType
+ };
+
+ static contextTypes = {
+ router: React.PropTypes.object
+ };
+
+ state = {
+ loading: true
+ };
+
+ componentWillMount () {
+ this.handleFromDateChange = this.handleFromDateChange.bind(this);
+ this.handleToDateChange = this.handleToDateChange.bind(this);
+ this.handleReset = this.handleReset.bind(this);
+ }
+
+ componentDidMount () {
+ this.mounted = true;
+ this.loadChangelog();
+ }
+
+ componentDidUpdate (prevProps) {
+ if (prevProps.location !== this.props.location) {
+ this.loadChangelog();
+ }
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ loadChangelog () {
+ this.setState({ loading: true });
+ const { query } = this.props.location;
+ const data = { profileKey: this.props.profile.key };
+ if (query.since) {
+ data.since = query.since;
+ }
+ if (query.to) {
+ data.to = query.to;
+ }
+
+ getProfileChangelog(data).then(r => {
+ if (this.mounted) {
+ this.setState({
+ events: r.events,
+ total: r.total,
+ page: r.p,
+ loading: false
+ });
+ }
+ });
+ }
+
+ handleFromDateChange (fromDate) {
+ const query = { ...this.props.location.query, since: fromDate };
+ this.context.router.push({ pathname: '/changelog', query });
+ }
+
+ handleToDateChange (toDate) {
+ const query = { ...this.props.location.query, to: toDate };
+ this.context.router.push({ pathname: '/changelog', query });
+ }
+
+ handleReset () {
+ const query = { key: this.props.profile.key };
+ this.context.router.push({ pathname: '/changelog', query });
+ }
+
+ render () {
+ const { query } = this.props.location;
+
+ return (
+ <div className="quality-profile-box js-profile-changelog">
+ <header className="spacer-bottom">
+ <ChangelogSearch
+ fromDate={query.since}
+ toDate={query.to}
+ onFromDateChange={this.handleFromDateChange}
+ onToDateChange={this.handleToDateChange}
+ onReset={this.handleReset}/>
+
+ {this.state.loading && (
+ <i className="spinner spacer-left"/>
+ )}
+ </header>
+
+ {this.state.events != null && this.state.events.length === 0 && (
+ <ChangelogEmpty/>
+ )}
+
+ {this.state.events != null && this.state.events.length > 0 && (
+ <Changelog events={this.state.events}/>
+ )}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profiles-empty-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogEmpty.js
index 9dbd11e1ea6..797e551454b 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/profiles-empty-view.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogEmpty.js
@@ -17,11 +17,15 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import Marionette from 'backbone.marionette';
-import Template from './templates/quality-profiles-empty.hbs';
-
-export default Marionette.ItemView.extend({
- className: 'list-group-item',
- template: Template
-});
+import React from 'react';
+import { translate } from '../../../helpers/l10n';
+export default class ChangelogEmpty extends React.Component {
+ render () {
+ return (
+ <div className="big-spacer-top">
+ {translate('no_results')}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogSearch.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogSearch.js
new file mode 100644
index 00000000000..1093f7fe3ac
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangelogSearch.js
@@ -0,0 +1,62 @@
+/*
+ * 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 DateInput from '../../../components/controls/DateInput';
+import { translate } from '../../../helpers/l10n';
+
+export default class ChangelogSearch extends React.Component {
+ static propTypes = {
+ fromDate: React.PropTypes.string,
+ toDate: React.PropTypes.string,
+ onFromDateChange: React.PropTypes.func.isRequired,
+ onToDateChange: React.PropTypes.func.isRequired,
+ onReset: React.PropTypes.func.isRequired
+ };
+
+ handleResetClick (e) {
+ e.preventDefault();
+ e.target.blur();
+ this.props.onReset();
+ }
+
+ render () {
+ return (
+ <div className="display-inline-block"
+ id="quality-profile-changelog-form">
+ <DateInput
+ name="since"
+ value={this.props.fromDate}
+ placeholder="From"
+ onChange={this.props.onFromDateChange}/>
+ {' — '}
+ <DateInput
+ name="to"
+ value={this.props.toDate}
+ placeholder="To"
+ onChange={this.props.onToDateChange}/>
+ <button
+ className="spacer-left"
+ onClick={this.handleResetClick.bind(this)}>
+ {translate('reset_verb')}
+ </button>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangesList.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangesList.js
new file mode 100644
index 00000000000..5a413b0a856
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ChangesList.js
@@ -0,0 +1,46 @@
+/*
+ * 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 SeverityChange from './SeverityChange';
+import ParameterChange from './ParameterChange';
+
+export default class ChangesList extends React.Component {
+ static propTypes = {
+ changes: React.PropTypes.object.isRequired
+ };
+
+ render () {
+ const { changes } = this.props;
+
+ return (
+ <ul>
+ {Object.keys(changes).map(key => (
+ <li key={key}>
+ {key === 'severity' ? (
+ <SeverityChange severity={changes[key]}/>
+ ) : (
+ <ParameterChange name={key} value={changes[key]}/>
+ )}
+ </li>
+ ))}
+ </ul>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ParameterChange.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ParameterChange.js
new file mode 100644
index 00000000000..82e685af470
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/ParameterChange.js
@@ -0,0 +1,53 @@
+/*
+ * 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 { translateWithParameters } from '../../../helpers/l10n';
+
+export default class ParameterChange extends React.Component {
+ static propTypes = {
+ name: React.PropTypes.string.isRequired,
+ value: React.PropTypes.any
+ };
+
+ render () {
+ const { name, value } = this.props;
+
+ if (value == null) {
+ return (
+ <div>
+ {translateWithParameters(
+ 'quality_profiles.changelog.parameter_reset_to_default_value',
+ name
+ )}
+ </div>
+ );
+ }
+
+ return (
+ <div>
+ {translateWithParameters(
+ 'quality_profiles.parameter_set_to',
+ name,
+ value
+ )}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js
new file mode 100644
index 00000000000..2b854311455
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.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 React from 'react';
+import SeverityHelper from '../../../components/shared/severity-helper';
+import { translate } from '../../../helpers/l10n';
+
+export default class SeverityChange extends React.Component {
+ static propTypes = {
+ severity: React.PropTypes.string.isRequired
+ };
+
+ render () {
+ return (
+ <div>
+ {translate('quality_profiles.severity_set_to')}
+ {' '}
+ <SeverityHelper severity={this.props.severity}/>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.js
new file mode 100644
index 00000000000..9a36c64207f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/Changelog-test.js
@@ -0,0 +1,84 @@
+/*
+ * 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 { expect } from 'chai';
+import { shallow } from 'enzyme';
+import React from 'react';
+import Changelog from '../Changelog';
+import ChangesList from '../ChangesList';
+
+function createEvent (overrides) {
+ return {
+ date: '2016-01-01',
+ authorName: 'John',
+ action: 'ACTIVATED',
+ ruleKey: 'squid1234',
+ ruleName: 'Do not do this',
+ params: {},
+ ...overrides
+ };
+}
+
+describe('Quality Profiles :: Changelog', () => {
+ it('should render events', () => {
+ const events = [createEvent(), createEvent()];
+ const changelog = shallow(<Changelog events={events}/>);
+ expect(changelog.find('tbody').find('tr')).to.have.length(2);
+ });
+
+ it('should render event date', () => {
+ const events = [createEvent()];
+ const changelog = shallow(<Changelog events={events}/>);
+ expect(changelog.text()).to.include('2016');
+ });
+
+ it('should render author', () => {
+ const events = [createEvent()];
+ const changelog = shallow(<Changelog events={events}/>);
+ expect(changelog.text()).to.include('John');
+ });
+
+ it('should render system author', () => {
+ const events = [createEvent({ authorName: undefined })];
+ const changelog = shallow(<Changelog events={events}/>);
+ expect(changelog.text()).to.include('System');
+ });
+
+ it('should render action', () => {
+ const events = [createEvent()];
+ const changelog = shallow(<Changelog events={events}/>);
+ expect(changelog.text()).to.include('ACTIVATED');
+ });
+
+ it('should render rule', () => {
+ const events = [createEvent()];
+ const changelog = shallow(<Changelog events={events}/>);
+ expect(changelog.text()).to.include('Do not do this');
+ expect(changelog.find('a').prop('href')).to.include('rule_key=squid1234');
+ });
+
+ it('should render ChangesList', () => {
+ const params = { severity: 'BLOCKER' };
+ const events = [createEvent({ params })];
+ const changelog = shallow(<Changelog events={events}/>);
+ const changesList = changelog.find(ChangesList);
+ expect(changesList).to.have.length(1);
+ expect(changesList.prop('changes')).to.equal(params);
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangelogSearch-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangelogSearch-test.js
new file mode 100644
index 00000000000..302273cedf4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangelogSearch-test.js
@@ -0,0 +1,68 @@
+/*
+ * 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 { expect } from 'chai';
+import { shallow } from 'enzyme';
+import sinon from 'sinon';
+import React from 'react';
+import ChangelogSearch from '../ChangelogSearch';
+import DateInput from '../../../../components/controls/DateInput';
+
+function click (element) {
+ return element.simulate('click', {
+ target: { blur () {} },
+ preventDefault () {}
+ });
+}
+
+describe('Quality Profiles :: ChangelogSearch', () => {
+ it('should render DateInput', () => {
+ const onFromDateChange = sinon.spy();
+ const onToDateChange = sinon.spy();
+ const output = shallow(
+ <ChangelogSearch
+ fromDate="2016-01-01"
+ toDate="2016-05-05"
+ onFromDateChange={onFromDateChange}
+ onToDateChange={onToDateChange}
+ onReset={sinon.spy()}/>
+ );
+ const dateInputs = output.find(DateInput);
+ expect(dateInputs).to.have.length(2);
+ expect(dateInputs.at(0).prop('value')).to.equal('2016-01-01');
+ expect(dateInputs.at(0).prop('onChange')).to.equal(onFromDateChange);
+ expect(dateInputs.at(1).prop('value')).to.equal('2016-05-05');
+ expect(dateInputs.at(1).prop('onChange')).to.equal(onToDateChange);
+ });
+
+ it('should reset', () => {
+ const onReset = sinon.spy();
+ const output = shallow(
+ <ChangelogSearch
+ fromDate="2016-01-01"
+ toDate="2016-05-05"
+ onFromDateChange={sinon.spy()}
+ onToDateChange={sinon.spy()}
+ onReset={onReset}/>
+ );
+ expect(onReset.called).to.equal(false);
+ click(output.find('button'));
+ expect(onReset.called).to.equal(true);
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangesList-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangesList-test.js
new file mode 100644
index 00000000000..86e194b22aa
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ChangesList-test.js
@@ -0,0 +1,54 @@
+/*
+ * 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 { expect } from 'chai';
+import { shallow } from 'enzyme';
+import React from 'react';
+import ChangesList from '../ChangesList';
+import SeverityChange from '../SeverityChange';
+import ParameterChange from '../ParameterChange';
+
+describe('Quality Profiles :: ChangesList', () => {
+ it('should render changes', () => {
+ const changes = { severity: 'BLOCKER', foo: 'bar' };
+ const output = shallow(
+ <ChangesList changes={changes}/>
+ );
+ expect(output.find('li')).to.have.length(2);
+ });
+
+ it('should render severity change', () => {
+ const changes = { severity: 'BLOCKER' };
+ const output = shallow(
+ <ChangesList changes={changes}/>
+ ).find(SeverityChange);
+ expect(output).to.have.length(1);
+ expect(output.prop('severity')).to.equal('BLOCKER');
+ });
+
+ it('should render parameter change', () => {
+ const changes = { foo: 'bar' };
+ const output = shallow(
+ <ChangesList changes={changes}/>
+ ).find(ParameterChange);
+ expect(output).to.have.length(1);
+ expect(output.prop('name')).to.equal('foo');
+ expect(output.prop('value')).to.equal('bar');
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ParameterChange-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ParameterChange-test.js
new file mode 100644
index 00000000000..1845b09c506
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/ParameterChange-test.js
@@ -0,0 +1,31 @@
+/*
+ * 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 { expect } from 'chai';
+import { shallow } from 'enzyme';
+import React from 'react';
+import ParameterChange from '../ParameterChange';
+
+describe('Quality Profiles :: ParameterChange', () => {
+ it('should render different messages', () => {
+ const first = shallow(<ParameterChange name="foo"/>);
+ const second = shallow(<ParameterChange name="foo" value="bar"/>);
+ expect(first.text()).to.not.be.equal(second.text());
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js
new file mode 100644
index 00000000000..3a7fcb37b29
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js
@@ -0,0 +1,34 @@
+/*
+ * 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 { expect } from 'chai';
+import { shallow } from 'enzyme';
+import React from 'react';
+import SeverityChange from '../SeverityChange';
+import SeverityHelper from '../../../../components/shared/severity-helper';
+
+describe('Quality Profiles :: SeverityChange', () => {
+ it('should render SeverityHelper', () => {
+ const output = shallow(
+ <SeverityChange severity="BLOCKER"/>
+ ).find(SeverityHelper);
+ expect(output).to.have.length(1);
+ expect(output.prop('severity')).to.equal('BLOCKER');
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.js
new file mode 100644
index 00000000000..03d5d145212
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonContainer.js
@@ -0,0 +1,122 @@
+/*
+ * 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 ComparisonForm from './ComparisonForm';
+import ComparisonResults from './ComparisonResults';
+import { ProfileType, ProfilesListType } from '../propTypes';
+import { compareProfiles } from '../../../api/quality-profiles';
+
+export default class ComparisonContainer extends React.Component {
+ static propTypes = {
+ profile: ProfileType,
+ profiles: ProfilesListType
+ };
+
+ static contextTypes = {
+ router: React.PropTypes.object
+ };
+
+ state = {
+ loading: false
+ };
+
+ componentWillMount () {
+ this.handleCompare = this.handleCompare.bind(this);
+ }
+
+ componentDidMount () {
+ this.mounted = true;
+ this.loadResults();
+ }
+
+ componentDidUpdate (prevProps) {
+ if (prevProps.profile !== this.props.profile ||
+ prevProps.location !== this.props.location) {
+ this.loadResults();
+ }
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ loadResults () {
+ const { withKey } = this.props.location.query;
+ if (!withKey) {
+ this.setState({ left: null, loading: false });
+ return;
+ }
+
+ this.setState({ loading: true });
+ compareProfiles(this.props.profile.key, withKey).then(r => {
+ if (this.mounted) {
+ this.setState({
+ left: r.left,
+ right: r.right,
+ inLeft: r.inLeft,
+ inRight: r.inRight,
+ modified: r.modified,
+ loading: false
+ });
+ }
+ });
+ }
+
+ handleCompare (withKey) {
+ this.context.router.push({
+ pathname: '/compare',
+ query: {
+ key: this.props.profile.key,
+ withKey
+ }
+ });
+ }
+
+ render () {
+ const { profile, profiles, location } = this.props;
+ const { withKey } = location.query;
+ const { left, right, inLeft, inRight, modified } = this.state;
+
+ return (
+ <div className="quality-profile-box js-profile-comparison">
+ <header className="spacer-bottom">
+ <ComparisonForm
+ withKey={withKey}
+ profile={profile}
+ profiles={profiles}
+ onCompare={this.handleCompare}/>
+
+ {this.state.loading && (
+ <i className="spinner spacer-left"/>
+ )}
+ </header>
+
+ {left != null && (
+ <ComparisonResults
+ left={left}
+ right={right}
+ inLeft={inLeft}
+ inRight={inRight}
+ modified={modified}/>
+ )}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/intro-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonEmpty.js
index 924a99ef640..d3e49aedf26 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/intro-view.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonEmpty.js
@@ -17,10 +17,15 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import Marionette from 'backbone.marionette';
-import Template from './templates/quality-profiles-intro.hbs';
-
-export default Marionette.ItemView.extend({
- template: Template
-});
+import React from 'react';
+import { translate } from '../../../helpers/l10n';
+export default class ComparisonEmpty extends React.Component {
+ render () {
+ return (
+ <div className="big-spacer-top">
+ {translate('quality_profile.empty_comparison')}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonForm.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonForm.js
new file mode 100644
index 00000000000..149a8092066
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonForm.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 React from 'react';
+import Select from 'react-select';
+import { ProfileType, ProfilesListType } from '../propTypes';
+import { translate } from '../../../helpers/l10n';
+
+export default class ComparisonForm extends React.Component {
+ static propTypes = {
+ profile: ProfileType.isRequired,
+ profiles: ProfilesListType.isRequired,
+ onCompare: React.PropTypes.func.isRequired
+ };
+
+ handleChange (option) {
+ this.props.onCompare(option.value);
+ }
+
+ render () {
+ const { profile, profiles, withKey } = this.props;
+ const options = profiles
+ .filter(p => p.language === profile.language && p !== profile)
+ .map(p => ({ value: p.key, label: p.name }));
+
+ return (
+ <div className="display-inline-block">
+ <label className="spacer-right">
+ {translate('quality_profiles.compare_with')}
+ </label>
+ <Select
+ value={withKey}
+ options={options}
+ clearable={false}
+ className="input-large"
+ onChange={this.handleChange.bind(this)}/>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js
new file mode 100644
index 00000000000..31bed46d757
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js
@@ -0,0 +1,176 @@
+/*
+ * 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 ComparisonEmpty from './ComparisonEmpty';
+import SeverityIcon from '../../../components/shared/severity-icon';
+import { translateWithParameters } from '../../../helpers/l10n';
+import { getRulesUrl } from '../../../helpers/urls';
+
+export default class ComparisonResults extends React.Component {
+ static propTypes = {
+ left: React.PropTypes.shape({
+ name: React.PropTypes.string.isRequired
+ }).isRequired,
+ right: React.PropTypes.shape({
+ name: React.PropTypes.string.isRequired
+ }).isRequired,
+ inLeft: React.PropTypes.array.isRequired,
+ inRight: React.PropTypes.array.isRequired,
+ modified: React.PropTypes.array.isRequired
+ };
+
+ renderRule (rule, severity) {
+ return (
+ <div>
+ <SeverityIcon severity={severity}/>
+ {' '}
+ <a href={getRulesUrl({ 'rule_key': rule.key })}>
+ {rule.name}
+ </a>
+ </div>
+ );
+ }
+
+ renderParameters (params) {
+ if (!params) {
+ return null;
+ }
+ return (
+ <ul>
+ {Object.keys(params).map(key => (
+ <li key={key} className="spacer-top">
+ <code>{key}{': '}{params[key]}</code>
+ </li>
+ ))}
+ </ul>
+ );
+ }
+
+ renderLeft () {
+ if (this.props.inLeft.length === 0) {
+ return null;
+ }
+ const header = (
+ <tr key="left-header">
+ <td>
+ <h6>
+ {translateWithParameters(
+ 'quality_profiles.x_rules_only_in',
+ this.props.inLeft.length
+ )}
+ {' '}
+ {this.props.left.name}
+ </h6>
+ </td>
+ <td>&nbsp;</td>
+ </tr>
+ );
+ const rows = this.props.inLeft.map(rule => (
+ <tr key={`left-${rule.key}`} className="js-comparison-in-left">
+ <td>{this.renderRule(rule, rule.severity)}</td>
+ <td>&nbsp;</td>
+ </tr>
+ ));
+ return [header, ...rows];
+ }
+
+ renderRight () {
+ if (this.props.inRight.length === 0) {
+ return null;
+ }
+ const header = (
+ <tr key="right-header">
+ <td>&nbsp;</td>
+ <td>
+ <h6>
+ {translateWithParameters(
+ 'quality_profiles.x_rules_only_in',
+ this.props.inRight.length
+ )}
+ {' '}
+ {this.props.right.name}
+ </h6>
+ </td>
+ </tr>
+ );
+ const rows = this.props.inRight.map(rule => (
+ <tr key={`right-${rule.key}`}
+ className="js-comparison-in-right">
+ <td>&nbsp;</td>
+ <td>{this.renderRule(rule, rule.severity)}</td>
+ </tr>
+ ));
+ return [header, ...rows];
+ }
+
+ renderModified () {
+ if (this.props.modified.length === 0) {
+ return null;
+ }
+ const header = (
+ <tr key="modified-header">
+ <td colSpan="2" className="text-center">
+ <h6>
+ {translateWithParameters(
+ 'quality_profiles.x_rules_have_different_configuration',
+ this.props.modified.length
+ )}
+ </h6>
+ </td>
+ </tr>
+ );
+ const secondHeader = (
+ <tr key="modified-second-header">
+ <td><h6>{this.props.left.name}</h6></td>
+ <td><h6>{this.props.right.name}</h6></td>
+ </tr>
+ );
+ const rows = this.props.modified.map(rule => (
+ <tr key={`modified-${rule.key}`}
+ className="js-comparison-modified">
+ <td>
+ {this.renderRule(rule, rule.left.severity)}
+ {this.renderParameters(rule.left.params)}
+ </td>
+ <td>
+ {this.renderRule(rule, rule.right.severity)}
+ {this.renderParameters(rule.right.params)}
+ </td>
+ </tr>
+ ));
+ return [header, secondHeader, ...rows];
+ }
+
+ render () {
+ if (!this.props.inLeft.length && !this.props.inRight.length && !this.props.modified.length) {
+ return <ComparisonEmpty/>;
+ }
+
+ return (
+ <table className="data zebra quality-profile-comparison-table">
+ <tbody>
+ {this.renderLeft()}
+ {this.renderRight()}
+ {this.renderModified()}
+ </tbody>
+ </table>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonForm-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonForm-test.js
new file mode 100644
index 00000000000..42822331754
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonForm-test.js
@@ -0,0 +1,49 @@
+/*
+ * 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 { expect } from 'chai';
+import { shallow } from 'enzyme';
+import React from 'react';
+import Select from 'react-select';
+import ComparisonForm from '../ComparisonForm';
+import { createFakeProfile } from '../../utils';
+
+describe('Quality Profiles :: ComparisonForm', () => {
+ it('should render Select with right options', () => {
+ const profile = createFakeProfile();
+ const profiles = [
+ profile,
+ createFakeProfile({ key: 'another', name: 'another name' }),
+ createFakeProfile({ key: 'java', name: 'java', language: 'java' })
+ ];
+
+ const output = shallow(
+ <ComparisonForm
+ withKey="another"
+ profile={profile}
+ profiles={profiles}
+ onCompare={() => true}/>
+ ).find(Select);
+ expect(output).to.have.length(1);
+ expect(output.prop('value')).to.equal('another');
+ expect(output.prop('options')).to.deep.equal([
+ { value: 'another', label: 'another name' }
+ ]);
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js
new file mode 100644
index 00000000000..0482bcb5a58
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js
@@ -0,0 +1,98 @@
+/*
+ * 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 { expect } from 'chai';
+import { shallow } from 'enzyme';
+import React from 'react';
+import ComparisonResults from '../ComparisonResults';
+import ComparisonEmpty from '../ComparisonEmpty';
+import SeverityIcon from '../../../../components/shared/severity-icon';
+
+describe('Quality Profiles :: ComparisonResults', () => {
+ it('should render ComparisonEmpty', () => {
+ const output = shallow(
+ <ComparisonResults
+ left={{ name: 'left' }}
+ right={{ name: 'right' }}
+ inLeft={[]}
+ inRight={[]}
+ modified={[]}/>
+ );
+ expect(output.is(ComparisonEmpty)).to.equal(true);
+ });
+
+ it('should compare', () => {
+ const inLeft = [
+ { key: 'rule1', name: 'rule1', severity: 'BLOCKER' }
+ ];
+ const inRight = [
+ { key: 'rule2', name: 'rule2', severity: 'CRITICAL' },
+ { key: 'rule3', name: 'rule3', severity: 'MAJOR' }
+ ];
+ const modified = [
+ {
+ key: 'rule4',
+ name: 'rule4',
+ left: {
+ severity: 'BLOCKER',
+ params: { foo: 'bar' }
+ },
+ right: {
+ severity: 'INFO',
+ params: { foo: 'qwe' }
+ }
+ }
+ ];
+
+ const output = shallow(
+ <ComparisonResults
+ left={{ name: 'left' }}
+ right={{ name: 'right' }}
+ inLeft={inLeft}
+ inRight={inRight}
+ modified={modified}/>
+ );
+
+ const leftDiffs = output.find('.js-comparison-in-left');
+ expect(leftDiffs).to.have.length(1);
+ expect(leftDiffs.find('a')).to.have.length(1);
+ expect(leftDiffs.find('a').prop('href')).to.include('rule_key=rule1');
+ expect(leftDiffs.find('a').text()).to.include('rule1');
+ expect(leftDiffs.find(SeverityIcon)).to.have.length(1);
+ expect(leftDiffs.find(SeverityIcon).prop('severity')).to.equal('BLOCKER');
+
+ const rightDiffs = output.find('.js-comparison-in-right');
+ expect(rightDiffs).to.have.length(2);
+ expect(rightDiffs.at(0).find('a')).to.have.length(1);
+ expect(rightDiffs.at(0).find('a').prop('href'))
+ .to.include('rule_key=rule2');
+ expect(rightDiffs.at(0).find('a').text()).to.include('rule2');
+ expect(rightDiffs.at(0).find(SeverityIcon)).to.have.length(1);
+ expect(rightDiffs.at(0).find(SeverityIcon).prop('severity'))
+ .to.equal('CRITICAL');
+
+ const modifiedDiffs = output.find('.js-comparison-modified');
+ expect(modifiedDiffs).to.have.length(1);
+ expect(modifiedDiffs.find('a').at(0).prop('href')).to.include('rule_key=rule4');
+ expect(modifiedDiffs.find('a').at(0).text()).to.include('rule4');
+ expect(modifiedDiffs.find(SeverityIcon)).to.have.length(2);
+ expect(modifiedDiffs.text()).to.include('bar');
+ expect(modifiedDiffs.text()).to.include('qwe');
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/App.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/App.js
new file mode 100644
index 00000000000..30d59329a05
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/App.js
@@ -0,0 +1,97 @@
+/*
+ * 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 { getLanguages } from '../../../api/languages';
+import {
+ getQualityProfiles,
+ getExporters
+} from '../../../api/quality-profiles';
+import { getCurrentUser } from '../../../api/users';
+import '../styles.css';
+import { sortProfiles } from '../utils';
+
+export default class App extends React.Component {
+ state = { loading: true };
+
+ componentWillMount () {
+ this.updateProfiles = this.updateProfiles.bind(this);
+ }
+
+ componentDidMount () {
+ this.mounted = true;
+ this.loadData();
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ loadData () {
+ this.setState({ loading: true });
+ Promise.all([
+ getCurrentUser(),
+ getLanguages(),
+ getExporters(),
+ getQualityProfiles()
+ ]).then(responses => {
+ if (this.mounted) {
+ const [user, languages, exporters, profiles] = responses;
+ const canAdmin = user.permissions.global.includes('profileadmin');
+ this.setState({
+ languages,
+ exporters,
+ canAdmin,
+ profiles: sortProfiles(profiles),
+ loading: false
+ });
+ }
+ });
+ }
+
+ updateProfiles () {
+ return getQualityProfiles().then(profiles => {
+ if (this.mounted) {
+ this.setState({ profiles: sortProfiles(profiles) });
+ }
+ });
+ }
+
+ renderChild () {
+ if (this.state.loading) {
+ return <i className="spinner"/>;
+ }
+
+ return React.cloneElement(this.props.children, {
+ profiles: this.state.profiles,
+ languages: this.state.languages,
+ exporters: this.state.exporters,
+ canAdmin: this.state.canAdmin,
+ updateProfiles: this.updateProfiles
+ });
+ }
+
+ render () {
+ return (
+ <div className="page page-limited-small">
+ {this.renderChild()}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.js
new file mode 100644
index 00000000000..d1fe79cfcbe
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.js
@@ -0,0 +1,72 @@
+/*
+ * 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 Helmet from 'react-helmet';
+import ProfileNotFound from './ProfileNotFound';
+import ProfileHeader from '../details/ProfileHeader';
+import { translate } from '../../../helpers/l10n';
+import { ProfilesListType } from '../propTypes';
+
+export default class ProfileContainer extends React.Component {
+ static propTypes = {
+ location: React.PropTypes.object,
+ profiles: ProfilesListType,
+ canAdmin: React.PropTypes.bool,
+ updateProfiles: React.PropTypes.func
+ };
+
+ componentWillMount () {
+ document.querySelector('html').classList.add('dashboard-page');
+ }
+
+ componentWillUnmount () {
+ document.querySelector('html').classList.remove('dashboard-page');
+ }
+
+ render () {
+ const { profiles, location, ...other } = this.props;
+ const { key } = location.query;
+ const profile = profiles.find(profile => profile.key === key);
+
+ if (!profile) {
+ return <ProfileNotFound/>;
+ }
+
+ const child = React.cloneElement(
+ this.props.children,
+ { profile, profiles, ...other });
+
+ const title = translate('quality_profiles.page') + ' - ' + profile.name;
+
+ return (
+ <div>
+ <Helmet
+ title={title}
+ titleTemplate="SonarQube - %s"/>
+
+ <ProfileHeader
+ profile={profile}
+ canAdmin={this.props.canAdmin}
+ updateProfiles={this.props.updateProfiles}/>
+ {child}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/router.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.js
index 388859710cf..974f2d80f96 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/router.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileDate.js
@@ -17,39 +17,33 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import Backbone from 'backbone';
+import React from 'react';
+import moment from 'moment';
+import shallowCompare from 'react-addons-shallow-compare';
+import { translate } from '../../../helpers/l10n';
-export default Backbone.Router.extend({
- routes: {
- '': 'index',
- 'index': 'index',
- 'show?key=:key': 'show',
- 'changelog*': 'changelog',
- 'compare*': 'compare'
- },
+export default class ProfileDate extends React.Component {
+ static propTypes = {
+ date: React.PropTypes.string
+ };
- initialize (options) {
- this.app = options.app;
- },
-
- index () {
- this.app.controller.index();
- },
-
- show (key) {
- this.app.controller.show(key);
- },
+ shouldComponentUpdate (nextProps, nextState) {
+ return shallowCompare(this, nextProps, nextState);
+ }
- changelog () {
- const params = window.getQueryParams();
- this.app.controller.changelog(params.key, params.since, params.to);
- },
+ render () {
+ const { date } = this.props;
- compare () {
- const params = window.getQueryParams();
- if (params.key && params.withKey) {
- this.app.controller.compare(params.key, params.withKey);
+ if (!date) {
+ return (
+ <span className="note">{translate('never')}</span>
+ );
}
- }
-});
+ return (
+ <span title={moment(date).format('LLL')} data-toggle="tooltip">
+ {moment(date).fromNow()}
+ </span>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profiles.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.js
index 5be52cc7d66..eda61f67fe0 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/profiles.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileLink.js
@@ -17,27 +17,21 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import Backbone from 'backbone';
-import Profile from './profile';
+import React from 'react';
+import { Link } from 'react-router';
-export default Backbone.Collection.extend({
- model: Profile,
- url: window.baseUrl + '/api/qualityprofiles/search',
- comparator: 'key',
+export default class ProfileLink extends React.Component {
+ static propTypes = {
+ profileKey: React.PropTypes.string.isRequired
+ };
- parse (r) {
- return r.profiles;
- },
-
- updateForLanguage (language) {
- this.fetch({
- data: {
- language
- },
- merge: true,
- reset: false,
- remove: false
- });
+ render () {
+ const { profileKey, children, ...other } = this.props;
+ const query = { key: profileKey };
+ return (
+ <Link to={{ pathname: '/show', query }} {...other}>
+ {children}
+ </Link>
+ );
}
-});
-
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.js
new file mode 100644
index 00000000000..9c37677df44
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileNotFound.js
@@ -0,0 +1,40 @@
+/*
+ * 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 { IndexLink } from 'react-router';
+import { translate } from '../../../helpers/l10n';
+
+export default class ProfileNotFound extends React.Component {
+ render () {
+ return (
+ <div className="quality-profile-not-found">
+ <div className="note spacer-bottom">
+ <IndexLink to="/" className="text-muted">
+ {translate('quality_profiles.page')}
+ </IndexLink>
+ </div>
+
+ <div>
+ {translate('quality_profiles.not_found')}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.js
new file mode 100644
index 00000000000..87dacc78d87
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.js
@@ -0,0 +1,89 @@
+/*
+ * 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 { expect } from 'chai';
+import { shallow } from 'enzyme';
+import sinon from 'sinon';
+import React from 'react';
+import Helmet from 'react-helmet';
+import ProfileContainer from '../ProfileContainer';
+import ProfileNotFound from '../ProfileNotFound';
+import ProfileHeader from '../../details/ProfileHeader';
+import { createFakeProfile } from '../../utils';
+
+describe('Quality Profiles :: ProfileContainer', () => {
+ it('should render ProfileHeader', () => {
+ const targetProfile = createFakeProfile({ key: 'profile1' });
+ const profiles = [
+ targetProfile,
+ createFakeProfile({ key: 'profile2' })
+ ];
+ const updateProfiles = sinon.spy();
+ const output = shallow(
+ <ProfileContainer
+ location={{ query: { key: 'profile1' } }}
+ profiles={profiles}
+ canAdmin={false}
+ updateProfiles={updateProfiles}>
+ <div/>
+ </ProfileContainer>
+ );
+ const header = output.find(ProfileHeader);
+ expect(header).to.have.length(1);
+ expect(header.prop('profile')).to.equal(targetProfile);
+ expect(header.prop('canAdmin')).to.equal(false);
+ expect(header.prop('updateProfiles')).to.equal(updateProfiles);
+ });
+
+ it('should render ProfileNotFound', () => {
+ const profiles = [
+ createFakeProfile({ key: 'profile1' }),
+ createFakeProfile({ key: 'profile2' })
+ ];
+ const output = shallow(
+ <ProfileContainer
+ location={{ query: { key: 'random' } }}
+ profiles={profiles}
+ canAdmin={false}
+ updateProfiles={() => true}>
+ <div/>
+ </ProfileContainer>
+ );
+ expect(output.is(ProfileNotFound)).to.equal(true);
+ });
+
+ it('should render Helmet', () => {
+ const profiles = [
+ createFakeProfile({ key: 'profile1', name: 'First Profile' })
+ ];
+ const updateProfiles = sinon.spy();
+ const output = shallow(
+ <ProfileContainer
+ location={{ query: { key: 'profile1' } }}
+ profiles={profiles}
+ canAdmin={false}
+ updateProfiles={updateProfiles}>
+ <div/>
+ </ProfileContainer>
+ );
+ const helmet = output.find(Helmet);
+ expect(helmet).to.have.length(1);
+ expect(helmet.prop('title')).to.include('First Profile');
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/controller.js b/server/sonar-web/src/main/js/apps/quality-profiles/controller.js
deleted file mode 100644
index e3303e5d227..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/controller.js
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import _ from 'underscore';
-import Marionette from 'backbone.marionette';
-import ProfileHeaderView from './profile-header-view';
-import ProfileDetailsView from './profile-details-view';
-
-export default Marionette.Controller.extend({
-
- initialize () {
- this.listenTo(this.options.app.profiles, 'select', this.onProfileSelect);
- this.listenTo(this.options.app.profiles, 'setAsDefault', this.onProfileSetAsDefault);
- this.listenTo(this.options.app.profiles, 'destroy', this.onProfileDestroy);
- },
-
- index () {
- this.fetchProfiles();
- },
-
- show (key) {
- const that = this;
- this.fetchProfiles().done(function () {
- const profile = that.options.app.profiles.findWhere({ key });
- if (profile != null) {
- profile.trigger('select', profile, { trigger: false });
- }
- });
- },
-
- changelog (key, since, to) {
- const that = this;
- this.anchor = 'changelog';
- this.fetchProfiles().done(function () {
- const profile = that.options.app.profiles.findWhere({ key });
- if (profile != null) {
- profile.trigger('select', profile, { trigger: false });
- profile.fetchChangelog({ since, to });
- }
- });
- },
-
- compare (key, withKey) {
- const that = this;
- this.anchor = 'comparison';
- this.fetchProfiles().done(function () {
- const profile = that.options.app.profiles.findWhere({ key });
- if (profile != null) {
- profile.trigger('select', profile, { trigger: false });
- profile.compareWith(withKey);
- }
- });
- },
-
- onProfileSelect (profile, options) {
- const that = this;
- const key = profile.get('key');
- const route = 'show?key=' + encodeURIComponent(key);
- const opts = _.defaults(options || {}, { trigger: true });
- if (opts.trigger) {
- this.options.app.router.navigate(route);
- }
- this.options.app.profilesView.highlight(key);
- this.fetchProfile(profile).done(function () {
- const profileHeaderView = new ProfileHeaderView({
- model: profile,
- canWrite: that.options.app.canWrite
- });
- that.options.app.layout.headerRegion.show(profileHeaderView);
-
- const profileDetailsView = new ProfileDetailsView({
- model: profile,
- canWrite: that.options.app.canWrite,
- exporters: that.options.app.exporters,
- anchor: that.anchor
- });
- that.options.app.layout.detailsRegion.show(profileDetailsView);
-
- that.anchor = null;
- });
- },
-
- onProfileSetAsDefault (profile) {
- const that = this;
- const url = window.baseUrl + '/api/qualityprofiles/set_default';
- const key = profile.get('key');
- const options = { profileKey: key };
- return $.post(url, options).done(function () {
- profile.set({ isDefault: true });
- that.fetchProfiles();
- });
- },
-
- onProfileDestroy () {
- this.options.app.router.navigate('');
- this.options.app.layout.headerRegion.reset();
- this.options.app.layout.detailsRegion.reset();
- this.options.app.layout.renderIntro();
- this.options.app.profilesView.highlight(null);
- },
-
- fetchProfiles () {
- return this.options.app.profiles.fetch({ reset: true });
- },
-
- fetchProfile (profile) {
- return profile.fetch();
- }
-
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.js
new file mode 100644
index 00000000000..f08eb868c1b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.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 ProfileRules from './ProfileRules';
+import ProfileProjects from './ProfileProjects';
+import ProfileInheritance from './ProfileInheritance';
+import ProfileEvolution from './ProfileEvolution';
+import ProfileExporters from './ProfileExporters';
+import { ProfileType } from '../propTypes';
+
+export default class ProfileDetails extends React.Component {
+ static propTypes = {
+ profile: ProfileType,
+ canAdmin: React.PropTypes.bool,
+ updateProfiles: React.PropTypes.func
+ };
+
+ render () {
+ return (
+ <div>
+ <div className="quality-profile-grid">
+ <div className="quality-profile-grid-left">
+ <ProfileRules {...this.props}/>
+ <ProfileExporters {...this.props}/>
+ <ProfileEvolution {...this.props}/>
+ </div>
+ <div className="quality-profile-grid-right">
+ <ProfileInheritance {...this.props}/>
+ <ProfileProjects {...this.props}/>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileEvolution.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileEvolution.js
new file mode 100644
index 00000000000..aa643f600a6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileEvolution.js
@@ -0,0 +1,61 @@
+/*
+ * 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 moment from 'moment';
+import { translate } from '../../../helpers/l10n';
+
+export default class ProfileEvolution extends React.Component {
+ render () {
+ const { profile } = this.props;
+
+ return (
+ <div className="quality-profile-evolution">
+ <div>
+ <h6 className="little-spacer-bottom">
+ {translate('quality_profiles.list.updated')}
+ </h6>
+ {profile.userUpdatedAt ? (
+ <div>
+ {moment(profile.userUpdatedAt).format('LL')}
+ </div>
+ ) : (
+ <div className="note">
+ {translate('never')}
+ </div>
+ )}
+ </div>
+ <div>
+ <h6 className="little-spacer-bottom">
+ {translate('quality_profiles.list.used')}
+ </h6>
+ {profile.lastUsed ? (
+ <div>
+ {moment(profile.lastUsed).format('LL')}
+ </div>
+ ) : (
+ <div className="note">
+ {translate('never')}
+ </div>
+ )}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.js
new file mode 100644
index 00000000000..bb18ba75ef9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileExporters.js
@@ -0,0 +1,64 @@
+/*
+ * 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 ProfileExporters extends React.Component {
+ static propTypes = {
+ exporters: React.PropTypes.array.isRequired
+ };
+
+ getExportUrl (exporter) {
+ return window.baseUrl + '/api/qualityprofiles/export' +
+ '?exporterKey=' + encodeURIComponent(exporter.key) +
+ '&language=' + encodeURIComponent(this.props.profile.language) +
+ '&name=' + encodeURIComponent(this.props.profile.name);
+ }
+
+ render () {
+ const { exporters, profile } = this.props;
+ const exportersForLanguage = exporters.filter(e => (
+ e.languages.includes(profile.language)
+ ));
+
+ if (exportersForLanguage.length === 0) {
+ return null;
+ }
+
+ return (
+ <div className="quality-profile-box quality-profile-exporters">
+ <header className="big-spacer-bottom">
+ <h2>{translate('quality_profiles.exporters')}</h2>
+ </header>
+ <ul>
+ {exportersForLanguage.map(exporter => (
+ <li key={exporter.key}
+ data-key={exporter.key}
+ className="spacer-top">
+ <a href={this.getExportUrl(exporter)} target="_blank">
+ {exporter.name}
+ </a>
+ </li>
+ ))}
+ </ul>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.js
new file mode 100644
index 00000000000..1062f4cca72
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.js
@@ -0,0 +1,183 @@
+/*
+ * 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 { Link, IndexLink } from 'react-router';
+import ProfileLink from '../components/ProfileLink';
+import RenameProfileView from '../views/RenameProfileView';
+import CopyProfileView from '../views/CopyProfileView';
+import DeleteProfileView from '../views/DeleteProfileView';
+import { ProfileType } from '../propTypes';
+import { translate } from '../../../helpers/l10n';
+import { setDefaultProfile } from '../../../api/quality-profiles';
+
+export default class ProfileHeader extends React.Component {
+ static propTypes = {
+ profile: ProfileType.isRequired,
+ canAdmin: React.PropTypes.bool.isRequired,
+ updateProfiles: React.PropTypes.func.isRequired
+ };
+
+ static contextTypes = {
+ router: React.PropTypes.object
+ };
+
+ handleRenameClick (e) {
+ e.preventDefault();
+ new RenameProfileView({
+ profile: this.props.profile
+ }).on('done', () => {
+ this.props.updateProfiles();
+ }).render();
+ }
+
+ handleCopyClick (e) {
+ e.preventDefault();
+ new CopyProfileView({
+ profile: this.props.profile
+ }).on('done', profile => {
+ this.props.updateProfiles().then(() => {
+ this.context.router.push({
+ pathname: '/show',
+ query: { key: profile.key }
+ });
+ });
+ }).render();
+ }
+
+ handleSetDefaultClick (e) {
+ e.preventDefault();
+ setDefaultProfile(this.props.profile.key)
+ .then(this.props.updateProfiles);
+ }
+
+ handleDeleteClick (e) {
+ e.preventDefault();
+ new DeleteProfileView({
+ profile: this.props.profile
+ }).on('done', () => {
+ this.context.router.replace('/');
+ this.props.updateProfiles();
+ }).render();
+ }
+
+ render () {
+ const { profile, canAdmin } = this.props;
+
+ const backupUrl = window.baseUrl +
+ '/api/qualityprofiles/backup?profileKey=' +
+ encodeURIComponent(profile.key);
+
+ // TODO fix inline styles
+
+ return (
+ <header className="page-header quality-profile-header">
+ <div className="note spacer-bottom">
+ <IndexLink to="/" className="text-muted">
+ {translate('quality_profiles.page')}
+ </IndexLink>
+ </div>
+
+ <h1 className="page-title">
+ <ProfileLink
+ profileKey={this.props.profile.key}
+ className="link-base-color">
+ {profile.name}
+ </ProfileLink>
+ <span className="spacer-left small text-muted">
+ {this.props.profile.languageName}
+ </span>
+ </h1>
+
+ <div className="pull-right">
+ <ul className="list-inline" style={{ lineHeight: '24px' }}>
+ <li>
+ <Link
+ to={{ pathname: '/changelog', query: { key: this.props.profile.key } }}
+ className="small text-muted"
+ activeClassName="link-active">
+ {translate('changelog')}
+ </Link>
+ </li>
+ <li>
+ <div className="pull-left dropdown">
+ <button className="dropdown-toggle"
+ data-toggle="dropdown">
+ {translate('actions')}
+ {' '}
+ <i className="icon-dropdown"/>
+ </button>
+ <ul className="dropdown-menu dropdown-menu-right">
+ <li>
+ <Link
+ to={{ pathname: '/compare', query: { key: profile.key } }}
+ id="quality-profile-compare">
+ {translate('compare')}
+ </Link>
+ </li>
+ <li>
+ <a id="quality-profile-backup" href={backupUrl}>
+ {translate('backup_verb')}
+ </a>
+ </li>
+ {canAdmin && (
+ <li>
+ <a id="quality-profile-rename"
+ href="#"
+ onClick={this.handleRenameClick.bind(this)}>
+ {translate('rename')}
+ </a>
+ </li>
+ )}
+ {canAdmin && (
+ <li>
+ <a id="quality-profile-copy"
+ href="#"
+ onClick={this.handleCopyClick.bind(this)}>
+ {translate('copy')}
+ </a>
+ </li>
+ )}
+ {canAdmin && !profile.isDefault && (
+ <li>
+ <a id="quality-profile-set-as-default"
+ href="#"
+ onClick={this.handleSetDefaultClick.bind(this)}>
+ {translate('set_as_default')}
+ </a>
+ </li>
+ )}
+ {canAdmin && !profile.isDefault && (
+ <li>
+ <a id="quality-profile-delete"
+ href="#"
+ onClick={this.handleDeleteClick.bind(this)}>
+ {translate('delete')}
+ </a>
+ </li>
+ )}
+ </ul>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </header>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.js
new file mode 100644
index 00000000000..1d972030e00
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.js
@@ -0,0 +1,126 @@
+/*
+ * 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 ProfileInheritanceBox from './ProfileInheritanceBox';
+import ChangeParentView from '../views/ChangeParentView';
+import { ProfileType, ProfilesListType } from '../propTypes';
+import { translate } from '../../../helpers/l10n';
+import { getProfileInheritance } from '../../../api/quality-profiles';
+
+export default class ProfileInheritance extends React.Component {
+ static propTypes = {
+ profile: ProfileType.isRequired,
+ canAdmin: React.PropTypes.bool.isRequired
+ };
+
+ state = {
+ loading: true
+ };
+
+ componentWillMount () {
+ this.handleChangeParent = this.handleChangeParent.bind(this);
+ }
+
+ componentDidMount () {
+ this.mounted = true;
+ this.loadData();
+ }
+
+ componentDidUpdate (prevProps) {
+ if (prevProps.profile !== this.props.profile) {
+ this.loadData();
+ }
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ loadData () {
+ getProfileInheritance(this.props.profile.key).then(r => {
+ if (this.mounted) {
+ const { ancestors, children } = r;
+ this.setState({
+ children,
+ ancestors: ancestors.reverse(),
+ profile: r.profile,
+ loading: false
+ });
+ }
+ });
+ }
+
+ handleChangeParent (e) {
+ e.preventDefault();
+ new ChangeParentView({
+ profile: this.props.profile,
+ profiles: this.props.profiles
+ }).on('done', () => {
+ this.props.updateProfiles();
+ }).render();
+ }
+
+ render () {
+ return (
+ <div className="quality-profile-inheritance">
+ <header className="big-spacer-bottom clearfix">
+ <h2 className="pull-left">
+ {translate('quality_profiles.profile_inheritance')}
+ </h2>
+ {this.props.canAdmin && (
+ <button
+ className="pull-right js-change-parent"
+ onClick={this.handleChangeParent}>
+ {translate('quality_profiles.change_parent')}
+ </button>
+ )}
+ </header>
+
+ {!this.state.loading && (
+ <table className="data condensed zebra">
+ <tbody>
+ {this.state.ancestors.map((ancestor, index) => (
+ <ProfileInheritanceBox
+ key={ancestor.key}
+ profile={ancestor}
+ depth={index}
+ className="js-inheritance-ancestor"/>
+ ))}
+
+ <ProfileInheritanceBox
+ profile={this.state.profile}
+ depth={this.state.ancestors.length}
+ displayLink={false}
+ className="js-inheritance-current"/>
+
+ {this.state.children.map(child => (
+ <ProfileInheritanceBox
+ key={child.key}
+ profile={child}
+ depth={this.state.ancestors.length + 1}
+ className="js-inheritance-child"/>
+ ))}
+ </tbody>
+ </table>
+ )}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.js
new file mode 100644
index 00000000000..5cf7ee908f7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.js
@@ -0,0 +1,77 @@
+/*
+ * 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 ProfileLink from '../components/ProfileLink';
+import { translateWithParameters } from '../../../helpers/l10n';
+
+export default class ProfileInheritanceBox extends React.Component {
+ static propTypes = {
+ profile: React.PropTypes.shape({
+ key: React.PropTypes.string.isRequired,
+ name: React.PropTypes.string.isRequired,
+ activeRuleCount: React.PropTypes.number.isRequired,
+ overridingRuleCount: React.PropTypes.number
+ }).isRequired,
+ depth: React.PropTypes.number.isRequired,
+ displayLink: React.PropTypes.bool.isRequired,
+ className: React.PropTypes.string
+ };
+
+ static defaultProps = {
+ displayLink: true
+ };
+
+ render () {
+ const { profile, className } = this.props;
+ const offset = 25 * this.props.depth;
+
+ return (
+ <tr className={className}>
+ <td>
+ <h6 style={{ paddingLeft: offset }}>
+ {this.props.displayLink ? (
+ <ProfileLink profileKey={profile.key}>
+ {profile.name}
+ </ProfileLink>
+ ) : profile.name}
+ </h6>
+ </td>
+
+ <td>
+ {translateWithParameters(
+ 'quality_profile.x_active_rules',
+ profile.activeRuleCount
+ )}
+ </td>
+
+ <td>
+ {profile.overridingRuleCount != null && (
+ <p>
+ {translateWithParameters(
+ 'quality_profiles.x_overridden_rules',
+ profile.overridingRuleCount
+ )}
+ </p>
+ )}
+ </td>
+ </tr>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js
new file mode 100644
index 00000000000..99b75fea77e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js
@@ -0,0 +1,148 @@
+/*
+ * 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 ChangeProjectsView from '../views/ChangeProjectsView';
+import { ProfileType } from '../propTypes';
+import { getProfileProjects } from '../../../api/quality-profiles';
+import { translate } from '../../../helpers/l10n';
+
+export default class ProfileProjects extends React.Component {
+ static propTypes = {
+ profile: ProfileType,
+ canAdmin: React.PropTypes.bool.isRequired
+ };
+
+ state = {
+ projects: null
+ };
+
+ componentWillMount () {
+ this.loadProjects = this.loadProjects.bind(this);
+ }
+
+ componentDidMount () {
+ this.mounted = true;
+ this.loadProjects();
+ }
+
+ componentDidUpdate (prevProps) {
+ if (prevProps.profile !== this.props.profile) {
+ this.loadProjects();
+ }
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ loadProjects () {
+ if (this.props.profile.isDefault) {
+ return;
+ }
+
+ const data = { key: this.props.profile.key };
+ getProfileProjects(data).then(r => {
+ if (this.mounted) {
+ this.setState({
+ projects: r.results,
+ more: r.more,
+ loading: false
+ });
+ }
+ });
+ }
+
+ handleChange (e) {
+ e.preventDefault();
+ e.target.blur();
+ new ChangeProjectsView({
+ profile: this.props.profile,
+ loadProjects: this.loadProjects
+ }).render();
+ }
+
+ renderDefault () {
+ return (
+ <div>
+ <span className="badge spacer-right">
+ {translate('default')}
+ </span>
+ {translate('quality_profiles.projects_for_default')}
+ </div>
+ );
+ }
+
+ renderProjects () {
+ const { projects } = this.state;
+
+ if (projects == null) {
+ return null;
+ }
+
+ if (projects.length === 0) {
+ return (
+ <div>
+ {translate('quality_profiles.no_projects_associated_to_profile')}
+ </div>
+ );
+ }
+
+ return (
+ <ul>
+ {projects.map(project => (
+ <li key={project.uuid}
+ className="spacer-top js-profile-project"
+ data-key={project.key}>
+ <i className="icon-checkbox icon-checkbox-checked"/>
+ {' '}
+ {project.name}
+ </li>
+ ))}
+ </ul>
+ );
+ }
+
+ render () {
+ return (
+ <div className="quality-profile-projects">
+ <header className="page-header">
+ <h2 className="page-title">
+ {translate('projects')}
+ </h2>
+
+ {this.props.canAdmin && !this.props.profile.isDefault && (
+ <div className="pull-right">
+ <button
+ className="js-change-projects"
+ onClick={this.handleChange.bind(this)}>
+ {translate('quality_profiles.change_projects')}
+ </button>
+ </div>
+ )}
+ </header>
+
+ {this.props.profile.isDefault ?
+ this.renderDefault() :
+ this.renderProjects()
+ }
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.js
new file mode 100644
index 00000000000..539878dec9f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRules.js
@@ -0,0 +1,237 @@
+/*
+ * 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 keyBy from 'lodash/keyBy';
+import ProfileRulesRow from './ProfileRulesRow';
+import { ProfileType } from '../propTypes';
+import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
+import { searchRules, takeFacet } from '../../../api/rules';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+import {
+ getRulesUrl,
+ getDeprecatedActiveRulesUrl
+} from '../../../helpers/urls';
+
+const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
+
+export default class ProfileRules extends React.Component {
+ static propTypes = {
+ profile: ProfileType.isRequired,
+ canAdmin: React.PropTypes.bool.isRequired
+ };
+
+ state = {
+ total: null,
+ activatedTotal: null,
+ allByType: keyBy(TYPES.map(t => ({ val: t, count: null })), 'val'),
+ activatedByType: keyBy(TYPES.map(t => ({ val: t, count: null })), 'val')
+ };
+
+ componentDidMount () {
+ this.mounted = true;
+ this.loadRules();
+ }
+
+ componentDidUpdate (prevProps) {
+ if (prevProps.profile !== this.props.profile) {
+ this.loadRules();
+ }
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ loadAllRules () {
+ return searchRules({
+ languages: this.props.profile.language,
+ ps: 1,
+ facets: 'types'
+ });
+ }
+
+ loadActivatedRules () {
+ return searchRules({
+ qprofile: this.props.profile.key,
+ activation: 'true',
+ ps: 1,
+ facets: 'types'
+ });
+ }
+
+ loadRules () {
+ Promise.all([
+ this.loadAllRules(),
+ this.loadActivatedRules()
+ ]).then(responses => {
+ if (this.mounted) {
+ const [allRules, activatedRules] = responses;
+ this.setState({
+ total: allRules.total,
+ activatedTotal: activatedRules.total,
+ allByType: keyBy(takeFacet(allRules, 'types'), 'val'),
+ activatedByType: keyBy(takeFacet(activatedRules, 'types'), 'val')
+ });
+ }
+ });
+ }
+
+ getTooltip (count, total) {
+ if (count == null || total == null) {
+ return '';
+ }
+
+ return translateWithParameters(
+ 'quality_profiles.x_activated_out_of_y',
+ formatMeasure(count, 'INT'),
+ formatMeasure(total, 'INT'));
+ }
+
+ renderActiveTitle () {
+ return (
+ <strong>
+ {translate('quality_profile.total_active_rules')}
+ </strong>
+ );
+ }
+
+ renderActiveCount () {
+ const rulesUrl = getRulesUrl({
+ qprofile: this.props.profile.key,
+ activation: 'true'
+ });
+
+ if (this.state.activatedTotal == null) {
+ return null;
+ }
+
+ return (
+ <a href={rulesUrl}>
+ <strong>
+ {formatMeasure(this.state.activatedTotal, 'SHORT_INT')}
+ </strong>
+ </a>
+ );
+ }
+
+ getTooltipForType (type) {
+ const { count } = this.state.activatedByType[type];
+ const total = this.state.allByType[type].count;
+ return this.getTooltip(count, total);
+ }
+
+ renderTitleForType (type) {
+ return <span>{translate('issue.type', type, 'plural')}</span>;
+ }
+
+ renderCountForType (type) {
+ const rulesUrl = getRulesUrl({
+ qprofile: this.props.profile.key,
+ activation: 'true',
+ types: type
+ });
+
+ const { count } = this.state.activatedByType[type];
+
+ if (count == null) {
+ return null;
+ }
+
+ return (
+ <a href={rulesUrl}>
+ {formatMeasure(count, 'SHORT_INT')}
+ </a>
+ );
+ }
+
+ renderDeprecated () {
+ const { profile } = this.props;
+
+ if (profile.activeDeprecatedRuleCount === 0) {
+ return null;
+ }
+
+ const url = getDeprecatedActiveRulesUrl({ qprofile: profile.key });
+
+ return (
+ <div className="quality-profile-rules-deprecated clearfix">
+ <div className="pull-left">
+ {translate('quality_profiles.deprecated_rules')}
+ </div>
+ <div className="pull-right">
+ <a href={url}>
+ {profile.activeDeprecatedRuleCount}
+ </a>
+ </div>
+ </div>
+ );
+ }
+
+ render () {
+ const { total, activatedTotal, allByType, activatedByType } = this.state;
+
+ const activateMoreUrl = getRulesUrl({
+ qprofile: this.props.profile.key,
+ activation: 'false'
+ });
+
+ return (
+ <div className="quality-profile-rules">
+ <header className="clearfix">
+ <h2 className="pull-left">{translate('rules')}</h2>
+
+ {this.props.canAdmin && (
+ <a href={activateMoreUrl}
+ className="button pull-right js-activate-rules">
+ {translate('quality_profiles.activate_more')}
+ </a>
+ )}
+ </header>
+
+ {this.renderDeprecated()}
+
+ <TooltipsContainer options={{ delay: { show: 250, hide: 0 } }}>
+ <ul className="quality-profile-rules-distribution">
+ <li key="all" className="big-spacer-bottom">
+ <ProfileRulesRow
+ count={activatedTotal}
+ total={total}
+ tooltip={this.getTooltip(activatedTotal, total)}
+ renderTitle={this.renderActiveTitle.bind(this)}
+ renderCount={this.renderActiveCount.bind(this)}/>
+ </li>
+
+ {TYPES.map(type => (
+ <li key={type} className="spacer-top">
+ <ProfileRulesRow
+ count={activatedByType[type].count}
+ total={allByType[type].count}
+ tooltip={this.getTooltipForType(type)}
+ renderTitle={this.renderTitleForType.bind(this, type)}
+ renderCount={this.renderCountForType.bind(this, type)}/>
+ </li>
+ ))}
+ </ul>
+ </TooltipsContainer>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.js
new file mode 100644
index 00000000000..6baf9ca811e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileRulesRow.js
@@ -0,0 +1,54 @@
+/*
+ * 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 ProgressBar from './ProgressBar';
+
+export default class ProfileRulesRow extends React.Component {
+ static propTypes = {
+ count: React.PropTypes.number,
+ total: React.PropTypes.number,
+ tooltip: React.PropTypes.string,
+ renderTitle: React.PropTypes.func.isRequired,
+ renderCount: React.PropTypes.func.isRequired
+ };
+
+ render () {
+ const { total, count, tooltip, renderTitle, renderCount } = this.props;
+
+ return (
+ <div title={tooltip} data-toggle="tooltip">
+ <div className="clearfix">
+ <div className="pull-left">
+ {renderTitle()}
+ </div>
+ <div className="pull-right">
+ {renderCount()}
+ </div>
+ </div>
+ <div className="little-spacer-top" style={{ height: 2 }}>
+ <ProgressBar
+ count={count || 0}
+ total={total || 0}
+ width={300}/>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProgressBar.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProgressBar.js
new file mode 100644
index 00000000000..5efc85f7907
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProgressBar.js
@@ -0,0 +1,54 @@
+/*
+ * 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';
+
+export default class ProgressBar extends React.Component {
+ static propTypes = {
+ width: React.PropTypes.number.isRequired,
+ height: React.PropTypes.number,
+ count: React.PropTypes.number.isRequired,
+ total: React.PropTypes.number.isRequired
+ };
+
+ static defaultProps = {
+ height: 2
+ };
+
+ render () {
+ const { width, height } = this.props;
+ const p = this.props.total > 0 ? this.props.count / this.props.total : 0;
+ const fillWidth = this.props.width * p;
+
+ const commonProps = { x: 0, y: 0, rx: 2, height };
+
+ return (
+ <svg width={width} height={height}>
+ <rect
+ {...commonProps}
+ width={width}
+ fill="#e6e6e6"/>
+ <rect
+ {...commonProps}
+ width={fillWidth}
+ className="bar-chart-bar quality-profile-progress-bar"/>
+ </svg>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/Evolution.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/Evolution.js
new file mode 100644
index 00000000000..82b6f601b51
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/Evolution.js
@@ -0,0 +1,40 @@
+/*
+ * 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 EvolutionDeprecated from './EvolutionDeprecated';
+import EvolutionStagnant from './EvolutionStagnant';
+import EvolutionRules from './EvolutionRules';
+import { ProfilesListType } from '../propTypes';
+
+export default class Evolution extends React.Component {
+ static propTypes = {
+ profiles: ProfilesListType.isRequired
+ };
+
+ render () {
+ return (
+ <div className="quality-profiles-evolution">
+ <EvolutionDeprecated profiles={this.props.profiles}/>
+ <EvolutionStagnant profiles={this.props.profiles}/>
+ <EvolutionRules/>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.js
new file mode 100644
index 00000000000..fb0fd938783
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionDeprecated.js
@@ -0,0 +1,82 @@
+/*
+ * 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 ProfileLink from '../components/ProfileLink';
+import { getDeprecatedActiveRulesUrl } from '../../../helpers/urls';
+import { ProfilesListType } from '../propTypes';
+import { translateWithParameters, translate } from '../../../helpers/l10n';
+
+export default class EvolutionDeprecated extends React.Component {
+ static propTypes = {
+ profiles: ProfilesListType.isRequired
+ };
+
+ render () {
+ const profilesWithDeprecations = this.props.profiles
+ .filter(profile => profile.activeDeprecatedRuleCount > 0);
+
+ if (profilesWithDeprecations.length === 0) {
+ return null;
+ }
+
+ const totalRules = profilesWithDeprecations
+ .map(p => p.activeDeprecatedRuleCount)
+ .reduce((p, c) => p + c, 0);
+
+ return (
+ <div className="quality-profiles-evolution-deprecated">
+ <div className="spacer-bottom">
+ <strong>{translate('quality_profiles.deprecated_rules')}</strong>
+ </div>
+ <div className="spacer-bottom">
+ {translateWithParameters(
+ 'quality_profiles.x_deprecated_rules_are_still_activated',
+ totalRules,
+ profilesWithDeprecations.length
+ )}
+ </div>
+ <ul>
+ {profilesWithDeprecations.map(profile => (
+ <li key={profile.key} className="spacer-top">
+ <div className="text-ellipsis">
+ <ProfileLink
+ profileKey={profile.key}
+ className="link-no-underline">
+ {profile.name}
+ </ProfileLink>
+ </div>
+ <div className="note">
+ {profile.languageName}
+ {', '}
+ <a className="text-muted"
+ href={getDeprecatedActiveRulesUrl({ qprofile: profile.key })}>
+ {translateWithParameters(
+ 'quality_profile.x_rules',
+ profile.activeDeprecatedRuleCount
+ )}
+ </a>
+ </div>
+ </li>
+ ))}
+ </ul>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.js
new file mode 100644
index 00000000000..e2956f2c329
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionRules.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 moment from 'moment';
+import sortBy from 'lodash/sortBy';
+import { searchRules } from '../../../api/rules';
+import { translateWithParameters, translate } from '../../../helpers/l10n';
+import { getRulesUrl } from '../../../helpers/urls';
+
+const RULES_LIMIT = 3;
+
+const PERIOD_START_MOMENT = moment().subtract(1, 'month');
+
+function parseRules (r) {
+ const { rules, actives } = r;
+ return rules.map(rule => {
+ const activations = actives[rule.key];
+ return { ...rule, activations: activations ? activations.length : 0 };
+ });
+}
+
+export default class EvolutionRules extends React.Component {
+ state = {};
+
+ componentDidMount () {
+ this.mounted = true;
+ this.loadLatestRules();
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ loadLatestRules () {
+ const data = {
+ 'available_since': PERIOD_START_MOMENT.format('YYYY-MM-DD'),
+ s: 'createdAt',
+ asc: false,
+ ps: RULES_LIMIT,
+ f: 'name,langName,actives'
+ };
+
+ searchRules(data).then(r => {
+ if (this.mounted) {
+ this.setState({
+ latestRules: sortBy(parseRules(r), 'langName'),
+ latestRulesTotal: r.total
+ });
+ }
+ });
+ }
+
+ render () {
+ if (!this.state.latestRulesTotal) {
+ return null;
+ }
+
+ const newRulesUrl = getRulesUrl({
+ 'available_since': PERIOD_START_MOMENT.format('YYYY-MM-DD')
+ });
+
+ return (
+ <div className="quality-profiles-evolution-rules">
+ <div className="clearfix">
+ <strong className="pull-left">
+ {translate('quality_profiles.latest_new_rules')}
+ </strong>
+
+ {this.state.latestRulesTotal > RULES_LIMIT && (
+ <a className="pull-right small text-muted"
+ href={newRulesUrl}>
+ {translate('see_all')}
+ </a>
+ )}
+ </div>
+ <ul>
+ {this.state.latestRules.map(rule => (
+ <li key={rule.key} className="spacer-top">
+ <div className="text-ellipsis">
+ <a className="link-no-underline"
+ href={getRulesUrl({ 'rule_key': rule.key })}>
+ {' '}
+ {rule.name}
+ </a>
+ <div className="note">
+ {rule.activations ? (
+ translateWithParameters(
+ 'quality_profiles.latest_new_rules.activated',
+ rule.langName,
+ rule.activations
+ )
+ ) : (
+ translateWithParameters(
+ 'quality_profiles.latest_new_rules.not_activated',
+ rule.langName
+ )
+ )}
+ </div>
+ </div>
+ </li>
+ ))}
+ </ul>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.js
new file mode 100644
index 00000000000..8b5b1e33f38
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/EvolutionStagnant.js
@@ -0,0 +1,71 @@
+/*
+ * 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 moment from 'moment';
+import ProfileLink from '../components/ProfileLink';
+import { ProfilesListType } from '../propTypes';
+import { translate } from '../../../helpers/l10n';
+
+export default class EvolutionStagnant extends React.Component {
+ static propTypes = {
+ profiles: ProfilesListType.isRequired
+ };
+
+ render () {
+ // TODO filter built-in out
+
+ const outdated = this.props.profiles.filter(profile => (
+ moment().diff(moment(profile.rulesUpdatedAt), 'years') >= 1
+ ));
+
+ if (outdated.length === 0) {
+ return null;
+ }
+
+ return (
+ <div className="quality-profiles-evolution-stagnant">
+ <div className="spacer-bottom">
+ <strong>{translate('quality_profiles.stagnant_profiles')}</strong>
+ </div>
+ <div className="spacer-bottom">
+ {translate('quality_profiles.not_updated_more_than_year')}
+ </div>
+ <ul>
+ {outdated.map(profile => (
+ <li key={profile.key} className="spacer-top">
+ <div className="text-ellipsis">
+ <ProfileLink
+ profileKey={profile.key}
+ className="link-no-underline">
+ {profile.name}
+ </ProfileLink>
+ </div>
+ <div className="note">
+ {profile.languageName}
+ {', '}
+ updated on {moment(profile.rulesUpdatedAt).format('LL')}
+ </div>
+ </li>
+ ))}
+ </ul>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.js
new file mode 100644
index 00000000000..94db2d7dc71
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.js
@@ -0,0 +1,41 @@
+/*
+ * 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 Helmet from 'react-helmet';
+import PageHeader from './PageHeader';
+import Evolution from './Evolution';
+import ProfilesList from './ProfilesList';
+import { translate } from '../../../helpers/l10n';
+
+export default class HomeContainer extends React.Component {
+ render () {
+ return (
+ <div>
+ <Helmet
+ title={translate('quality_profiles.page')}
+ titleTemplate="SonarQube - %s"/>
+
+ <PageHeader {...this.props}/>
+ <Evolution {...this.props}/>
+ <ProfilesList {...this.props}/>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.js
new file mode 100644
index 00000000000..1fcd7aa646e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.js
@@ -0,0 +1,114 @@
+/*
+ * 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 CreateProfileView from '../views/CreateProfileView';
+import RestoreProfileView from '../views/RestoreProfileView';
+import { translate } from '../../../helpers/l10n';
+import { getImporters } from '../../../api/quality-profiles';
+
+export default class PageHeader extends React.Component {
+ static propTypes = {
+ canAdmin: React.PropTypes.bool.isRequired
+ };
+
+ static contextTypes = {
+ router: React.PropTypes.object
+ };
+
+ state = {};
+
+ componentDidMount () {
+ this.mounted = true;
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ retrieveImporters () {
+ if (this.state.importers) {
+ return Promise.resolve(this.state.importers);
+ } else {
+ return getImporters().then(importers => {
+ this.setState({ importers });
+ return importers;
+ });
+ }
+ }
+
+ handleCreateClick (e) {
+ e.preventDefault();
+ e.target.blur();
+ this.retrieveImporters().then(importers => {
+ new CreateProfileView({
+ languages: this.props.languages,
+ importers
+ }).on('done', profile => {
+ this.props.updateProfiles().then(() => {
+ this.context.router.push({
+ pathname: '/show',
+ query: { key: profile.key }
+ });
+ });
+ }).render();
+ });
+ }
+
+ handleRestoreClick (e) {
+ e.preventDefault();
+ e.target.blur();
+ new RestoreProfileView()
+ .on('done', this.props.updateProfiles)
+ .render();
+ }
+
+ render () {
+ return (
+ <header className="page-header">
+ <h1 className="page-title">
+ {translate('quality_profiles.page')}
+ </h1>
+
+ {this.props.canAdmin && (
+ <div className="page-actions button-group">
+ <button
+ id="quality-profiles-create"
+ onClick={this.handleCreateClick.bind(this)}>
+ {translate('create')}
+ </button>
+
+ <button
+ id="quality-profiles-restore"
+ className="spacer-left"
+ onClick={this.handleRestoreClick.bind(this)}>
+ {translate('quality_profiles.restore_profile')}
+ </button>
+ </div>
+ )}
+
+ <div className="page-description markdown">
+ {translate('quality_profiles.intro1')}
+ <br/>
+ {translate('quality_profiles.intro2')}
+ </div>
+ </header>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.js
new file mode 100644
index 00000000000..b4b10f4e4ae
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.js
@@ -0,0 +1,134 @@
+/*
+ * 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 { PropTypes as RouterPropTypes } from 'react-router';
+import groupBy from 'lodash/groupBy';
+import pick from 'lodash/pick';
+import ProfilesListRow from './ProfilesListRow';
+import ProfilesListHeader from './ProfilesListHeader';
+import RestoreBuiltInProfilesView from '../views/RestoreBuiltInProfilesView';
+import { ProfilesListType, LanguagesListType } from '../propTypes';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
+
+export default class ProfilesList extends React.Component {
+ static propTypes = {
+ profiles: ProfilesListType,
+ languages: LanguagesListType,
+ location: RouterPropTypes.location
+ };
+
+ handleRestoreBuiltIn (languageKey, e) {
+ e.preventDefault();
+ const language = this.props.languages.find(l => l.key === languageKey);
+ new RestoreBuiltInProfilesView({ language })
+ .on('done', this.props.updateProfiles)
+ .render();
+ }
+
+ renderProfiles (profiles) {
+ return profiles.map(profile => (
+ <ProfilesListRow key={profile.key} profile={profile}/>
+ ));
+ }
+
+ renderHeader (languageKey, profilesCount) {
+ const language = this.props.languages.find(l => l.key === languageKey);
+ return (
+ <thead>
+ <tr>
+ <th>
+ {language.name}
+ {' ('}
+ {translateWithParameters(
+ 'quality_profiles.x_profiles',
+ profilesCount
+ )}
+ {')'}
+ {this.props.canAdmin && (
+ <button
+ className="spacer-left js-restore-built-in"
+ data-language={languageKey}
+ onClick={this.handleRestoreBuiltIn.bind(this, languageKey)}>
+ {translate('quality_profiles.restore_built_in_profiles')}
+ </button>
+ )}
+ </th>
+ <th className="text-right nowrap">
+ {translate('quality_profiles.list.projects')}
+ </th>
+ <th className="text-right nowrap">
+ {translate('quality_profiles.list.rules')}
+ </th>
+ <th className="text-right nowrap">
+ {translate('quality_profiles.list.updated')}
+ </th>
+ <th className="text-right nowrap">
+ {translate('quality_profiles.list.used')}
+ </th>
+ </tr>
+ </thead>
+ );
+ }
+
+ render () {
+ const { profiles, languages } = this.props;
+ const { language } = this.props.location.query;
+
+ const profilesIndex = groupBy(profiles, profile => profile.language);
+ const profilesToShow = language ?
+ pick(profilesIndex, language) :
+ profilesIndex;
+
+ return (
+ <div>
+ <ProfilesListHeader
+ languages={languages}
+ currentFilter={language}/>
+
+ {Object.keys(profilesToShow).length === 0 && (
+ <div className="alert alert-warning">
+ {translate('no_results')}
+ </div>
+ )}
+
+ {Object.keys(profilesToShow).map(languageKey => (
+ <table
+ key={languageKey}
+ data-language={languageKey}
+ className="data zebra zebra-hover quality-profiles-table">
+
+ {this.renderHeader(
+ languageKey,
+ profilesToShow[languageKey].length)}
+
+ <TooltipsContainer>
+ <tbody>
+ {this.renderProfiles(profilesToShow[languageKey])}
+ </tbody>
+ </TooltipsContainer>
+
+ </table>
+ ))}
+
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.js
new file mode 100644
index 00000000000..532d76e5295
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.js
@@ -0,0 +1,96 @@
+/*
+ * 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 { IndexLink } from 'react-router';
+import { LanguagesListType } from '../propTypes';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+export default class ProfilesListHeader extends React.Component {
+ static propTypes = {
+ languages: LanguagesListType.isRequired,
+ currentFilter: React.PropTypes.string
+ };
+
+ renderFilterToggle () {
+ const { languages, currentFilter } = this.props;
+ const currentLanguage = currentFilter &&
+ languages.find(l => l.key === currentFilter);
+
+ const label = currentFilter ?
+ translateWithParameters(
+ 'quality_profiles.x_profiles',
+ currentLanguage.name) :
+ translate('quality_profiles.all_profiles');
+
+ return (
+ <a className="dropdown-toggle link-no-underline js-language-filter"
+ href="#"
+ data-toggle="dropdown">
+ {label} <i className="icon-dropdown"/>
+ </a>
+ );
+ }
+
+ renderFilterMenu () {
+ return (
+ <ul className="dropdown-menu">
+ <li>
+ <IndexLink to="/">
+ {translate('quality_profiles.all_profiles')}
+ </IndexLink>
+ </li>
+ {this.props.languages.map(language => (
+ <li key={language.key}>
+ <IndexLink
+ to={{ pathname: '/', query: { language: language.key } }}
+ className="js-language-filter-option"
+ data-language={language.key}>
+ {language.name}
+ </IndexLink>
+ </li>
+ ))}
+ </ul>
+ );
+ }
+
+ render () {
+ if (this.props.languages.length < 2) {
+ return null;
+ }
+
+ const { languages, currentFilter } = this.props;
+ const currentLanguage = currentFilter &&
+ languages.find(l => l.key === currentFilter);
+
+ // if unknown language, then
+ if (currentFilter && !currentLanguage) {
+ return null;
+ }
+
+ return (
+ <header className="quality-profiles-list-header clearfix">
+ <div className="dropdown">
+ {this.renderFilterToggle()}
+ {this.renderFilterMenu()}
+ </div>
+ </header>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js
new file mode 100644
index 00000000000..8a326ade01d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js
@@ -0,0 +1,132 @@
+/*
+ * 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 shallowCompare from 'react-addons-shallow-compare';
+import ProfileLink from '../components/ProfileLink';
+import ProfileDate from '../components/ProfileDate';
+import { ProfileType } from '../propTypes';
+import { translate } from '../../../helpers/l10n';
+import { getRulesUrl } from '../../../helpers/urls';
+
+export default class ProfilesListRow extends React.Component {
+ static propTypes = {
+ profile: ProfileType.isRequired
+ };
+
+ shouldComponentUpdate (nextProps, nextState) {
+ return shallowCompare(this, nextProps, nextState);
+ }
+
+ renderName () {
+ const { profile } = this.props;
+ const offset = 25 * (profile.depth - 1);
+ return (
+ <h4 style={{ paddingLeft: offset }}>
+ <ProfileLink profileKey={profile.key}>
+ {profile.name}
+ </ProfileLink>
+ </h4>
+ );
+ }
+
+ renderProjects () {
+ const { profile } = this.props;
+
+ if (profile.isDefault) {
+ return (
+ <span className="badge">
+ {translate('default')}
+ </span>
+ );
+ }
+
+ return (
+ <span>
+ {profile.projectCount}
+ </span>
+ );
+ }
+
+ renderRules () {
+ const { profile } = this.props;
+
+ const activeRulesUrl = getRulesUrl({
+ qprofile: profile.key,
+ activation: 'true'
+ });
+
+ const deprecatedRulesUrl = getRulesUrl({
+ qprofile: profile.key,
+ activation: 'true',
+ statuses: 'DEPRECATED'
+ });
+
+ return (
+ <div>
+ {profile.activeDeprecatedRuleCount > 0 && (
+ <span className="spacer-right">
+ <a className="badge badge-warning"
+ href={deprecatedRulesUrl}
+ title={translate('quality_profiles.deprecated_rules')}
+ data-toggle="tooltip">
+ {profile.activeDeprecatedRuleCount}
+ </a>
+ </span>
+ )}
+
+ <a href={activeRulesUrl}>
+ {profile.activeRuleCount}
+ </a>
+ </div>
+ );
+ }
+
+ renderUpdateDate () {
+ return <ProfileDate date={this.props.profile.userUpdatedAt}/>;
+ }
+
+ renderUsageDate () {
+ return <ProfileDate date={this.props.profile.lastUsed}/>;
+ }
+
+ render () {
+ return (
+ <tr className="quality-profiles-table-row"
+ data-key={this.props.profile.key}
+ data-name={this.props.profile.name}>
+ <td className="quality-profiles-table-name">
+ {this.renderName()}
+ </td>
+ <td className="quality-profiles-table-projects thin nowrap text-right">
+ {this.renderProjects()}
+ </td>
+ <td className="quality-profiles-table-rules thin nowrap text-right">
+ {this.renderRules()}
+ </td>
+ <td className="quality-profiles-table-date thin nowrap text-right">
+ {this.renderUpdateDate()}
+ </td>
+ <td className="quality-profiles-table-date thin nowrap text-right">
+ {this.renderUsageDate()}
+ </td>
+ </tr>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/layout.js b/server/sonar-web/src/main/js/apps/quality-profiles/layout.js
deleted file mode 100644
index 5102782a0b1..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/layout.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import Marionette from 'backbone.marionette';
-import IntroView from './intro-view';
-import Template from './templates/quality-profiles-layout.hbs';
-
-export default Marionette.LayoutView.extend({
- template: Template,
-
- regions: {
- headerRegion: '.search-navigator-workspace-header',
- actionsRegion: '.search-navigator-filters',
- resultsRegion: '.quality-profiles-results',
- detailsRegion: '.search-navigator-workspace-details'
- },
-
- onRender () {
- const navigator = this.$('.search-navigator');
- navigator.addClass('sticky search-navigator-extended-view');
- const top = navigator.offset().top;
- this.$('.search-navigator-workspace-header').css({ top });
- this.$('.search-navigator-side').css({ top }).isolatedScroll();
- this.renderIntro();
- },
-
- renderIntro () {
- this.detailsRegion.show(new IntroView());
- }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profile-changelog-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profile-changelog-view.js
deleted file mode 100644
index 25fa859bb43..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/profile-changelog-view.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import Marionette from 'backbone.marionette';
-import Template from './templates/quality-profile-changelog.hbs';
-
-export default Marionette.ItemView.extend({
- template: Template,
-
- events: {
- 'submit #quality-profile-changelog-form': 'onFormSubmit',
- 'click .js-show-more-changelog': 'onShowMoreChangelogClick',
- 'click .js-hide-changelog': 'onHideChangelogClick'
- },
-
- onFormSubmit (e) {
- e.preventDefault();
- this.model.fetchChangelog(this.getSearchParameters());
- },
-
- onShowMoreChangelogClick (e) {
- e.preventDefault();
- this.model.fetchMoreChangelog();
- },
-
- onHideChangelogClick (e) {
- e.preventDefault();
- this.model.resetChangelog();
- },
-
- getSearchParameters () {
- const form = this.$('#quality-profile-changelog-form');
- return {
- since: form.find('[name="since"]').val(),
- to: form.find('[name="to"]').val()
- };
- }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profile-comparison-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profile-comparison-view.js
deleted file mode 100644
index 79d79b7a123..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/profile-comparison-view.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import _ from 'underscore';
-import Marionette from 'backbone.marionette';
-import Template from './templates/quality-profile-comparison.hbs';
-
-export default Marionette.ItemView.extend({
- template: Template,
-
- events: {
- 'submit #quality-profile-comparison-form': 'onFormSubmit',
- 'click .js-hide-comparison': 'onHideComparisonClick'
- },
-
- onRender () {
- this.$('select').select2({
- width: '250px',
- minimumResultsForSearch: 50
- });
- },
-
- onFormSubmit (e) {
- e.preventDefault();
- const withKey = this.$('#quality-profile-comparison-with-key').val();
- this.model.compareWith(withKey);
- },
-
- onHideComparisonClick (e) {
- e.preventDefault();
- this.model.resetComparison();
- },
-
- getProfilesForComparison () {
- const profiles = this.model.collection.toJSON();
- const key = this.model.id;
- const language = this.model.get('language');
- return profiles.filter(function (profile) {
- return profile.language === language && key !== profile.key;
- });
- },
-
- serializeData () {
- return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
- profiles: this.getProfilesForComparison()
- });
- }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profile-details-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profile-details-view.js
deleted file mode 100644
index a914b3ac6be..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/profile-details-view.js
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import _ from 'underscore';
-import Marionette from 'backbone.marionette';
-import ChangeProfileParentView from './change-profile-parent-view';
-import ProfileChangelogView from './profile-changelog-view';
-import ProfileComparisonView from './profile-comparison-view';
-import '../../components/SelectList';
-import Template from './templates/quality-profiles-profile-details.hbs';
-import { translate } from '../../helpers/l10n';
-
-export default Marionette.LayoutView.extend({
- template: Template,
-
- regions: {
- changelogRegion: '#quality-profile-changelog',
- comparisonRegion: '#quality-profile-comparison'
- },
-
- modelEvents: {
- 'change': 'onChange',
- 'flashChangelog': 'flashChangelog'
- },
-
- events: {
- 'click .js-profile': 'onProfileClick',
- 'click #quality-profile-change-parent': 'onChangeParentClick'
- },
-
- onRender () {
- if (!this.model.get('isDefault')) {
- this.initProjectsSelect();
- }
- this.changelogRegion.show(new ProfileChangelogView({ model: this.model }));
- this.comparisonRegion.show(new ProfileComparisonView({ model: this.model }));
- if (this.options.anchor === 'changelog') {
- this.scrollToChangelog();
- this.flashChangelog();
- }
- if (this.options.anchor === 'comparison') {
- this.scrollToComparison();
- }
- this.$('#quality-profile-changelog-form input')
- .datepicker({
- dateFormat: 'yy-mm-dd',
- changeMonth: true,
- changeYear: true
- });
- },
-
- onChange () {
- const changed = Object.keys(this.model.changedAttributes());
- if (!(changed.length === 1 && changed[0] === 'projectCount')) {
- this.render();
- }
- },
-
- initProjectsSelect () {
- const key = this.model.get('key');
- this.projectsSelectList = new window.SelectList({
- el: this.$('#quality-profile-projects-list'),
- width: '100%',
- height: 200,
- readOnly: !this.options.canWrite,
- focusSearch: false,
- format (item) {
- return item.name;
- },
- searchUrl: window.baseUrl + '/api/qualityprofiles/projects?key=' + encodeURIComponent(key),
- selectUrl: window.baseUrl + '/api/qualityprofiles/add_project',
- deselectUrl: window.baseUrl + '/api/qualityprofiles/remove_project',
- extra: {
- profileKey: key
- },
- selectParameter: 'projectUuid',
- selectParameterValue: 'uuid',
- labels: {
- selected: translate('quality_gates.projects.with'),
- deselected: translate('quality_gates.projects.without'),
- all: translate('quality_gates.projects.all'),
- noResults: translate('quality_gates.projects.noResults')
- },
- tooltips: {
- select: translate('quality_profiles.projects.select_hint'),
- deselect: translate('quality_profiles.projects.deselect_hint')
- }
- });
- this.listenTo(this.projectsSelectList.collection, 'change:selected', this.onProjectsChange);
- },
-
- onProfileClick (e) {
- const key = $(e.currentTarget).data('key');
- const profile = this.model.collection.get(key);
- if (profile != null) {
- e.preventDefault();
- this.model.collection.trigger('select', profile);
- }
- },
-
- onChangeParentClick (e) {
- e.preventDefault();
- this.changeParent();
- },
-
- onProjectsChange () {
- this.model.collection.updateForLanguage(this.model.get('language'));
- },
-
- changeParent () {
- new ChangeProfileParentView({
- model: this.model
- }).render();
- },
-
- scrollTo (selector) {
- const el = this.$(selector);
- const parent = el.scrollParent();
- const elOffset = el.offset();
- let parentOffset = parent.offset();
- if (parent.is(document)) {
- parentOffset = { top: 0 };
- }
- if (elOffset != null && parentOffset != null) {
- const scrollTop = elOffset.top - parentOffset.top - 53;
- parent.scrollTop(scrollTop);
- }
- },
-
- scrollToChangelog () {
- this.scrollTo('#quality-profile-changelog');
- },
-
- scrollToComparison () {
- this.scrollTo('#quality-profile-comparison');
- },
-
- getExporters () {
- const language = this.model.get('language');
- return this.options.exporters.filter(function (exporter) {
- return exporter.languages.indexOf(language) !== -1;
- });
- },
-
- flashChangelog () {
- const changelogEl = this.$(this.changelogRegion.el);
- changelogEl.addClass('flash in');
- setTimeout(function () {
- changelogEl.removeClass('in');
- }, 2000);
- },
-
- serializeData () {
- const key = this.model.get('key');
- const rulesSearchUrl = `/coding_rules#qprofile=${encodeURIComponent(key)}|activation=true`;
- const activateRulesUrl = `/coding_rules#qprofile=${encodeURIComponent(key)}|activation=false`;
- return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
- rulesSearchUrl,
- activateRulesUrl,
- canWrite: this.options.canWrite,
- exporters: this.getExporters()
- });
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profile-header-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profile-header-view.js
deleted file mode 100644
index d1b29b01c42..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/profile-header-view.js
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import _ from 'underscore';
-import Marionette from 'backbone.marionette';
-import ProfileCopyView from './copy-profile-view';
-import ProfileRenameView from './rename-profile-view';
-import ProfileDeleteView from './delete-profile-view';
-import Template from './templates/quality-profiles-profile-header.hbs';
-
-export default Marionette.ItemView.extend({
- template: Template,
-
- modelEvents: {
- 'change': 'render'
- },
-
- events: {
- 'click #quality-profile-backup': 'onBackupClick',
- 'click #quality-profile-copy': 'onCopyClick',
- 'click #quality-profile-rename': 'onRenameClick',
- 'click #quality-profile-set-as-default': 'onDefaultClick',
- 'click #quality-profile-delete': 'onDeleteClick'
- },
-
- onBackupClick (e) {
- $(e.currentTarget).blur();
- },
-
- onCopyClick (e) {
- e.preventDefault();
- this.copy();
- },
-
- onRenameClick (e) {
- e.preventDefault();
- this.rename();
- },
-
- onDefaultClick (e) {
- e.preventDefault();
- this.setAsDefault();
- },
-
- onDeleteClick (e) {
- e.preventDefault();
- this.delete();
- },
-
- copy () {
- new ProfileCopyView({ model: this.model }).render();
- },
-
- rename () {
- new ProfileRenameView({ model: this.model }).render();
- },
-
- setAsDefault () {
- this.model.trigger('setAsDefault', this.model);
- },
-
- delete () {
- new ProfileDeleteView({ model: this.model }).render();
- },
-
- serializeData () {
- const key = this.model.get('key');
- return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
- encodedKey: encodeURIComponent(key),
- canWrite: this.options.canWrite
- });
- }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profile-view.js
deleted file mode 100644
index 09c5f751bcf..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/profile-view.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import _ from 'underscore';
-import Marionette from 'backbone.marionette';
-import Template from './templates/quality-profiles-profile.hbs';
-import { formatMeasure } from '../../helpers/measures';
-
-export default Marionette.ItemView.extend({
- tagName: 'a',
- className: 'list-group-item',
- template: Template,
-
- modelEvents: {
- 'change': 'render'
- },
-
- events: {
- 'click': 'onClick'
- },
-
- onRender () {
- this.$el.toggleClass('active', this.options.highlighted);
- this.$el.attr('data-key', this.model.id);
- this.$el.attr('data-language', this.model.get('language'));
- this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
- },
-
- onDestroy () {
- this.$('[data-toggle="tooltip"]').tooltip('destroy');
- },
-
- onClick (e) {
- e.preventDefault();
- this.model.trigger('select', this.model);
- },
-
- serializeData () {
- return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
- projectCountFormatted: formatMeasure(this.model.get('projectCount'), 'INT')
- });
- }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profile.js b/server/sonar-web/src/main/js/apps/quality-profiles/profile.js
deleted file mode 100644
index 00935378225..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/profile.js
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import _ from 'underscore';
-import Backbone from 'backbone';
-
-export default Backbone.Model.extend({
- idAttribute: 'key',
-
- defaults: {
- activeRuleCount: 0,
- projectCount: 0
- },
-
- fetch () {
- const that = this;
- this.fetchChanged = {};
- return $.when(
- this.fetchProfileRules(),
- this.fetchInheritance()
- ).done(function () {
- that.set(that.fetchChanged);
- });
- },
-
- fetchProfileRules () {
- const that = this;
- const url = window.baseUrl + '/api/rules/search';
- const key = this.id;
- const options = {
- ps: 1,
- facets: 'types',
- qprofile: key,
- activation: 'true'
- };
- return $.get(url, options).done(function (r) {
- const typesFacet = _.findWhere(r.facets, { property: 'types' });
- if (typesFacet != null) {
- const order = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
- const types = typesFacet.values;
- const typesComparator = function (t) {
- return order.indexOf(t.val);
- };
- const sortedTypes = _.sortBy(types, typesComparator);
- _.extend(that.fetchChanged, { rulesTypes: sortedTypes });
- }
- });
- },
-
- fetchInheritance () {
- const that = this;
- const url = window.baseUrl + '/api/qualityprofiles/inheritance';
- const options = { profileKey: this.id };
- return $.get(url, options).done(function (r) {
- _.extend(that.fetchChanged, r.profile, {
- ancestors: r.ancestors,
- children: r.children
- });
- });
- },
-
- fetchChangelog (options) {
- const that = this;
- const url = window.baseUrl + '/api/qualityprofiles/changelog';
- const opts = _.extend({}, options, { profileKey: this.id });
- return $.get(url, opts).done(function (r) {
- that.set({
- events: r.events,
- eventsPage: r.p,
- totalEvents: r.total,
- eventsParameters: _.clone(options)
- });
- });
- },
-
- fetchMoreChangelog () {
- const that = this;
- const url = window.baseUrl + '/api/qualityprofiles/changelog';
- const page = this.get('eventsPage') || 0;
- const parameters = this.get('eventsParameters') || {};
- const opts = _.extend({}, parameters, { profileKey: this.id, p: page + 1 });
- return $.get(url, opts).done(function (r) {
- const events = that.get('events') || [];
- that.set({
- events: [].concat(events, r.events),
- eventsPage: r.p,
- totalEvents: r.total
- });
- });
- },
-
- resetChangelog () {
- this.unset('events', { silent: true });
- this.unset('eventsPage', { silent: true });
- this.unset('totalEvents');
- },
-
- compareWith (withKey) {
- const that = this;
- const url = window.baseUrl + '/api/qualityprofiles/compare';
- const options = { leftKey: this.id, rightKey: withKey };
- return $.get(url, options).done(function (r) {
- const comparison = _.extend(r, {
- inLeftSize: _.size(r.inLeft),
- inRightSize: _.size(r.inRight),
- modifiedSize: _.size(r.modified)
- });
- that.set({
- comparison,
- comparedWith: withKey
- });
- });
- },
-
- resetComparison () {
- this.unset('comparedWith', { silent: true });
- this.unset('comparison');
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/profiles-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/profiles-view.js
deleted file mode 100644
index 99f2ee93a51..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/profiles-view.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import Marionette from 'backbone.marionette';
-import ProfileView from './profile-view';
-import ProfilesEmptyView from './profiles-empty-view';
-import Template from './templates/quality-profiles-profiles.hbs';
-import LanguageTemplate from './templates/quality-profiles-profiles-language.hbs';
-
-export default Marionette.CompositeView.extend({
- className: 'list-group',
- template: Template,
- languageTemplate: LanguageTemplate,
- childView: ProfileView,
- childViewContainer: '.js-list',
- emptyView: ProfilesEmptyView,
-
- collectionEvents: {
- 'filter': 'filterByLanguage'
- },
-
- childViewOptions (model) {
- return {
- collectionView: this,
- highlighted: model.get('key') === this.highlighted
- };
- },
-
- highlight (key) {
- this.highlighted = key;
- this.render();
- },
-
- attachHtml (compositeView, childView, index) {
- const $container = this.getChildViewContainer(compositeView);
- const model = this.collection.at(index);
- if (model != null) {
- const prev = this.collection.at(index - 1);
- let putLanguage = prev == null;
- if (prev != null) {
- const lang = model.get('language');
- const prevLang = prev.get('language');
- if (lang !== prevLang) {
- putLanguage = true;
- }
- }
- if (putLanguage) {
- $container.append(this.languageTemplate(model.toJSON()));
- }
- }
- compositeView._insertAfter(childView);
- },
-
- destroyChildren () {
- Marionette.CompositeView.prototype.destroyChildren.apply(this, arguments);
- this.$('.js-list-language').remove();
- },
-
- filterByLanguage (language) {
- if (language) {
- this.$('[data-language]').addClass('hidden');
- this.$(`[data-language="${language}"]`).removeClass('hidden');
- } else {
- this.$('[data-language]').removeClass('hidden');
- }
- }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/propTypes.js b/server/sonar-web/src/main/js/apps/quality-profiles/propTypes.js
new file mode 100644
index 00000000000..c8e40424e5a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/propTypes.js
@@ -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.
+ */
+import { PropTypes } from 'react';
+
+const { shape, string, number, bool, arrayOf } = PropTypes;
+
+export const ProfileType = shape({
+ key: string.isRequired,
+ name: string.isRequired,
+ isDefault: bool.isRequired,
+ isInherited: bool.isRequired,
+ language: string.isRequired,
+ languageName: string.isRequired,
+ activeRuleCount: number.isRequired,
+ activeDeprecatedRuleCount: number.isRequired,
+ projectCount: number,
+ parentKey: string,
+ parentName: string
+});
+
+export const ProfilesListType = arrayOf(ProfileType);
+
+const LanguageType = shape({
+ key: string.isRequired,
+ name: string.isRequired
+});
+
+export const LanguagesListType = arrayOf(LanguageType);
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/restore-built-in-profiles-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/restore-built-in-profiles-view.js
deleted file mode 100644
index a46487a49ec..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/restore-built-in-profiles-view.js
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import $ from 'jquery';
-import _ from 'underscore';
-import ModalFormView from '../../components/common/modal-form';
-import Template from './templates/quality-profiles-restore-built-in-profiles.hbs';
-import TemplateSuccess from './templates/quality-profiles-restore-built-in-profiles-success.hbs';
-
-export default ModalFormView.extend({
- template: Template,
- successTemplate: TemplateSuccess,
-
- getTemplate () {
- return this.selectedLanguage ? this.successTemplate : this.template;
- },
-
- onFormSubmit () {
- ModalFormView.prototype.onFormSubmit.apply(this, arguments);
- this.disableForm();
- this.sendRequest();
- },
-
- onRender () {
- ModalFormView.prototype.onRender.apply(this, arguments);
- this.$('select').select2({
- width: '250px',
- minimumResultsForSearch: 50
- });
- },
-
- sendRequest () {
- const that = this;
- const url = window.baseUrl + '/api/qualityprofiles/restore_built_in';
- const lang = this.$('#restore-built-in-profiles-language').val();
- const options = { language: lang };
- this.selectedLanguage = _.findWhere(this.options.languages, { key: lang }).name;
- return $.ajax({
- url,
- type: 'POST',
- data: options,
- statusCode: {
- // do not show global error
- 400: null
- }
- }).done(function () {
- that.collection.fetch({ reset: true });
- that.collection.trigger('destroy');
- that.render();
- }).fail(function (jqXHR) {
- that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
- that.enableForm();
- });
- },
-
- serializeData () {
- return _.extend(ModalFormView.prototype.serializeData.apply(this, arguments), {
- languages: this.options.languages,
- selectedLanguage: this.selectedLanguage
- });
- }
-});
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/styles.css b/server/sonar-web/src/main/js/apps/quality-profiles/styles.css
new file mode 100644
index 00000000000..c71d00c68a7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/styles.css
@@ -0,0 +1,149 @@
+.quality-profile-box {
+ padding: 20px;
+ border: 1px solid #e6e6e6;
+ border-radius: 2px;
+ background-color: #fff;
+}
+
+.quality-profiles-table {
+ margin-top: 30px;
+}
+
+.quality-profiles-table-name { }
+
+.quality-profiles-table-inheritance {
+ width: 280px;
+}
+
+.quality-profiles-table-projects,
+.quality-profiles-table-rules,
+.quality-profiles-table-date {
+ min-width: 120px;
+}
+
+.quality-profiles-list-header {
+ line-height: 24px;
+ margin-bottom: 15px;
+ padding: 5px 10px;
+ border-radius: 3px;
+ background-color: #f3f3f3;
+}
+
+.quality-profile-grid {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+}
+
+.quality-profile-grid-left {
+ width: 340px;
+ flex-shrink: 0;
+}
+
+.quality-profile-grid-right {
+ flex-grow: 1;
+ margin-left: 20px;
+}
+
+.quality-profile-rules,
+.quality-profile-projects,
+.quality-profile-inheritance,
+.quality-profile-evolution {
+ border: 1px solid #e6e6e6;
+ border-radius: 2px;
+ background-color: #fff;
+}
+
+.quality-profile-evolution {
+ padding: 20px;
+}
+
+.quality-profile-projects,
+.quality-profile-inheritance {
+ padding: 15px 20px 20px;
+}
+
+.quality-profile-rules {
+}
+
+.quality-profile-rules > header {
+ padding: 15px 20px;
+}
+
+.quality-profile-rules-distribution {
+ padding: 0 20px 20px;
+}
+
+.quality-profile-rules-deprecated {
+ margin-bottom: 20px;
+ padding: 15px 20px;
+ border-top: 1px solid #e6e6e6;
+ border-bottom: 1px solid #e6e6e6;
+ background-color: #fcf8e3;
+}
+
+.quality-profile-exporters {
+ margin-top: 20px;
+}
+
+.quality-profile-evolution {
+ display: flex;
+ margin-top: 20px;
+}
+
+.quality-profile-evolution > div {
+ width: 50%;
+ text-align: center;
+}
+
+.quality-profile-projects {
+ margin-top: 20px;
+}
+
+.quality-profile-inheritance {
+}
+
+.quality-profile-progress-bar {
+ transition: width 0.5s ease;
+ animation: appear 0.5s forwards;
+}
+
+@keyframes appear {
+ 0% { transform: translateX(-100%); }
+ 100% { transform: translateX(0); }
+}
+
+.quality-profile-not-found {
+ padding-top: 100px;
+ text-align: center;
+}
+
+.quality-profiles-evolution {
+ display: flex;
+ justify-content: flex-start;
+ align-items: stretch;
+ margin-bottom: 30px;
+}
+
+.quality-profiles-evolution-deprecated,
+.quality-profiles-evolution-stagnant,
+.quality-profiles-evolution-rules {
+ width: 325px;
+ padding: 15px 20px;
+ box-sizing: border-box;
+}
+
+.quality-profiles-evolution-deprecated,
+.quality-profiles-evolution-stagnant {
+ margin-right: 30px;
+ border: 1px solid #faebcc;
+ background-color: #fcf8e3;
+}
+
+.quality-profiles-evolution-rules {
+ border: 1px solid #e6e6e6;
+}
+
+.quality-profile-comparison-table {
+ table-layout: fixed;
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profile-changelog.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profile-changelog.hbs
deleted file mode 100644
index aa3bebc7903..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profile-changelog.hbs
+++ /dev/null
@@ -1,66 +0,0 @@
-<header class="page-header">
- <div class="page-title">
- <span class="h3">{{t 'changelog'}}</span>
- </div>
-</header>
-
-<form class="spacer-bottom" id="quality-profile-changelog-form">
- {{t 'quality_profiles.changelog_from'}}
- <input name="since" type="text" value="{{eventsParameters.since}}" placeholder="{{t 'optional'}}">
- {{t 'to'}}
- <input name="to" type="text" value="{{eventsParameters.to}}" placeholder="{{t 'optional'}}">
- <button id="quality-profile-changelog-form-submit">{{t 'search_verb'}}</button>
-</form>
-
-{{#notEmpty events}}
- <table class="width-100 data zebra">
- <thead>
- <tr>
- <th>{{t 'date'}}</th>
- <th>{{t 'user'}}</th>
- <th>{{t 'action'}}</th>
- <th>{{t 'rule'}}</th>
- <th>{{t 'parameters'}}</th>
- </tr>
- </thead>
- <tbody>
- {{#each events}}
- <tr>
- <td class="text-top nowrap thin">{{dt date}}</td>
- <td class="text-top nowrap thin">{{default authorName 'System'}}</td>
- <td class="text-top nowrap">{{t 'quality_profiles.changelog' action}}</td>
- <td class="text-top"><a href="{{rulePermalink ruleKey}}">{{ruleName}}</a></td>
- <td class="text-top thin">
- <ul>
- {{#each params}}
- <li>
- {{#eq @key 'severity'}}
- <span class="nowrap">{{severityChangelog this}}</span>
- {{else}}
- {{parameterChangelog @key this}}
- {{/eq}}
- </li>
- {{/each}}
- </ul>
- </td>
- </tr>
- {{/each}}
- </tbody>
- </table>
-
-
- <p class="spacer-top text-center">
- {{#unlessLength events totalEvents}}
- <a class="js-show-more-changelog spacer-right" href="#">{{t 'show_more'}}</a>
- {{/unlessLength}}
- <a class="js-hide-changelog" href="#">{{t 'hide'}}</a>
- </p>
-
-{{else}}
- {{#notNull totalEvents}}
- <div class="alert alert-info">
- {{t 'quality_profiles.changelog.empty'}}
- <a class="js-hide-changelog spacer-left" href="#">{{t 'hide'}}</a>
- </div>
- {{/notNull}}
-{{/notEmpty}}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profile-comparison.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profile-comparison.hbs
deleted file mode 100644
index e26259b6bad..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profile-comparison.hbs
+++ /dev/null
@@ -1,89 +0,0 @@
-<header class="page-header">
- <div class="page-title">
- <span class="h3">{{t 'compare'}}</span>
- </div>
-</header>
-
-{{#notEmpty profiles}}
- <form class="spacer-bottom" id="quality-profile-comparison-form">
- <label class="text-middle" for="quality-profile-comparison-with-key">{{t 'with'}}</label>
- <select id="quality-profile-comparison-with-key">
- {{#each profiles}}
- <option value="{{key}}" {{#eq key ../comparedWith}}selected{{/eq}}>{{name}}</option>
- {{/each}}
- </select>
- <button class="text-middle" id="quality-profile-comparison-form-submit">{{t 'compare'}}</button>
- </form>
-{{else}}
- <div class="alert alert-info">{{t 'quality_profiles.no_profiles_for_comparison'}}</div>
-{{/notEmpty}}
-
-{{#notNull comparison}}
- <table class="width-100 data zebra">
- {{#notEmpty comparison.inLeft}}
- <tr>
- <td class="width-50"><h6>{{tp 'quality_profiles.x_rules_only_in' comparison.inLeftSize}} {{comparison.left.name}}</h6></td>
- <td class="width-50"></td>
- </tr>
- {{#each comparison.inLeft}}
- <tr class="js-comparison-in-left">
- <td class="width-50">{{severityIcon severity}}&nbsp;<a href="{{rulePermalink key}}">{{name}}</a></td>
- <td class="width-50"></td>
- </tr>
- {{/each}}
- {{/notEmpty}}
-
- {{#notEmpty comparison.inRight}}
- <tr>
- <td class="width-50"></td>
- <td class="width-50"><h6>{{tp 'quality_profiles.x_rules_only_in' comparison.inRightSize}} {{comparison.right.name}}</h6></td>
- </tr>
- {{#each comparison.inRight}}
- <tr class="js-comparison-in-right">
- <td class="width-50"></td>
- <td class="width-50">{{severityIcon severity}}&nbsp;<a href="{{rulePermalink key}}">{{name}}</a></td>
- </tr>
- {{/each}}
- {{/notEmpty}}
-
- {{#notEmpty comparison.modified}}
- <tr>
- <td class="text-center width-50" colspan="2">
- <h6>{{tp 'quality_profiles.x_rules_have_different_configuration' comparison.modifiedSize}}</h6>
- </td>
- </tr>
- <tr>
- <td class="width-50"><h6>{{comparison.left.name}}</h6></td>
- <td class="width-50"><h6>{{comparison.right.name}}</h6></td>
- </tr>
- {{#each comparison.modified}}
- <tr class="js-comparison-modified">
- <td class="width-50">
- <p>{{severityIcon left.severity}}&nbsp;<a href="{{rulePermalink key}}">{{name}}</a></p>
- {{#notNull left.params}}
- <ul>
- {{#each left.params}}
- <li class="spacer-top"><code>{{@key}}: {{this}}</code></li>
- {{/each}}
- </ul>
- {{/notNull}}
- </td>
- <td class="width-50">
- <p>{{severityIcon right.severity}}&nbsp;<a href="{{rulePermalink key}}">{{name}}</a></p>
- {{#notNull right.params}}
- <ul>
- {{#each right.params}}
- <li class="spacer-top"><code>{{@key}}: {{this}}</code></li>
- {{/each}}
- </ul>
- {{/notNull}}
- </td>
- </tr>
- {{/each}}
- {{/notEmpty}}
- </table>
-
- <p class="spacer-top text-center">
- <a class="js-hide-comparison" href="#">{{t 'hide'}}</a>
- </p>
-{{/notNull}}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-actions.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-actions.hbs
deleted file mode 100644
index 672a7a6ec17..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-actions.hbs
+++ /dev/null
@@ -1,40 +0,0 @@
-<header class="page-header">
- <h1 class="page-title">{{t 'quality_profiles.page'}}</h1>
-
- {{#if canWrite}}
- <div class="page-actions">
- <div class="button-group dropdown">
- <button id="quality-profiles-create">{{t 'create'}}</button>
- <a id="quality-profiles-actions" class="button dropdown-toggle" href="#"
- data-toggle="dropdown"><i class="icon-dropdown"></i></a>
- <ul class="dropdown-menu dropdown-menu-right">
- <li>
- <a id="quality-profiles-restore" href="#">{{t 'quality_profiles.restore_profile'}}</a>
- </li>
- <li>
- <a id="quality-profiles-restore-built-in" href="#">{{t 'quality_profiles.restore_built_in_profiles'}}</a>
- </li>
- </ul>
- </div>
- </div>
- {{/if}}
-</header>
-
-<div class="dropdown" id="quality-profiles-filter-by-language">
- <span>Show:</span>
- {{#if selectedLanguage}}
- <a class="dropdown-toggle link-no-underline" href="#" data-toggle="dropdown">
- {{tp 'quality_profiles.x_profiles' selectedLanguage.name}} <i class="icon-dropdown"></i>
- </a>
- {{else}}
- <a class="dropdown-toggle link-no-underline" href="#" data-toggle="dropdown">
- {{t 'quality_profiles.all_profiles'}} <i class="icon-dropdown"></i>
- </a>
- {{/if}}
- <ul class="dropdown-menu">
- <li><a class="js-filter-by-language" href="#">{{t 'quality_profiles.all_profiles'}}</a></li>
- {{#each languages}}
- <li><a class="js-filter-by-language" href="#" data-language="{{key}}">{{name}}</a></li>
- {{/each}}
- </ul>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-change-projects.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-change-projects.hbs
new file mode 100644
index 00000000000..09e591327e2
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-change-projects.hbs
@@ -0,0 +1,12 @@
+<div class="modal-head">
+ <h2>{{t 'projects'}}</h2>
+</div>
+
+<div class="modal-body">
+ <div class="js-modal-messages"></div>
+ <div id="profile-projects"></div>
+</div>
+
+<div class="modal-foot">
+ <a href="#" class="js-modal-close">{{t 'close'}}</a>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-create-profile.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-create-profile.hbs
index 50fbc195fff..845e4843ef6 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-create-profile.hbs
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-create-profile.hbs
@@ -6,9 +6,9 @@
<div class="modal-body">
<div class="js-modal-messages"></div>
<div class="modal-field">
- <label for="create-profile-name">{{t 'name'}}<em class="mandatory">*</em></label>
- <input id="create-profile-name" name="name" type="text" size="50" maxlength="100" required>
- </div>
+ <label for="create-profile-name">{{t 'name'}}<em class="mandatory">*</em></label>
+ <input id="create-profile-name" name="name" type="text" size="50" maxlength="100" required>
+ </div>
<div class="modal-field spacer-bottom">
<label for="create-profile-language">{{t 'language'}}<em class="mandatory">*</em></label>
<select id="create-profile-language" name="language" required>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-delete-profile.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-delete-profile.hbs
index 99ee29ce611..17438526340 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-delete-profile.hbs
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-delete-profile.hbs
@@ -4,12 +4,12 @@
</div>
<div class="modal-body">
<div class="js-modal-messages"></div>
- {{#notEmpty children}}
+ {{#if childrenCount}}
<div class="alert alert-warning">{{t 'quality_profiles.this_profile_has_descendants'}}</div>
<p>{{tp 'quality_profiles.are_you_sure_want_delete_profile_x_and_descendants' name languageName}}</p>
{{else}}
<p>{{tp 'quality_profiles.are_you_sure_want_delete_profile_x' name languageName}}</p>
- {{/notEmpty}}
+ {{/if}}
</div>
<div class="modal-foot">
<button id="delete-profile-submit">{{t 'delete'}}</button>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-empty.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-empty.hbs
deleted file mode 100644
index eb1e821fdea..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-empty.hbs
+++ /dev/null
@@ -1 +0,0 @@
-{{t 'quality_profiles.no_results'}}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-intro.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-intro.hbs
deleted file mode 100644
index add14e79374..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-intro.hbs
+++ /dev/null
@@ -1,4 +0,0 @@
-<div class="search-navigator-intro markdown">
- <p>{{t 'quality_profiles.intro1'}}</p>
- <p>{{t 'quality_profiles.intro2'}}</p>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-layout.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-layout.hbs
deleted file mode 100644
index ac6f40bcaa4..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-layout.hbs
+++ /dev/null
@@ -1,11 +0,0 @@
-<div class="search-navigator">
- <div class="search-navigator-side search-navigator-side-light">
- <div class="search-navigator-filters"></div>
- <div class="quality-profiles-results panel"></div>
- </div>
-
- <div class="search-navigator-workspace">
- <div class="search-navigator-workspace-header"></div>
- <div class="search-navigator-workspace-details"></div>
- </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile-details.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile-details.hbs
deleted file mode 100644
index d7db852ce6e..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile-details.hbs
+++ /dev/null
@@ -1,134 +0,0 @@
-<div class="panel panel-vertical">
- <div class="columns">
- <div class="column-two-thirds" id="quality-profile-rules">
- <header class="page-header">
- <h3 class="page-title">{{t 'coding_rules'}}</h3>
- </header>
-
- <div>
- <div class="display-inline-block text-right little-spacer-right" style="width: 40px">
- <strong>
- <a href="{{link rulesSearchUrl}}">{{formatMeasure activeRuleCount 'INT'}}</a>
- </strong>
- </div>
- <strong>
- {{tp 'quality_profile.x_active_rules' ''}}
- </strong>
- </div>
-
- {{#notEmpty rulesTypes}}
- <div class="abs-width-400 spacer-top">
- {{#each rulesTypes}}
- <div class="spacer-top">
- <div class="display-inline-block text-right little-spacer-right" style="width: 40px">
- <a href="{{link ../rulesSearchUrl '|types=' val}}">{{formatMeasure count 'INT'}}</a>
- </div>
- {{issueType val}}
- </div>
- {{/each}}
- </div>
- {{/notEmpty}}
-
- <div class="spacer-top">
- <a class="button" href="{{link activateRulesUrl}}">
- {{t 'quality_profiles.activate_more'}}
- </a>
- </div>
- </div>
-
- {{#notEmpty exporters}}
- <div class="column-third" id="quality-profile-permalinks">
- <header class="page-header">
- <h3 class="page-title">{{t 'permalinks'}}</h3>
- </header>
- <ul>
- {{#each exporters}}
- <li class="spacer-bottom" style="line-height: 1.5">
- <a class="link-with-icon" href="{{exporterUrl ../this key}}" target="_blank">
- <i class="icon-detach"></i>
- <span>{{name}}</span>
- </a>
- </li>
- {{/each}}
- </ul>
- </div>
- {{/notEmpty}}
- </div>
-</div>
-
-<div class="panel panel-vertical" id="quality-profile-projects">
- <header class="page-header">
- <h3 class="page-title">{{t 'projects'}}</h3>
- </header>
- {{#if isDefault}}
- {{#if canWrite}}
- <p class="alert alert-info">{{t 'quality_profiles.projects_for_default.edit'}}</p>
- {{else}}
- <p class="alert alert-info">{{t 'quality_profiles.projects_for_default'}}</p>
- {{/if}}
- {{else}}
- <div id="quality-profile-projects-list" class="select-list-on-full-width"></div>
- {{/if}}
-</div>
-
-<div class="panel panel-vertical" id="quality-profile-inheritance">
- <header class="page-header">
- <h3 class="page-title">{{t 'quality_profiles.profile_inheritance'}}</h3>
- {{#if canWrite}}
- <div class="button-group big-spacer-left">
- <button id="quality-profile-change-parent">{{t 'quality_profiles.change_parent'}}</button>
- </div>
- {{/if}}
- </header>
- <div class="text-center">
- {{#notEmpty ancestors}}
- <ul id="quality-profile-ancestors">
- {{#eachReverse ancestors}}
- <li>
- <div class="alert alert-inline alert-info">
- <h6><a class="js-profile" data-key="{{key}}" href="{{profileUrl key}}">{{name}}</a></h6>
- <p class="note">{{tp 'quality_profile.x_active_rules' activeRuleCount}}</p>
- {{#if overridingRuleCount}}
- <p class="note">{{tp 'quality_profiles.x_overridden_rules' overridingRuleCount}}</p>
- {{/if}}
- </div>
- <div class="spacer-top spacer-bottom">
- <i class="icon-move-down"></i>
- </div>
- </li>
- {{/eachReverse}}
- </ul>
- {{/notEmpty}}
-
- <div id="quality-profile-inheritance-current" class="alert alert-inline alert-success">
- <h6>{{name}}</h6>
- <p class="note">{{tp 'quality_profile.x_active_rules' activeRuleCount}}</p>
- {{#if overridingRuleCount}}
- <p class="note">{{tp 'quality_profiles.x_overridden_rules' overridingRuleCount}}</p>
- {{/if}}
- </div>
-
- {{#notEmpty children}}
- <div class="spacer-top spacer-bottom">
- <i class="icon-move-down"></i>
- </div>
- <ul id="quality-profile-children" class="list-inline">
- {{#eachReverse children}}
- <li>
- <div class="alert alert-inline alert-info">
- <h6><a class="js-profile" data-key="{{key}}" href="{{profileUrl key}}">{{name}}</a></h6>
- <p class="note">{{tp 'quality_profile.x_active_rules' activeRuleCount}}</p>
- {{#if overridingRuleCount}}
- <p class="note">{{tp 'quality_profiles.x_overridden_rules' overridingRuleCount}}</p>
- {{/if}}
- </div>
- </li>
- {{/eachReverse}}
- </ul>
- {{/notEmpty}}
- </div>
-</div>
-
-<div class="panel panel-vertical" id="quality-profile-changelog"></div>
-
-<div class="panel panel-vertical" id="quality-profile-comparison"></div>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile-header.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile-header.hbs
deleted file mode 100644
index cd674f4601e..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile-header.hbs
+++ /dev/null
@@ -1,19 +0,0 @@
-<h2 class="search-navigator-header-component">
- {{name}}
- <span class="note">{{languageName}}</span>
-</h2>
-
-<div class="search-navigator-header-actions">
- <div class="button-group">
- <a class="button" href="{{link '/api/qualityprofiles/backup?profileKey=' encodedKey}}"
- id="quality-profile-backup">{{t 'backup_verb'}}</a>
- {{#if canWrite}}
- <button id="quality-profile-rename">{{t 'rename'}}</button>
- <button id="quality-profile-copy">{{t 'copy'}}</button>
- {{#unless isDefault}}
- <button id="quality-profile-set-as-default">{{t 'set_as_default'}}</button>
- <button id="quality-profile-delete" class="button-red">{{t 'delete'}}</button>
- {{/unless}}
- {{/if}}
- </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile.hbs
deleted file mode 100644
index f6cc63068b8..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profile.hbs
+++ /dev/null
@@ -1,21 +0,0 @@
-<table>
- <tr>
- <td class="text-top">{{name}}</td>
- {{#if isInherited}}
- <td class="text-top thin spacer-left">
- <i class="icon-inheritance" title="{{tp 'quality_profiles.inherits' parentName}}"
- data-toggle="tooltip" data-placement="bottom"></i>
- </td>
- {{/if}}
- <td class="text-top thin nowrap spacer-left">
- {{#if isDefault}}
- <span class="badge pull-right">{{t 'default'}}</span>
- {{else}}
- <span class="note pull-right">{{tp 'quality_profiles.x_projects' projectCountFormatted}}</span>
- {{/if}}
- </td>
- </tr>
-</table>
-
-
-
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profiles-language.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profiles-language.hbs
deleted file mode 100644
index f3ec16f0a9c..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profiles-language.hbs
+++ /dev/null
@@ -1 +0,0 @@
-<h6 class="spacer-top js-list-language" data-language="{{language}}">{{languageName}}</h6>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profiles.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profiles.hbs
deleted file mode 100644
index 8022059ffad..00000000000
--- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-profiles.hbs
+++ /dev/null
@@ -1 +0,0 @@
-<div class="js-list"></div>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-restore-built-in-profiles-success.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-restore-built-in-profiles-success.hbs
index 41c449e88d4..01ccc528718 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-restore-built-in-profiles-success.hbs
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-restore-built-in-profiles-success.hbs
@@ -4,7 +4,7 @@
</div>
<div class="modal-body">
<div class="alert alert-success">
- {{tp 'quality_profiles.restore_built_in_profiles_success_message' selectedLanguage}}
+ {{tp 'quality_profiles.restore_built_in_profiles_success_message' language.name}}
</div>
</div>
<div class="modal-foot">
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-restore-built-in-profiles.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-restore-built-in-profiles.hbs
index 9df1cc04587..f7473ea8e26 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-restore-built-in-profiles.hbs
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-restore-built-in-profiles.hbs
@@ -4,18 +4,10 @@
</div>
<div class="modal-body">
<div class="js-modal-messages"></div>
- <div id="restore-built-in-profiles-form-success" class="alert alert-success hidden"></div>
- <div class="modal-field">
- <label for="restore-built-in-profiles-language">{{t 'language'}}<em class="mandatory">*</em></label>
- <select id="restore-built-in-profiles-language" name="language">
- {{#each languages}}
- <option value="{{key}}">{{name}}</option>
- {{/each}}
- </select>
- </div>
+ {{tp 'quality_profiles.restore_built_in_profiles_confirmation' language.name}}
</div>
<div class="modal-foot">
<button id="restore-built-in-profiles-submit">{{t 'restore'}}</button>
- <a href="#" class="js-modal-close" id="restore-built-in-profiles-cancel">{{t 'cancel'}}</a>
+ <a href="#" class="js-modal-close">{{t 'close'}}</a>
</div>
</form>
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/utils.js b/server/sonar-web/src/main/js/apps/quality-profiles/utils.js
new file mode 100644
index 00000000000..cba412ba81a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/utils.js
@@ -0,0 +1,61 @@
+/*
+ * 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 sortBy from 'lodash/sortBy';
+
+export function sortProfiles (profiles) {
+ const result = [];
+ const sorted = sortBy(profiles, 'name');
+
+ function retrieveChildren (parent) {
+ return sorted.filter(p => (
+ (parent == null && p.parentKey == null) ||
+ (parent != null && p.parentKey === parent.key)
+ ));
+ }
+
+ function putProfile (profile = null, depth = 0) {
+ const children = retrieveChildren(profile);
+
+ if (profile != null) {
+ result.push({ ...profile, childrenCount: children.length, depth });
+ }
+
+ children.forEach(child => putProfile(child, depth + 1));
+ }
+
+ putProfile();
+
+ return result;
+}
+
+export function createFakeProfile (overrides) {
+ return {
+ key: 'key',
+ name: 'name',
+ isDefault: false,
+ isInherited: false,
+ language: 'js',
+ languageName: 'JavaScript',
+ activeRuleCount: 10,
+ activeDeprecatedRuleCount: 2,
+ projectCount: 3,
+ ...overrides
+ };
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/change-profile-parent-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeParentView.js
index e86554b516e..6b405179e44 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/change-profile-parent-view.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeParentView.js
@@ -17,11 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import $ from 'jquery';
-import _ from 'underscore';
-import Marionette from 'backbone.marionette';
-import ModalFormView from '../../components/common/modal-form';
-import Template from './templates/quality-profiles-change-profile-parent.hbs';
+import ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-change-profile-parent.hbs';
+import { changeProfileParent } from '../../../api/quality-profiles';
export default ModalFormView.extend({
template: Template,
@@ -41,40 +39,25 @@ export default ModalFormView.extend({
},
sendRequest () {
- const that = this;
- const url = window.baseUrl + '/api/qualityprofiles/change_parent';
const parent = this.$('#change-profile-parent').val();
- const options = {
- profileKey: this.model.get('key'),
- parentKey: parent
- };
- return $.ajax({
- url,
- type: 'POST',
- data: options,
- statusCode: {
- // do not show global error
- 400: null
- }
- }).done(function () {
- that.model.collection.fetch();
- that.model.trigger('select', that.model);
- that.destroy();
- }).fail(function (jqXHR) {
- that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
- that.enableForm();
- });
+ changeProfileParent(this.options.profile.key, parent)
+ .then(() => {
+ this.destroy();
+ this.trigger('done');
+ })
+ .catch(e => {
+ if (e.response.status === 400) {
+ this.enableForm();
+ e.response.json().then(r => this.showErrors(r.errors, r.warnings));
+ }
+ });
},
serializeData () {
- const that = this;
- const profilesData = this.model.collection.toJSON();
- const profiles = _.filter(profilesData, function (profile) {
- return profile.language === that.model.get('language') && profile.key !== that.model.id;
- });
- return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
- profiles
- });
+ const { profile } = this.options;
+ const profiles = this.options.profiles
+ .filter(p => p !== profile && p.language === profile.language);
+ return { ...profile, profiles };
}
});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeProjectsView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeProjectsView.js
new file mode 100644
index 00000000000..5292bfa183e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeProjectsView.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 ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-change-projects.hbs';
+import { translate } from '../../../helpers/l10n';
+import '../../../components/SelectList';
+
+export default ModalFormView.extend({
+ template: Template,
+
+ onRender () {
+ ModalFormView.prototype.onRender.apply(this, arguments);
+
+ const { key } = this.options.profile;
+
+ const searchUrl = window.baseUrl + '/api/qualityprofiles/projects?key=' +
+ encodeURIComponent(key);
+
+ new window.SelectList({
+ searchUrl,
+ el: this.$('#profile-projects'),
+ width: '100%',
+ readOnly: false,
+ focusSearch: false,
+ format (item) {
+ return item.name;
+ },
+ selectUrl: window.baseUrl + '/api/qualityprofiles/add_project',
+ deselectUrl: window.baseUrl + '/api/qualityprofiles/remove_project',
+ extra: {
+ profileKey: key
+ },
+ selectParameter: 'projectUuid',
+ selectParameterValue: 'uuid',
+ labels: {
+ selected: translate('quality_gates.projects.with'),
+ deselected: translate('quality_gates.projects.without'),
+ all: translate('quality_gates.projects.all'),
+ noResults: translate('quality_gates.projects.noResults')
+ },
+ tooltips: {
+ select: translate('quality_profiles.projects.select_hint'),
+ deselect: translate('quality_profiles.projects.deselect_hint')
+ }
+ });
+ },
+
+ onDestroy() {
+ this.options.loadProjects();
+ ModalFormView.prototype.onDestroy.apply(this, arguments);
+ }
+});
+
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/copy-profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/CopyProfileView.js
index e391f8bc274..5809894287e 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/copy-profile-view.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/views/CopyProfileView.js
@@ -17,10 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import $ from 'jquery';
-import ModalFormView from '../../components/common/modal-form';
-import Profile from './profile';
-import Template from './templates/quality-profiles-copy-profile.hbs';
+import ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-copy-profile.hbs';
+import { copyProfile } from '../../../api/quality-profiles';
export default ModalFormView.extend({
template: Template,
@@ -32,34 +31,22 @@ export default ModalFormView.extend({
},
sendRequest () {
- const that = this;
- const url = window.baseUrl + '/api/qualityprofiles/copy';
const name = this.$('#copy-profile-name').val();
- const options = {
- fromKey: this.model.get('key'),
- toName: name
- };
- return $.ajax({
- url,
- type: 'POST',
- data: options,
- statusCode: {
- // do not show global error
- 400: null
- }
- }).done(function (r) {
- that.addProfile(r);
- that.destroy();
- }).fail(function (jqXHR) {
- that.enableForm();
- that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
- });
+ copyProfile(this.options.profile.key, name)
+ .then(profile => {
+ this.destroy();
+ this.trigger('done', profile);
+ })
+ .catch(e => {
+ if (e.response.status === 400) {
+ this.enableForm();
+ e.response.json().then(r => this.showErrors(r.errors, r.warnings));
+ }
+ });
},
- addProfile (profileData) {
- const profile = new Profile(profileData);
- this.model.collection.add([profile]);
- profile.trigger('select', profile);
+ serializeData () {
+ return this.options.profile;
}
});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/create-profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/CreateProfileView.js
index 68aa25279dd..60d800bc9fa 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/create-profile-view.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/views/CreateProfileView.js
@@ -19,10 +19,9 @@
*/
import $ from 'jquery';
import _ from 'underscore';
-import ModalFormView from '../../components/common/modal-form';
-import Profile from './profile';
-import Template from './templates/quality-profiles-create-profile.hbs';
-import { createQualityProfile } from '../../api/quality-profiles';
+import ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-create-profile.hbs';
+import { createQualityProfile } from '../../../api/quality-profiles';
export default ModalFormView.extend({
template: Template,
@@ -41,7 +40,7 @@ export default ModalFormView.extend({
createQualityProfile(data)
.then(r => {
- this.addProfile(r.profile);
+ this.trigger('done', r.profile);
this.destroy();
})
.catch(e => {
@@ -76,12 +75,6 @@ export default ModalFormView.extend({
e.unwrap();
},
- addProfile (profileData) {
- const profile = new Profile(profileData);
- this.collection.add([profile]);
- profile.trigger('select', profile);
- },
-
getImportersForLanguages (language) {
if (language != null) {
return this.options.importers.filter(function (importer) {
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/delete-profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/DeleteProfileView.js
index b75d930867b..b9bb6023c71 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/delete-profile-view.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/views/DeleteProfileView.js
@@ -17,9 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import $ from 'jquery';
-import ModalFormView from '../../components/common/modal-form';
-import Template from './templates/quality-profiles-delete-profile.hbs';
+import ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-delete-profile.hbs';
+import { deleteProfile } from '../../../api/quality-profiles';
export default ModalFormView.extend({
template: Template,
@@ -35,24 +35,21 @@ export default ModalFormView.extend({
},
sendRequest () {
- const that = this;
- const url = window.baseUrl + '/api/qualityprofiles/delete';
- const options = { profileKey: this.model.get('key') };
- return $.ajax({
- url,
- type: 'POST',
- data: options,
- statusCode: {
- // do not show global error
- 400: null
- }
- }).done(function () {
- that.model.collection.fetch();
- that.model.trigger('destroy', that.model, that.model.collection);
- }).fail(function (jqXHR) {
- that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
- that.enableForm();
- });
+ deleteProfile(this.options.profile.key)
+ .then(() => {
+ this.destroy();
+ this.trigger('done');
+ })
+ .catch(e => {
+ if (e.response.status === 400) {
+ this.enableForm();
+ e.response.json().then(r => this.showErrors(r.errors, r.warnings));
+ }
+ });
+ },
+
+ serializeData () {
+ return this.options.profile;
}
});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/rename-profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/RenameProfileView.js
index 52c4e7eaf50..964a4b58115 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/rename-profile-view.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/views/RenameProfileView.js
@@ -17,9 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import $ from 'jquery';
-import ModalFormView from '../../components/common/modal-form';
-import Template from './templates/quality-profiles-rename-profile.hbs';
+import ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-rename-profile.hbs';
+import { renameProfile } from '../../../api/quality-profiles';
export default ModalFormView.extend({
template: Template,
@@ -30,27 +30,22 @@ export default ModalFormView.extend({
},
sendRequest () {
- const that = this;
- const url = window.baseUrl + '/api/qualityprofiles/rename';
const name = this.$('#rename-profile-name').val();
- const options = {
- key: this.model.get('key'),
- name
- };
- return $.ajax({
- url,
- type: 'POST',
- data: options,
- statusCode: {
- // do not show global error
- 400: null
- }
- }).done(function () {
- that.model.set({ name });
- that.destroy();
- }).fail(function (jqXHR) {
- that.showErrors(jqXHR.responseJSON.errors, jqXHR.responseJSON.warnings);
- });
+ renameProfile(this.options.profile.key, name)
+ .then(profile => {
+ this.destroy();
+ this.trigger('done', profile);
+ })
+ .catch(e => {
+ if (e.response.status === 400) {
+ this.enableForm();
+ e.response.json().then(r => this.showErrors(r.errors, r.warnings));
+ }
+ });
+ },
+
+ serializeData () {
+ return this.options.profile;
}
});
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreBuiltInProfilesView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreBuiltInProfilesView.js
new file mode 100644
index 00000000000..f71f85c06af
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreBuiltInProfilesView.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 ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-restore-built-in-profiles.hbs';
+import TemplateSuccess from '../templates/quality-profiles-restore-built-in-profiles-success.hbs';
+import { restoreBuiltInProfiles } from '../../../api/quality-profiles';
+
+export default ModalFormView.extend({
+ template: Template,
+ successTemplate: TemplateSuccess,
+
+ getTemplate () {
+ return this.done ? this.successTemplate : this.template;
+ },
+
+ onFormSubmit () {
+ ModalFormView.prototype.onFormSubmit.apply(this, arguments);
+ this.disableForm();
+ this.sendRequest();
+ },
+
+ sendRequest () {
+ restoreBuiltInProfiles(this.options.language.key)
+ .then(() => {
+ this.done = true;
+ this.render();
+ this.trigger('done');
+ })
+ .catch(e => {
+ this.enableForm();
+ e.response.json().then(r => this.showErrors(r.errors, r.warnings));
+ });
+ },
+
+ serializeData () {
+ return { language: this.options.language };
+ }
+});
+
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/restore-profile-view.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreProfileView.js
index 3c5bad5adb7..4fceffda657 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/restore-profile-view.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreProfileView.js
@@ -17,10 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import ModalFormView from '../../components/common/modal-form';
-import Profile from './profile';
-import Template from './templates/quality-profiles-restore-profile.hbs';
-import { restoreQualityProfile } from '../../api/quality-profiles';
+import ModalFormView from '../../../components/common/modal-form';
+import Template from '../templates/quality-profiles-restore-profile.hbs';
+import { restoreQualityProfile } from '../../../api/quality-profiles';
export default ModalFormView.extend({
template: Template,
@@ -37,7 +36,7 @@ export default ModalFormView.extend({
this.ruleSuccesses = r.ruleSuccesses;
this.ruleFailures = r.ruleFailures;
this.render();
- this.addProfile(r.profile);
+ this.trigger('done');
})
.catch(e => {
this.enableForm();
@@ -45,20 +44,12 @@ export default ModalFormView.extend({
});
},
- addProfile (profileData) {
- const profile = new Profile(profileData);
- this.collection.add([profile], { merge: true });
- const addedProfile = this.collection.get(profile.id);
- if (addedProfile != null) {
- addedProfile.trigger('select', addedProfile);
- }
- },
-
serializeData() {
- return Object.assign({}, ModalFormView.prototype.serializeData.apply(this, arguments), {
+ return {
+ ...ModalFormView.prototype.serializeData.apply(this, arguments),
profile: this.profile,
ruleSuccesses: this.ruleSuccesses,
ruleFailures: this.ruleFailures
- });
+ };
}
});