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;
}
&-menuIconStack--wrapper {
&:hover {
color: @at-blue;
}
}
&-row {
display: flex;

View File

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

View File

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

View File

@ -24,10 +24,9 @@
<i class="at-Stdout-menuIcon fa" ng-if="vm.toggleLineEnabled"
ng-class="{ 'fa-minus': vm.isMenuExpanded, 'fa-plus': !vm.isMenuExpanded }"></i>
</div>
<div class="pull-right" ng-click="vm.menu.last()">
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-down"
ng-class=" { 'at-Stdout-menuIcon--active': vm.menu.isLocked }"></i>
<i class="at-Stdout-menuIcon--lg fa fa-angle-double-down"
ng-class=" { 'at-Stdout-menuIconStackTop--active': false }"></i>
</div>
<div class="pull-right" ng-click="vm.menu.first()">
<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>
</div>
<div class="pull-right" ng-click="vm.menu.up()">
<i class="at-Stdout-menuIcon--lg fa fa-angle-up"
ng-class=" { 'at-Stdout-menuIcon--active': vm.isFollowing }"></i>
<i class="at-Stdout-menuIcon--lg fa fa-angle-up"></i>
</div>
<div class="at-u-clear"></div>
@ -48,15 +46,6 @@
<div id="atStdoutResultTable"></div>
<div class="at-Stdout-borderFooter"></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>
</at-panel>
</div>

View File

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

View File

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

View File

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

View File

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