Merge lp:~ricgard/maas/vs-repeat-ui-improvement into lp:~maas-committers/maas/trunk

Proposed by Richard McCartney
Status: Merged
Approved by: Mike Pontillo
Approved revision: no longer in the source branch.
Merged at revision: 6088
Proposed branch: lp:~ricgard/maas/vs-repeat-ui-improvement
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 771 lines (+630/-10)
10 files modified
src/maasserver/static/js/angular/3rdparty/vs-repeat.js (+608/-0)
src/maasserver/static/js/angular/maas.js (+2/-1)
src/maasserver/static/partials/dashboard.html (+3/-1)
src/maasserver/static/partials/machines-table.html (+1/-1)
src/maasserver/static/partials/networks-list.html (+2/-2)
src/maasserver/static/partials/node-events.html (+1/-1)
src/maasserver/static/partials/nodes-list.html (+3/-3)
src/maasserver/static/partials/pods-list.html (+1/-1)
src/maasserver/templates/maasserver/js-conf.html (+3/-0)
src/maasserver/views/combo.py (+6/-0)
To merge this branch: bzr merge lp:~ricgard/maas/vs-repeat-ui-improvement
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+325562@code.launchpad.net

Commit message

Created vertical scrolling repeat to improve the loading times of heavy table content pages such as device discovery

Description of the change

Work done
========================

- Added the vs-repeat angular module to MAAS. Module details and code can be found here https://github.com/kamilkp/angular-vs-repeat
- Applied the vs-repeat directive to the following pages:

Device discovery, Node listing, Device listing, Controller listing, Pod listing, Subnet listing, Node events page

How to test
========================

- Pull down the branch and run 'make' then 'make sampledata'
- Go to http://localhost:5240 and install MAAS via the first user journey. Login in with 'admin' 'test.
- View the device discovery page and attempt to rapidly scroll the page. The directive should pull in the table row content and improve the loading time on the page.
- Test in a similar way on other pages such as node events

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

This is awesome! Thanks for doing it.

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (227.8 KiB)

The attempt to merge lp:~ricgard/maas/vs-repeat-ui-improvement into lp:maas failed. Below is the output from the failed tests.

Get:1 http://security.ubuntu.com/ubuntu xenial-security InRelease [102 kB]
Hit:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease
Get:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease [102 kB]
Get:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease [102 kB]
Get:5 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates/main Sources [252 kB]
Get:6 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates/main amd64 Packages [553 kB]
Fetched 1,112 kB in 0s (2,080 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind avahi-utils bash bind9 bind9utils build-essential bzr bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common isc-dhcp-server libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libnss-wrapper libpq-dev make nodejs-legacy npm postgresql psmisc pxelinux python3-all python3-apt python3-attr python3-bson python3-convoy python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-dnspython python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-netaddr python3-netifaces python3-novaclient python3-oauth python3-oauthlib python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-requests python3-seamicroclient python3-setuptools python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-simplejson python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
Reading package lists...
Building dependency tree...
Reading state information...
authbind is already the newest version (2.1.1+nmu1).
avahi-utils is already the newest version (0.6.32~rc+dfsg-1ubuntu2).
build-essential is already the newest version (12.1ubuntu2).
debhelper is already the newest version (9.20160115ubuntu3).
distro-info is already the newest version (0.14build1).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
psmisc is already the newest version (22.21-2.1build1).
pxelinux is already the newest version (3:6.03+dfsg-11ubuntu1).
python-formencode is already the n...

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (125.6 KiB)

The attempt to merge lp:~ricgard/maas/vs-repeat-ui-improvement into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease
Hit:2 http://security.ubuntu.com/ubuntu xenial-security InRelease
Hit:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease
Hit:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind avahi-utils bash bind9 bind9utils build-essential bzr bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common isc-dhcp-server libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libnss-wrapper libpq-dev make nodejs-legacy npm postgresql psmisc pxelinux python3-all python3-apt python3-attr python3-bson python3-convoy python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-dnspython python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-netaddr python3-netifaces python3-novaclient python3-oauth python3-oauthlib python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-requests python3-seamicroclient python3-setuptools python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-simplejson python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
Reading package lists...
Building dependency tree...
Reading state information...
authbind is already the newest version (2.1.1+nmu1).
avahi-utils is already the newest version (0.6.32~rc+dfsg-1ubuntu2).
build-essential is already the newest version (12.1ubuntu2).
debhelper is already the newest version (9.20160115ubuntu3).
distro-info is already the newest version (0.14build1).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
psmisc is already the newest version (22.21-2.1build1).
pxelinux is already the newest version (3:6.03+dfsg-11ubuntu1).
python-formencode is already the newest version (1.3.0-0ubuntu5).
python-lxml is already the newest version (3.5.0-1build1).
python-netaddr is already the newest version (0.7.18-1).
python-netifaces is already the newest version (0.10.4-0.1build2).
python-psycopg2 is already the newest version (2.6.1-1b...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'src/maasserver/static/js/angular/3rdparty/vs-repeat.js'
2--- src/maasserver/static/js/angular/3rdparty/vs-repeat.js 1970-01-01 00:00:00 +0000
3+++ src/maasserver/static/js/angular/3rdparty/vs-repeat.js 2017-06-15 11:16:28 +0000
4@@ -0,0 +1,608 @@
5+//
6+// Copyright Kamil Pękala http://github.com/kamilkp
7+// Angular Virtual Scroll Repeat v1.1.7 2016/03/08
8+//
9+
10+(function(window, angular) {
11+ 'use strict';
12+ /* jshint eqnull:true */
13+ /* jshint -W038 */
14+
15+ // DESCRIPTION:
16+ // vsRepeat directive stands for Virtual Scroll Repeat. It turns a standard ngRepeated set of elements in a scrollable container
17+ // into a component, where the user thinks he has all the elements rendered and all he needs to do is scroll (without any kind of
18+ // pagination - which most users loath) and at the same time the browser isn't overloaded by that many elements/angular bindings etc.
19+ // The directive renders only so many elements that can fit into current container's clientHeight/clientWidth.
20+
21+ // LIMITATIONS:
22+ // - current version only supports an Array as a right-hand-side object for ngRepeat
23+ // - all rendered elements must have the same height/width or the sizes of the elements must be known up front
24+
25+ // USAGE:
26+ // In order to use the vsRepeat directive you need to place a vs-repeat attribute on a direct parent of an element with ng-repeat
27+ // example:
28+ // <div vs-repeat>
29+ // <div ng-repeat="item in someArray">
30+ // <!-- content -->
31+ // </div>
32+ // </div>
33+ //
34+ // or:
35+ // <div vs-repeat>
36+ // <div ng-repeat-start="item in someArray">
37+ // <!-- content -->
38+ // </div>
39+ // <div>
40+ // <!-- something in the middle -->
41+ // </div>
42+ // <div ng-repeat-end>
43+ // <!-- content -->
44+ // </div>
45+ // </div>
46+ //
47+ // You can also measure the single element's height/width (including all paddings and margins), and then speficy it as a value
48+ // of the attribute 'vs-repeat'. This can be used if one wants to override the automatically computed element size.
49+ // example:
50+ // <div vs-repeat="50"> <!-- the specified element height is 50px -->
51+ // <div ng-repeat="item in someArray">
52+ // <!-- content -->
53+ // </div>
54+ // </div>
55+ //
56+ // IMPORTANT!
57+ //
58+ // - the vsRepeat directive must be applied to a direct parent of an element with ngRepeat
59+ // - the value of vsRepeat attribute is the single element's height/width measured in pixels. If none provided, the directive
60+ // will compute it automatically
61+
62+ // OPTIONAL PARAMETERS (attributes):
63+ // vs-repeat-container="selector" - selector for element containing ng-repeat. (defaults to the current element)
64+ // vs-scroll-parent="selector" - selector to the scrollable container. The directive will look for a closest parent matching
65+ // the given selector (defaults to the current element)
66+ // vs-horizontal - stack repeated elements horizontally instead of vertically
67+ // vs-offset-before="value" - top/left offset in pixels (defaults to 0)
68+ // vs-offset-after="value" - bottom/right offset in pixels (defaults to 0)
69+ // vs-excess="value" - an integer number representing the number of elements to be rendered outside of the current container's viewport
70+ // (defaults to 2)
71+ // vs-size - a property name of the items in collection that is a number denoting the element size (in pixels)
72+ // vs-autoresize - use this attribute without vs-size and without specifying element's size. The automatically computed element style will
73+ // readjust upon window resize if the size is dependable on the viewport size
74+ // vs-scrolled-to-end="callback" - callback will be called when the last item of the list is rendered
75+ // vs-scrolled-to-end-offset="integer" - set this number to trigger the scrolledToEnd callback n items before the last gets rendered
76+ // vs-scrolled-to-beginning="callback" - callback will be called when the first item of the list is rendered
77+ // vs-scrolled-to-beginning-offset="integer" - set this number to trigger the scrolledToBeginning callback n items before the first gets rendered
78+
79+ // EVENTS:
80+ // - 'vsRepeatTrigger' - an event the directive listens for to manually trigger reinitialization
81+ // - 'vsRepeatReinitialized' - an event the directive emits upon reinitialization done
82+
83+ var dde = document.documentElement,
84+ matchingFunction = dde.matches ? 'matches' :
85+ dde.matchesSelector ? 'matchesSelector' :
86+ dde.webkitMatches ? 'webkitMatches' :
87+ dde.webkitMatchesSelector ? 'webkitMatchesSelector' :
88+ dde.msMatches ? 'msMatches' :
89+ dde.msMatchesSelector ? 'msMatchesSelector' :
90+ dde.mozMatches ? 'mozMatches' :
91+ dde.mozMatchesSelector ? 'mozMatchesSelector' : null;
92+
93+ var closestElement = angular.element.prototype.closest || function (selector) {
94+ var el = this[0].parentNode;
95+ while (el !== document.documentElement && el != null && !el[matchingFunction](selector)) {
96+ el = el.parentNode;
97+ }
98+
99+ if (el && el[matchingFunction](selector)) {
100+ return angular.element(el);
101+ }
102+ else {
103+ return angular.element();
104+ }
105+ };
106+
107+ function getWindowScroll() {
108+ if ('pageYOffset' in window) {
109+ return {
110+ scrollTop: pageYOffset,
111+ scrollLeft: pageXOffset
112+ };
113+ }
114+ else {
115+ var sx, sy, d = document, r = d.documentElement, b = d.body;
116+ sx = r.scrollLeft || b.scrollLeft || 0;
117+ sy = r.scrollTop || b.scrollTop || 0;
118+ return {
119+ scrollTop: sy,
120+ scrollLeft: sx
121+ };
122+ }
123+ }
124+
125+ function getClientSize(element, sizeProp) {
126+ if (element === window) {
127+ return sizeProp === 'clientWidth' ? window.innerWidth : window.innerHeight;
128+ }
129+ else {
130+ return element[sizeProp];
131+ }
132+ }
133+
134+ function getScrollPos(element, scrollProp) {
135+ return element === window ? getWindowScroll()[scrollProp] : element[scrollProp];
136+ }
137+
138+ function getScrollOffset(vsElement, scrollElement, isHorizontal) {
139+ var vsPos = vsElement.getBoundingClientRect()[isHorizontal ? 'left' : 'top'];
140+ var scrollPos = scrollElement === window ? 0 : scrollElement.getBoundingClientRect()[isHorizontal ? 'left' : 'top'];
141+ var correction = vsPos - scrollPos +
142+ (scrollElement === window ? getWindowScroll() : scrollElement)[isHorizontal ? 'scrollLeft' : 'scrollTop'];
143+
144+ return correction;
145+ }
146+
147+ var vsRepeatModule = angular.module('vs-repeat', []).directive('vsRepeat', ['$compile', '$parse', function($compile, $parse) {
148+ return {
149+ restrict: 'A',
150+ scope: true,
151+ compile: function($element, $attrs) {
152+ var repeatContainer = angular.isDefined($attrs.vsRepeatContainer) ? angular.element($element[0].querySelector($attrs.vsRepeatContainer)) : $element,
153+ ngRepeatChild = repeatContainer.children().eq(0),
154+ ngRepeatExpression,
155+ childCloneHtml = ngRepeatChild[0].outerHTML,
156+ expressionMatches,
157+ lhs,
158+ rhs,
159+ rhsSuffix,
160+ originalNgRepeatAttr,
161+ collectionName = '$vs_collection',
162+ isNgRepeatStart = false,
163+ attributesDictionary = {
164+ 'vsRepeat': 'elementSize',
165+ 'vsOffsetBefore': 'offsetBefore',
166+ 'vsOffsetAfter': 'offsetAfter',
167+ 'vsScrolledToEndOffset': 'scrolledToEndOffset',
168+ 'vsScrolledToBeginningOffset': 'scrolledToBeginningOffset',
169+ 'vsExcess': 'excess'
170+ };
171+
172+ if (ngRepeatChild.attr('ng-repeat')) {
173+ originalNgRepeatAttr = 'ng-repeat';
174+ ngRepeatExpression = ngRepeatChild.attr('ng-repeat');
175+ }
176+ else if (ngRepeatChild.attr('data-ng-repeat')) {
177+ originalNgRepeatAttr = 'data-ng-repeat';
178+ ngRepeatExpression = ngRepeatChild.attr('data-ng-repeat');
179+ }
180+ else if (ngRepeatChild.attr('ng-repeat-start')) {
181+ isNgRepeatStart = true;
182+ originalNgRepeatAttr = 'ng-repeat-start';
183+ ngRepeatExpression = ngRepeatChild.attr('ng-repeat-start');
184+ }
185+ else if (ngRepeatChild.attr('data-ng-repeat-start')) {
186+ isNgRepeatStart = true;
187+ originalNgRepeatAttr = 'data-ng-repeat-start';
188+ ngRepeatExpression = ngRepeatChild.attr('data-ng-repeat-start');
189+ }
190+ else {
191+ throw new Error('angular-vs-repeat: no ng-repeat directive on a child element');
192+ }
193+
194+ expressionMatches = /^\s*(\S+)\s+in\s+([\S\s]+?)(track\s+by\s+\S+)?$/.exec(ngRepeatExpression);
195+ lhs = expressionMatches[1];
196+ rhs = expressionMatches[2];
197+ rhsSuffix = expressionMatches[3];
198+
199+ if (isNgRepeatStart) {
200+ var index = 0;
201+ var repeaterElement = repeatContainer.children().eq(0);
202+ while(repeaterElement.attr('ng-repeat-end') == null && repeaterElement.attr('data-ng-repeat-end') == null) {
203+ index++;
204+ repeaterElement = repeatContainer.children().eq(index);
205+ childCloneHtml += repeaterElement[0].outerHTML;
206+ }
207+ }
208+
209+ repeatContainer.empty();
210+ return {
211+ pre: function($scope, $element, $attrs) {
212+ var repeatContainer = angular.isDefined($attrs.vsRepeatContainer) ? angular.element($element[0].querySelector($attrs.vsRepeatContainer)) : $element,
213+ childClone = angular.element(childCloneHtml),
214+ childTagName = childClone[0].tagName.toLowerCase(),
215+ originalCollection = [],
216+ originalLength,
217+ $$horizontal = typeof $attrs.vsHorizontal !== 'undefined',
218+ $beforeContent = angular.element('<' + childTagName + ' class="vs-repeat-before-content"></' + childTagName + '>'),
219+ $afterContent = angular.element('<' + childTagName + ' class="vs-repeat-after-content"></' + childTagName + '>'),
220+ autoSize = !$attrs.vsRepeat,
221+ sizesPropertyExists = !!$attrs.vsSize || !!$attrs.vsSizeProperty,
222+ $scrollParent = $attrs.vsScrollParent ?
223+ $attrs.vsScrollParent === 'window' ? angular.element(window) :
224+ closestElement.call(repeatContainer, $attrs.vsScrollParent) : repeatContainer,
225+ $$options = 'vsOptions' in $attrs ? $scope.$eval($attrs.vsOptions) : {},
226+ clientSize = $$horizontal ? 'clientWidth' : 'clientHeight',
227+ offsetSize = $$horizontal ? 'offsetWidth' : 'offsetHeight',
228+ scrollPos = $$horizontal ? 'scrollLeft' : 'scrollTop';
229+
230+ $scope.totalSize = 0;
231+ if (!('vsSize' in $attrs) && 'vsSizeProperty' in $attrs) {
232+ console.warn('vs-size-property attribute is deprecated. Please use vs-size attribute which also accepts angular expressions.');
233+ }
234+
235+ if ($scrollParent.length === 0) {
236+ throw 'Specified scroll parent selector did not match any element';
237+ }
238+ $scope.$scrollParent = $scrollParent;
239+
240+ if (sizesPropertyExists) {
241+ $scope.sizesCumulative = [];
242+ }
243+
244+ //initial defaults
245+ $scope.elementSize = (+$attrs.vsRepeat) || getClientSize($scrollParent[0], clientSize) || 50;
246+ $scope.offsetBefore = 0;
247+ $scope.offsetAfter = 0;
248+ $scope.excess = 2;
249+
250+ if ($$horizontal) {
251+ $beforeContent.css('height', '100%');
252+ $afterContent.css('height', '100%');
253+ }
254+ else {
255+ $beforeContent.css('width', '100%');
256+ $afterContent.css('width', '100%');
257+ }
258+
259+ Object.keys(attributesDictionary).forEach(function(key) {
260+ if ($attrs[key]) {
261+ $attrs.$observe(key, function(value) {
262+ // '+' serves for getting a number from the string as the attributes are always strings
263+ $scope[attributesDictionary[key]] = +value;
264+ reinitialize();
265+ });
266+ }
267+ });
268+
269+
270+ $scope.$watchCollection(rhs, function(coll) {
271+ originalCollection = coll || [];
272+ refresh();
273+ });
274+
275+ function refresh() {
276+ if (!originalCollection || originalCollection.length < 1) {
277+ $scope[collectionName] = [];
278+ originalLength = 0;
279+ $scope.sizesCumulative = [0];
280+ }
281+ else {
282+ originalLength = originalCollection.length;
283+ if (sizesPropertyExists) {
284+ $scope.sizes = originalCollection.map(function(item) {
285+ var s = $scope.$new(false);
286+ angular.extend(s, item);
287+ s[lhs] = item;
288+ var size = ($attrs.vsSize || $attrs.vsSizeProperty) ?
289+ s.$eval($attrs.vsSize || $attrs.vsSizeProperty) :
290+ $scope.elementSize;
291+ s.$destroy();
292+ return size;
293+ });
294+ var sum = 0;
295+ $scope.sizesCumulative = $scope.sizes.map(function(size) {
296+ var res = sum;
297+ sum += size;
298+ return res;
299+ });
300+ $scope.sizesCumulative.push(sum);
301+ }
302+ else {
303+ setAutoSize();
304+ }
305+ }
306+
307+ reinitialize();
308+ }
309+
310+ function setAutoSize() {
311+ if (autoSize) {
312+ $scope.$$postDigest(function() {
313+ if (repeatContainer[0].offsetHeight || repeatContainer[0].offsetWidth) { // element is visible
314+ var children = repeatContainer.children(),
315+ i = 0,
316+ gotSomething = false,
317+ insideStartEndSequence = false;
318+
319+ while (i < children.length) {
320+ if (children[i].attributes[originalNgRepeatAttr] != null || insideStartEndSequence) {
321+ if (!gotSomething) {
322+ $scope.elementSize = 0;
323+ }
324+
325+ gotSomething = true;
326+ if (children[i][offsetSize]) {
327+ $scope.elementSize += children[i][offsetSize];
328+ }
329+
330+ if (isNgRepeatStart) {
331+ if (children[i].attributes['ng-repeat-end'] != null || children[i].attributes['data-ng-repeat-end'] != null) {
332+ break;
333+ }
334+ else {
335+ insideStartEndSequence = true;
336+ }
337+ }
338+ else {
339+ break;
340+ }
341+ }
342+ i++;
343+ }
344+
345+ if (gotSomething) {
346+ reinitialize();
347+ autoSize = false;
348+ if ($scope.$root && !$scope.$root.$$phase) {
349+ $scope.$apply();
350+ }
351+ }
352+ }
353+ else {
354+ var dereg = $scope.$watch(function() {
355+ if (repeatContainer[0].offsetHeight || repeatContainer[0].offsetWidth) {
356+ dereg();
357+ setAutoSize();
358+ }
359+ });
360+ }
361+ });
362+ }
363+ }
364+
365+ function getLayoutProp() {
366+ var layoutPropPrefix = childTagName === 'tr' ? '' : 'min-';
367+ var layoutProp = $$horizontal ? layoutPropPrefix + 'width' : layoutPropPrefix + 'height';
368+ return layoutProp;
369+ }
370+
371+ childClone.eq(0).attr(originalNgRepeatAttr, lhs + ' in ' + collectionName + (rhsSuffix ? ' ' + rhsSuffix : ''));
372+ childClone.addClass('vs-repeat-repeated-element');
373+
374+ repeatContainer.append($beforeContent);
375+ repeatContainer.append(childClone);
376+ $compile(childClone)($scope);
377+ repeatContainer.append($afterContent);
378+
379+ $scope.startIndex = 0;
380+ $scope.endIndex = 0;
381+
382+ function scrollHandler() {
383+ if (updateInnerCollection()) {
384+ $scope.$digest();
385+ }
386+ }
387+
388+ $scrollParent.on('scroll', scrollHandler);
389+
390+ function onWindowResize() {
391+ if (typeof $attrs.vsAutoresize !== 'undefined') {
392+ autoSize = true;
393+ setAutoSize();
394+ if ($scope.$root && !$scope.$root.$$phase) {
395+ $scope.$apply();
396+ }
397+ }
398+ if (updateInnerCollection()) {
399+ $scope.$apply();
400+ }
401+ }
402+
403+ angular.element(window).on('resize', onWindowResize);
404+ $scope.$on('$destroy', function() {
405+ angular.element(window).off('resize', onWindowResize);
406+ $scrollParent.off('scroll', scrollHandler);
407+ });
408+
409+ $scope.$on('vsRepeatTrigger', refresh);
410+
411+ $scope.$on('vsRepeatResize', function() {
412+ autoSize = true;
413+ setAutoSize();
414+ });
415+
416+ var _prevStartIndex,
417+ _prevEndIndex,
418+ _minStartIndex,
419+ _maxEndIndex;
420+
421+ $scope.$on('vsRenderAll', function() {//e , quantum) {
422+ if($$options.latch) {
423+ setTimeout(function() {
424+ // var __endIndex = Math.min($scope.endIndex + (quantum || 1), originalLength);
425+ var __endIndex = originalLength;
426+ _maxEndIndex = Math.max(__endIndex, _maxEndIndex);
427+ $scope.endIndex = $$options.latch ? _maxEndIndex : __endIndex;
428+ $scope[collectionName] = originalCollection.slice($scope.startIndex, $scope.endIndex);
429+ _prevEndIndex = $scope.endIndex;
430+
431+ $scope.$$postDigest(function() {
432+ $beforeContent.css(getLayoutProp(), 0);
433+ $afterContent.css(getLayoutProp(), 0);
434+ });
435+
436+ $scope.$apply(function() {
437+ $scope.$emit('vsRenderAllDone');
438+ });
439+ });
440+ }
441+ });
442+
443+ function reinitialize() {
444+ _prevStartIndex = void 0;
445+ _prevEndIndex = void 0;
446+ _minStartIndex = originalLength;
447+ _maxEndIndex = 0;
448+ updateTotalSize(sizesPropertyExists ?
449+ $scope.sizesCumulative[originalLength] :
450+ $scope.elementSize * originalLength
451+ );
452+ updateInnerCollection();
453+
454+ $scope.$emit('vsRepeatReinitialized', $scope.startIndex, $scope.endIndex);
455+ }
456+
457+ function updateTotalSize(size) {
458+ $scope.totalSize = $scope.offsetBefore + size + $scope.offsetAfter;
459+ }
460+
461+ var _prevClientSize;
462+ function reinitOnClientHeightChange() {
463+ var ch = getClientSize($scrollParent[0], clientSize);
464+ if (ch !== _prevClientSize) {
465+ reinitialize();
466+ if ($scope.$root && !$scope.$root.$$phase) {
467+ $scope.$apply();
468+ }
469+ }
470+ _prevClientSize = ch;
471+ }
472+
473+ $scope.$watch(function() {
474+ if (typeof window.requestAnimationFrame === 'function') {
475+ window.requestAnimationFrame(reinitOnClientHeightChange);
476+ }
477+ else {
478+ reinitOnClientHeightChange();
479+ }
480+ });
481+
482+ function updateInnerCollection() {
483+ var $scrollPosition = getScrollPos($scrollParent[0], scrollPos);
484+ var $clientSize = getClientSize($scrollParent[0], clientSize);
485+
486+ var scrollOffset = repeatContainer[0] === $scrollParent[0] ? 0 : getScrollOffset(
487+ repeatContainer[0],
488+ $scrollParent[0],
489+ $$horizontal
490+ );
491+
492+ var __startIndex = $scope.startIndex;
493+ var __endIndex = $scope.endIndex;
494+
495+ if (sizesPropertyExists) {
496+ __startIndex = 0;
497+ while ($scope.sizesCumulative[__startIndex] < $scrollPosition - $scope.offsetBefore - scrollOffset) {
498+ __startIndex++;
499+ }
500+ if (__startIndex > 0) { __startIndex--; }
501+
502+ // Adjust the start index according to the excess
503+ __startIndex = Math.max(
504+ Math.floor(__startIndex - $scope.excess / 2),
505+ 0
506+ );
507+
508+ __endIndex = __startIndex;
509+ while ($scope.sizesCumulative[__endIndex] < $scrollPosition - $scope.offsetBefore - scrollOffset + $clientSize) {
510+ __endIndex++;
511+ }
512+
513+ // Adjust the end index according to the excess
514+ __endIndex = Math.min(
515+ Math.ceil(__endIndex + $scope.excess / 2),
516+ originalLength
517+ );
518+ }
519+ else {
520+ __startIndex = Math.max(
521+ Math.floor(
522+ ($scrollPosition - $scope.offsetBefore - scrollOffset) / $scope.elementSize
523+ ) - $scope.excess / 2,
524+ 0
525+ );
526+
527+ __endIndex = Math.min(
528+ __startIndex + Math.ceil(
529+ $clientSize / $scope.elementSize
530+ ) + $scope.excess,
531+ originalLength
532+ );
533+ }
534+
535+ _minStartIndex = Math.min(__startIndex, _minStartIndex);
536+ _maxEndIndex = Math.max(__endIndex, _maxEndIndex);
537+
538+ $scope.startIndex = $$options.latch ? _minStartIndex : __startIndex;
539+ $scope.endIndex = $$options.latch ? _maxEndIndex : __endIndex;
540+
541+ var digestRequired = false;
542+ if (_prevStartIndex == null) {
543+ digestRequired = true;
544+ }
545+ else if (_prevEndIndex == null) {
546+ digestRequired = true;
547+ }
548+
549+ if (!digestRequired) {
550+ if ($$options.hunked) {
551+ if (Math.abs($scope.startIndex - _prevStartIndex) >= $scope.excess / 2 ||
552+ ($scope.startIndex === 0 && _prevStartIndex !== 0)) {
553+ digestRequired = true;
554+ }
555+ else if (Math.abs($scope.endIndex - _prevEndIndex) >= $scope.excess / 2 ||
556+ ($scope.endIndex === originalLength && _prevEndIndex !== originalLength)) {
557+ digestRequired = true;
558+ }
559+ }
560+ else {
561+ digestRequired = $scope.startIndex !== _prevStartIndex ||
562+ $scope.endIndex !== _prevEndIndex;
563+ }
564+ }
565+
566+ if (digestRequired) {
567+ $scope[collectionName] = originalCollection.slice($scope.startIndex, $scope.endIndex);
568+
569+ // Emit the event
570+ $scope.$emit('vsRepeatInnerCollectionUpdated', $scope.startIndex, $scope.endIndex, _prevStartIndex, _prevEndIndex);
571+ var triggerIndex;
572+ if ($attrs.vsScrolledToEnd) {
573+ triggerIndex = originalCollection.length - ($scope.scrolledToEndOffset || 0);
574+ if (($scope.endIndex >= triggerIndex && _prevEndIndex < triggerIndex) || (originalCollection.length && $scope.endIndex === originalCollection.length)) {
575+ $scope.$eval($attrs.vsScrolledToEnd);
576+ }
577+ }
578+ if ($attrs.vsScrolledToBeginning) {
579+ triggerIndex = $scope.scrolledToBeginningOffset || 0;
580+ if (($scope.startIndex <= triggerIndex && _prevStartIndex > $scope.startIndex)) {
581+ $scope.$eval($attrs.vsScrolledToBeginning);
582+ }
583+ }
584+
585+ _prevStartIndex = $scope.startIndex;
586+ _prevEndIndex = $scope.endIndex;
587+
588+ var offsetCalculationString = sizesPropertyExists ?
589+ '(sizesCumulative[$index + startIndex] + offsetBefore)' :
590+ '(($index + startIndex) * elementSize + offsetBefore)';
591+
592+ var parsed = $parse(offsetCalculationString);
593+ var o1 = parsed($scope, {$index: 0});
594+ var o2 = parsed($scope, {$index: $scope[collectionName].length});
595+ var total = $scope.totalSize;
596+
597+ $beforeContent.css(getLayoutProp(), o1 + 'px');
598+ $afterContent.css(getLayoutProp(), (total - o2) + 'px');
599+ }
600+
601+ return digestRequired;
602+ }
603+ }
604+ };
605+ }
606+ };
607+ }]);
608+
609+ if (typeof module !== 'undefined' && module.exports) {
610+ module.exports = vsRepeatModule.name;
611+ }
612+})(window, window.angular);
613
614=== modified file 'src/maasserver/static/js/angular/maas.js'
615--- src/maasserver/static/js/angular/maas.js 2017-03-10 18:38:46 +0000
616+++ src/maasserver/static/js/angular/maas.js 2017-06-15 11:16:28 +0000
617@@ -9,7 +9,8 @@
618 */
619
620 angular.module('MAAS',
621- ['ngRoute', 'ngCookies', 'ngSanitize', 'ngTagsInput', 'sticky']).config(
622+ ['ngRoute', 'ngCookies', 'ngSanitize', 'ngTagsInput', 'sticky',
623+ 'vs-repeat']).config(
624 function($interpolateProvider, $routeProvider, $httpProvider) {
625 $interpolateProvider.startSymbol('{$');
626 $interpolateProvider.endSymbol('$}');
627
628=== modified file 'src/maasserver/static/partials/dashboard.html'
629--- src/maasserver/static/partials/dashboard.html 2017-04-18 10:57:48 +0000
630+++ src/maasserver/static/partials/dashboard.html 2017-06-15 11:16:28 +0000
631@@ -46,7 +46,8 @@
632 No new discoveries
633 </div>
634 </div>
635- <div class="table__row"
636+ <div vs-repeat vs-scroll-parent="window">
637+ <div class="table__row"
638 data-ng-repeat="discovery in discoveredDevices | orderBy:'-last_seen' track by discovery.first_seen"
639 data-ng-class="{'is-active' : discovery.first_seen === selectedDevice}">
640 <div data-ng-if="discovery.first_seen !== selectedDevice"
641@@ -179,6 +180,7 @@
642 </div>
643 </maas-obj-form>
644 </div>
645+ </div>
646 </div>
647 </div>
648 </div>
649
650=== modified file 'src/maasserver/static/partials/machines-table.html'
651--- src/maasserver/static/partials/machines-table.html 2017-04-18 11:31:25 +0000
652+++ src/maasserver/static/partials/machines-table.html 2017-06-15 11:16:28 +0000
653@@ -39,7 +39,7 @@
654 </th>
655 </tr>
656 </thead>
657- <tbody>
658+ <tbody vs-repeat vs-scroll-parent="window">
659 <tr
660 data-ng-repeat="node in filteredMachines = (machines | nodesFilter:search | orderBy:table.predicate:table.reverse) track by node.system_id"
661 data-ng-class="{ 'table--error': machineHasError({ $machine: node }), selected: node.$selected }">
662
663=== modified file 'src/maasserver/static/partials/networks-list.html'
664--- src/maasserver/static/partials/networks-list.html 2017-04-10 15:35:18 +0000
665+++ src/maasserver/static/partials/networks-list.html 2017-06-15 11:16:28 +0000
666@@ -147,7 +147,7 @@
667 <th class="table__column--13 align-right">Available IPs</th>
668 </tr>
669 </thead>
670- <tbody>
671+ <tbody vs-repeat vs-scroll-parent="window">
672 <tr class="table-listing__row" data-ng-repeat="row in group.spaces.rows">
673 <!-- <td class="table-listing__cell table__column--3 ng-hide">
674 <div data-ng-if="row.space_name">
675@@ -207,7 +207,7 @@
676 <th class="table__column--25">Space</th>
677 </tr>
678 </thead>
679- <tbody>
680+ <tbody vs-repeat vs-scroll-parent="window">
681 <tr class="table-listing__row" data-ng-repeat="row in group.fabrics.rows">
682 <!-- <td class="table-listing__cell table__column--3 ng-hide">
683 <div data-ng-if="row.fabric_name">
684
685=== modified file 'src/maasserver/static/partials/node-events.html'
686--- src/maasserver/static/partials/node-events.html 2017-04-05 02:36:06 +0000
687+++ src/maasserver/static/partials/node-events.html 2017-06-15 11:16:28 +0000
688@@ -35,7 +35,7 @@
689 <th class="table-col--20">Time</th>
690 </tr>
691 </thead>
692- <tbody>
693+ <tbody vs-repeat vs-scroll-parent="window">
694 <tr
695 data-ng-repeat="event in events | filter:search | orderByDate:'created':'id' track by event.id">
696 <td class="table-col--1 u-padding--right-none u-padding--left-none">
697
698=== modified file 'src/maasserver/static/partials/nodes-list.html'
699--- src/maasserver/static/partials/nodes-list.html 2017-05-25 13:56:07 +0000
700+++ src/maasserver/static/partials/nodes-list.html 2017-06-15 11:16:28 +0000
701@@ -452,7 +452,7 @@
702 <th class="table-col--5"></th>
703 </tr>
704 </thead>
705- <tbody>
706+ <tbody vs-repeat vs-scroll-parent="window">
707 <tr data-ng-repeat="interface in device.interfaces">
708 <td class="table-col--20" aria-label="MAC address">
709 <input type="text" id="mac-address1" placeholder="00:00:00:00:00:00"
710@@ -857,7 +857,7 @@
711 </th>
712 </tr>
713 </thead>
714- <tbody>
715+ <tbody vs-repeat vs-scroll-parent="window">
716 <!-- XXX rvba 2015-02-25 - Need to add e2e test. This really needs lots of tests. -->
717 <tr
718 data-ng-repeat="device in tabs.devices.filtered_items = (devices | nodesFilter:tabs.devices.search | orderBy:tabs.devices.predicate:tabs.devices.reverse) track by device.system_id"
719@@ -921,7 +921,7 @@
720 </th>
721 </tr>
722 </thead>
723- <tbody>
724+ <tbody vs-repeat vs-scroll-parent="window">
725 <!-- XXX rvba 2015-02-25 - Need to add e2e test. This really needs lots of tests. -->
726 <tr
727 data-ng-repeat="controller in tabs.controllers.filtered_items = (controllers | nodesFilter:tabs.controllers.search | orderBy:tabs.controllers.predicate:tabs.controllers.reverse) track by controller.system_id"
728
729=== modified file 'src/maasserver/static/partials/pods-list.html'
730--- src/maasserver/static/partials/pods-list.html 2017-05-01 20:31:46 +0000
731+++ src/maasserver/static/partials/pods-list.html 2017-06-15 11:16:28 +0000
732@@ -123,7 +123,7 @@
733 </div>
734 </div>
735 </header>
736- <div class="table__body">
737+ <div class="table__body" vs-repeat vs-scroll-parent="window">
738 <div class="table__row" data-ng-repeat="pod in filteredItems = (pods | nodesFilter:search | orderBy:predicate:reverse) track by pod.id"
739 data-ng-class="{ selected: pod.$selected, 'is-active': pod.$selected && pod.action_failed }">
740 <div data-ng-if="isSuperUser()">
741
742=== modified file 'src/maasserver/templates/maasserver/js-conf.html'
743--- src/maasserver/templates/maasserver/js-conf.html 2017-03-10 15:30:26 +0000
744+++ src/maasserver/templates/maasserver/js-conf.html 2017-06-15 11:16:28 +0000
745@@ -42,6 +42,9 @@
746 src="{% url "merge" filename="sticky.js" %}?v={{files_version}}">
747 </script>
748 <script type="text/javascript"
749+ src="{% url "merge" filename="vs-repeat.js" %}?v={{files_version}}">
750+</script>
751+<script type="text/javascript"
752 src="{% url "merge" filename="maas-angular.js" %}?v={{files_version}}">
753 </script>
754
755
756=== modified file 'src/maasserver/views/combo.py'
757--- src/maasserver/views/combo.py 2017-05-25 13:56:07 +0000
758+++ src/maasserver/views/combo.py 2017-06-15 11:16:28 +0000
759@@ -53,6 +53,12 @@
760 "js/angular/3rdparty/sticky.js",
761 ]
762 },
763+ "vs-repeat.js": {
764+ "content_type": "text/javascript; charset=UTF-8",
765+ "files": [
766+ "js/angular/3rdparty/vs-repeat.js",
767+ ]
768+ },
769 "maas-angular.js": {
770 "content_type": "text/javascript; charset=UTF-8",
771 "files": [