diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index ba1bc2252e..ce8a047e7f 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -16,11 +16,9 @@ let $q; const record = {}; let parent = null; -let cache = []; -let buffer = []; -const SCROLL_BUFFER = 250; -const SCROLL_LOAD_DELAY = 50; +const SCROLL_THRESHOLD = 0.1; +const SCROLL_DELAY = 1000; const EVENT_START_TASK = 'playbook_on_task_start'; const EVENT_START_PLAY = 'playbook_on_play_start'; const EVENT_STATS_PLAY = 'playbook_on_stats'; @@ -78,6 +76,8 @@ function JobsIndexController ( isLocked: false, showBackToTop: false, isActive: false, + position: 0, + time: 0, home: scrollHome, end: scrollEnd, down: scrollPageDown, @@ -111,10 +111,6 @@ function JobsIndexController ( }); } -// TODO: Determine how to manage buffered events (store in page cache vs. separate) -// Leaning towards keeping separate (same as they come in over WS). On resume of scroll, -// Clear/reset cache, append buffered events, then back to normal render cycle - function processWebSocketEvents (scope, data) { let done; @@ -173,74 +169,47 @@ function render (events) { } function devClear () { - cache = []; page.init(resource); clear(); } function next () { - const config = { - related: resource.related, - page: vm.scroll.lastPage + 1, - params: { - order_by: 'start_line' - } - }; - - // console.log('[2] getting next page', config.page, cache); - return model.goToPage(config) - .then(data => { - if (!data || !data.results) { - return $q.resolve(); + return page.next() + .then(events => { + if (!events) { + return; } - cache.push({ page: data.page, events: [] }); - - vm.scroll.lastPage = data.page; - return shift() - .then(() => append(data.results)); - }); + .then(() => append(events)); + }) } -function prev () { +function previous () { const container = $(ELEMENT_CONTAINER)[0]; - const config = { - related: resource.related, - page: vm.scroll.firstPage - 1, - params: { - order_by: 'start_line' - } - }; + let previousHeight; - console.log(cache); - // console.log('[2] getting previous page', config.page, cache); - return model.goToPage(config) - .then(data => { - if (!data || !data.results) { - return $q.resolve(); + return page.previous() + .then(events => { + if (!events) { + return; } - cache.unshift({ page: data.page, events: [] }); - - vm.scroll.firstPage = data.page; - - const previousHeight = container.scrollHeight; - - console.log(cache); return pop() - .then(() => prepend(data.results)) - .then(lines => { - const currentHeight = container.scrollHeight; + .then(() => { + previousHeight = container.scrollHeight; + return prepend(events); + }) + .then(() => { + const currentHeight = container.scrollHeight; container.scrollTop = currentHeight - previousHeight; }); }); } function append (events) { - // console.log('[4] appending next page'); return $q(resolve => { window.requestAnimationFrame(() => { const parsed = parseEvents(events); @@ -258,62 +227,52 @@ function append (events) { } function prepend (events) { - // console.log('[4] prepending next page'); - return $q(resolve => { window.requestAnimationFrame(() => { const parsed = parseEvents(events); const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html))); const table = $(ELEMENT_TBODY); - cache[0].lines = parsed.lines; + page.updateLineCount('current', parsed.lines); table.prepend(rows); $compile(rows.contents())($scope); - return resolve(parsed.lines); + $scope.$apply(() => { + return resolve(parsed.lines); + }); }); }); } function pop () { - // console.log('[3] popping old page'); return $q(resolve => { - if (cache.length <= resource.page.pageLimit) { - // console.log('[3.1] nothing to pop'); + if (!page.isOverCapacity()) { return resolve(); } window.requestAnimationFrame(() => { - const ejected = cache.pop(); - // console.log('[3.1] popping', ejected); - const rows = $(ELEMENT_TBODY).children().slice(-ejected.lines); - - vm.scroll.firstPage = cache[0].page; + const lines = page.trim('right'); + const rows = $(ELEMENT_TBODY).children().slice(-lines); rows.empty(); rows.remove(); - return resolve(ejected); + return resolve(); }); }); } function shift () { - // console.log('[3] shifting old page', cache.length); return $q(resolve => { if (!page.isOverCapacity()) { - // console.log('[3.1] nothing to shift'); return resolve(); } window.requestAnimationFrame(() => { - const lines = page.trim(); - //console.log('[3.1] shifting', lines); + const lines = page.trim('left'); const rows = $(ELEMENT_TBODY).children().slice(0, lines); - vm.scroll.firstPage = page.getPageNumber('first'); - rows.empty(); rows.remove(); @@ -323,7 +282,6 @@ function shift () { } function clear () { - // console.log('[3] clearing pages'); return $q(resolve => { window.requestAnimationFrame(() => { const rows = $(ELEMENT_TBODY).children(); @@ -574,65 +532,71 @@ function onScroll () { return; } + if (vm.scroll.register) { + $timeout.cancel(vm.scroll.register); + } + + vm.scroll.register = $timeout(registerScrollEvent, SCROLL_DELAY); +} + +function registerScrollEvent () { vm.scroll.isActive = true; - $timeout(() => { - const top = container[0].scrollTop; - const bottom = top + SCROLL_BUFFER + container[0].offsetHeight; + const position = container[0].scrollTop; + const height = container[0].offsetHeight; + const downward = position > vm.scroll.position; - if (top <= SCROLL_BUFFER) { - // console.log('[1] scroll to top'); - vm.scroll.showBackToTop = false; + let promise; - prev() - .then(() => { - // console.log('[5] scroll reset'); - vm.scroll.isActive = false; - }); + if (position !== 0 ) { + vm.scroll.showBackToTop = true; + } else { + vm.scroll.showBackToTop = false; + } - return; - } else { - vm.scroll.showBackToTop = true; - if (bottom >= container[0].scrollHeight) { - // console.log('[1] scroll to bottom'); - - next() - .then(() => { - // console.log('[5] scroll reset'); - vm.scroll.isActive = false; - }); - } else { - vm.scroll.isActive = false; - } + console.log('downward', downward); + if (downward) { + if (((height - position) / height) < SCROLL_THRESHOLD) { + promise = next; } - }, SCROLL_LOAD_DELAY); + } else { + if ((position / height) < SCROLL_THRESHOLD) { + console.log('previous'); + promise = previous; + } + } + + vm.scroll.position = position; + + if (!promise) { + vm.scroll.isActive = false; + + return $q.resolve(); + } + + return promise() + .then(() => { + console.log('done'); + vm.scroll.isActive = false; + /* + *$timeout(() => { + * vm.scroll.isActive = false; + *}, SCROLL_DELAY); + */ + }); + } function scrollHome () { - const config = { - related: resource.related, - page: 'first', - params: { - order_by: 'start_line' - } - }; - - vm.scroll.isActive = true; - - // console.log('[2] getting first page', config.page, cache); - return model.goToPage(config) - .then(data => { - if (!data || !data.results) { - return $q.resolve(); + return page.first() + .then(events => { + if (!events) { + return; } - cache = [{ - page: data.page - }] - return clear() - .then(() => prepend(data.results)) + .then(() => prepend(events)) .then(() => { vm.scroll.isActive = false; }); @@ -641,45 +605,31 @@ function scrollHome () { function scrollEnd () { if (vm.scroll.isLocked) { - // Make note of current page when unlocked -- keep buffered events for that page for - // continuity + page.bookmark(); - vm.scroll.firstPage = cache[0].page; - vm.scroll.lastPage = cache[cache.length - 1].page; vm.scroll.isLocked = false; vm.scroll.isActive = false; return; } else if (!vm.scroll.isLocked && vm.stream.isActive) { + page.bookmark(); + vm.scroll.isActive = true; vm.scroll.isLocked = true; return; } - const config = { - related: resource.related, - page: 'last', - params: { - order_by: 'start_line' - } - }; - vm.scroll.isActive = true; - // console.log('[2] getting last page', config.page, cache); - return model.goToPage(config) - .then(data => { - if (!data || !data.results) { - return $q.resolve(); + return page.last() + .then(events => { + if (!events) { + return; } - cache = [{ - page: data.page - }] - return clear() - .then(() => append(data.results)) + .then(() => append(events)) .then(() => { const container = $(ELEMENT_CONTAINER)[0]; diff --git a/awx/ui/client/features/output/page.service.js b/awx/ui/client/features/output/page.service.js index 1d1227c68d..b82eba4705 100644 --- a/awx/ui/client/features/output/page.service.js +++ b/awx/ui/client/features/output/page.service.js @@ -1,4 +1,4 @@ -function JobPageService () { +function JobPageService ($q) { this.page = null; this.resource = null; this.result = null; @@ -13,7 +13,14 @@ function JobPageService () { size: resource.page.size, current: 0, index: -1, - count: 0 + count: 0, + first: 0, + last: 0, + bookmark: { + first: 0, + last: 0, + current: 0 + } }; this.result = { @@ -28,14 +35,25 @@ function JobPageService () { this.cache = []; }; - this.add = (page, position) => { + this.add = (page, position, bookmark) => { page.events = page.events || []; page.lines = page.lines || 0; - if (!position) { + if (position === 'first') { + this.cache.unshift(page); + this.page.first = page.number; + this.page.last = this.cache[this.cache.length -1].number; + } else { this.cache.push(page); + this.page.last = page.number; + this.page.first = this.cache[0].number; } + if (bookmark) { + this.page.bookmark.current = page.number; + } + + this.page.current = page.number; this.page.count++; }; @@ -88,19 +106,33 @@ function JobPageService () { return data; }; + this.emptyCache = () => { + this.page.first = this.page.current; + this.page.last = this.page.current; + this.cache = []; + }; + this.isOverCapacity = () => { return (this.cache.length - this.page.limit) > 0; }; - this.trim = () => { + this.trim = side => { const count = this.cache.length - this.page.limit; - const ejected = this.cache.splice(0, count); - const linesRemoved = ejected.reduce((total, page) => total + page.lines, 0); - return linesRemoved; + let ejected; + + if (side === 'left') { + ejected = this.cache.splice(0, count); + this.page.first = this.cache[0].number; + } else { + ejected = this.cache.splice(-count); + this.page.last = this.cache[this.cache.length - 1].number; + } + + return ejected.reduce((total, page) => total + page.lines, 0); }; - this.getPageNumber = (page) => { + this.getPageNumber = page => { let index; if (page === 'first') { @@ -114,22 +146,118 @@ function JobPageService () { let index; if (page === 'current') { - index = this.cache.length - 1; + index = this.cache.findIndex(item => item.number === this.page.current); } - if (this.cache[index].lines) { - this.cache[index].lines += lines; - } else { - this.cache[index].lines = lines; - } + this.cache[index].lines += lines; } - this.next = () => { + this.bookmark = () => { + console.log('b,current', this.page.current); + if (!this.page.bookmark.active) { + this.page.bookmark.first = this.page.first; + this.page.bookmark.last = this.page.last; + this.page.bookmark.current = this.page.current; + console.log('b,bookmark', this.page.bookmark.current); + this.page.bookmark.active = true; + } else { + this.page.bookmark.active = false; + } }; - this.prev = () => { + this.next = () => { + let page; + let bookmark; + if (this.page.bookmark.active) { + page = this.page.bookmark.current + 1; + bookmark = true; + } else { + page = this.page.last + 1; + } + + const config = this.buildRequestConfig(page); + + return this.resource.model.goToPage(config) + .then(data => { + if (!data || !data.results) { + return $q.resolve(); + } + + this.add({ number: data.page, events: [], lines: 0 }, 'last', bookmark); + + return data.results; + }); + }; + + this.previous = () => { + let page; + let bookmark; + + if (this.page.bookmark.active) { + page = this.page.bookmark.current - 1; + bookmark = true; + } else { + page = this.page.first - 1; + } + + const config = this.buildRequestConfig(page); + + return this.resource.model.goToPage(config) + .then(data => { + if (!data || !data.results) { + return $q.resolve(); + } + + this.add({ number: data.page, events: [], lines: 0 }, 'first', bookmark); + + return data.results; + }); + }; + + this.last = () => { + const config = this.buildRequestConfig('last'); + + this.emptyCache(); + + return this.resource.model.goToPage(config) + .then(data => { + if (!data || !data.results) { + return $q.resolve(); + } + + this.add({ number: data.page, events: [], lines: 0 }, 'last'); + + return data.results; + }); + }; + + this.first = () => { + const config = this.buildRequestConfig('first'); + + this.emptyCache(); + + return this.resource.model.goToPage(config) + .then(data => { + if (!data || !data.results) { + return $q.resolve(); + } + + this.add({ number: data.page, events: [], lines: 0 }, 'first'); + + return data.results; + }); + }; + + this.buildRequestConfig = (page) => { + return { + page, + related: this.resource.related, + params: { + order_by: 'start_line' + } + }; }; this.current = () => { @@ -137,4 +265,6 @@ function JobPageService () { }; } +JobPageService.$inject = ['$q']; + export default JobPageService; diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index b2e91f97df..98f2b25007 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -466,9 +466,7 @@ function goToPage (config) { if (pagesInCache.length > this.page.limit) { const pageToDelete = pagesInCache.shift(); - console.log(pageCache); delete pageCache[pageToDelete]; - console.log(this.page.cache); } }