@@ -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: |
@@ -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); | |||
} |
@@ -0,0 +1,6 @@ | |||
import $ from 'jquery'; | |||
export function getGlobalNavigation () { | |||
let url = baseUrl + '/api/navigation/global'; | |||
return $.get(url); | |||
} |
@@ -0,0 +1,6 @@ | |||
import $ from 'jquery'; | |||
export function getCurrentUser () { | |||
let url = baseUrl + '/api/users/current'; | |||
return $.get(url); | |||
} |
@@ -55,10 +55,13 @@ export default React.createClass({ | |||
{window.t('sidebar.projects')} <i className="icon-dropdown"></i> | |||
</a> | |||
<ul className="dropdown-menu"> | |||
{this.renderNewLink('/projects', 'Management')} | |||
{this.renderNewLink('/background_tasks', 'Background Tasks')} | |||
<li className="divider"/> | |||
{this.state.showProvisioning ? this.renderLink('/provisioning', window.t('provisioning.page')) : null} | |||
{this.renderLink('/bulk_deletion', window.t('bulk_deletion.page'))} | |||
{this.renderLink('/computation', window.t('analysis_reports.page'))} | |||
{this.renderNewLink('/background_tasks', 'Background Tasks')} | |||
</ul> | |||
</li> | |||
@@ -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(<Main hasProvisionPermission={hasProvisionPermission} | |||
topLevelQualifiers={topLevelQualifiers}/>, el); | |||
}); | |||
} | |||
}; |
@@ -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' | |||
}; |
@@ -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 }]); | |||
}); | |||
} | |||
}); |
@@ -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(); | |||
} | |||
}); | |||
@@ -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(); | |||
} | |||
}); | |||
@@ -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 <button onClick={this.createProject}>Create Project</button>; | |||
}, | |||
render() { | |||
return ( | |||
<header className="page-header"> | |||
<h1 className="page-title">Projects Management</h1> | |||
<div className="page-actions">{this.renderCreateButton()}</div> | |||
<p className="page-description">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.</p> | |||
</header> | |||
); | |||
} | |||
}); |
@@ -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 ( | |||
<div className="page"> | |||
<Header | |||
hasProvisionPermission={this.props.hasProvisionPermission} | |||
refresh={this.requestProjects}/> | |||
<Search {...this.props} {...this.state} | |||
onSearch={this.onSearch} | |||
onTypeChanged={this.onTypeChanged} | |||
onQualifierChanged={this.onQualifierChanged} | |||
onAllSelected={this.onAllSelected} | |||
onAllDeselected={this.onAllDeselected} | |||
deleteProjects={this.deleteProjects}/> | |||
<Projects | |||
projects={this.state.projects} | |||
refresh={this.requestProjects} | |||
selection={this.state.selection} | |||
onProjectSelected={this.onProjectSelected} | |||
onProjectDeselected={this.onProjectDeselected}/> | |||
<ListFooter | |||
count={this.state.projects.length} | |||
total={this.state.total} | |||
loadMore={this.loadMore}/> | |||
</div> | |||
); | |||
} | |||
}); |
@@ -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 ( | |||
<tr key={project.id}> | |||
<td className="thin"> | |||
<Checkbox onCheck={this.onProjectCheck.bind(this, project)} | |||
initiallyChecked={this.isProjectSelected(project)}/> | |||
</td> | |||
<td className="thin"> | |||
<QualifierIcon qualifier={project.qualifier}/> | |||
</td> | |||
<td className="nowrap"> | |||
<a href={getProjectUrl(project.key)}>{project.name}</a> | |||
</td> | |||
<td className="nowrap"> | |||
<span className="note">{project.key}</span> | |||
</td> | |||
</tr> | |||
); | |||
}, | |||
render() { | |||
return ( | |||
<table className="data zebra"> | |||
<tbody>{this.props.projects.map(this.renderProject)}</tbody> | |||
</table> | |||
); | |||
} | |||
}); |
@@ -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 <Checkbox onCheck={this.onCheck} initiallyChecked={isChecked} thirdState={thirdState}/>; | |||
}, | |||
onCheck(checked) { | |||
if (checked) { | |||
this.props.onAllSelected(); | |||
} else { | |||
this.props.onAllDeselected(); | |||
} | |||
}, | |||
renderGhostsDescription () { | |||
if (this.props.type !== TYPE.GHOSTS) { | |||
return null; | |||
} | |||
return <div className="spacer-top alert alert-info">{window.t('bulk_deletion.ghosts.description')}</div>; | |||
}, | |||
deleteProjects() { | |||
new DeleteView({ | |||
deleteProjects: this.props.deleteProjects | |||
}).render(); | |||
}, | |||
renderQualifierFilter() { | |||
let options = this.getQualifierOptions(); | |||
if (options.length < 2) { | |||
return null; | |||
} | |||
return ( | |||
<td className="thin nowrap text-middle"> | |||
<RadioToggle options={this.getQualifierOptions()} value={this.props.qualifiers} | |||
name="projects-qualifier" onCheck={this.props.onQualifierChanged}/> | |||
</td> | |||
); | |||
}, | |||
render() { | |||
let isSomethingSelected = this.props.projects.length > 0 && this.props.selection.length > 0; | |||
return ( | |||
<div className="panel panel-vertical bordered-bottom spacer-bottom"> | |||
<table className="data"> | |||
<tr> | |||
<td className="thin text-middle"> | |||
{this.renderCheckbox()} | |||
</td> | |||
{this.renderQualifierFilter()} | |||
<td className="thin nowrap text-middle"> | |||
<RadioToggle options={this.getTypeOptions()} value={this.props.type} | |||
name="projects-type" onCheck={this.props.onTypeChanged}/> | |||
</td> | |||
<td className="text-middle"> | |||
<form onSubmit={this.onSubmit} className="search-box"> | |||
<button className="search-box-submit button-clean"> | |||
<i className="icon-search"></i> | |||
</button> | |||
<input onChange={this.search} value={this.props.query} ref="input" className="search-box-input" | |||
type="search" placeholder="Search"/> | |||
</form> | |||
</td> | |||
<td className="thin text-middle"> | |||
<button onClick={this.deleteProjects} className="button-red" | |||
disabled={!isSomethingSelected}>Delete | |||
</button> | |||
</td> | |||
</tr> | |||
</table> | |||
{this.renderGhostsDescription()} | |||
</div> | |||
); | |||
} | |||
}); |
@@ -0,0 +1,30 @@ | |||
<form id="create-project-form" autocomplete="off"> | |||
<div class="modal-head"> | |||
<h2>Create Project</h2> | |||
</div> | |||
<div class="modal-body"> | |||
<div class="js-modal-messages"></div> | |||
<div class="modal-field"> | |||
<label for="create-project-name">Name<em class="mandatory">*</em></label> | |||
{{! keep this fake field to hack browser autofill }} | |||
<input id="create-project-name-fake" name="name-fake" type="text" class="hidden"> | |||
<input id="create-project-name" name="name" type="text" size="50" maxlength="200" required> | |||
</div> | |||
<div class="modal-field"> | |||
<label for="create-project-branch">Branch</label> | |||
{{! keep this fake field to hack browser autofill }} | |||
<input id="create-project-branch-fake" name="branch-fake" type="text" class="hidden"> | |||
<input id="create-project-branch" name="branch" type="text" size="50" maxlength="200"> | |||
</div> | |||
<div class="modal-field"> | |||
<label for="create-project-key">Key<em class="mandatory">*</em></label> | |||
{{! keep this fake field to hack browser autofill }} | |||
<input id="create-project-key-fake" name="key-fake" type="text" class="hidden"> | |||
<input id="create-project-key" name="key" type="text" size="50" maxlength="50" required> | |||
</div> | |||
</div> | |||
<div class="modal-foot"> | |||
<button id="create-project-submit">Create</button> | |||
<a href="#" class="js-modal-close" id="create-project-cancel">Cancel</a> | |||
</div> | |||
</form> |
@@ -0,0 +1,13 @@ | |||
<form id="delete-project-form"> | |||
<div class="modal-head"> | |||
<h2>Delete Projects</h2> | |||
</div> | |||
<div class="modal-body"> | |||
<div class="js-modal-messages"></div> | |||
Are you sure you want to delete selected projects? | |||
</div> | |||
<div class="modal-foot"> | |||
<button id="delete-project-submit" class="button-red">Delete</button> | |||
<a href="#" class="js-modal-close" id="delete-project-cancel">Cancel</a> | |||
</div> | |||
</form> |
@@ -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 <a onClick={this.toggle} className={className} href="#"/>; | |||
} | |||
}); |
@@ -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; |
@@ -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 { |
@@ -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 |
@@ -0,0 +1,7 @@ | |||
<% content_for :extra_script do %> | |||
<script> | |||
require(['apps/projects/app'], function (App) { | |||
App.start({ el: '#content' }); | |||
}); | |||
</script> | |||
<% end %> |
@@ -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( | |||
<Projects projects={projects} selection={[]} refresh={sinon.spy()}/>); | |||
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( | |||
<Projects projects={projects} selection={selection} refresh={sinon.spy()}/>); | |||
expect(TestUtils.scryRenderedDOMComponentsWithTag(result, 'tr')).to.have.length(2); | |||
expect(TestUtils.scryRenderedDOMComponentsWithClass(result, 'icon-checkbox-checked')).to.have.length(1); | |||
}); | |||
}); | |||
}); |
@@ -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.<br/>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 |