diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2015-10-01 12:06:21 +0200 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2015-10-02 10:06:00 +0200 |
commit | 3aa4e0789f603f3c579a5f0184fa024afd3792fa (patch) | |
tree | 56f90d9d920db588cbd7c486901b08363df8dca4 | |
parent | bcb4b7ae52e9cda07ea97c3bea08ac50a779e1d7 (diff) | |
download | sonarqube-3aa4e0789f603f3c579a5f0184fa024afd3792fa.tar.gz sonarqube-3aa4e0789f603f3c579a5f0184fa024afd3792fa.zip |
SONAR-6848 Merge the "Bulk Deletion" and "Provisioning" pages
23 files changed, 685 insertions, 5 deletions
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')} <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> 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(<Main hasProvisionPermission={hasProvisionPermission} + topLevelQualifiers={topLevelQualifiers}/>, 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 <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> + ); + } +}); 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 ( + <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> + ); + } +}); 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 ( + <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> + ); + } +}); 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 <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> + ); + } +}); 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 @@ +<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> 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 @@ +<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> 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 <a onClick={this.toggle} className={className} href="#"/>; } }); 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 %> + <script> + require(['apps/projects/app'], function (App) { + App.start({ el: '#content' }); + }); + </script> +<% 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( + <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); + }); + }); +}); 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.<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 |