aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-10-01 12:06:21 +0200
committerStas Vilchik <vilchiks@gmail.com>2015-10-02 10:06:00 +0200
commit3aa4e0789f603f3c579a5f0184fa024afd3792fa (patch)
tree56f90d9d920db588cbd7c486901b08363df8dca4 /server/sonar-web/src/main/js/apps
parentbcb4b7ae52e9cda07ea97c3bea08ac50a779e1d7 (diff)
downloadsonarqube-3aa4e0789f603f3c579a5f0184fa024afd3792fa.tar.gz
sonarqube-3aa4e0789f603f3c579a5f0184fa024afd3792fa.zip
SONAR-6848 Merge the "Bulk Deletion" and "Provisioning" pages
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
-rw-r--r--server/sonar-web/src/main/js/apps/nav/settings/settings-nav.jsx5
-rw-r--r--server/sonar-web/src/main/js/apps/projects/app.js21
-rw-r--r--server/sonar-web/src/main/js/apps/projects/constants.js9
-rw-r--r--server/sonar-web/src/main/js/apps/projects/create-view.js46
-rw-r--r--server/sonar-web/src/main/js/apps/projects/delete-view.js14
-rw-r--r--server/sonar-web/src/main/js/apps/projects/form-view.js23
-rw-r--r--server/sonar-web/src/main/js/apps/projects/header.js33
-rw-r--r--server/sonar-web/src/main/js/apps/projects/main.js190
-rw-r--r--server/sonar-web/src/main/js/apps/projects/projects.js52
-rw-r--r--server/sonar-web/src/main/js/apps/projects/search.js118
-rw-r--r--server/sonar-web/src/main/js/apps/projects/templates/projects-create-form.hbs30
-rw-r--r--server/sonar-web/src/main/js/apps/projects/templates/projects-delete.hbs13
12 files changed, 553 insertions, 1 deletions
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')}&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>
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>