diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2015-11-16 15:46:23 +0100 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2015-11-16 16:43:49 +0100 |
commit | 9477e9e64502bcd33836e3ea8f7ae0bb3b53baec (patch) | |
tree | 92742343754ef5a4ecfc41ff6c449ea282c28775 | |
parent | 8f1d094510778a31f02f797db624f5d5896d1b0c (diff) | |
download | sonarqube-9477e9e64502bcd33836e3ea8f7ae0bb3b53baec.tar.gz sonarqube-9477e9e64502bcd33836e3ea8f7ae0bb3b53baec.zip |
SONAR-6972 improve loading UX of administration pages
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 |