From 3aa4e0789f603f3c579a5f0184fa024afd3792fa Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Thu, 1 Oct 2015 12:06:21 +0200 Subject: [PATCH] SONAR-6848 Merge the "Bulk Deletion" and "Provisioning" pages --- server/sonar-web/Gruntfile.coffee | 4 + .../sonar-web/src/main/js/api/components.js | 27 +++ server/sonar-web/src/main/js/api/nav.js | 6 + server/sonar-web/src/main/js/api/users.js | 6 + .../js/apps/nav/settings/settings-nav.jsx | 5 +- .../src/main/js/apps/projects/app.js | 21 ++ .../src/main/js/apps/projects/constants.js | 9 + .../src/main/js/apps/projects/create-view.js | 46 +++++ .../src/main/js/apps/projects/delete-view.js | 14 ++ .../src/main/js/apps/projects/form-view.js | 23 +++ .../src/main/js/apps/projects/header.js | 33 +++ .../src/main/js/apps/projects/main.js | 190 ++++++++++++++++++ .../src/main/js/apps/projects/projects.js | 52 +++++ .../src/main/js/apps/projects/search.js | 118 +++++++++++ .../templates/projects-create-form.hbs | 30 +++ .../projects/templates/projects-delete.hbs | 13 ++ .../main/js/components/shared/checkbox.jsx | 12 +- .../src/main/less/components/page.less | 3 +- .../sonar-web/src/main/less/init/tables.less | 4 + .../app/controllers/projects_controller.rb | 30 +++ .../WEB-INF/app/views/projects/index.html.erb | 7 + server/sonar-web/tests/apps/projects-test.js | 35 ++++ .../resources/org/sonar/l10n/core.properties | 2 +- 23 files changed, 685 insertions(+), 5 deletions(-) create mode 100644 server/sonar-web/src/main/js/api/components.js create mode 100644 server/sonar-web/src/main/js/api/nav.js create mode 100644 server/sonar-web/src/main/js/api/users.js create mode 100644 server/sonar-web/src/main/js/apps/projects/app.js create mode 100644 server/sonar-web/src/main/js/apps/projects/constants.js create mode 100644 server/sonar-web/src/main/js/apps/projects/create-view.js create mode 100644 server/sonar-web/src/main/js/apps/projects/delete-view.js create mode 100644 server/sonar-web/src/main/js/apps/projects/form-view.js create mode 100644 server/sonar-web/src/main/js/apps/projects/header.js create mode 100644 server/sonar-web/src/main/js/apps/projects/main.js create mode 100644 server/sonar-web/src/main/js/apps/projects/projects.js create mode 100644 server/sonar-web/src/main/js/apps/projects/search.js create mode 100644 server/sonar-web/src/main/js/apps/projects/templates/projects-create-form.hbs create mode 100644 server/sonar-web/src/main/js/apps/projects/templates/projects-delete.hbs create mode 100644 server/sonar-web/src/main/webapp/WEB-INF/app/controllers/projects_controller.rb create mode 100644 server/sonar-web/src/main/webapp/WEB-INF/app/views/projects/index.html.erb create mode 100644 server/sonar-web/tests/apps/projects-test.js diff --git a/server/sonar-web/Gruntfile.coffee b/server/sonar-web/Gruntfile.coffee index e11220c026c..e87032768af 100644 --- a/server/sonar-web/Gruntfile.coffee +++ b/server/sonar-web/Gruntfile.coffee @@ -156,6 +156,7 @@ module.exports = (grunt) -> 'build-app:metrics' 'build-app:nav' 'build-app:permission-templates' + 'build-app:projects' 'build-app:project-permissions' 'build-app:provisioning' 'build-app:quality-gates' @@ -258,6 +259,9 @@ module.exports = (grunt) -> '<%= BUILD_PATH %>/js/apps/permission-templates/templates.js': [ '<%= SOURCE_PATH %>/js/apps/permission-templates/templates/**/*.hbs' ] + '<%= BUILD_PATH %>/js/apps/projects/templates.js': [ + '<%= SOURCE_PATH %>/js/apps/projects/templates/**/*.hbs' + ] clean: diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js new file mode 100644 index 00000000000..8fe69f9c6d2 --- /dev/null +++ b/server/sonar-web/src/main/js/api/components.js @@ -0,0 +1,27 @@ +import $ from 'jquery'; + +export function getComponents (data) { + let url = baseUrl + '/api/components/search'; + return $.get(url, data); +} + +export function getProvisioned (data) { + let url = baseUrl + '/api/projects/provisioned'; + return $.get(url, data); +} + +export function getGhosts (data) { + let url = baseUrl + '/api/projects/ghosts'; + return $.get(url, data); +} + +export function deleteComponents (data) { + let url = baseUrl + '/api/projects/bulk_delete'; + return $.post(url, data); +} + +export function createProject (options) { + options.url = baseUrl + '/api/projects/create'; + options.type = 'POST'; + return $.ajax(options); +} diff --git a/server/sonar-web/src/main/js/api/nav.js b/server/sonar-web/src/main/js/api/nav.js new file mode 100644 index 00000000000..86cc425795a --- /dev/null +++ b/server/sonar-web/src/main/js/api/nav.js @@ -0,0 +1,6 @@ +import $ from 'jquery'; + +export function getGlobalNavigation () { + let url = baseUrl + '/api/navigation/global'; + return $.get(url); +} diff --git a/server/sonar-web/src/main/js/api/users.js b/server/sonar-web/src/main/js/api/users.js new file mode 100644 index 00000000000..90910dd0d95 --- /dev/null +++ b/server/sonar-web/src/main/js/api/users.js @@ -0,0 +1,6 @@ +import $ from 'jquery'; + +export function getCurrentUser () { + let url = baseUrl + '/api/users/current'; + return $.get(url); +} diff --git a/server/sonar-web/src/main/js/apps/nav/settings/settings-nav.jsx b/server/sonar-web/src/main/js/apps/nav/settings/settings-nav.jsx index 8d483131f98..66923b96cf7 100644 --- a/server/sonar-web/src/main/js/apps/nav/settings/settings-nav.jsx +++ b/server/sonar-web/src/main/js/apps/nav/settings/settings-nav.jsx @@ -55,10 +55,13 @@ export default React.createClass({ {window.t('sidebar.projects')}  diff --git a/server/sonar-web/src/main/js/apps/projects/app.js b/server/sonar-web/src/main/js/apps/projects/app.js new file mode 100644 index 00000000000..8e98bb1686b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/app.js @@ -0,0 +1,21 @@ +import $ from 'jquery'; +import React from 'react'; +import Main from './main'; +import {getCurrentUser} from '../../api/users'; +import {getGlobalNavigation} from '../../api/nav'; + +export default { + start(options) { + $.when( + getCurrentUser(), + getGlobalNavigation(), + window.requestMessages() + ).then((user, nav) => { + let el = document.querySelector(options.el), + hasProvisionPermission = user[0].permissions.global.indexOf('provisioning') !== -1, + topLevelQualifiers = nav[0].qualifiers; + React.render(
, el); + }); + } +}; diff --git a/server/sonar-web/src/main/js/apps/projects/constants.js b/server/sonar-web/src/main/js/apps/projects/constants.js new file mode 100644 index 00000000000..70cbf8f4f86 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/constants.js @@ -0,0 +1,9 @@ +export const PAGE_SIZE = 30; + +export const QUALIFIERS_ORDER = ['TRK', 'VW', 'DEV']; + +export const TYPE = { + ALL: 'ALL', + PROVISIONED: 'PROVISIONED', + GHOSTS: 'GHOSTS' +}; diff --git a/server/sonar-web/src/main/js/apps/projects/create-view.js b/server/sonar-web/src/main/js/apps/projects/create-view.js new file mode 100644 index 00000000000..6307b708614 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/create-view.js @@ -0,0 +1,46 @@ +import ModalForm from 'components/common/modal-form'; +import {createProject} from '../../api/components'; +import './templates'; + +export default ModalForm.extend({ + template: Templates['projects-create-form'], + + onRender: function () { + this._super(); + this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); + }, + + onDestroy: function () { + this._super(); + this.$('[data-toggle="tooltip"]').tooltip('destroy'); + }, + + onFormSubmit: function (e) { + this._super(e); + this.sendRequest(); + }, + + sendRequest: function () { + let data = { + name: this.$('#create-project-name').val(), + branch: this.$('#create-project-branch').val(), + key: this.$('#create-project-key').val() + }; + this.disableForm(); + return createProject({ + data, + statusCode: { + // do not show global error + 400: null + } + }).done(() => { + if (this.options.refresh) { + this.options.refresh(); + } + this.destroy(); + }).fail((jqXHR) => { + this.enableForm(); + this.showErrors([{ msg: jqXHR.responseJSON.err_msg }]); + }); + } +}); diff --git a/server/sonar-web/src/main/js/apps/projects/delete-view.js b/server/sonar-web/src/main/js/apps/projects/delete-view.js new file mode 100644 index 00000000000..56d5f1587b8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/delete-view.js @@ -0,0 +1,14 @@ +import ModalForm from 'components/common/modal-form'; +import './templates'; + +export default ModalForm.extend({ + template: Templates['projects-delete'], + + onFormSubmit: function (e) { + this._super(e); + this.options.deleteProjects(); + this.destroy(); + } +}); + + diff --git a/server/sonar-web/src/main/js/apps/projects/form-view.js b/server/sonar-web/src/main/js/apps/projects/form-view.js new file mode 100644 index 00000000000..c5e12ab40d8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/form-view.js @@ -0,0 +1,23 @@ +import ModalForm from 'components/common/modal-form'; +import './templates'; + +export default ModalForm.extend({ + + onRender: function () { + this._super(); + this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); + }, + + onDestroy: function () { + this._super(); + this.$('[data-toggle="tooltip"]').tooltip('destroy'); + }, + + onFormSubmit: function (e) { + this._super(e); + this.sendRequest(); + } + +}); + + diff --git a/server/sonar-web/src/main/js/apps/projects/header.js b/server/sonar-web/src/main/js/apps/projects/header.js new file mode 100644 index 00000000000..b7c71f64f26 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/header.js @@ -0,0 +1,33 @@ +import React from 'react'; +import CreateView from './create-view'; + +export default React.createClass({ + propTypes: { + hasProvisionPermission: React.PropTypes.bool.isRequired + }, + + createProject() { + new CreateView({ + refresh: this.props.refresh + }).render(); + }, + + renderCreateButton() { + if (!this.props.hasProvisionPermission) { + return null; + } + return ; + }, + + render() { + return ( +
+

Projects Management

+
{this.renderCreateButton()}
+

Use this page to delete multiple projects at once, or to provision projects + if you would like to configure them before the first analysis. Note that once a project is provisioned, you + have access to perform all project configurations on it.

+
+ ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/projects/main.js b/server/sonar-web/src/main/js/apps/projects/main.js new file mode 100644 index 00000000000..8bb5492ac02 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/main.js @@ -0,0 +1,190 @@ +import _ from 'underscore'; +import React from 'react'; +import Header from './header'; +import Search from './search'; +import Projects from './projects'; +import {PAGE_SIZE, TYPE} from './constants'; +import {getComponents, getProvisioned, getGhosts, deleteComponents} from '../../api/components'; +import ListFooter from '../../components/shared/list-footer'; + +export default React.createClass({ + propTypes: { + hasProvisionPermission: React.PropTypes.bool.isRequired, + topLevelQualifiers: React.PropTypes.array.isRequired + }, + + getInitialState() { + return { + projects: [], + total: 0, + page: 1, + query: '', + qualifiers: 'TRK', + type: TYPE.ALL, + selection: [] + }; + }, + + componentWillMount: function () { + this.requestProjects = _.debounce(this.requestProjects, 250); + }, + + componentDidMount() { + this.requestProjects(); + }, + + getFilters() { + let filters = { ps: PAGE_SIZE }; + if (this.state.page !== 1) { + filters.p = this.state.page; + } + if (this.state.query) { + filters.q = this.state.query; + } + return filters; + }, + + requestProjects() { + switch (this.state.type) { + case TYPE.ALL: + this.requestAllProjects(); + break; + case TYPE.PROVISIONED: + this.requestProvisioned(); + break; + case TYPE.GHOSTS: + this.requestGhosts(); + break; + default: + // should never happen + } + }, + + requestGhosts() { + let data = this.getFilters(); + getGhosts(data).done(r => { + let projects = r.projects.map(project => { + return _.extend(project, { id: project.uuid, qualifier: 'TRK' }); + }); + if (this.state.page > 1) { + projects = [].concat(this.state.projects, projects); + } + this.setState({ projects: projects, total: r.total }); + }); + }, + + requestProvisioned() { + let data = this.getFilters(); + getProvisioned(data).done(r => { + let projects = r.projects.map(project => { + return _.extend(project, { id: project.uuid, qualifier: 'TRK' }); + }); + if (this.state.page > 1) { + projects = [].concat(this.state.projects, projects); + } + this.setState({ projects: projects, total: r.total }); + }); + }, + + requestAllProjects() { + let data = this.getFilters(); + data.qualifiers = this.state.qualifiers; + getComponents(data).done(r => { + let projects = r.components; + if (this.state.page > 1) { + projects = [].concat(this.state.projects, projects); + } + this.setState({ projects: projects, total: r.paging.total }); + }); + }, + + loadMore() { + this.setState({ page: this.state.page + 1 }, this.requestProjects); + }, + + onSearch(query) { + this.setState({ + page: 1, + query, + selection: [] + }, this.requestProjects); + }, + + onTypeChanged(newType) { + this.setState({ + page: 1, + query: '', + type: newType, + qualifiers: 'TRK', + selection: [] + }, this.requestProjects); + }, + + onQualifierChanged(newQualifier) { + this.setState({ + page: 1, + query: '', + type: TYPE.ALL, + qualifiers: newQualifier, + selection: [] + }, this.requestProjects); + }, + + onProjectSelected(project) { + let newSelection = _.uniq([].concat(this.state.selection, project.id)); + this.setState({ selection: newSelection }); + }, + + onProjectDeselected(project) { + let newSelection = _.without(this.state.selection, project.id); + this.setState({ selection: newSelection }); + }, + + onAllSelected() { + let newSelection = this.state.projects.map(project => { + return project.id; + }); + this.setState({ selection: newSelection }); + }, + + onAllDeselected() { + this.setState({ selection: [] }); + }, + + deleteProjects() { + let ids = this.state.selection.join(','); + deleteComponents({ ids }).done(() => { + this.setState({ selection: [] }, this.requestProjects); + }); + }, + + render() { + return ( +
+
+ + + + + + +
+ ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/projects/projects.js b/server/sonar-web/src/main/js/apps/projects/projects.js new file mode 100644 index 00000000000..6aa42f53c0b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/projects.js @@ -0,0 +1,52 @@ +import React from 'react'; +import {getProjectUrl} from '../../helpers/Url'; +import Checkbox from '../../components/shared/checkbox'; +import QualifierIcon from '../../components/shared/qualifier-icon'; + +export default React.createClass({ + propTypes: { + projects: React.PropTypes.array.isRequired, + selection: React.PropTypes.array.isRequired, + refresh: React.PropTypes.func.isRequired + }, + + onProjectCheck(project, checked) { + if (checked) { + this.props.onProjectSelected(project); + } else { + this.props.onProjectDeselected(project); + } + }, + + isProjectSelected(project) { + return this.props.selection.indexOf(project.id) !== -1; + }, + + renderProject(project) { + return ( + + + + + + + + + {project.name} + + + {project.key} + + + ); + }, + + render() { + return ( + + {this.props.projects.map(this.renderProject)} +
+ ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/projects/search.js b/server/sonar-web/src/main/js/apps/projects/search.js new file mode 100644 index 00000000000..5c5668af1e5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/search.js @@ -0,0 +1,118 @@ +import _ from 'underscore'; +import React from 'react'; +import {TYPE, QUALIFIERS_ORDER} from './constants'; +import DeleteView from './delete-view'; +import RadioToggle from '../../components/shared/radio-toggle'; +import Checkbox from '../../components/shared/checkbox'; + +export default React.createClass({ + propTypes: { + onSearch: React.PropTypes.func.isRequired + }, + + onSubmit(e) { + e.preventDefault(); + this.search(); + }, + + search() { + let q = React.findDOMNode(this.refs.input).value; + this.props.onSearch(q); + }, + + getTypeOptions() { + return [ + { value: TYPE.ALL, label: 'All' }, + { value: TYPE.PROVISIONED, label: 'Provisioned' }, + { value: TYPE.GHOSTS, label: 'Ghosts' } + ]; + }, + + getQualifierOptions() { + let options = this.props.topLevelQualifiers.map(q => { + return { value: q, label: window.t('qualifiers', q) }; + }); + return _.sortBy(options, option => { + return QUALIFIERS_ORDER.indexOf(option.value); + }); + }, + + renderCheckbox() { + let isAllChecked = this.props.projects.length > 0 && + this.props.selection.length === this.props.projects.length, + thirdState = this.props.projects.length > 0 && + this.props.selection.length > 0 && + this.props.selection.length < this.props.projects.length, + isChecked = isAllChecked || thirdState; + return ; + }, + + onCheck(checked) { + if (checked) { + this.props.onAllSelected(); + } else { + this.props.onAllDeselected(); + } + }, + + renderGhostsDescription () { + if (this.props.type !== TYPE.GHOSTS) { + return null; + } + return
{window.t('bulk_deletion.ghosts.description')}
; + }, + + deleteProjects() { + new DeleteView({ + deleteProjects: this.props.deleteProjects + }).render(); + }, + + renderQualifierFilter() { + let options = this.getQualifierOptions(); + if (options.length < 2) { + return null; + } + return ( + + + + ); + }, + + render() { + let isSomethingSelected = this.props.projects.length > 0 && this.props.selection.length > 0; + return ( +
+ + + + {this.renderQualifierFilter()} + + + + +
+ {this.renderCheckbox()} + + + +
+ + +
+
+ +
+ {this.renderGhostsDescription()} +
+ ); + } +}); diff --git a/server/sonar-web/src/main/js/apps/projects/templates/projects-create-form.hbs b/server/sonar-web/src/main/js/apps/projects/templates/projects-create-form.hbs new file mode 100644 index 00000000000..e931b66efd7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/templates/projects-create-form.hbs @@ -0,0 +1,30 @@ +
+ + + +
diff --git a/server/sonar-web/src/main/js/apps/projects/templates/projects-delete.hbs b/server/sonar-web/src/main/js/apps/projects/templates/projects-delete.hbs new file mode 100644 index 00000000000..2ab69b28f72 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projects/templates/projects-delete.hbs @@ -0,0 +1,13 @@ +
+ + + +
diff --git a/server/sonar-web/src/main/js/components/shared/checkbox.jsx b/server/sonar-web/src/main/js/components/shared/checkbox.jsx index 8504da5038d..633c23c8354 100644 --- a/server/sonar-web/src/main/js/components/shared/checkbox.jsx +++ b/server/sonar-web/src/main/js/components/shared/checkbox.jsx @@ -3,7 +3,8 @@ import React from 'react'; export default React.createClass({ propTypes: { onCheck: React.PropTypes.func.isRequired, - initiallyChecked: React.PropTypes.bool + initiallyChecked: React.PropTypes.bool, + thirdState: React.PropTypes.bool }, getInitialState() { @@ -23,7 +24,14 @@ export default React.createClass({ }, render() { - const className = this.state.checked ? 'icon-checkbox icon-checkbox-checked' : 'icon-checkbox'; + let classNames = ['icon-checkbox']; + if (this.state.checked) { + classNames.push('icon-checkbox-checked'); + } + if (this.props.thirdState) { + classNames.push('icon-checkbox-single'); + } + let className = classNames.join(' '); return ; } }); diff --git a/server/sonar-web/src/main/less/components/page.less b/server/sonar-web/src/main/less/components/page.less index 46fbc5d3ec3..fd6f2f7f756 100644 --- a/server/sonar-web/src/main/less/components/page.less +++ b/server/sonar-web/src/main/less/components/page.less @@ -48,6 +48,8 @@ .page-actions { float: right; + margin-bottom: 10px; + margin-left: 10px; .badge { margin: 3px 0; @@ -55,7 +57,6 @@ } .page-description { - float: left; clear: left; font-size: @smallFontSize; line-height: 1.5; diff --git a/server/sonar-web/src/main/less/init/tables.less b/server/sonar-web/src/main/less/init/tables.less index cea19f373b8..4f63315ce92 100644 --- a/server/sonar-web/src/main/less/init/tables.less +++ b/server/sonar-web/src/main/less/init/tables.less @@ -37,6 +37,10 @@ table.data > tbody > tr > td { padding: 5px 5px; vertical-align: text-top; line-height: 16px; + + &.text-middle { + vertical-align: middle; + } } table.data td.small, table.data th.small { diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/projects_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/projects_controller.rb new file mode 100644 index 00000000000..6661201822c --- /dev/null +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/projects_controller.rb @@ -0,0 +1,30 @@ +# +# SonarQube, open source software quality management tool. +# Copyright (C) 2008-2014 SonarSource +# mailto:contact AT sonarsource DOT com +# +# SonarQube 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. +# +# SonarQube 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. +# +class ProjectsController < ApplicationController + + before_filter :admin_required + + SECTION=Navigation::SECTION_CONFIGURATION + + def index + + end + +end diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/projects/index.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/projects/index.html.erb new file mode 100644 index 00000000000..0ccfdf38a4e --- /dev/null +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/projects/index.html.erb @@ -0,0 +1,7 @@ +<% content_for :extra_script do %> + +<% end %> diff --git a/server/sonar-web/tests/apps/projects-test.js b/server/sonar-web/tests/apps/projects-test.js new file mode 100644 index 00000000000..0bb891f7496 --- /dev/null +++ b/server/sonar-web/tests/apps/projects-test.js @@ -0,0 +1,35 @@ +import React from 'react/addons'; +import Projects from '../../src/main/js/apps/projects/projects'; + +let TestUtils = React.addons.TestUtils; +let expect = require('chai').expect; +let sinon = require('sinon'); + +describe('Projects', function () { + describe('Projects', () => { + it('should render list of projects with no selection', () => { + let projects = [ + { id: '1', key: 'a', name: 'A', qualifier: 'TRK' }, + { id: '2', key: 'b', name: 'B', qualifier: 'TRK' } + ]; + + let result = TestUtils.renderIntoDocument( + ); + expect(TestUtils.scryRenderedDOMComponentsWithTag(result, 'tr')).to.have.length(2); + expect(TestUtils.scryRenderedDOMComponentsWithClass(result, 'icon-checkbox-checked')).to.be.empty; + }); + + it('should render list of projects with one selected', () => { + let projects = [ + { id: '1', key: 'a', name: 'A', qualifier: 'TRK' }, + { id: '2', key: 'b', name: 'B', qualifier: 'TRK' } + ], + selection = ['1']; + + let result = TestUtils.renderIntoDocument( + ); + expect(TestUtils.scryRenderedDOMComponentsWithTag(result, 'tr')).to.have.length(2); + expect(TestUtils.scryRenderedDOMComponentsWithClass(result, 'icon-checkbox-checked')).to.have.length(1); + }); + }); +}); diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 82e2eabf235..cfc4626d9c9 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2182,7 +2182,7 @@ bulk_deletion.deletion_manager.deletion_completed=Component deletion completed. bulk_deletion.deletion_manager.however_failures_occurred=However, some failures occurred. bulk_deletion.started_since_x=Started {0} ago bulk_deletion.ghosts=Ghosts -bulk_deletion.ghosts.description=A ghost is the result of constantly failed attempts to analyse a project. In such a case, the project is not linked to any successful analysis, and therefore cannot be displayed in SonarQube.
When the user authentication is forced, leaving a ghost can even prevent further analyses of the corresponding project. +bulk_deletion.ghosts.description=A ghost is the result of constantly failed attempts to analyse a project. In such a case, the project is not linked to any successful analysis, and therefore cannot be displayed in SonarQube. When the user authentication is forced, leaving a ghost can even prevent further analyses of the corresponding project. bulk_deletion.no_ghosts=There is currently no ghost. bulk_deletion.following_ghosts_can_be_deleted=The following ghosts can be safely deleted: bulk_deletion.delete_all_ghosts=Delete all ghosts -- 2.39.5