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:
parent
790a8435be
commit
c219a3efc6
@ -17,36 +17,6 @@
|
||||
|
||||
angular
|
||||
.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', {
|
||||
'delay': 3000,
|
||||
'dimissible': ['alert-success', 'alert-info']
|
||||
|
@ -54,32 +54,18 @@
|
||||
.module('horizon.framework.widgets.modal-wait-spinner')
|
||||
.directive('waitSpinner', waitSpinner);
|
||||
|
||||
waitSpinner.$inject = ['horizon.framework.conf.spinner_options'];
|
||||
waitSpinner.$inject = ['horizon.framework.widgets.basePath'];
|
||||
|
||||
function waitSpinner(spinnerOptions) {
|
||||
function waitSpinner(basePath) {
|
||||
|
||||
var directive = {
|
||||
scope: {
|
||||
text: '@text' // One-direction binding (reads from parent)
|
||||
},
|
||||
restrict: 'A',
|
||||
link: link,
|
||||
template: '<p><i>{$text$}…</i></p>'
|
||||
templateUrl: basePath + 'modal-wait-spinner/modal-wait-spinner.template.html',
|
||||
restrict: 'A'
|
||||
};
|
||||
|
||||
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%'});
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
@ -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);
|
||||
}
|
@ -19,20 +19,23 @@
|
||||
.module('horizon.framework.widgets.modal-wait-spinner')
|
||||
.factory('horizon.framework.widgets.modal-wait-spinner.service', WaitSpinnerService);
|
||||
|
||||
WaitSpinnerService.$inject = ['$uibModal'];
|
||||
WaitSpinnerService.$inject = [
|
||||
'$interpolate',
|
||||
'$templateCache',
|
||||
'horizon.framework.widgets.basePath',
|
||||
'$uibModal'
|
||||
];
|
||||
|
||||
/*
|
||||
* @ngdoc factory
|
||||
* @name horizon.framework.widgets.modal-wait-spinner.factory:WaitSpinnerService
|
||||
* @description
|
||||
* In order to provide a seamless transition to a Horizon that uses more Angular
|
||||
* based pages, the service is currently implemented using the existing
|
||||
* Spin.js library and the corresponding jQuery plugin (jquery.spin.js). This widget
|
||||
* 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.
|
||||
* based pages, the service is currently implemented using the same markup as the
|
||||
* client side loader, which is composed of HTML and a spinner Icon Font.
|
||||
*/
|
||||
function WaitSpinnerService ($uibModal) {
|
||||
|
||||
function WaitSpinnerService ($interpolate, $templateCache, basePath, $uibModal) {
|
||||
var spinner = this;
|
||||
var service = {
|
||||
showModalSpinner: showModalSpinner,
|
||||
@ -44,15 +47,12 @@
|
||||
////////////////////
|
||||
|
||||
function showModalSpinner(spinnerText) {
|
||||
var templateUrl = basePath + 'modal-wait-spinner/modal-wait-spinner.template.html';
|
||||
var html = $templateCache.get(templateUrl);
|
||||
var modalOptions = {
|
||||
backdrop: 'static',
|
||||
/*
|
||||
* Using <div> for wait-spinner instead of a wait-spinner element
|
||||
* 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'
|
||||
template: $interpolate(html)({text: spinnerText}),
|
||||
windowClass: 'modal-wait-spinner'
|
||||
};
|
||||
spinner.modalInstance = $uibModal.open(modalOptions);
|
||||
}
|
||||
|
@ -18,12 +18,33 @@
|
||||
|
||||
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…</div>\n</div>\n';
|
||||
|
||||
beforeEach(module('ui.bootstrap'));
|
||||
beforeEach(module('templates'));
|
||||
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');
|
||||
|
||||
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() {
|
||||
@ -37,17 +58,16 @@
|
||||
});
|
||||
|
||||
it('opens modal with the correct object', inject(function($uibModal) {
|
||||
var wanted = { backdrop: 'static',
|
||||
template: '<div wait-spinner class="modal-body" text="my text"></div>',
|
||||
windowClass: 'modal-wait-spinner modal_wrapper loading'
|
||||
};
|
||||
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]);
|
||||
}));
|
||||
spyOn($uibModal, 'open').and.callThrough();
|
||||
service.showModalSpinner('wait');
|
||||
$scope.$apply();
|
||||
|
||||
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() {
|
||||
@ -60,19 +80,20 @@
|
||||
var modal = {dismiss: function() {}};
|
||||
spyOn($uibModal, 'open').and.returnValue(modal);
|
||||
service.showModalSpinner('asdf');
|
||||
|
||||
spyOn(modal, 'dismiss');
|
||||
service.hideModalSpinner();
|
||||
|
||||
expect(modal.dismiss).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Wait Spinner Directive', function() {
|
||||
var $scope, $element;
|
||||
|
||||
beforeEach(module('ui.bootstrap'));
|
||||
beforeEach(module('templates'));
|
||||
beforeEach(module('horizon.framework'));
|
||||
|
||||
beforeEach(inject(function($injector) {
|
||||
@ -82,14 +103,17 @@
|
||||
var markup = '<div wait-spinner text="hello!"></div>';
|
||||
$element = angular.element(markup);
|
||||
$compile($element)($scope);
|
||||
|
||||
$scope.$apply();
|
||||
}));
|
||||
|
||||
it("creates a p element", function() {
|
||||
var elems = $element.find('p');
|
||||
it("creates a div element with correct text", function() {
|
||||
var elems = $element.find('div div');
|
||||
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 … after the text, however
|
||||
//jasmine does not convert … to the three dots and thinks they don't match
|
||||
//when compared with toEqual
|
||||
expect(elems[0].innerText.indexOf('hello!')).toBe(0);
|
||||
});
|
||||
|
||||
});
|
||||
})();
|
||||
|
@ -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$}…</div>
|
||||
</div>
|
@ -3,7 +3,6 @@
|
||||
@import "charts/chart-tooltip";
|
||||
@import "charts/pie-chart";
|
||||
@import "action-list/action-list";
|
||||
@import "modal-wait-spinner/modal-wait-spinner";
|
||||
@import "metadata/metadata";
|
||||
@import "magic-search/magic-search";
|
||||
@import "table/hz-dynamic-table";
|
||||
|
@ -222,6 +222,7 @@ horizon.d3_line_chart = {
|
||||
self.chart_module = chart_module;
|
||||
self.html_element = html_element;
|
||||
self.jquery_element = jquery_element;
|
||||
self.$spinner = horizon.loader.inline(gettext('Loading')).hide().appendTo(jquery_element);
|
||||
|
||||
/************************************************************************/
|
||||
/*********************** Initialization methods *************************/
|
||||
@ -437,8 +438,8 @@ horizon.d3_line_chart = {
|
||||
self.refresh = function (){
|
||||
var self = this;
|
||||
|
||||
self.start_loading();
|
||||
if (typeof self.data === 'string') {
|
||||
self.start_loading();
|
||||
horizon.ajax.queue({
|
||||
url: self.final_url,
|
||||
success: function (data) {
|
||||
@ -453,6 +454,7 @@ horizon.d3_line_chart = {
|
||||
});
|
||||
} else if (self.data) {
|
||||
self.load_data(self.data);
|
||||
self.finish_loading();
|
||||
} else {
|
||||
self.error_message(gettext('No data available.'));
|
||||
}
|
||||
@ -620,34 +622,17 @@ horizon.d3_line_chart = {
|
||||
self.start_loading = function () {
|
||||
var self = this;
|
||||
|
||||
/* Find and remove backdrops and spinners that could be already there.*/
|
||||
$(self.html_element).find('.modal-backdrop').remove();
|
||||
$(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);
|
||||
$(self.html_element).addClass('has-spinner');
|
||||
self.$spinner.show();
|
||||
|
||||
// Hide the legend.
|
||||
$(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
|
||||
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;
|
||||
// Showing the legend.
|
||||
$(self.legend_element).removeClass('disabled');
|
||||
$(self.html_element).removeClass('has-spinner');
|
||||
self.$spinner.hide();
|
||||
};
|
||||
},
|
||||
|
||||
|
19
horizon/static/horizon/js/horizon.loader.js
Normal file
19
horizon/static/horizon/js/horizon.loader.js
Normal 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});
|
||||
};
|
||||
|
@ -67,13 +67,15 @@ horizon.modals.success = function (data) {
|
||||
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.
|
||||
var template = horizon.templates.compiled_templates["#spinner-modal"];
|
||||
horizon.modals.spinner = $(template.render({text: text}));
|
||||
horizon.modals.spinner.appendTo("#modal_wrapper");
|
||||
horizon.modals.spinner = horizon.loader.modal(text);
|
||||
horizon.modals.spinner.appendTo($container);
|
||||
horizon.modals.spinner.modal({backdrop: 'static'});
|
||||
horizon.modals.spinner.find(".modal-body").spin(horizon.conf.spinner_options.modal);
|
||||
};
|
||||
|
||||
horizon.modals.progress_bar = function (text) {
|
||||
|
@ -20,32 +20,34 @@ horizon.tabs.addTabLoadFunction = function (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) {
|
||||
f(tab);
|
||||
f($tab);
|
||||
});
|
||||
recompileAngularContent($(tab));
|
||||
recompileAngularContent($tab);
|
||||
};
|
||||
|
||||
horizon.tabs.load_tab = function () {
|
||||
var $this = $(this),
|
||||
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.
|
||||
tab_pane.append("<span style='margin-left: 30px;'>" + gettext("Loading") + "…</span>");
|
||||
tab_pane.spin(horizon.conf.spinner_options.inline);
|
||||
$(tab_pane.data().spinner.el).css('top', '9px');
|
||||
$(tab_pane.data().spinner.el).css('left', '15px');
|
||||
// Set up the client side template to append
|
||||
var $template = horizon.loader.inline(gettext('Loading'));
|
||||
|
||||
$tab_pane
|
||||
.append($template)
|
||||
.addClass('tab-loading');
|
||||
|
||||
// If query params exist, append tab id.
|
||||
if(window.location.search.length > 0) {
|
||||
tab_pane.load(window.location.search + "&tab=" + tab_id.replace('#', ''), function() {
|
||||
horizon.tabs.initTabLoad(tab_pane);
|
||||
$tab_pane.load(window.location.search + "&tab=" + tab_id.replace('#', ''), function() {
|
||||
horizon.tabs.initTabLoad($tab_pane);
|
||||
});
|
||||
} else {
|
||||
tab_pane.load("?tab=" + tab_id.replace('#', ''), function() {
|
||||
horizon.tabs.initTabLoad(tab_pane);
|
||||
$tab_pane.load("?tab=" + tab_id.replace('#', ''), function() {
|
||||
horizon.tabs.initTabLoad($tab_pane);
|
||||
});
|
||||
}
|
||||
$this.attr("data-loaded", "true");
|
||||
|
@ -19,7 +19,8 @@ horizon.templates = {
|
||||
"#modal_template",
|
||||
"#empty_row_template",
|
||||
"#alert_message_template",
|
||||
"#spinner-modal",
|
||||
"#loader-modal",
|
||||
"#loader-inline",
|
||||
"#membership_template",
|
||||
"#confirm_modal",
|
||||
"#progress-modal"
|
||||
@ -28,10 +29,37 @@ horizon.templates = {
|
||||
};
|
||||
|
||||
/* Pre-loads and compiles the client-side templates. */
|
||||
horizon.templates.compile_templates = function () {
|
||||
$.each(horizon.templates.template_ids, function (ind, template_id) {
|
||||
horizon.templates.compiled_templates[template_id] = Hogan.compile($(template_id).html());
|
||||
});
|
||||
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) {
|
||||
if (!(template_id in horizon.templates.compiled_templates)) {
|
||||
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 () {
|
||||
|
11
horizon/templates/horizon/client_side/_loading_inline.html
Normal file
11
horizon/templates/horizon/client_side/_loading_inline.html
Normal 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]]…</div>
|
||||
</div>
|
||||
{% endjstemplate %}{% endspaceless %}{% endblock %}
|
@ -1,14 +1,15 @@
|
||||
{% extends "horizon/client_side/template.html" %}
|
||||
{% load i18n horizon %}
|
||||
|
||||
{% block id %}spinner-modal{% endblock %}
|
||||
{% block id %}loader-modal{% endblock %}
|
||||
|
||||
{% block template %}{% spaceless %}{% jstemplate %}
|
||||
<div class="modal loading">
|
||||
<div class="modal-dialog modal-xs">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<p class="text-center">[[text]]…</p>
|
||||
<span class="loader fa fa-spinner fa-spin fa-5x text-center"></span>
|
||||
<div class="loader-caption h4 text-center">[[text]]…</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,7 +1,8 @@
|
||||
{% include "horizon/client_side/_modal.html" %}
|
||||
{% include "horizon/client_side/_table_row.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/_confirm.html" %}
|
||||
{% include "horizon/client_side/_progress.html" %}
|
@ -43,36 +43,6 @@
|
||||
};
|
||||
conf.disable_password_reveal =
|
||||
{{ 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
|
||||
horizon.test_cookies = {};
|
||||
|
@ -89,8 +89,6 @@ module.exports = function (config) {
|
||||
xstaticPath + 'rickshaw/data/rickshaw.js',
|
||||
xstaticPath + 'angular_smart_table/data/smart-table.js',
|
||||
xstaticPath + 'angular_lrdragndrop/data/lrdragndrop.js',
|
||||
xstaticPath + 'spin/data/spin.js',
|
||||
xstaticPath + 'spin/data/spin.jquery.js',
|
||||
xstaticPath + 'tv4/data/tv4.js',
|
||||
xstaticPath + 'objectpath/data/ObjectPath.js',
|
||||
xstaticPath + 'angular_schema_form/data/schema-form.js',
|
||||
|
@ -94,7 +94,6 @@
|
||||
|
||||
updateHorizon.$inject = [
|
||||
'gettextCatalog',
|
||||
'horizon.framework.conf.spinner_options',
|
||||
'horizon.framework.util.tech-debt.helper-functions',
|
||||
'horizon.framework.widgets.toast.service',
|
||||
'$cookieStore',
|
||||
@ -105,7 +104,6 @@
|
||||
|
||||
function updateHorizon(
|
||||
gettextCatalog,
|
||||
spinnerOptions,
|
||||
hzUtils,
|
||||
toastService,
|
||||
$cookieStore,
|
||||
@ -119,8 +117,6 @@
|
||||
// expose the legacy utils module
|
||||
horizon.utils = hzUtils;
|
||||
|
||||
horizon.conf.spinner_options = spinnerOptions;
|
||||
|
||||
horizon.toast = toastService;
|
||||
|
||||
if (angular.version.major === 1 && angular.version.minor < 4) {
|
||||
|
@ -16,7 +16,9 @@
|
||||
'use strict';
|
||||
|
||||
describe('RedirectController', function() {
|
||||
var $location, $window, controller, waitSpinnerService;
|
||||
var $location, $window, controller, $scope, waitSpinnerService;
|
||||
|
||||
beforeEach(module('templates'));
|
||||
|
||||
beforeEach(function() {
|
||||
$window = {location: { replace: jasmine.createSpy()} };
|
||||
@ -30,7 +32,22 @@
|
||||
inject(function ($injector) {
|
||||
$location = $injector.get('$location');
|
||||
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.
|
||||
spyOn($location, 'absUrl').and.returnValue('path');
|
||||
@ -47,7 +64,6 @@
|
||||
});
|
||||
|
||||
it('should start the spinner', function() {
|
||||
spyOn(waitSpinnerService, 'showModalSpinner');
|
||||
createController();
|
||||
expect(waitSpinnerService.showModalSpinner).toHaveBeenCalledWith('Loading');
|
||||
});
|
||||
|
@ -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-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.
|
||||
// If ng-disabled this will be overridden by cursor: not-allowed.
|
||||
// https://angular-ui.github.io/bootstrap/
|
||||
|
@ -1,13 +1,17 @@
|
||||
.loading {
|
||||
&.modal {
|
||||
.loader {
|
||||
width: 100%;
|
||||
|
||||
.spinner {
|
||||
height: calc(100% - #{$font-size-base});
|
||||
}
|
||||
&-caption {
|
||||
margin-bottom: 0;
|
||||
padding-top: ($line-height-computed / 2); // Matches h4 margin in _type.scss
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
height: $modal-xs;
|
||||
overflow: hidden;
|
||||
}
|
||||
&-inline {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// Special Angular Override
|
||||
.modal-wait-spinner .modal-dialog {
|
||||
@extend .modal-xs;
|
||||
}
|
@ -32,5 +32,4 @@
|
||||
.modal-xl {
|
||||
width: $modal-xl;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -81,7 +81,9 @@ horizon.flat_network_topology = {
|
||||
},
|
||||
init:function() {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@ -114,6 +116,7 @@ horizon.flat_network_topology = {
|
||||
self.data_convert();
|
||||
});
|
||||
|
||||
self.$loading_template.show();
|
||||
$('#networktopology').on('change', function() {
|
||||
self.load_network_info();
|
||||
});
|
||||
@ -203,10 +206,10 @@ horizon.flat_network_topology = {
|
||||
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.draw_topology();
|
||||
self.$loading_template.hide();
|
||||
},
|
||||
draw_topology:function() {
|
||||
var self = this;
|
||||
$(self.svg_container).spin(false);
|
||||
$(self.svg_container).removeClass('noinfo');
|
||||
if (self.model.networks.length <= 0) {
|
||||
$('g.network').remove();
|
||||
|
@ -97,7 +97,9 @@ horizon.network_topology = {
|
||||
|
||||
init:function() {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@ -155,9 +157,10 @@ horizon.network_topology = {
|
||||
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.loading();
|
||||
self.force_direction(0.05,70,-700);
|
||||
if(horizon.networktopologyloader.model !== null) {
|
||||
self.retrieve_network_info(true);
|
||||
@ -232,7 +235,7 @@ horizon.network_topology = {
|
||||
// Setup the main visualisation
|
||||
create_vis: function() {
|
||||
var self = this;
|
||||
angular.element('#topologyCanvasContainer').html('');
|
||||
angular.element('#topologyCanvasContainer').find('svg').remove();
|
||||
|
||||
// Main svg
|
||||
self.outer_group = d3.select('#topologyCanvasContainer').append('svg')
|
||||
@ -264,34 +267,6 @@ horizon.network_topology = {
|
||||
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
|
||||
convex_hulls: function(nodes) {
|
||||
var net, _i, _len, _ref, _h, i;
|
||||
@ -785,6 +760,7 @@ horizon.network_topology = {
|
||||
self.force.start();
|
||||
}
|
||||
self.load_config();
|
||||
self.$loading_template.hide();
|
||||
},
|
||||
|
||||
removeNode: function(obj) {
|
||||
|
@ -62,6 +62,11 @@ horizon.networktopologyloader = {
|
||||
stop_update:function() {
|
||||
var self = this;
|
||||
clearTimeout(self.update_timer);
|
||||
},
|
||||
|
||||
// Set up loader template
|
||||
setup_loader: function($container) {
|
||||
return horizon.loader.inline(gettext('Loading')).hide().prependTo($container);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -24,6 +24,7 @@
|
||||
<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.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.modals.js'></script>
|
||||
<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.d3barchart.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.metering.js'></script>
|
||||
<script src='{{ STATIC_URL }}js/horizon.networktopologycommon.js'></script>
|
||||
|
@ -21,7 +21,7 @@ from selenium.webdriver.support import wait
|
||||
|
||||
class BaseWebObject(unittest.TestCase):
|
||||
"""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):
|
||||
self.driver = driver
|
||||
|
@ -63,3 +63,5 @@ $nav-disabled-link-hover-color: $gray-dark;
|
||||
|
||||
// Give modals more room to breathe in -md space
|
||||
$modal-md: 732px;
|
||||
|
||||
$modal-backdrop-opacity: .35 !default;
|
@ -4,6 +4,9 @@
|
||||
@import "components/dropdowns";
|
||||
@import "components/hamburger";
|
||||
@import "components/help_panel";
|
||||
@import "components/loader_circular_example";
|
||||
@import "components/loader_line_example";
|
||||
@import "components/loader_spinner";
|
||||
@import "components/magic_search";
|
||||
@import "components/messages";
|
||||
@import "components/navbar";
|
||||
|
@ -1,22 +1,6 @@
|
||||
// HAMBURGLER!!!!
|
||||
// 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 {
|
||||
display: block;
|
||||
border: none;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 %}
|
@ -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 %}
|
@ -195,7 +195,6 @@ BASE_XSTATIC_MODULES = [
|
||||
('xstatic.pkg.d3', ['d3.js']),
|
||||
('xstatic.pkg.jquery_quicksearch', ['jquery.quicksearch.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.bootstrap_scss', ['js/bootstrap.js']),
|
||||
('xstatic.pkg.bootstrap_datepicker', ['bootstrap-datepicker.js']),
|
||||
|
Loading…
Reference in New Issue
Block a user