Selaa lähdekoodia

SONAR-6972 improve loading UX of administration pages

tags/5.3-RC1
Stas Vilchik 8 vuotta sitten
vanhempi
commit
9477e9e645
23 muutettua tiedostoa jossa 204 lisäystä ja 65 poistoa
  1. 9
    10
      server/sonar-web/src/main/js/api/components.js
  2. 11
    3
      server/sonar-web/src/main/js/apps/global-permissions/main.js
  3. 5
    1
      server/sonar-web/src/main/js/apps/global-permissions/permissions-list.js
  4. 13
    0
      server/sonar-web/src/main/js/apps/groups/header-view.js
  5. 14
    1
      server/sonar-web/src/main/js/apps/groups/list-view.js
  6. 1
    0
      server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs
  7. 8
    0
      server/sonar-web/src/main/js/apps/permission-templates/header.js
  8. 4
    3
      server/sonar-web/src/main/js/apps/permission-templates/main.js
  9. 5
    1
      server/sonar-web/src/main/js/apps/permission-templates/permission-templates.js
  10. 26
    13
      server/sonar-web/src/main/js/apps/project-permissions/main.js
  11. 4
    1
      server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.js
  12. 5
    1
      server/sonar-web/src/main/js/apps/project-permissions/permissions.js
  13. 15
    16
      server/sonar-web/src/main/js/apps/projects/create-view.js
  14. 14
    8
      server/sonar-web/src/main/js/apps/projects/main.js
  15. 3
    1
      server/sonar-web/src/main/js/apps/projects/projects.js
  16. 6
    2
      server/sonar-web/src/main/js/apps/projects/search.js
  17. 13
    0
      server/sonar-web/src/main/js/apps/users/header-view.js
  18. 14
    1
      server/sonar-web/src/main/js/apps/users/list-view.js
  19. 1
    0
      server/sonar-web/src/main/js/apps/users/templates/users-header.hbs
  20. 12
    3
      server/sonar-web/src/main/js/components/shared/list-footer.js
  21. 10
    0
      server/sonar-web/src/main/js/helpers/request.js
  22. 6
    0
      server/sonar-web/src/main/less/components/page.less
  23. 5
    0
      server/sonar-web/src/main/less/init/misc.less

+ 9
- 10
server/sonar-web/src/main/js/api/components.js Näytä tiedosto

@@ -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 = []) {

+ 11
- 3
server/sonar-web/src/main/js/apps/global-permissions/main.js Näytä tiedosto

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

+ 5
- 1
server/sonar-web/src/main/js/apps/global-permissions/permissions-list.js Näytä tiedosto

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

+ 13
- 0
server/sonar-web/src/main/js/apps/groups/header-view.js Näytä tiedosto

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

+ 14
- 1
server/sonar-web/src/main/js/apps/groups/list-view.js Näytä tiedosto

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



+ 1
- 0
server/sonar-web/src/main/js/apps/groups/templates/groups-header.hbs Näytä tiedosto

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

+ 8
- 0
server/sonar-web/src/main/js/apps/permission-templates/header.js Näytä tiedosto

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

+ 4
- 3
server/sonar-web/src/main/js/apps/permission-templates/main.js Näytä tiedosto

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

+ 5
- 1
server/sonar-web/src/main/js/apps/permission-templates/permission-templates.js Näytä tiedosto

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

+ 26
- 13
server/sonar-web/src/main/js/apps/project-permissions/main.js Näytä tiedosto

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

+ 4
- 1
server/sonar-web/src/main/js/apps/project-permissions/permissions-footer.js Näytä tiedosto

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

+ 5
- 1
server/sonar-web/src/main/js/apps/project-permissions/permissions.js Näytä tiedosto

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

+ 15
- 16
server/sonar-web/src/main/js/apps/projects/create-view.js Näytä tiedosto

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

+ 14
- 8
server/sonar-web/src/main/js/apps/projects/main.js Näytä tiedosto

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

+ 3
- 1
server/sonar-web/src/main/js/apps/projects/projects.js Näytä tiedosto

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

+ 6
- 2
server/sonar-web/src/main/js/apps/projects/search.js Näytä tiedosto

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

+ 13
- 0
server/sonar-web/src/main/js/apps/users/header-view.js Näytä tiedosto

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

+ 14
- 1
server/sonar-web/src/main/js/apps/users/list-view.js Näytä tiedosto

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



+ 1
- 0
server/sonar-web/src/main/js/apps/users/templates/users-header.hbs Näytä tiedosto

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

+ 12
- 3
server/sonar-web/src/main/js/components/shared/list-footer.js Näytä tiedosto

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

+ 10
- 0
server/sonar-web/src/main/js/helpers/request.js Näytä tiedosto

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

+ 6
- 0
server/sonar-web/src/main/less/components/page.less Näytä tiedosto

@@ -49,6 +49,12 @@ body {
.page-header {
.clearfix;
margin-bottom: 10px;

.spinner {
position: relative;
top: 3px;
margin-left: 8px;
}
}

.page-title {

+ 5
- 0
server/sonar-web/src/main/less/init/misc.less Näytä tiedosto

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


Loading…
Peruuta
Tallenna