rewrite output scrolling service

This commit is contained in:
Jake McDermott
2018-07-30 18:16:54 -04:00
parent 4c3370bd34
commit fed729f101
8 changed files with 200 additions and 168 deletions

View File

@@ -74,6 +74,12 @@
color: @at-blue; color: @at-blue;
} }
&-menuIconStack--wrapper {
&:hover {
color: @at-blue;
}
}
&-row { &-row {
display: flex; display: flex;

View File

@@ -5,8 +5,8 @@ import {
} from './constants'; } from './constants';
const BASE_PARAMS = { const BASE_PARAMS = {
order_by: OUTPUT_ORDER_BY,
page_size: OUTPUT_PAGE_SIZE, page_size: OUTPUT_PAGE_SIZE,
order_by: OUTPUT_ORDER_BY,
}; };
const merge = (...objs) => _.merge({}, ...objs); const merge = (...objs) => _.merge({}, ...objs);
@@ -20,12 +20,6 @@ function JobEventsApiService ($http, $q) {
this.cache = {}; this.cache = {};
}; };
this.clearCache = () => {
Object.keys(this.cache).forEach(key => {
delete this.cache[key];
});
};
this.fetch = () => this.getLast() this.fetch = () => this.getLast()
.then(results => { .then(results => {
this.cache.last = results; this.cache.last = results;
@@ -33,20 +27,31 @@ function JobEventsApiService ($http, $q) {
return this; return this;
}); });
this.clearCache = () => {
Object.keys(this.cache).forEach(key => {
delete this.cache[key];
});
};
this.pushMaxCounter = events => {
const maxCounter = Math.max(...events.map(({ counter }) => counter));
if (maxCounter > this.state.maxCounter) {
this.state.maxCounter = maxCounter;
}
return maxCounter;
};
this.getFirst = () => { this.getFirst = () => {
const page = 1; const params = merge(this.params, { page: 1 });
const params = merge(this.params, { page });
return $http.get(this.endpoint, { params }) return $http.get(this.endpoint, { params })
.then(({ data }) => { .then(({ data }) => {
const { results, count } = data; const { results, count } = data;
const maxCounter = Math.max(...results.map(({ counter }) => counter));
this.state.count = count; this.state.count = count;
this.pushMaxCounter(results);
if (maxCounter > this.state.maxCounter) {
this.state.maxCounter = maxCounter;
}
return results; return results;
}); });
@@ -62,13 +67,9 @@ function JobEventsApiService ($http, $q) {
return $http.get(this.endpoint, { params }) return $http.get(this.endpoint, { params })
.then(({ data }) => { .then(({ data }) => {
const { results, count } = data; const { results, count } = data;
const maxCounter = Math.max(...results.map(({ counter }) => counter));
this.state.count = count; this.state.count = count;
this.pushMaxCounter(results);
if (maxCounter > this.state.maxCounter) {
this.state.maxCounter = maxCounter;
}
return results; return results;
}); });
@@ -84,7 +85,6 @@ function JobEventsApiService ($http, $q) {
return $http.get(this.endpoint, { params }) return $http.get(this.endpoint, { params })
.then(({ data }) => { .then(({ data }) => {
const { results, count } = data; const { results, count } = data;
const maxCounter = Math.max(...results.map(({ counter }) => counter));
let rotated = results; let rotated = results;
@@ -97,10 +97,7 @@ function JobEventsApiService ($http, $q) {
} }
this.state.count = count; this.state.count = count;
this.pushMaxCounter(results);
if (maxCounter > this.state.maxCounter) {
this.state.maxCounter = maxCounter;
}
return rotated; return rotated;
}); });
@@ -119,11 +116,8 @@ function JobEventsApiService ($http, $q) {
return $http.get(this.endpoint, { params }) return $http.get(this.endpoint, { params })
.then(({ data }) => { .then(({ data }) => {
const { results } = data; const { results } = data;
const maxCounter = Math.max(...results.map(({ counter }) => counter));
if (maxCounter > this.state.maxCounter) { this.pushMaxCounter(results);
this.state.maxCounter = maxCounter;
}
return results; return results;
}); });

View File

@@ -22,9 +22,6 @@ const bufferState = [0, 0]; // [length, count]
const listeners = []; const listeners = [];
const rx = []; const rx = [];
let following = false;
let attach = true;
function bufferInit () { function bufferInit () {
rx.length = 0; rx.length = 0;
@@ -57,59 +54,91 @@ function bufferEmpty (min, max) {
return removed; return removed;
} }
let attached = false;
let noframes = false;
let isOnLastPage = false;
function onFrames (events) { function onFrames (events) {
if (!following) { if (noframes) {
return $q.resolve();
}
if (!attached) {
const minCounter = Math.min(...events.map(({ counter }) => counter)); const minCounter = Math.min(...events.map(({ counter }) => counter));
// attachment range
const max = slide.getTailCounter() + 1;
const min = Math.max(1, slide.getHeadCounter(), max - 50);
if (minCounter > max || minCounter < min) { if (minCounter > slide.getTailCounter() + 1) {
return $q.resolve(); return $q.resolve();
} }
if (!attach) { attached = true;
return $q.resolve(); }
}
follow(); if (vm.isInFollowMode) {
vm.isFollowing = true;
} }
const capacity = slide.getCapacity(); const capacity = slide.getCapacity();
if (capacity <= 0 && !isOnLastPage) {
attached = false;
return $q.resolve();
}
return slide.popBack(events.length - capacity) return slide.popBack(events.length - capacity)
.then(() => slide.pushFront(events)) .then(() => slide.pushFront(events))
.then(() => { .then(() => {
scroll.setScrollPosition(scroll.getScrollHeight()); if (vm.isFollowing && scroll.isBeyondLowerThreshold()) {
scroll.scrollToBottom();
}
return $q.resolve(); return $q.resolve();
}); });
} }
function first () { function first () {
unfollow();
scroll.pause(); scroll.pause();
unfollow();
return slide.getFirst() attached = false;
noframes = true;
isOnLastPage = false;
slide.getFirst()
.then(() => { .then(() => {
scroll.resume(); scroll.resume();
noframes = false;
return $q.resolve(); return $q.resolve();
}); });
} }
function next () { function next () {
if (vm.isFollowing) {
return $q.resolve();
}
scroll.pause(); scroll.pause();
return slide.getNext() return slide.getNext()
.then(() => {
isOnLastPage = slide.isOnLastPage();
if (isOnLastPage) {
stream.setMissingCounterThreshold(slide.getTailCounter() + 1);
if (scroll.isBeyondLowerThreshold()) {
scroll.scrollToBottom();
follow();
}
}
})
.finally(() => scroll.resume()); .finally(() => scroll.resume());
} }
function previous () { function previous () {
unfollow();
scroll.pause(); scroll.pause();
const initialPosition = scroll.getScrollPosition(); const initialPosition = scroll.getScrollPosition();
isOnLastPage = false;
return slide.getPrevious() return slide.getPrevious()
.then(popHeight => { .then(popHeight => {
@@ -121,6 +150,22 @@ function previous () {
.finally(() => scroll.resume()); .finally(() => scroll.resume());
} }
function menuLast () {
if (vm.isFollowing) {
unfollow();
return $q.resolve();
}
if (isOnLastPage) {
scroll.scrollToBottom();
return $q.resolve();
}
return last();
}
function last () { function last () {
scroll.pause(); scroll.pause();
@@ -129,7 +174,8 @@ function last () {
stream.setMissingCounterThreshold(slide.getTailCounter() + 1); stream.setMissingCounterThreshold(slide.getTailCounter() + 1);
scroll.setScrollPosition(scroll.getScrollHeight()); scroll.setScrollPosition(scroll.getScrollHeight());
attach = true; isOnLastPage = true;
follow();
scroll.resume(); scroll.resume();
return $q.resolve(); return $q.resolve();
@@ -141,28 +187,21 @@ function down () {
} }
function up () { function up () {
if (following) { scroll.moveUp();
unfollow();
} else {
scroll.moveUp();
}
} }
function follow () { function follow () {
scroll.pause(); isOnLastPage = slide.isOnLastPage();
scroll.hide();
following = true; if (resource.model.get('event_processing_finished')) return;
vm.isFollowing = following; if (!isOnLastPage) return;
vm.isInFollowMode = true;
} }
function unfollow () { function unfollow () {
attach = false; vm.isInFollowMode = false;
following = false; vm.isFollowing = false;
vm.isFollowing = following;
scroll.unhide();
scroll.resume();
} }
function togglePanelExpand () { function togglePanelExpand () {
@@ -276,6 +315,13 @@ function reloadState (params) {
return $state.transitionTo($state.current, params, { inherit: false, location: 'replace' }); return $state.transitionTo($state.current, params, { inherit: false, location: 'replace' });
} }
function getMaxCounter () {
const apiMax = resource.events.getMaxCounter();
const wsMax = stream.getMaxCounter();
return Math.max(apiMax, wsMax);
}
function OutputIndexController ( function OutputIndexController (
_$compile_, _$compile_,
_$q_, _$q_,
@@ -318,9 +364,10 @@ function OutputIndexController (
vm.togglePanelExpand = togglePanelExpand; vm.togglePanelExpand = togglePanelExpand;
// Stdout Navigation // Stdout Navigation
vm.menu = { last, first, down, up }; vm.menu = { last: menuLast, first, down, up };
vm.isMenuExpanded = true; vm.isMenuExpanded = true;
vm.isFollowing = following; vm.isFollowing = false;
vm.isInFollowMode = false;
vm.toggleMenuExpand = toggleMenuExpand; vm.toggleMenuExpand = toggleMenuExpand;
vm.toggleLineExpand = toggleLineExpand; vm.toggleLineExpand = toggleLineExpand;
vm.showHostDetails = showHostDetails; vm.showHostDetails = showHostDetails;
@@ -330,10 +377,21 @@ function OutputIndexController (
bufferInit(); bufferInit();
status.init(resource); status.init(resource);
slide.init(render, resource.events, scroll); slide.init(render, resource.events, scroll, { getMaxCounter });
render.init({ compile, toggles: vm.toggleLineEnabled }); render.init({ compile, toggles: vm.toggleLineEnabled });
scroll.init({ previous, next });
scroll.init({
next,
previous,
onLeaveLower () {
unfollow();
return $q.resolve();
},
onEnterLower () {
follow();
return $q.resolve();
},
});
stream.init({ stream.init({
bufferAdd, bufferAdd,

View File

@@ -24,10 +24,9 @@
<i class="at-Stdout-menuIcon fa" ng-if="vm.toggleLineEnabled" <i class="at-Stdout-menuIcon fa" ng-if="vm.toggleLineEnabled"
ng-class="{ 'fa-minus': vm.isMenuExpanded, 'fa-plus': !vm.isMenuExpanded }"></i> ng-class="{ 'fa-minus': vm.isMenuExpanded, 'fa-plus': !vm.isMenuExpanded }"></i>
</div> </div>
<div class="pull-right" ng-click="vm.menu.last()"> <div class="pull-right" ng-click="vm.menu.last()">
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-down" <i class="at-Stdout-menuIcon--lg fa fa-angle-double-down"
ng-class=" { 'at-Stdout-menuIcon--active': vm.menu.isLocked }"></i> ng-class=" { 'at-Stdout-menuIconStackTop--active': false }"></i>
</div> </div>
<div class="pull-right" ng-click="vm.menu.first()"> <div class="pull-right" ng-click="vm.menu.first()">
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-up"></i> <i class="at-Stdout-menuIcon--lg fa fa-angle-double-up"></i>
@@ -36,8 +35,7 @@
<i class="at-Stdout-menuIcon--lg fa fa-angle-down"></i> <i class="at-Stdout-menuIcon--lg fa fa-angle-down"></i>
</div> </div>
<div class="pull-right" ng-click="vm.menu.up()"> <div class="pull-right" ng-click="vm.menu.up()">
<i class="at-Stdout-menuIcon--lg fa fa-angle-up" <i class="at-Stdout-menuIcon--lg fa fa-angle-up"></i>
ng-class=" { 'at-Stdout-menuIcon--active': vm.isFollowing }"></i>
</div> </div>
<div class="at-u-clear"></div> <div class="at-u-clear"></div>
@@ -48,15 +46,6 @@
<div id="atStdoutResultTable"></div> <div id="atStdoutResultTable"></div>
<div class="at-Stdout-borderFooter"></div> <div class="at-Stdout-borderFooter"></div>
</div> </div>
<div ng-show="vm.menu.showBackToTop" class="at-Stdout-menuBottom">
<div class="at-Stdout-menuIconGroup" ng-click="vm.menu.home()">
<p class="pull-left"><i class="fa fa-angle-double-up"></i></p>
<p class="pull-right">{{:: vm.strings.get('stdout.BACK_TO_TOP') }}</p>
</div>
<div class="at-u-clear"></div>
</div>
</div> </div>
</at-panel> </at-panel>
</div> </div>

View File

@@ -235,6 +235,7 @@ function PageService ($q) {
}) })
.then(() => this.getNext()); .then(() => this.getNext());
this.isOnLastPage = () => this.api.getLastPageNumber() === this.state.tail;
this.getRecordCount = () => Object.keys(this.records).length; this.getRecordCount = () => Object.keys(this.records).length;
this.getTailCounter = () => this.state.tail; this.getTailCounter = () => this.state.tail;
} }

View File

@@ -6,7 +6,7 @@ import {
} from './constants'; } from './constants';
function JobScrollService ($q, $timeout) { function JobScrollService ($q, $timeout) {
this.init = ({ next, previous }) => { this.init = ({ next, previous, onLeaveLower, onEnterLower }) => {
this.el = $(OUTPUT_ELEMENT_CONTAINER); this.el = $(OUTPUT_ELEMENT_CONTAINER);
this.timer = null; this.timer = null;
@@ -15,18 +15,23 @@ function JobScrollService ($q, $timeout) {
current: 0 current: 0
}; };
this.threshold = {
previous: 0,
current: 0,
};
this.hooks = { this.hooks = {
next, next,
previous, previous,
isAtRest: () => $q.resolve() onLeaveLower,
onEnterLower,
}; };
this.state = { this.state = {
hidden: false,
paused: false, paused: false,
top: true,
}; };
this.chain = $q.resolve();
this.el.scroll(this.listen); this.el.scroll(this.listen);
}; };
@@ -42,70 +47,82 @@ function JobScrollService ($q, $timeout) {
this.timer = $timeout(this.register, OUTPUT_SCROLL_DELAY); this.timer = $timeout(this.register, OUTPUT_SCROLL_DELAY);
}; };
this.isBeyondThreshold = () => {
const position = this.getScrollPosition();
const viewport = this.getScrollHeight() - this.getViewableHeight();
const threshold = position / viewport;
return (1 - threshold) < OUTPUT_SCROLL_THRESHOLD;
};
this.register = () => { this.register = () => {
this.pause(); this.pause();
const current = this.getScrollPosition(); const position = this.getScrollPosition();
const downward = current > this.position.previous; const viewport = this.getScrollHeight() - this.getViewableHeight();
let promise; const threshold = position / viewport;
const downward = position > this.position.previous;
if (downward && this.isBeyondThreshold(downward, current)) { const isBeyondUpperThreshold = threshold < OUTPUT_SCROLL_THRESHOLD;
promise = this.hooks.next; const isBeyondLowerThreshold = (1 - threshold) < OUTPUT_SCROLL_THRESHOLD;
} else if (!downward && this.isBeyondThreshold(downward, current)) {
promise = this.hooks.previous; const wasBeyondUpperThreshold = this.threshold.previous < OUTPUT_SCROLL_THRESHOLD;
const wasBeyondLowerThreshold = (1 - this.threshold.previous) < OUTPUT_SCROLL_THRESHOLD;
const transitions = [];
if (position <= 0 || (isBeyondUpperThreshold && !wasBeyondUpperThreshold)) {
transitions.push(this.hooks.previous);
} }
if (!promise) { if (!isBeyondLowerThreshold && wasBeyondLowerThreshold) {
this.setScrollPosition(current); transitions.push(this.hooks.onLeaveLower);
this.isAtRest();
this.resume();
return $q.resolve();
} }
return promise() if (isBeyondLowerThreshold && !wasBeyondLowerThreshold) {
transitions.push(this.hooks.onEnterLower);
transitions.push(this.hooks.next);
} else if (threshold >= 1) {
transitions.push(this.hooks.next);
}
if (!downward) {
transitions.reverse();
}
this.position.current = position;
this.threshold.current = threshold;
transitions.forEach(promise => {
this.chain = this.chain.then(() => promise());
});
return this.chain
.then(() => { .then(() => {
this.setScrollPosition(this.getScrollPosition());
this.isAtRest();
this.resume(); this.resume();
this.setScrollPosition(this.getScrollPosition());
return $q.resolve();
}); });
}; };
this.isBeyondThreshold = (downward, current) => {
const height = this.getScrollHeight();
if (downward) {
current += this.getViewableHeight();
if (current >= height || ((height - current) / height) < OUTPUT_SCROLL_THRESHOLD) {
return true;
}
} else if (current <= 0 || (current / height) < OUTPUT_SCROLL_THRESHOLD) {
return true;
}
return false;
};
/** /**
* Move scroll position up by one page of visible content. * Move scroll position up by one page of visible content.
*/ */
this.moveUp = () => { this.moveUp = () => {
const top = this.getScrollPosition(); const position = this.getScrollPosition() - this.getViewableHeight();
const height = this.getViewableHeight();
this.setScrollPosition(top - height); this.setScrollPosition(position);
}; };
/** /**
* Move scroll position down by one page of visible content. * Move scroll position down by one page of visible content.
*/ */
this.moveDown = () => { this.moveDown = () => {
const top = this.getScrollPosition(); const position = this.getScrollPosition() + this.getViewableHeight();
const height = this.getViewableHeight();
this.setScrollPosition(top + height); this.setScrollPosition(position);
}; };
this.getScrollHeight = () => this.el[0].scrollHeight; this.getScrollHeight = () => this.el[0].scrollHeight;
@@ -119,33 +136,27 @@ function JobScrollService ($q, $timeout) {
this.getScrollPosition = () => this.el[0].scrollTop; this.getScrollPosition = () => this.el[0].scrollTop;
this.setScrollPosition = position => { this.setScrollPosition = position => {
const viewport = this.getScrollHeight() - this.getViewableHeight();
this.position.previous = this.position.current; this.position.previous = this.position.current;
this.threshold.previous = this.position.previous / viewport;
this.position.current = position; this.position.current = position;
this.el[0].scrollTop = position; this.el[0].scrollTop = position;
this.isAtRest();
}; };
this.resetScrollPosition = () => { this.resetScrollPosition = () => {
this.threshold.previous = 0;
this.position.previous = 0; this.position.previous = 0;
this.position.current = 0; this.position.current = 0;
this.el[0].scrollTop = 0; this.el[0].scrollTop = 0;
this.isAtRest();
}; };
this.scrollToBottom = () => { this.scrollToBottom = () => {
this.setScrollPosition(this.getScrollHeight()); this.setScrollPosition(this.getScrollHeight());
}; };
this.isAtRest = () => {
if (this.position.current === 0 && !this.state.top) {
this.state.top = true;
this.hooks.isAtRest(true);
} else if (this.position.current > 0 && this.state.top) {
this.state.top = false;
this.hooks.isAtRest(false);
}
};
this.resume = () => { this.resume = () => {
this.state.paused = false; this.state.paused = false;
}; };
@@ -154,32 +165,8 @@ function JobScrollService ($q, $timeout) {
this.state.paused = true; this.state.paused = true;
}; };
this.isPaused = () => this.state.paused;
this.lock = () => {
this.state.locked = true;
};
this.unlock = () => {
this.state.locked = false;
};
this.hide = () => {
if (!this.state.hidden) {
this.el.css('overflow', 'hidden');
this.state.hidden = true;
}
};
this.unhide = () => {
if (this.state.hidden) {
this.el.css('overflow', 'auto');
this.state.hidden = false;
}
};
this.isLocked = () => this.state.locked;
this.isMissing = () => $(OUTPUT_ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight(); this.isMissing = () => $(OUTPUT_ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight();
this.isPaused = () => this.state.paused;
} }
JobScrollService.$inject = ['$q', '$timeout']; JobScrollService.$inject = ['$q', '$timeout'];

View File

@@ -77,15 +77,15 @@ function getBoundedRange (range, other) {
} }
function SlidingWindowService ($q) { function SlidingWindowService ($q) {
this.init = (storage, api, { getScrollHeight }) => { this.init = (storage, api, { getScrollHeight }, { getMaxCounter }) => {
const { prepend, append, shift, pop, deleteRecord } = storage; const { prepend, append, shift, pop, deleteRecord } = storage;
const { getMaxCounter, getRange, getFirst, getLast } = api; const { getRange, getFirst, getLast } = api;
this.api = { this.api = {
getMaxCounter,
getRange, getRange,
getFirst, getFirst,
getLast, getLast,
getMaxCounter,
}; };
this.storage = { this.storage = {
@@ -352,13 +352,8 @@ function SlidingWindowService ($q) {
return Number.isFinite(head) ? head : 0; return Number.isFinite(head) ? head : 0;
}; };
this.getMaxCounter = () => { this.isOnLastPage = () => this.getTailCounter() >= (this.getMaxCounter() - OUTPUT_PAGE_SIZE);
const counter = this.api.getMaxCounter(); this.getMaxCounter = () => this.api.getMaxCounter();
const tail = this.getTailCounter();
return Number.isFinite(counter) ? Math.max(tail, counter) : tail;
};
this.getRange = () => [this.getHeadCounter(), this.getTailCounter()]; this.getRange = () => [this.getHeadCounter(), this.getTailCounter()];
this.getRecordCount = () => Object.keys(this.records).length; this.getRecordCount = () => Object.keys(this.records).length;
this.getCapacity = () => OUTPUT_EVENT_LIMIT - this.getRecordCount(); this.getCapacity = () => OUTPUT_EVENT_LIMIT - this.getRecordCount();

View File

@@ -160,6 +160,8 @@ function OutputStream ($q) {
this.counters.ready.length = 0; this.counters.ready.length = 0;
return $q.resolve(); return $q.resolve();
}); });
this.getMaxCounter = () => this.counters.max;
} }
OutputStream.$inject = ['$q']; OutputStream.$inject = ['$q'];