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

Proposed by Richard McCartney
Status: Merged
Approved by: Mike Pontillo
Approved revision: no longer in the source branch.
Merged at revision: 6066
Proposed branch: lp:~ricgard/maas/vs-repeat-ui-improvement--2.2
Merge into: lp:maas/2.2
Diff against target: 792 lines (+651/-10)
10 files modified
src/maasserver/static/js/angular/3rdparty/vs-repeat.js (+629/-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--2.2
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Richard McCartney (community) Needs Resubmitting
Review via email: mp+325848@code.launchpad.net

Commit message

Backport: Commit 6088: 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
Mike Pontillo (mpontillo) wrote :

I tested this and it's working great on my MAAS 2.2 setup. Nice work!

However, this cannot land unless we fulfill the licensing requirements for including this third-party code[1]. (I guess that means the original branch that landed in trunk needs to be fixed, too; I didn't review that one.)

Per the MIT license, we need to acknowledge our usage of his code by redistributing the copyright notice and MIT license stating freely-available permission to copy the work.

Perhaps we should have some kind of "About" page in MAAS which includes a list of all third-party code included in MAAS, like I've seen many other projects do.

For now, since I don't think we want to design an about page at the moment, if we just modify the 'vs-repeat.js' file to include the MIT license in JavaScript comments at the top, in my (non-lawyer) opinion we would fulfill the requirements of the license.

[1]: https://github.com/kamilkp/angular-vs-repeat/blob/master/LICENSE

review: Needs Fixing
Revision history for this message
Andres Rodriguez (andreserl) wrote :

Actually, the way we have always done this is by :

1. having the right headers
2. updating the packaging to highlight this file is with a different license.

Since we are now releasing snaps that do not include a debian/copyright file, we need to include a LICENSE file listing all those files with different licenses.

Revision history for this message
Richard McCartney (ricgard) wrote :

Mike can you just check now, I've added the license

Rich

review: Needs Resubmitting
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Looks good to me. Per the comments from Andres above, we need to also update the licenses that ship with our packaging (both snap and deb). But that can be done in a separate branch.

review: Approve

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

Subscribers

People subscribed via source and target branches