aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2015-11-16 15:46:23 +0100
committerStas Vilchik <vilchiks@gmail.com>2015-11-16 16:43:49 +0100
commit9477e9e64502bcd33836e3ea8f7ae0bb3b53baec (patch)
tree92742343754ef5a4ecfc41ff6c449ea282c28775
parent8f1d094510778a31f02f797db624f5d5896d1b0c (diff)
downloadsonarqube-9477e9e64502bcd33836e3ea8f7ae0bb3b53baec.tar.gz
sonarqube-9477e9e64502bcd33836e3ea8f7ae0bb3b53baec.zip
SONAR-6972 improve loading UX of administration pages
-rw-r--r--server/sonar-web/src/main/js/api/components.js19
-rw-r--r--server/sonar-web/src/main/js/apps/global-permissions/main.js14
-rw-r--r--server/sonar-web/src/main/js/apps/global-permissions/permissions-list.js6
-rw-r--r--server/sonar-web/src/main/js/apps/groups/header-view.js13
-rw-r--r--server/sonar-web/src/main/js/apps/groups/list-view.js15
-rw-r--r--server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs1
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/header.js8
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/main.js7
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/permission-templates.js6
-rw-r--r--server/sonar-web/src/main/js/apps/project-permissions/main.js39
-rw-r--r--server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.js5
-rw-r--r--server/sonar-web/src/main/js/apps/project-permissions/permissions.js6
-rw-r--r--server/sonar-web/src/main/js/apps/projects/create-view.js31
-rw-r--r--server/sonar-web/src/main/js/apps/projects/main.js22
-rw-r--r--server/sonar-web/src/main/js/apps/projects/projects.js4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/search.js8
-rw-r--r--server/sonar-web/src/main/js/apps/users/header-view.js13
-rw-r--r--server/sonar-web/src/main/js/apps/users/list-view.js15
-rw-r--r--server/sonar-web/src/main/js/apps/users/templates/users-header.hbs1
-rw-r--r--server/sonar-web/src/main/js/components/shared/list-footer.js15
-rw-r--r--server/sonar-web/src/main/js/helpers/request.js10
-rw-r--r--server/sonar-web/src/main/less/components/page.less6
-rw-r--r--server/sonar-web/src/main/less/init/misc.less5
23 files changed, 204 insertions, 65 deletions
diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js
index a4eb949a2d7..e4f60f8110b 100644
--- a/server/sonar-web/src/main/js/api/components.js
+++ b/server/sonar-web/src/main/js/api/components.js
@@ -1,30 +1,29 @@
-import { getJSON } from '../helpers/request.js';
-import $ from 'jquery';
+import { getJSON, postJSON, post } from '../helpers/request.js';
+
export function getComponents (data) {
let url = baseUrl + '/api/components/search';
- return $.get(url, data);
+ return getJSON(url, data);
}
export function getProvisioned (data) {
let url = baseUrl + '/api/projects/provisioned';
- return $.get(url, data);
+ return getJSON(url, data);
}
export function getGhosts (data) {
let url = baseUrl + '/api/projects/ghosts';
- return $.get(url, data);
+ return getJSON(url, data);
}
export function deleteComponents (data) {
let url = baseUrl + '/api/projects/bulk_delete';
- return $.post(url, data);
+ return post(url, data);
}
-export function createProject (options) {
- options.url = baseUrl + '/api/projects/create';
- options.type = 'POST';
- return $.ajax(options);
+export function createProject (data) {
+ let url = baseUrl + '/api/projects/create';
+ return postJSON(url, data);
}
export function getChildren (componentKey, metrics = []) {
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/main.js b/server/sonar-web/src/main/js/apps/global-permissions/main.js
index 11f6b032ebf..99efd69cbc2 100644
--- a/server/sonar-web/src/main/js/apps/global-permissions/main.js
+++ b/server/sonar-web/src/main/js/apps/global-permissions/main.js
@@ -4,7 +4,7 @@ import PermissionsList from './permissions-list';
export default React.createClass({
getInitialState() {
- return { permissions: [] };
+ return { ready: false, permissions: [] };
},
componentDidMount() {
@@ -14,18 +14,26 @@ export default React.createClass({
requestPermissions() {
const url = `${window.baseUrl}/api/permissions/search_global_permissions`;
$.get(url).done(r => {
- this.setState({ permissions: r.permissions });
+ this.setState({ ready: true, permissions: r.permissions });
});
},
+ renderSpinner () {
+ if (this.state.ready) {
+ return null;
+ }
+ return <i className="spinner"/>;
+ },
+
render() {
return (
<div className="page">
<header id="global-permissions-header" className="page-header">
<h1 className="page-title">{window.t('global_permissions.page')}</h1>
+ {this.renderSpinner()}
<p className="page-description">{window.t('global_permissions.page.description')}</p>
</header>
- <PermissionsList permissions={this.state.permissions}/>
+ <PermissionsList ready={this.state.ready} permissions={this.state.permissions}/>
</div>
);
}
diff --git a/server/sonar-web/src/main/js/apps/global-permissions/permissions-list.js b/server/sonar-web/src/main/js/apps/global-permissions/permissions-list.js
index e019fbcbfec..c48769daab6 100644
--- a/server/sonar-web/src/main/js/apps/global-permissions/permissions-list.js
+++ b/server/sonar-web/src/main/js/apps/global-permissions/permissions-list.js
@@ -1,6 +1,9 @@
+import classNames from 'classnames';
import React from 'react';
+
import Permission from './permission';
+
export default React.createClass({
propTypes:{
permissions: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
@@ -13,6 +16,7 @@ export default React.createClass({
},
render() {
- return <ul id="global-permissions-list">{this.renderPermissions()}</ul>;
+ let className = classNames({ 'new-loading': !this.props.ready });
+ return <ul id="global-permissions-list" className={className}>{this.renderPermissions()}</ul>;
}
});
diff --git a/server/sonar-web/src/main/js/apps/groups/header-view.js b/server/sonar-web/src/main/js/apps/groups/header-view.js
index e4a118f2822..3ac152c4e98 100644
--- a/server/sonar-web/src/main/js/apps/groups/header-view.js
+++ b/server/sonar-web/src/main/js/apps/groups/header-view.js
@@ -5,10 +5,23 @@ import Template from './templates/groups-header.hbs';
export default Marionette.ItemView.extend({
template: Template,
+ collectionEvents: {
+ 'request': 'showSpinner',
+ 'sync': 'hideSpinner'
+ },
+
events: {
'click #groups-create': 'onCreateClick'
},
+ showSpinner: function () {
+ this.$('.spinner').removeClass('hidden');
+ },
+
+ hideSpinner: function () {
+ this.$('.spinner').addClass('hidden');
+ },
+
onCreateClick: function (e) {
e.preventDefault();
this.createGroup();
diff --git a/server/sonar-web/src/main/js/apps/groups/list-view.js b/server/sonar-web/src/main/js/apps/groups/list-view.js
index 699e9c76a85..22f699697e9 100644
--- a/server/sonar-web/src/main/js/apps/groups/list-view.js
+++ b/server/sonar-web/src/main/js/apps/groups/list-view.js
@@ -3,7 +3,20 @@ import ListItemView from './list-item-view';
export default Marionette.CollectionView.extend({
tagName: 'ul',
- childView: ListItemView
+ childView: ListItemView,
+
+ collectionEvents: {
+ 'request': 'showLoading',
+ 'sync': 'hideLoading'
+ },
+
+ showLoading: function () {
+ this.$el.addClass('new-loading');
+ },
+
+ hideLoading: function () {
+ this.$el.removeClass('new-loading');
+ }
});
diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs
index 19ba74febf8..94cf4a1ec34 100644
--- a/server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs
+++ b/server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs
@@ -1,5 +1,6 @@
<header class="page-header">
<h1 class="page-title">{{t 'user_groups.page'}}</h1>
+ <i class="spinner hidden"></i>
<div class="page-actions">
<div class="button-group">
<button id="groups-create">Create Group</button>
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/header.js b/server/sonar-web/src/main/js/apps/permission-templates/header.js
index 0325d4bf6cb..eb367d830bf 100644
--- a/server/sonar-web/src/main/js/apps/permission-templates/header.js
+++ b/server/sonar-web/src/main/js/apps/permission-templates/header.js
@@ -9,10 +9,18 @@ export default React.createClass({
}).render();
},
+ renderSpinner () {
+ if (this.props.ready) {
+ return null;
+ }
+ return <i className="spinner"/>;
+ },
+
render() {
return (
<header id="project-permissions-header" className="page-header">
<h1 className="page-title">{window.t('permission_templates.page')}</h1>
+ {this.renderSpinner()}
<div className="page-actions">
<button onClick={this.onCreate}>Create</button>
</div>
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/main.js b/server/sonar-web/src/main/js/apps/permission-templates/main.js
index 1a0abfc8ead..5032bef5547 100644
--- a/server/sonar-web/src/main/js/apps/permission-templates/main.js
+++ b/server/sonar-web/src/main/js/apps/permission-templates/main.js
@@ -12,7 +12,7 @@ export default React.createClass({
},
getInitialState() {
- return { permissions: [], permissionTemplates: [] };
+ return { ready: false, permissions: [], permissionTemplates: [] };
},
componentDidMount() {
@@ -53,6 +53,7 @@ export default React.createClass({
let permissionTemplates = this.mergePermissionsToTemplates(r.permissionTemplates, permissions);
let permissionTemplatesWithDefaults = this.mergeDefaultsToTemplates(permissionTemplates, r.defaultTemplates);
this.setState({
+ ready: true,
permissionTemplates: permissionTemplatesWithDefaults,
permissions: permissions
});
@@ -62,10 +63,10 @@ export default React.createClass({
render() {
return (
<div className="page">
- <Header
- refresh={this.requestPermissions}/>
+ <Header ready={this.state.ready} refresh={this.requestPermissions}/>
<PermissionTemplates
+ ready={this.state.ready}
permissionTemplates={this.state.permissionTemplates}
permissions={this.state.permissions}
topQualifiers={this.props.topQualifiers}
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/permission-templates.js b/server/sonar-web/src/main/js/apps/permission-templates/permission-templates.js
index a86379e256d..030fec04c2f 100644
--- a/server/sonar-web/src/main/js/apps/permission-templates/permission-templates.js
+++ b/server/sonar-web/src/main/js/apps/permission-templates/permission-templates.js
@@ -1,7 +1,10 @@
+import classNames from 'classnames';
import React from 'react';
+
import PermissionsHeader from './permissions-header';
import PermissionTemplate from './permission-template';
+
export default React.createClass({
propTypes:{
permissionTemplates: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
@@ -18,8 +21,9 @@ export default React.createClass({
topQualifiers={this.props.topQualifiers}
refresh={this.props.refresh}/>;
});
+ let className = classNames('data zebra', { 'new-loading': !this.props.ready });
return (
- <table id="permission-templates" className="data zebra">
+ <table id="permission-templates" className={className}>
<PermissionsHeader permissions={this.props.permissions}/>
<tbody>{permissionTemplates}</tbody>
</table>
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/main.js b/server/sonar-web/src/main/js/apps/project-permissions/main.js
index 2c248f81344..6dbc6a4c837 100644
--- a/server/sonar-web/src/main/js/apps/project-permissions/main.js
+++ b/server/sonar-web/src/main/js/apps/project-permissions/main.js
@@ -14,7 +14,7 @@ export default React.createClass({
},
getInitialState() {
- return { permissions: [], projects: [], total: 0 };
+ return { ready: false, permissions: [], projects: [], total: 0 };
},
componentDidMount() {
@@ -42,18 +42,21 @@ export default React.createClass({
if (this.props.componentId) {
data = { projectId: this.props.componentId };
}
- $.get(url, data).done(r => {
- let permissions = this.sortPermissions(r.permissions);
- let projects = this.mergePermissionsToProjects(r.projects, permissions);
- if (page > 1) {
- projects = [].concat(this.state.projects, projects);
- }
- this.setState({
- projects: projects,
- permissions: permissions,
- total: r.paging.total,
- page: r.paging.pageIndex,
- query: query
+ this.setState({ ready: false }, () => {
+ $.get(url, data).done(r => {
+ let permissions = this.sortPermissions(r.permissions);
+ let projects = this.mergePermissionsToProjects(r.projects, permissions);
+ if (page > 1) {
+ projects = [].concat(this.state.projects, projects);
+ }
+ this.setState({
+ ready: true,
+ projects: projects,
+ permissions: permissions,
+ total: r.paging.total,
+ page: r.paging.pageIndex,
+ query: query
+ });
});
});
},
@@ -88,11 +91,19 @@ export default React.createClass({
);
},
+ renderSpinner () {
+ if (this.state.ready) {
+ return null;
+ }
+ return <i className="spinner"/>;
+ },
+
render() {
return (
<div className="page">
<header id="project-permissions-header" className="page-header">
<h1 className="page-title">{window.t('roles.page')}</h1>
+ {this.renderSpinner()}
<div className="page-actions">
{this.renderBulkApplyButton()}
</div>
@@ -103,12 +114,14 @@ export default React.createClass({
search={this.search}/>
<Permissions
+ ready={this.state.ready}
projects={this.state.projects}
permissions={this.state.permissions}
permissionTemplates={this.props.permissionTemplates}
refresh={this.refresh}/>
<PermissionsFooter {...this.props}
+ ready={this.state.ready}
count={this.state.projects.length}
total={this.state.total}
loadMore={this.loadMore}/>
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.js b/server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.js
index cab1354e3ff..ad03541cbd6 100644
--- a/server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.js
+++ b/server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.js
@@ -1,5 +1,7 @@
+import classNames from 'classnames';
import React from 'react';
+
export default React.createClass({
propTypes:{
count: React.PropTypes.number.isRequired,
@@ -13,8 +15,9 @@ export default React.createClass({
}
let hasMore = this.props.total > this.props.count;
let loadMoreLink = <a onClick={this.props.loadMore} className="spacer-left" href="#">show more</a>;
+ let className = classNames('spacer-top note text-center', { 'new-loading': !this.props.ready });
return (
- <footer className="spacer-top note text-center">
+ <footer className={className}>
{this.props.count}/{this.props.total} shown
{hasMore ? loadMoreLink : null}
</footer>
diff --git a/server/sonar-web/src/main/js/apps/project-permissions/permissions.js b/server/sonar-web/src/main/js/apps/project-permissions/permissions.js
index 26da7da40d6..c3ae66271a4 100644
--- a/server/sonar-web/src/main/js/apps/project-permissions/permissions.js
+++ b/server/sonar-web/src/main/js/apps/project-permissions/permissions.js
@@ -1,7 +1,10 @@
+import classNames from 'classnames';
import React from 'react';
+
import PermissionsHeader from './permissions-header';
import Project from './project';
+
export default React.createClass({
propTypes:{
projects: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
@@ -18,8 +21,9 @@ export default React.createClass({
permissionTemplates={this.props.permissionTemplates}
refresh={this.props.refresh}/>;
});
+ let className = classNames('data zebra', { 'new-loading': !this.props.ready });
return (
- <table id="projects" className="data zebra">
+ <table id="projects" className={className}>
<PermissionsHeader permissions={this.props.permissions}/>
<tbody>{projects}</tbody>
</table>
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
index 89425a212f8..ce7b7d1f9f2 100644
--- a/server/sonar-web/src/main/js/apps/projects/create-view.js
+++ b/server/sonar-web/src/main/js/apps/projects/create-view.js
@@ -1,7 +1,8 @@
import ModalForm from '../../components/common/modal-form';
-import {createProject} from '../../api/components';
+import { createProject } from '../../api/components';
import Template from './templates/projects-create-form.hbs';
+
export default ModalForm.extend({
template: Template,
@@ -27,20 +28,18 @@ export default ModalForm.extend({
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 }]);
- });
+ return createProject(data)
+ .then(() => {
+ if (this.options.refresh) {
+ this.options.refresh();
+ }
+ this.destroy();
+ })
+ .catch(error => {
+ this.enableForm();
+ if (error.response.status === 400) {
+ error.response.json().then(obj => this.showErrors([{ msg: obj.err_msg }]));
+ }
+ });
}
});
diff --git a/server/sonar-web/src/main/js/apps/projects/main.js b/server/sonar-web/src/main/js/apps/projects/main.js
index 5db96f6ede9..051de7c515a 100644
--- a/server/sonar-web/src/main/js/apps/projects/main.js
+++ b/server/sonar-web/src/main/js/apps/projects/main.js
@@ -15,6 +15,7 @@ export default React.createClass({
getInitialState() {
return {
+ ready: false,
projects: [],
total: 0,
page: 1,
@@ -62,48 +63,49 @@ export default React.createClass({
requestGhosts() {
let data = this.getFilters();
- getGhosts(data).done(r => {
+ getGhosts(data).then(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 });
+ this.setState({ ready: true, projects: projects, total: r.total });
});
},
requestProvisioned() {
let data = this.getFilters();
- getProvisioned(data).done(r => {
+ getProvisioned(data).then(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 });
+ this.setState({ ready: true, projects: projects, total: r.total });
});
},
requestAllProjects() {
let data = this.getFilters();
data.qualifiers = this.state.qualifiers;
- getComponents(data).done(r => {
+ getComponents(data).then(r => {
let projects = r.components;
if (this.state.page > 1) {
projects = [].concat(this.state.projects, projects);
}
- this.setState({ projects: projects, total: r.paging.total });
+ this.setState({ ready: true, projects: projects, total: r.paging.total });
});
},
loadMore() {
- this.setState({ page: this.state.page + 1 }, this.requestProjects);
+ this.setState({ ready: false, page: this.state.page + 1 }, this.requestProjects);
},
onSearch(query) {
this.setState({
+ ready: false,
page: 1,
query,
selection: []
@@ -112,6 +114,7 @@ export default React.createClass({
onTypeChanged(newType) {
this.setState({
+ ready: false,
page: 1,
query: '',
type: newType,
@@ -122,6 +125,7 @@ export default React.createClass({
onQualifierChanged(newQualifier) {
this.setState({
+ ready: false,
page: 1,
query: '',
type: TYPE.ALL,
@@ -153,7 +157,7 @@ export default React.createClass({
deleteProjects() {
let ids = this.state.selection.join(',');
- deleteComponents({ ids }).done(() => {
+ deleteComponents({ ids }).then(() => {
this.setState({ page: 1, selection: [] }, this.requestProjects);
});
},
@@ -174,6 +178,7 @@ export default React.createClass({
deleteProjects={this.deleteProjects}/>
<Projects
+ ready={this.state.ready}
projects={this.state.projects}
refresh={this.requestProjects}
selection={this.state.selection}
@@ -181,6 +186,7 @@ export default React.createClass({
onProjectDeselected={this.onProjectDeselected}/>
<ListFooter
+ ready={this.state.ready}
count={this.state.projects.length}
total={this.state.total}
loadMore={this.loadMore}/>
diff --git a/server/sonar-web/src/main/js/apps/projects/projects.js b/server/sonar-web/src/main/js/apps/projects/projects.js
index 1f3babcd067..27432016944 100644
--- a/server/sonar-web/src/main/js/apps/projects/projects.js
+++ b/server/sonar-web/src/main/js/apps/projects/projects.js
@@ -1,3 +1,4 @@
+import classNames from 'classnames';
import React from 'react';
import { getComponentUrl } from '../../helpers/urls';
import Checkbox from '../../components/shared/checkbox';
@@ -43,8 +44,9 @@ export default React.createClass({
},
render() {
+ let className = classNames('data', 'zebra', { 'new-loading': !this.props.ready });
return (
- <table className="data zebra">
+ <table className={className}>
<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
index a5cebad2a63..02b7229421a 100644
--- a/server/sonar-web/src/main/js/apps/projects/search.js
+++ b/server/sonar-web/src/main/js/apps/projects/search.js
@@ -47,6 +47,10 @@ export default React.createClass({
return <Checkbox onCheck={this.onCheck} initiallyChecked={isChecked} thirdState={thirdState}/>;
},
+ renderSpinner() {
+ return <i className="spinner"/>;
+ },
+
onCheck(checked) {
if (checked) {
this.props.onAllSelected();
@@ -56,7 +60,7 @@ export default React.createClass({
},
renderGhostsDescription () {
- if (this.props.type !== TYPE.GHOSTS) {
+ if (this.props.type !== TYPE.GHOSTS || !this.props.ready) {
return null;
}
return <div className="spacer-top alert alert-info">{window.t('bulk_deletion.ghosts.description')}</div>;
@@ -89,7 +93,7 @@ export default React.createClass({
<tbody>
<tr>
<td className="thin text-middle">
- {this.renderCheckbox()}
+ {this.props.ready ? this.renderCheckbox() : this.renderSpinner()}
</td>
{this.renderQualifierFilter()}
<td className="thin nowrap text-middle">
diff --git a/server/sonar-web/src/main/js/apps/users/header-view.js b/server/sonar-web/src/main/js/apps/users/header-view.js
index 66e5df75b1a..85140d2ef7a 100644
--- a/server/sonar-web/src/main/js/apps/users/header-view.js
+++ b/server/sonar-web/src/main/js/apps/users/header-view.js
@@ -5,10 +5,23 @@ import Template from './templates/users-header.hbs';
export default Marionette.ItemView.extend({
template: Template,
+ collectionEvents: {
+ 'request': 'showSpinner',
+ 'sync': 'hideSpinner'
+ },
+
events: {
'click #users-create': 'onCreateClick'
},
+ showSpinner: function () {
+ this.$('.spinner').removeClass('hidden');
+ },
+
+ hideSpinner: function () {
+ this.$('.spinner').addClass('hidden');
+ },
+
onCreateClick: function (e) {
e.preventDefault();
this.createUser();
diff --git a/server/sonar-web/src/main/js/apps/users/list-view.js b/server/sonar-web/src/main/js/apps/users/list-view.js
index 699e9c76a85..22f699697e9 100644
--- a/server/sonar-web/src/main/js/apps/users/list-view.js
+++ b/server/sonar-web/src/main/js/apps/users/list-view.js
@@ -3,7 +3,20 @@ import ListItemView from './list-item-view';
export default Marionette.CollectionView.extend({
tagName: 'ul',
- childView: ListItemView
+ childView: ListItemView,
+
+ collectionEvents: {
+ 'request': 'showLoading',
+ 'sync': 'hideLoading'
+ },
+
+ showLoading: function () {
+ this.$el.addClass('new-loading');
+ },
+
+ hideLoading: function () {
+ this.$el.removeClass('new-loading');
+ }
});
diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-header.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-header.hbs
index e3560039288..66dff8a39b6 100644
--- a/server/sonar-web/src/main/js/apps/users/templates/users-header.hbs
+++ b/server/sonar-web/src/main/js/apps/users/templates/users-header.hbs
@@ -1,5 +1,6 @@
<header class="page-header">
<h1 class="page-title">{{t 'users.page'}}</h1>
+ <i class="spinner hidden"></i>
<div class="page-actions">
<div class="button-group">
<button id="users-create">Create User</button>
diff --git a/server/sonar-web/src/main/js/components/shared/list-footer.js b/server/sonar-web/src/main/js/components/shared/list-footer.js
index 31ba9e1f0d6..68922f39b2d 100644
--- a/server/sonar-web/src/main/js/components/shared/list-footer.js
+++ b/server/sonar-web/src/main/js/components/shared/list-footer.js
@@ -1,5 +1,7 @@
+import classNames from 'classnames';
import React from 'react';
+
export default React.createClass({
propTypes: {
count: React.PropTypes.number.isRequired,
@@ -11,18 +13,25 @@ export default React.createClass({
return typeof this.props.loadMore === 'function';
},
- loadMoreProxy(e) {
+ handleLoadMore(e) {
e.preventDefault();
if (this.canLoadMore()) {
this.props.loadMore();
}
},
+ renderLoading() {
+ return <footer className="spacer-top note text-center">
+ {window.t('loading')}
+ </footer>;
+ },
+
render() {
let hasMore = this.props.total > this.props.count,
- loadMoreLink = <a onClick={this.loadMoreProxy} className="spacer-left" href="#">show more</a>;
+ loadMoreLink = <a onClick={this.handleLoadMore} className="spacer-left" href="#">show more</a>;
+ let className = classNames('spacer-top note text-center', { 'new-loading': !this.props.ready });
return (
- <footer className="spacer-top note text-center">
+ <footer className={className}>
{this.props.count}/{this.props.total} shown
{this.canLoadMore() && hasMore ? loadMoreLink : null}
</footer>
diff --git a/server/sonar-web/src/main/js/helpers/request.js b/server/sonar-web/src/main/js/helpers/request.js
index 86e9b8242e3..8cc20149f8b 100644
--- a/server/sonar-web/src/main/js/helpers/request.js
+++ b/server/sonar-web/src/main/js/helpers/request.js
@@ -147,3 +147,13 @@ export function post (url, data) {
.submit()
.then(checkStatus);
}
+
+
+/**
+ * Delay promise for testing purposes
+ * @param response
+ * @returns {Promise}
+ */
+export function delay (response) {
+ return new Promise(resolve => setTimeout(() => resolve(response), 3000));
+}
diff --git a/server/sonar-web/src/main/less/components/page.less b/server/sonar-web/src/main/less/components/page.less
index bd752a1e912..77d560efb23 100644
--- a/server/sonar-web/src/main/less/components/page.less
+++ b/server/sonar-web/src/main/less/components/page.less
@@ -49,6 +49,12 @@ body {
.page-header {
.clearfix;
margin-bottom: 10px;
+
+ .spinner {
+ position: relative;
+ top: 3px;
+ margin-left: 8px;
+ }
}
.page-title {
diff --git a/server/sonar-web/src/main/less/init/misc.less b/server/sonar-web/src/main/less/init/misc.less
index 803bc92954c..28ea70fb7a9 100644
--- a/server/sonar-web/src/main/less/init/misc.less
+++ b/server/sonar-web/src/main/less/init/misc.less
@@ -104,6 +104,11 @@ td.big-spacer-top { padding-top: 16px; }
justify-content: space-between !important;
}
+.new-loading {
+ opacity: 0.5;
+ transition: opacity 0.5s ease;
+}
+
// Background Color