Browse Source

SONAR-6848 Merge the "Bulk Deletion" and "Provisioning" pages

tags/5.2-RC1
Stas Vilchik 8 years ago
parent
commit
3aa4e0789f
23 changed files with 685 additions and 5 deletions
  1. 4
    0
      server/sonar-web/Gruntfile.coffee
  2. 27
    0
      server/sonar-web/src/main/js/api/components.js
  3. 6
    0
      server/sonar-web/src/main/js/api/nav.js
  4. 6
    0
      server/sonar-web/src/main/js/api/users.js
  5. 4
    1
      server/sonar-web/src/main/js/apps/nav/settings/settings-nav.jsx
  6. 21
    0
      server/sonar-web/src/main/js/apps/projects/app.js
  7. 9
    0
      server/sonar-web/src/main/js/apps/projects/constants.js
  8. 46
    0
      server/sonar-web/src/main/js/apps/projects/create-view.js
  9. 14
    0
      server/sonar-web/src/main/js/apps/projects/delete-view.js
  10. 23
    0
      server/sonar-web/src/main/js/apps/projects/form-view.js
  11. 33
    0
      server/sonar-web/src/main/js/apps/projects/header.js
  12. 190
    0
      server/sonar-web/src/main/js/apps/projects/main.js
  13. 52
    0
      server/sonar-web/src/main/js/apps/projects/projects.js
  14. 118
    0
      server/sonar-web/src/main/js/apps/projects/search.js
  15. 30
    0
      server/sonar-web/src/main/js/apps/projects/templates/projects-create-form.hbs
  16. 13
    0
      server/sonar-web/src/main/js/apps/projects/templates/projects-delete.hbs
  17. 10
    2
      server/sonar-web/src/main/js/components/shared/checkbox.jsx
  18. 2
    1
      server/sonar-web/src/main/less/components/page.less
  19. 4
    0
      server/sonar-web/src/main/less/init/tables.less
  20. 30
    0
      server/sonar-web/src/main/webapp/WEB-INF/app/controllers/projects_controller.rb
  21. 7
    0
      server/sonar-web/src/main/webapp/WEB-INF/app/views/projects/index.html.erb
  22. 35
    0
      server/sonar-web/tests/apps/projects-test.js
  23. 1
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 4
- 0
server/sonar-web/Gruntfile.coffee View File

@@ -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:

+ 27
- 0
server/sonar-web/src/main/js/api/components.js View File

@@ -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);
}

+ 6
- 0
server/sonar-web/src/main/js/api/nav.js View File

@@ -0,0 +1,6 @@
import $ from 'jquery';

export function getGlobalNavigation () {
let url = baseUrl + '/api/navigation/global';
return $.get(url);
}

+ 6
- 0
server/sonar-web/src/main/js/api/users.js View File

@@ -0,0 +1,6 @@
import $ from 'jquery';

export function getCurrentUser () {
let url = baseUrl + '/api/users/current';
return $.get(url);
}

+ 4
- 1
server/sonar-web/src/main/js/apps/nav/settings/settings-nav.jsx View File

@@ -55,10 +55,13 @@ export default React.createClass({
{window.t('sidebar.projects')}&nbsp;<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>


+ 21
- 0
server/sonar-web/src/main/js/apps/projects/app.js View File

@@ -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);
});
}
};

+ 9
- 0
server/sonar-web/src/main/js/apps/projects/constants.js View File

@@ -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'
};

+ 46
- 0
server/sonar-web/src/main/js/apps/projects/create-view.js View File

@@ -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 }]);
});
}
});

+ 14
- 0
server/sonar-web/src/main/js/apps/projects/delete-view.js View File

@@ -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();
}
});



+ 23
- 0
server/sonar-web/src/main/js/apps/projects/form-view.js View File

@@ -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();
}

});



+ 33
- 0
server/sonar-web/src/main/js/apps/projects/header.js View File

@@ -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>
);
}
});

+ 190
- 0
server/sonar-web/src/main/js/apps/projects/main.js View File

@@ -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>
);
}
});

+ 52
- 0
server/sonar-web/src/main/js/apps/projects/projects.js View File

@@ -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>
);
}
});

+ 118
- 0
server/sonar-web/src/main/js/apps/projects/search.js View File

@@ -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>
);
}
});

+ 30
- 0
server/sonar-web/src/main/js/apps/projects/templates/projects-create-form.hbs View File

@@ -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>

+ 13
- 0
server/sonar-web/src/main/js/apps/projects/templates/projects-delete.hbs View File

@@ -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>

+ 10
- 2
server/sonar-web/src/main/js/components/shared/checkbox.jsx View File

@@ -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="#"/>;
}
});

+ 2
- 1
server/sonar-web/src/main/less/components/page.less View File

@@ -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;

+ 4
- 0
server/sonar-web/src/main/less/init/tables.less View File

@@ -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 {

+ 30
- 0
server/sonar-web/src/main/webapp/WEB-INF/app/controllers/projects_controller.rb View File

@@ -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

+ 7
- 0
server/sonar-web/src/main/webapp/WEB-INF/app/views/projects/index.html.erb View File

@@ -0,0 +1,7 @@
<% content_for :extra_script do %>
<script>
require(['apps/projects/app'], function (App) {
App.start({ el: '#content' });
});
</script>
<% end %>

+ 35
- 0
server/sonar-web/tests/apps/projects-test.js View File

@@ -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);
});
});
});

+ 1
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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

Loading…
Cancel
Save