From fed729f1014b4912e3766c9ead6864a34ccb7604 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 30 Jul 2018 18:16:54 -0400 Subject: [PATCH] rewrite output scrolling service --- awx/ui/client/features/output/_index.less | 6 + .../features/output/api.events.service.js | 50 +++--- .../features/output/index.controller.js | 132 +++++++++++----- awx/ui/client/features/output/index.view.html | 17 +- awx/ui/client/features/output/page.service.js | 1 + .../client/features/output/scroll.service.js | 145 ++++++++---------- .../client/features/output/slide.service.js | 15 +- .../client/features/output/stream.service.js | 2 + 8 files changed, 200 insertions(+), 168 deletions(-) diff --git a/awx/ui/client/features/output/_index.less b/awx/ui/client/features/output/_index.less index 67224f387a..3e14f353a1 100644 --- a/awx/ui/client/features/output/_index.less +++ b/awx/ui/client/features/output/_index.less @@ -74,6 +74,12 @@ color: @at-blue; } + &-menuIconStack--wrapper { + &:hover { + color: @at-blue; + } + } + &-row { display: flex; diff --git a/awx/ui/client/features/output/api.events.service.js b/awx/ui/client/features/output/api.events.service.js index 8db532cc25..7b7571fa72 100644 --- a/awx/ui/client/features/output/api.events.service.js +++ b/awx/ui/client/features/output/api.events.service.js @@ -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; }); diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 8532bf4416..5c71a1b861 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -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, diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index c588f2b375..511da3c6dc 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -24,10 +24,9 @@ -
- +
@@ -36,8 +35,7 @@
- +
@@ -48,15 +46,6 @@
- -
-
-

-

{{:: vm.strings.get('stdout.BACK_TO_TOP') }}

-
- -
-
diff --git a/awx/ui/client/features/output/page.service.js b/awx/ui/client/features/output/page.service.js index 7ba5e2b88e..e655420526 100644 --- a/awx/ui/client/features/output/page.service.js +++ b/awx/ui/client/features/output/page.service.js @@ -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; } diff --git a/awx/ui/client/features/output/scroll.service.js b/awx/ui/client/features/output/scroll.service.js index bbe6e91427..192cc40114 100644 --- a/awx/ui/client/features/output/scroll.service.js +++ b/awx/ui/client/features/output/scroll.service.js @@ -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']; diff --git a/awx/ui/client/features/output/slide.service.js b/awx/ui/client/features/output/slide.service.js index e2a0735354..3d1a26cfce 100644 --- a/awx/ui/client/features/output/slide.service.js +++ b/awx/ui/client/features/output/slide.service.js @@ -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(); diff --git a/awx/ui/client/features/output/stream.service.js b/awx/ui/client/features/output/stream.service.js index c243f597be..6e8da83420 100644 --- a/awx/ui/client/features/output/stream.service.js +++ b/awx/ui/client/features/output/stream.service.js @@ -160,6 +160,8 @@ function OutputStream ($q) { this.counters.ready.length = 0; return $q.resolve(); }); + + this.getMaxCounter = () => this.counters.max; } OutputStream.$inject = ['$q'];