Horizon Spinner/Loader should inherit from theme

The Horizon spinner was using a spinner generated and animated
entirely out of JavaScript. Since CSS3 provides animates and we have
access to icon fonts, doing everything with JavaScript is not
necessary and actually taxing on the browser. Plus, all of the
spinner options were being passed in and around with JavaScript,
including the colors.  This makes it supremely difficult to use the
theme to style the spinner.

The new spinner is just defined by a handful of templates now. There
are two clientside templates to support Legacy Horizon, and one
template in the Angular to support spinners going forward.  Legacy
Horizon had two forms of spinners, so it was broken up. Angular as
not yet made use of the inline spinner, but should follow the same
markup when it is made.

There are two types of spinners, inline spinners (those shown when a
dynamic tab content is loading) and modal spinners (various other
places).  These are consistent with each other for the 'default'
experience, but their experience can be entirely customized separate
from each other.  'material' has been augmented with loaders defined
within their design spec to show the power of this new feature.

horizon.templates.js was augmented with this refactor to support only
having to compile one tempalte at a time (instead of all of them) and
caching that template so that all of them can be recompiled later.
Also, horizon.loader.js was added to house template compilation code
that was repeated in several locations.

To test overwriting page modal spinner and inline-modal spinner
examples, please follow the instructions in _loading_inline_exmaple.html,
_loading_modal_example.html under
openstack_dashboard/themes/material/templates/horizon/client_side

Change-Id: I92bc786160e070d30691eeabd4f2a50d6e2bb395
Partially-implements: blueprint horizon-theme-css-reorg
Partially-Implements: blueprint bootstrap-html-standards
Closes-bug: #1570485
This commit is contained in:
Diana Whitten 2016-02-09 11:20:38 -07:00 committed by gugl
parent 790a8435be
commit c219a3efc6
36 changed files with 439 additions and 252 deletions

View File

@ -17,36 +17,6 @@
angular angular
.module('horizon.framework.conf', []) .module('horizon.framework.conf', [])
.constant('horizon.framework.conf.spinner_options', {
inline: {
lines: 10,
length: 5,
width: 2,
radius: 3,
color: '#000',
speed: 0.8,
trail: 50,
zIndex: 100
},
modal: {
lines: 10,
length: 15,
width: 4,
radius: 10,
color: '#000',
speed: 0.8,
trail: 50
},
line_chart: {
lines: 10,
length: 15,
width: 4,
radius: 11,
color: '#000',
speed: 0.8,
trail: 50
}
})
.value('horizon.framework.conf.toastOptions', { .value('horizon.framework.conf.toastOptions', {
'delay': 3000, 'delay': 3000,
'dimissible': ['alert-success', 'alert-info'] 'dimissible': ['alert-success', 'alert-info']

View File

@ -54,32 +54,18 @@
.module('horizon.framework.widgets.modal-wait-spinner') .module('horizon.framework.widgets.modal-wait-spinner')
.directive('waitSpinner', waitSpinner); .directive('waitSpinner', waitSpinner);
waitSpinner.$inject = ['horizon.framework.conf.spinner_options']; waitSpinner.$inject = ['horizon.framework.widgets.basePath'];
function waitSpinner(spinnerOptions) { function waitSpinner(basePath) {
var directive = { var directive = {
scope: { scope: {
text: '@text' // One-direction binding (reads from parent) text: '@text' // One-direction binding (reads from parent)
}, },
restrict: 'A', templateUrl: basePath + 'modal-wait-spinner/modal-wait-spinner.template.html',
link: link, restrict: 'A'
template: '<p><i>{$text$}&hellip;</i></p>'
}; };
return directive; return directive;
////////////////////
/*
* At the time link is executed, element may not have been sized by the browser.
* Spin.js may mistakenly places the spinner at 50% of 0 (left:0, top:0). To work around
* this, explicitly set 50% left and top to center the spinner in the parent
* container
*/
function link(scope, element) {
element.spin(spinnerOptions.modal);
element.find('.spinner').css({'left': '50%', 'top': '50%'});
}
} }
})(); })();

View File

@ -1,22 +0,0 @@
/*
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Disable the Angular Bootstrap slide in animation for wait spinner modals
*/
.modal-wait-spinner.modal.fade .modal-dialog, .modal.in .modal-dialog {
@include translate(0, 0);
}

View File

@ -19,20 +19,23 @@
.module('horizon.framework.widgets.modal-wait-spinner') .module('horizon.framework.widgets.modal-wait-spinner')
.factory('horizon.framework.widgets.modal-wait-spinner.service', WaitSpinnerService); .factory('horizon.framework.widgets.modal-wait-spinner.service', WaitSpinnerService);
WaitSpinnerService.$inject = ['$uibModal']; WaitSpinnerService.$inject = [
'$interpolate',
'$templateCache',
'horizon.framework.widgets.basePath',
'$uibModal'
];
/* /*
* @ngdoc factory * @ngdoc factory
* @name horizon.framework.widgets.modal-wait-spinner.factory:WaitSpinnerService * @name horizon.framework.widgets.modal-wait-spinner.factory:WaitSpinnerService
* @description * @description
* In order to provide a seamless transition to a Horizon that uses more Angular * In order to provide a seamless transition to a Horizon that uses more Angular
* based pages, the service is currently implemented using the existing * based pages, the service is currently implemented using the same markup as the
* Spin.js library and the corresponding jQuery plugin (jquery.spin.js). This widget * client side loader, which is composed of HTML and a spinner Icon Font.
* looks and feels the same as the existing spinner we are familiar with in Horizon.
* Over time, uses of the existing Horizon spinner ( horizon.modals.modal_spinner() )
* can be phased out, or refactored to use this component.
*/ */
function WaitSpinnerService ($uibModal) {
function WaitSpinnerService ($interpolate, $templateCache, basePath, $uibModal) {
var spinner = this; var spinner = this;
var service = { var service = {
showModalSpinner: showModalSpinner, showModalSpinner: showModalSpinner,
@ -44,15 +47,12 @@
//////////////////// ////////////////////
function showModalSpinner(spinnerText) { function showModalSpinner(spinnerText) {
var templateUrl = basePath + 'modal-wait-spinner/modal-wait-spinner.template.html';
var html = $templateCache.get(templateUrl);
var modalOptions = { var modalOptions = {
backdrop: 'static', backdrop: 'static',
/* template: $interpolate(html)({text: spinnerText}),
* Using <div> for wait-spinner instead of a wait-spinner element windowClass: 'modal-wait-spinner'
* because the existing Horizon spinner CSS styling expects a div
* for the modal-body
*/
template: '<div wait-spinner class="modal-body" text="' + spinnerText + '"></div>',
windowClass: 'modal-wait-spinner modal_wrapper loading'
}; };
spinner.modalInstance = $uibModal.open(modalOptions); spinner.modalInstance = $uibModal.open(modalOptions);
} }

View File

@ -18,12 +18,33 @@
describe('Wait Spinner Tests', function() { describe('Wait Spinner Tests', function() {
var service; var service, $scope, $element, markup;
var expectedTemplateResult =
'<!-- Maintain parity with _loading_modal.html -->\n' +
'<div class="modal-body">\n <span class="loader fa fa-spinner ' +
'fa-spin fa-5x text-center"></span>\n <div class="loader-caption h4 text-center">' +
'wait&hellip;</div>\n</div>\n';
beforeEach(module('ui.bootstrap')); beforeEach(module('ui.bootstrap'));
beforeEach(module('templates'));
beforeEach(module('horizon.framework')); beforeEach(module('horizon.framework'));
beforeEach(inject(function ($injector) { beforeEach(inject(function ($injector) {
var $compile = $injector.get('$compile');
var $templateCache = $injector.get('$templateCache');
var basePath = $injector.get('horizon.framework.widgets.basePath');
$scope = $injector.get('$rootScope').$new();
service = $injector.get('horizon.framework.widgets.modal-wait-spinner.service'); service = $injector.get('horizon.framework.widgets.modal-wait-spinner.service');
markup = $templateCache
.get(basePath + 'modal-wait-spinner/modal-wait-spinner.template.html');
$element = angular.element(markup);
$compile($element)($scope);
$scope.$apply();
})); }));
it('returns the service', function() { it('returns the service', function() {
@ -37,17 +58,16 @@
}); });
it('opens modal with the correct object', inject(function($uibModal) { it('opens modal with the correct object', inject(function($uibModal) {
var wanted = { backdrop: 'static', spyOn($uibModal, 'open').and.callThrough();
template: '<div wait-spinner class="modal-body" text="my text"></div>', service.showModalSpinner('wait');
windowClass: 'modal-wait-spinner modal_wrapper loading' $scope.$apply();
};
spyOn($uibModal, 'open');
service.showModalSpinner('my text');
expect($uibModal.open).toHaveBeenCalled();
expect($uibModal.open.calls.count()).toBe(1);
expect($uibModal.open.calls.argsFor(0)).toEqual([wanted]);
}));
expect($uibModal.open).toHaveBeenCalled();
expect($uibModal.open.calls.count()).toEqual(1);
expect($uibModal.open.calls.argsFor(0)[0].backdrop).toEqual('static');
expect($uibModal.open.calls.argsFor(0)[0].template).toEqual(expectedTemplateResult);
expect($uibModal.open.calls.argsFor(0)[0].windowClass).toEqual('modal-wait-spinner');
}));
}); });
describe('hideModalSpinner', function() { describe('hideModalSpinner', function() {
@ -60,19 +80,20 @@
var modal = {dismiss: function() {}}; var modal = {dismiss: function() {}};
spyOn($uibModal, 'open').and.returnValue(modal); spyOn($uibModal, 'open').and.returnValue(modal);
service.showModalSpinner('asdf'); service.showModalSpinner('asdf');
spyOn(modal, 'dismiss'); spyOn(modal, 'dismiss');
service.hideModalSpinner(); service.hideModalSpinner();
expect(modal.dismiss).toHaveBeenCalled(); expect(modal.dismiss).toHaveBeenCalled();
})); }));
}); });
}); });
describe('Wait Spinner Directive', function() { describe('Wait Spinner Directive', function() {
var $scope, $element; var $scope, $element;
beforeEach(module('ui.bootstrap')); beforeEach(module('ui.bootstrap'));
beforeEach(module('templates'));
beforeEach(module('horizon.framework')); beforeEach(module('horizon.framework'));
beforeEach(inject(function($injector) { beforeEach(inject(function($injector) {
@ -82,14 +103,17 @@
var markup = '<div wait-spinner text="hello!"></div>'; var markup = '<div wait-spinner text="hello!"></div>';
$element = angular.element(markup); $element = angular.element(markup);
$compile($element)($scope); $compile($element)($scope);
$scope.$apply(); $scope.$apply();
})); }));
it("creates a p element", function() { it("creates a div element with correct text", function() {
var elems = $element.find('p'); var elems = $element.find('div div');
expect(elems.length).toBe(1); expect(elems.length).toBe(1);
//The spinner is a nested div with the "text" set according to the attribute
//indexOf is used because the spinner puts &hellip; after the text, however
//jasmine does not convert &hellip; to the three dots and thinks they don't match
//when compared with toEqual
expect(elems[0].innerText.indexOf('hello!')).toBe(0);
}); });
}); });
})(); })();

View File

@ -0,0 +1,5 @@
<!-- Maintain parity with _loading_modal.html -->
<div class="modal-body">
<span class="loader fa fa-spinner fa-spin fa-5x text-center"></span>
<div class="loader-caption h4 text-center">{$text$}&hellip;</div>
</div>

View File

@ -3,7 +3,6 @@
@import "charts/chart-tooltip"; @import "charts/chart-tooltip";
@import "charts/pie-chart"; @import "charts/pie-chart";
@import "action-list/action-list"; @import "action-list/action-list";
@import "modal-wait-spinner/modal-wait-spinner";
@import "metadata/metadata"; @import "metadata/metadata";
@import "magic-search/magic-search"; @import "magic-search/magic-search";
@import "table/hz-dynamic-table"; @import "table/hz-dynamic-table";

View File

@ -222,6 +222,7 @@ horizon.d3_line_chart = {
self.chart_module = chart_module; self.chart_module = chart_module;
self.html_element = html_element; self.html_element = html_element;
self.jquery_element = jquery_element; self.jquery_element = jquery_element;
self.$spinner = horizon.loader.inline(gettext('Loading')).hide().appendTo(jquery_element);
/************************************************************************/ /************************************************************************/
/*********************** Initialization methods *************************/ /*********************** Initialization methods *************************/
@ -437,8 +438,8 @@ horizon.d3_line_chart = {
self.refresh = function (){ self.refresh = function (){
var self = this; var self = this;
if (typeof self.data === 'string') {
self.start_loading(); self.start_loading();
if (typeof self.data === 'string') {
horizon.ajax.queue({ horizon.ajax.queue({
url: self.final_url, url: self.final_url,
success: function (data) { success: function (data) {
@ -453,6 +454,7 @@ horizon.d3_line_chart = {
}); });
} else if (self.data) { } else if (self.data) {
self.load_data(self.data); self.load_data(self.data);
self.finish_loading();
} else { } else {
self.error_message(gettext('No data available.')); self.error_message(gettext('No data available.'));
} }
@ -620,34 +622,17 @@ horizon.d3_line_chart = {
self.start_loading = function () { self.start_loading = function () {
var self = this; var self = this;
/* Find and remove backdrops and spinners that could be already there.*/ $(self.html_element).addClass('has-spinner');
$(self.html_element).find('.modal-backdrop').remove(); self.$spinner.show();
$(self.html_element).find('.spinner_wrapper').remove();
// Display the backdrop that will be over the chart.
self.backdrop = $('<div class="modal-backdrop"></div>');
self.backdrop.css('width', self.width).css('height', self.height);
$(self.html_element).append(self.backdrop);
// Hide the legend. // Hide the legend.
$(self.legend_element).empty().addClass('disabled'); $(self.legend_element).empty().addClass('disabled');
// Show the spinner.
self.spinner = $('<div class="spinner_wrapper"></div>');
$(self.html_element).append(self.spinner);
/* /*
TODO(lsmola) a loader for in-line tables spark-lines has to be TODO(lsmola) a loader for in-line tables spark-lines has to be
prepared, the parameters of loader could be sent in settings. prepared, the parameters of loader could be sent in settings.
*/ */
self.spinner.spin(horizon.conf.spinner_options.line_chart);
// Center the spinner considering the size of the spinner.
var radius = horizon.conf.spinner_options.line_chart.radius;
var length = horizon.conf.spinner_options.line_chart.length;
var spinner_size = radius + length;
var top = (self.height / 2) - spinner_size / 2;
var left = (self.width / 2) - spinner_size / 2;
self.spinner.css('top', top).css('left', left);
}; };
/** /**
@ -658,6 +643,8 @@ horizon.d3_line_chart = {
var self = this; var self = this;
// Showing the legend. // Showing the legend.
$(self.legend_element).removeClass('disabled'); $(self.legend_element).removeClass('disabled');
$(self.html_element).removeClass('has-spinner');
self.$spinner.hide();
}; };
}, },

View File

@ -0,0 +1,19 @@
/*
Simple loader rendering logic
*/
horizon.loader = {
templates: {
inline: '#loader-inline',
modal: '#loader-modal'
}
};
horizon.loader.inline = function(text) {
return horizon.templates.compile(horizon.loader.templates.inline, {text: text});
};
horizon.loader.modal = function(text) {
return horizon.templates.compile(horizon.loader.templates.modal, {text: text});
};

View File

@ -67,13 +67,15 @@ horizon.modals.success = function (data) {
return modal; return modal;
}; };
horizon.modals.modal_spinner = function (text) { horizon.modals.modal_spinner = function (text, $container) {
if (!$container) {
$container = $('#modal_wrapper');
}
// Adds a spinner with the desired text in a modal window. // Adds a spinner with the desired text in a modal window.
var template = horizon.templates.compiled_templates["#spinner-modal"]; horizon.modals.spinner = horizon.loader.modal(text);
horizon.modals.spinner = $(template.render({text: text})); horizon.modals.spinner.appendTo($container);
horizon.modals.spinner.appendTo("#modal_wrapper");
horizon.modals.spinner.modal({backdrop: 'static'}); horizon.modals.spinner.modal({backdrop: 'static'});
horizon.modals.spinner.find(".modal-body").spin(horizon.conf.spinner_options.modal);
}; };
horizon.modals.progress_bar = function (text) { horizon.modals.progress_bar = function (text) {

View File

@ -20,32 +20,34 @@ horizon.tabs.addTabLoadFunction = function (f) {
horizon.tabs._init_load_functions.push(f); horizon.tabs._init_load_functions.push(f);
}; };
horizon.tabs.initTabLoad = function (tab) { horizon.tabs.initTabLoad = function ($tab) {
$tab.removeClass('tab-loading');
$(horizon.tabs._init_load_functions).each(function (index, f) { $(horizon.tabs._init_load_functions).each(function (index, f) {
f(tab); f($tab);
}); });
recompileAngularContent($(tab)); recompileAngularContent($tab);
}; };
horizon.tabs.load_tab = function () { horizon.tabs.load_tab = function () {
var $this = $(this), var $this = $(this),
tab_id = $this.attr('data-target'), tab_id = $this.attr('data-target'),
tab_pane = $(tab_id); $tab_pane = $(tab_id);
// FIXME(gabriel): This style mucking shouldn't be in the javascript. // Set up the client side template to append
tab_pane.append("<span style='margin-left: 30px;'>" + gettext("Loading") + "&hellip;</span>"); var $template = horizon.loader.inline(gettext('Loading'));
tab_pane.spin(horizon.conf.spinner_options.inline);
$(tab_pane.data().spinner.el).css('top', '9px'); $tab_pane
$(tab_pane.data().spinner.el).css('left', '15px'); .append($template)
.addClass('tab-loading');
// If query params exist, append tab id. // If query params exist, append tab id.
if(window.location.search.length > 0) { if(window.location.search.length > 0) {
tab_pane.load(window.location.search + "&tab=" + tab_id.replace('#', ''), function() { $tab_pane.load(window.location.search + "&tab=" + tab_id.replace('#', ''), function() {
horizon.tabs.initTabLoad(tab_pane); horizon.tabs.initTabLoad($tab_pane);
}); });
} else { } else {
tab_pane.load("?tab=" + tab_id.replace('#', ''), function() { $tab_pane.load("?tab=" + tab_id.replace('#', ''), function() {
horizon.tabs.initTabLoad(tab_pane); horizon.tabs.initTabLoad($tab_pane);
}); });
} }
$this.attr("data-loaded", "true"); $this.attr("data-loaded", "true");

View File

@ -19,7 +19,8 @@ horizon.templates = {
"#modal_template", "#modal_template",
"#empty_row_template", "#empty_row_template",
"#alert_message_template", "#alert_message_template",
"#spinner-modal", "#loader-modal",
"#loader-inline",
"#membership_template", "#membership_template",
"#confirm_modal", "#confirm_modal",
"#progress-modal" "#progress-modal"
@ -28,10 +29,37 @@ horizon.templates = {
}; };
/* Pre-loads and compiles the client-side templates. */ /* Pre-loads and compiles the client-side templates. */
horizon.templates.compile_templates = function () { horizon.templates.compile_templates = function (id) {
// If an id is passed in, only compile that template
if (id) {
horizon.templates.compiled_templates[id] = Hogan.compile($(id).html());
} else {
// If its never been set, make it an empty object
horizon.templates.compiled_templates =
$.isEmptyObject(horizon.templates.compiled_templates) ? {} : horizon.templates.compiled_templates;
// Over each template found, only recompile ones that need it
$.each(horizon.templates.template_ids, function (ind, template_id) { $.each(horizon.templates.template_ids, function (ind, template_id) {
if (!(template_id in horizon.templates.compiled_templates)) {
horizon.templates.compiled_templates[template_id] = Hogan.compile($(template_id).html()); horizon.templates.compiled_templates[template_id] = Hogan.compile($(template_id).html());
}
}); });
}
};
/* Takes a template id, as defined in horizon.templates.template_ids, and returns the compiled
template given the context passed in, as a jQuery object
*/
horizon.templates.compile = function(id, context) {
var template = horizon.templates.compiled_templates[id];
// If its not available, maybe we didn't compile it yet, try one more time
if (!template) {
horizon.templates.compile_templates(id);
template = horizon.templates.compiled_templates[id];
}
return $(template.render(context));
}; };
horizon.addInitFunction(horizon.templates.init = function () { horizon.addInitFunction(horizon.templates.init = function () {

View File

@ -0,0 +1,11 @@
{% extends "horizon/client_side/template.html" %}
{% load i18n horizon %}
{% block id %}loader-inline{% endblock %}
{% block template %}{% spaceless %}{% jstemplate %}
<div class="loader-inline">
<span class="loader fa fa-spinner fa-spin fa-4x text-center"></span>
<div class="loader-caption h4 text-center">[[text]]&hellip;</div>
</div>
{% endjstemplate %}{% endspaceless %}{% endblock %}

View File

@ -1,14 +1,15 @@
{% extends "horizon/client_side/template.html" %} {% extends "horizon/client_side/template.html" %}
{% load i18n horizon %} {% load i18n horizon %}
{% block id %}spinner-modal{% endblock %} {% block id %}loader-modal{% endblock %}
{% block template %}{% spaceless %}{% jstemplate %} {% block template %}{% spaceless %}{% jstemplate %}
<div class="modal loading"> <div class="modal loading">
<div class="modal-dialog modal-xs"> <div class="modal-dialog modal-xs">
<div class="modal-content"> <div class="modal-content">
<div class="modal-body"> <div class="modal-body">
<p class="text-center">[[text]]&hellip;</p> <span class="loader fa fa-spinner fa-spin fa-5x text-center"></span>
<div class="loader-caption h4 text-center">[[text]]&hellip;</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,8 @@
{% include "horizon/client_side/_modal.html" %} {% include "horizon/client_side/_modal.html" %}
{% include "horizon/client_side/_table_row.html" %} {% include "horizon/client_side/_table_row.html" %}
{% include "horizon/client_side/_alert_message.html" %} {% include "horizon/client_side/_alert_message.html" %}
{% include "horizon/client_side/_loading.html" %} {% include "horizon/client_side/_loading_modal.html" %}
{% include "horizon/client_side/_loading_inline.html" %}
{% include "horizon/client_side/_membership.html" %} {% include "horizon/client_side/_membership.html" %}
{% include "horizon/client_side/_confirm.html" %} {% include "horizon/client_side/_confirm.html" %}
{% include "horizon/client_side/_progress.html" %} {% include "horizon/client_side/_progress.html" %}

View File

@ -43,36 +43,6 @@
}; };
conf.disable_password_reveal = conf.disable_password_reveal =
{{ HORIZON_CONFIG.disable_password_reveal|yesno:"true,false" }}; {{ HORIZON_CONFIG.disable_password_reveal|yesno:"true,false" }};
conf.spinner_options = {
inline: {
lines: 10,
length: 5,
width: 2,
radius: 3,
color: '#000',
speed: 0.8,
trail: 50,
zIndex: 100
},
modal: {
lines: 10,
length: 15,
width: 4,
radius: 10,
color: '#000',
speed: 0.8,
trail: 50
},
line_chart: {
lines: 10,
length: 15,
width: 4,
radius: 11,
color: '#000',
speed: 0.8,
trail: 50
}
};
// minimal cookie store implementation for testing // minimal cookie store implementation for testing
horizon.test_cookies = {}; horizon.test_cookies = {};

View File

@ -89,8 +89,6 @@ module.exports = function (config) {
xstaticPath + 'rickshaw/data/rickshaw.js', xstaticPath + 'rickshaw/data/rickshaw.js',
xstaticPath + 'angular_smart_table/data/smart-table.js', xstaticPath + 'angular_smart_table/data/smart-table.js',
xstaticPath + 'angular_lrdragndrop/data/lrdragndrop.js', xstaticPath + 'angular_lrdragndrop/data/lrdragndrop.js',
xstaticPath + 'spin/data/spin.js',
xstaticPath + 'spin/data/spin.jquery.js',
xstaticPath + 'tv4/data/tv4.js', xstaticPath + 'tv4/data/tv4.js',
xstaticPath + 'objectpath/data/ObjectPath.js', xstaticPath + 'objectpath/data/ObjectPath.js',
xstaticPath + 'angular_schema_form/data/schema-form.js', xstaticPath + 'angular_schema_form/data/schema-form.js',

View File

@ -94,7 +94,6 @@
updateHorizon.$inject = [ updateHorizon.$inject = [
'gettextCatalog', 'gettextCatalog',
'horizon.framework.conf.spinner_options',
'horizon.framework.util.tech-debt.helper-functions', 'horizon.framework.util.tech-debt.helper-functions',
'horizon.framework.widgets.toast.service', 'horizon.framework.widgets.toast.service',
'$cookieStore', '$cookieStore',
@ -105,7 +104,6 @@
function updateHorizon( function updateHorizon(
gettextCatalog, gettextCatalog,
spinnerOptions,
hzUtils, hzUtils,
toastService, toastService,
$cookieStore, $cookieStore,
@ -119,8 +117,6 @@
// expose the legacy utils module // expose the legacy utils module
horizon.utils = hzUtils; horizon.utils = hzUtils;
horizon.conf.spinner_options = spinnerOptions;
horizon.toast = toastService; horizon.toast = toastService;
if (angular.version.major === 1 && angular.version.minor < 4) { if (angular.version.major === 1 && angular.version.minor < 4) {

View File

@ -16,7 +16,9 @@
'use strict'; 'use strict';
describe('RedirectController', function() { describe('RedirectController', function() {
var $location, $window, controller, waitSpinnerService; var $location, $window, controller, $scope, waitSpinnerService;
beforeEach(module('templates'));
beforeEach(function() { beforeEach(function() {
$window = {location: { replace: jasmine.createSpy()} }; $window = {location: { replace: jasmine.createSpy()} };
@ -30,7 +32,22 @@
inject(function ($injector) { inject(function ($injector) {
$location = $injector.get('$location'); $location = $injector.get('$location');
controller = $injector.get('$controller'); controller = $injector.get('$controller');
waitSpinnerService = $injector.get('horizon.framework.widgets.modal-wait-spinner.service');
var $compile = $injector.get('$compile');
var $templateCache = $injector.get('$templateCache');
var basePath = $injector.get('horizon.framework.widgets.basePath');
// mock waitSpinnerService.showModalSpinner
$scope = $injector.get('$rootScope').$new();
waitSpinnerService =
$injector.get('horizon.framework.widgets.modal-wait-spinner.service');
var markup = $templateCache
.get(basePath + 'modal-wait-spinner/modal-wait-spinner.template.html');
var $element = angular.element(markup);
$compile($element)($scope);
spyOn(waitSpinnerService, 'showModalSpinner');
$scope.$apply();
// NOTE: This is using absUrl, so requests will already include WEBROOT. // NOTE: This is using absUrl, so requests will already include WEBROOT.
spyOn($location, 'absUrl').and.returnValue('path'); spyOn($location, 'absUrl').and.returnValue('path');
@ -47,7 +64,6 @@
}); });
it('should start the spinner', function() { it('should start the spinner', function() {
spyOn(waitSpinnerService, 'showModalSpinner');
createController(); createController();
expect(waitSpinnerService.showModalSpinner).toHaveBeenCalledWith('Loading'); expect(waitSpinnerService.showModalSpinner).toHaveBeenCalledWith('Loading');
}); });

View File

@ -31,6 +31,25 @@ $bs-modal-footer-height: $modal-inner-padding*2 + $bs-button-height + $bs-modal-
$bs-modal-height: $bs-modal-margin*2 + $bs-modal-header-height + $bs-modal-footer-height; $bs-modal-height: $bs-modal-margin*2 + $bs-modal-header-height + $bs-modal-footer-height;
$bs-modal-height-small-screen: $bs-modal-margin-small-screen*2 + $bs-modal-header-height + $bs-modal-footer-height; $bs-modal-height-small-screen: $bs-modal-margin-small-screen*2 + $bs-modal-header-height + $bs-modal-footer-height;
// Missing Vendor Prefix Mixins
// keyframes
@mixin keyframes($name) {
@-webkit-keyframes #{$name} {
@content;
}
@-moz-keyframes #{$name} {
@content;
}
@-ms-keyframes #{$name} {
@content;
}
@keyframes #{$name} {
@content;
}
}
// Ensures that linked components will have the correct cursor without href attributes. // Ensures that linked components will have the correct cursor without href attributes.
// If ng-disabled this will be overridden by cursor: not-allowed. // If ng-disabled this will be overridden by cursor: not-allowed.
// https://angular-ui.github.io/bootstrap/ // https://angular-ui.github.io/bootstrap/

View File

@ -1,13 +1,17 @@
.loading { .loader {
&.modal { width: 100%;
.spinner { &-caption {
height: calc(100% - #{$font-size-base}); margin-bottom: 0;
padding-top: ($line-height-computed / 2); // Matches h4 margin in _type.scss
} }
.modal-body { &-inline {
height: $modal-xs;
overflow: hidden; overflow: hidden;
} }
} }
// Special Angular Override
.modal-wait-spinner .modal-dialog {
@extend .modal-xs;
} }

View File

@ -32,5 +32,4 @@
.modal-xl { .modal-xl {
width: $modal-xl; width: $modal-xl;
} }
} }

View File

@ -81,7 +81,9 @@ horizon.flat_network_topology = {
}, },
init:function() { init:function() {
var self = this; var self = this;
$(self.svg_container).spin(horizon.conf.spinner_options.modal); self.$container = $(self.svg_container);
self.$loading_template = horizon.networktopologyloader.setup_loader($(self.$container));
if($('#networktopology').length === 0) { if($('#networktopology').length === 0) {
return; return;
} }
@ -114,6 +116,7 @@ horizon.flat_network_topology = {
self.data_convert(); self.data_convert();
}); });
self.$loading_template.show();
$('#networktopology').on('change', function() { $('#networktopology').on('change', function() {
self.load_network_info(); self.load_network_info();
}); });
@ -203,10 +206,10 @@ horizon.flat_network_topology = {
self.network_height += element_properties.top_margin; self.network_height += element_properties.top_margin;
self.network_height = (self.network_height > element_properties.network_min_height) ? self.network_height : element_properties.network_min_height; self.network_height = (self.network_height > element_properties.network_min_height) ? self.network_height : element_properties.network_min_height;
self.draw_topology(); self.draw_topology();
self.$loading_template.hide();
}, },
draw_topology:function() { draw_topology:function() {
var self = this; var self = this;
$(self.svg_container).spin(false);
$(self.svg_container).removeClass('noinfo'); $(self.svg_container).removeClass('noinfo');
if (self.model.networks.length <= 0) { if (self.model.networks.length <= 0) {
$('g.network').remove(); $('g.network').remove();

View File

@ -97,7 +97,9 @@ horizon.network_topology = {
init:function() { init:function() {
var self = this; var self = this;
angular.element(self.svg_container).spin(horizon.conf.spinner_options.modal);
self.$loading_template = horizon.networktopologyloader.setup_loader($(self.svg_container));
if (angular.element('#networktopology').length === 0) { if (angular.element('#networktopology').length === 0) {
return; return;
} }
@ -155,9 +157,10 @@ horizon.network_topology = {
horizon.cookies.put('are_networks_collapsed', !current); horizon.cookies.put('are_networks_collapsed', !current);
}); });
angular.element('#topologyCanvasContainer').spin(horizon.conf.spinner_options.modal); // set up loader first thing
self.$loading_template.show();
self.create_vis(); self.create_vis();
self.loading();
self.force_direction(0.05,70,-700); self.force_direction(0.05,70,-700);
if(horizon.networktopologyloader.model !== null) { if(horizon.networktopologyloader.model !== null) {
self.retrieve_network_info(true); self.retrieve_network_info(true);
@ -232,7 +235,7 @@ horizon.network_topology = {
// Setup the main visualisation // Setup the main visualisation
create_vis: function() { create_vis: function() {
var self = this; var self = this;
angular.element('#topologyCanvasContainer').html(''); angular.element('#topologyCanvasContainer').find('svg').remove();
// Main svg // Main svg
self.outer_group = d3.select('#topologyCanvasContainer').append('svg') self.outer_group = d3.select('#topologyCanvasContainer').append('svg')
@ -264,34 +267,6 @@ horizon.network_topology = {
self.vis = self.outer_group.append('g'); self.vis = self.outer_group.append('g');
}, },
loading: function() {
var self = this;
var load_text = self.vis.append('text')
.style('fill', 'black')
.style('font-size', '40')
.attr('x', '50%')
.attr('y', '50%')
.text('');
var counter = 0;
var timer = setInterval(function() {
var i;
var str = '';
for (i = 0; i <= counter; i++) {
str += '.';
}
load_text.text(str);
if (counter >= 9) {
counter = 0;
} else {
counter++;
}
if (self.data_loaded) {
clearInterval(timer);
load_text.remove();
}
}, 100);
},
// Calculate the hulls that surround networks // Calculate the hulls that surround networks
convex_hulls: function(nodes) { convex_hulls: function(nodes) {
var net, _i, _len, _ref, _h, i; var net, _i, _len, _ref, _h, i;
@ -785,6 +760,7 @@ horizon.network_topology = {
self.force.start(); self.force.start();
} }
self.load_config(); self.load_config();
self.$loading_template.hide();
}, },
removeNode: function(obj) { removeNode: function(obj) {

View File

@ -62,6 +62,11 @@ horizon.networktopologyloader = {
stop_update:function() { stop_update:function() {
var self = this; var self = this;
clearTimeout(self.update_timer); clearTimeout(self.update_timer);
},
// Set up loader template
setup_loader: function($container) {
return horizon.loader.inline(gettext('Loading')).hide().prependTo($container);
} }
}; };

View File

@ -24,6 +24,7 @@
<script src='{{ STATIC_URL }}horizon/js/horizon.datepickers.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.datepickers.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.forms.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.forms.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.formset_table.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.formset_table.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.loader.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.messages.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.messages.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.modals.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.modals.js'></script>
<script type="text/javascript"> <script type="text/javascript">
@ -43,7 +44,6 @@
<script src='{{ STATIC_URL }}horizon/js/horizon.d3linechart.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.d3linechart.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.d3barchart.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.d3barchart.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.sidebar.js'></script> <script src='{{ STATIC_URL }}horizon/js/horizon.sidebar.js'></script>
<script src='{{ STATIC_URL }}js/horizon.instances.js'></script>
<script src='{{ STATIC_URL }}js/horizon.quota.js'></script> <script src='{{ STATIC_URL }}js/horizon.quota.js'></script>
<script src='{{ STATIC_URL }}js/horizon.metering.js'></script> <script src='{{ STATIC_URL }}js/horizon.metering.js'></script>
<script src='{{ STATIC_URL }}js/horizon.networktopologycommon.js'></script> <script src='{{ STATIC_URL }}js/horizon.networktopologycommon.js'></script>

View File

@ -21,7 +21,7 @@ from selenium.webdriver.support import wait
class BaseWebObject(unittest.TestCase): class BaseWebObject(unittest.TestCase):
"""Base class for all web objects.""" """Base class for all web objects."""
_spinner_locator = (by.By.CSS_SELECTOR, '.modal-body > .spinner') _spinner_locator = (by.By.CSS_SELECTOR, '.modal-body > .loader')
def __init__(self, driver, conf): def __init__(self, driver, conf):
self.driver = driver self.driver = driver

View File

@ -63,3 +63,5 @@ $nav-disabled-link-hover-color: $gray-dark;
// Give modals more room to breathe in -md space // Give modals more room to breathe in -md space
$modal-md: 732px; $modal-md: 732px;
$modal-backdrop-opacity: .35 !default;

View File

@ -4,6 +4,9 @@
@import "components/dropdowns"; @import "components/dropdowns";
@import "components/hamburger"; @import "components/hamburger";
@import "components/help_panel"; @import "components/help_panel";
@import "components/loader_circular_example";
@import "components/loader_line_example";
@import "components/loader_spinner";
@import "components/magic_search"; @import "components/magic_search";
@import "components/messages"; @import "components/messages";
@import "components/navbar"; @import "components/navbar";

View File

@ -1,22 +1,6 @@
// HAMBURGLER!!!! // HAMBURGLER!!!!
// Adapted with <3 from http://codepen.io/swirlycheetah/pen/cFtzb // Adapted with <3 from http://codepen.io/swirlycheetah/pen/cFtzb
// keyframes mixin
@mixin keyframes($name) {
@-webkit-keyframes #{$name} {
@content;
}
@-moz-keyframes #{$name} {
@content;
}
@-ms-keyframes #{$name} {
@content;
}
@keyframes #{$name} {
@content;
}
}
.md-hamburger-trigger { .md-hamburger-trigger {
display: block; display: block;
border: none; border: none;

View File

@ -0,0 +1,70 @@
// Adapted with <3 from http://codepen.io/kazuyu/pen/gHjoC/
.material-loader {
position: absolute;
top: 20%;
left: 50%;
width: 50px;
height: 50px;
@include animation(material-loader-rotate 2s linear infinite);
.path {
stroke-dasharray: 1,200;
stroke-dashoffset: 0;
stroke-linecap: round;
stroke: #db652d;
@include animation(material-loader-dash 1.5s ease-in-out infinite, material-loader-color 6s ease-in-out infinite);
}
}
@include keyframes(material-loader-rotate) {
from {
@include rotate(0deg);
}
to {
@include rotate(360deg);
}
}
@include keyframes(material-loader-dash) {
0% {
stroke-dasharray: 1,200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89,200;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 89,200;
stroke-dashoffset: -124;
}
}
@include keyframes(material-loader-color) {
0% {
stroke: $brand-info;
}
20% {
stroke: $brand-info;
}
25% {
stroke: $brand-danger;
}
45% {
stroke: $brand-danger;
}
50% {
stroke: $brand-warning;
}
70% {
stroke: $brand-warning;
}
75% {
stroke: $brand-success;
}
95% {
stroke: $brand-success;
}
}

View File

@ -0,0 +1,65 @@
// Adapter with <3 from http://codepen.io/max-scopp/pen/FArlb
// keyframes
@mixin animate-loader($duration) {
@include animation(material-loader-stretch 2.8s ease $duration infinite)
}
.material-line-loader {
width: 100%;
height: 4px;
position: relative;
overflow: hidden;
& > .loader-section {
position: absolute;
height: 100%;
left: 50%;
&:first-child {
background: $brand-success;
@include animate-loader(0s);
}
&:nth-child(2) {
background: $brand-danger;
@include animate-loader(.4s);
}
&:nth-child(3) {
background: $brand-info;
@include animate-loader(.8s);
}
&:nth-child(4) {
background: $brand-warning;
@include animate-loader(1.2s);
}
}
}
@include keyframes(material-loader-stretch) {
0% {
padding: 0 0 0 0;
left: 50%;
z-index: 4;
}
25% {
z-index: 3;
}
50% {
padding: 0 50% 0 50%;
left: 0;
z-index: 2;
}
100% {
padding: 0 50% 0 50%;
left: 0;
z-index: 1;
}
}
// Move it up over the tab line, it looks cleaner
.tab-loading .material-line-loader {
top: -2px;
}

View File

@ -0,0 +1,14 @@
.loader.fa-spinner {
padding: $padding-xs-horizontal;
font-size: $font-size-h2;
}
.loader-caption {
padding-top: 0;
}
.loading {
.modal-body {
overflow-x: hidden;
}
}

View File

@ -0,0 +1,27 @@
<!--this is the example to overwrite existing material theme's page
inline spinner. You can observe the inline spinner by clicking the
tabbed page.
To try this modal spinner:
* make a copy of this file and call it _loading_inline.html
* rm -rf static
* run command "tox -e manage collectstatic"
* restart horizon
* reload horizon page in web browser
-->
{% extends "horizon/client_side/template.html" %}
{% load i18n horizon %}
{% block id %}loader-inline{% endblock %}
{% block template %}{% spaceless %}{% jstemplate %}
<div class="loader-inline">
<div class="material-line-loader">
<div class="loader-section"></div>
<div class="loader-section"></div>
<div class="loader-section"></div>
<div class="loader-section"></div>
</div>
</div>
{% endjstemplate %}{% endspaceless %}{% endblock %}

View File

@ -0,0 +1,24 @@
<!--this is the example to overwrite existing material theme's page
modal spinner. You can observe the page modal spinner by clicking
a panel page.
To try this modal spinner:
* make a copy of this file and call it _loading_modal.html
* rm -rf static
* run command "tox -e manage collectstatic"
* restart horizon
* reload horizon page in web browser
-->
{% extends "horizon/client_side/template.html" %}
{% load i18n horizon %}
{% block id %}loader-modal{% endblock %}
{% block template %}{% spaceless %}{% jstemplate %}
<div class="modal loading">
<svg class="material-loader" height="100" width="100">
<circle class="path" cx="25" cy="25.2" r="19.9" fill="none" stroke-width="6" stroke-miterlimit="10" />
</svg>
</div>
{% endjstemplate %}{% endspaceless %}{% endblock %}

View File

@ -195,7 +195,6 @@ BASE_XSTATIC_MODULES = [
('xstatic.pkg.d3', ['d3.js']), ('xstatic.pkg.d3', ['d3.js']),
('xstatic.pkg.jquery_quicksearch', ['jquery.quicksearch.js']), ('xstatic.pkg.jquery_quicksearch', ['jquery.quicksearch.js']),
('xstatic.pkg.jquery_tablesorter', ['jquery.tablesorter.js']), ('xstatic.pkg.jquery_tablesorter', ['jquery.tablesorter.js']),
('xstatic.pkg.spin', ['spin.js', 'spin.jquery.js']),
('xstatic.pkg.jquery_ui', ['jquery-ui.js']), ('xstatic.pkg.jquery_ui', ['jquery-ui.js']),
('xstatic.pkg.bootstrap_scss', ['js/bootstrap.js']), ('xstatic.pkg.bootstrap_scss', ['js/bootstrap.js']),
('xstatic.pkg.bootstrap_datepicker', ['bootstrap-datepicker.js']), ('xstatic.pkg.bootstrap_datepicker', ['bootstrap-datepicker.js']),