diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-06-27 16:08:44 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-06-27 16:08:44 +0200 |
commit | 4ecb6fc3ec08ee37a1bb5c19d8ca44e2b832b5a7 (patch) | |
tree | 763ce4b5a9d7b37982ab54fc64a0597167c2bc3d /server/sonar-web/src/main/js/apps | |
parent | c467e84eed1a60bbe8c0fe800e2cada2bb26bb4c (diff) | |
download | sonarqube-4ecb6fc3ec08ee37a1bb5c19d8ca44e2b832b5a7.tar.gz sonarqube-4ecb6fc3ec08ee37a1bb5c19d8ca44e2b832b5a7.zip |
refactor quality profiles page (#1056)
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
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> </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> </td> + </tr> + )); + return [header, ...rows]; + } + + renderRight () { + if (this.props.inRight.length === 0) { + return null; + } + const header = ( + <tr key="right-header"> + <td> </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> </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}} <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}} <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}} <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}} <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 - }); + }; } }); |