diff --git a/awx/ui/client/features/output/api.events.service.js b/awx/ui/client/features/output/api.events.service.js index 39499eff46..ecf247633c 100644 --- a/awx/ui/client/features/output/api.events.service.js +++ b/awx/ui/client/features/output/api.events.service.js @@ -29,7 +29,7 @@ function JobEventsApiService ($http, $q) { this.getLastPage = count => Math.ceil(count / this.state.params.page_size); - this.fetch = () => { + this.clearCache = () => { delete this.cache; delete this.keys; delete this.pageSizes; @@ -37,10 +37,10 @@ function JobEventsApiService ($http, $q) { this.cache = {}; this.keys = []; this.pageSizes = {}; - - return this.getPage(1).then(() => this); }; + this.fetch = () => this.first().then(() => this); + this.getPage = number => { if (number < 1 || number > this.state.last) { return $q.resolve(); @@ -79,11 +79,18 @@ function JobEventsApiService ($http, $q) { return { results, page: number }; }); + if (number === 1) { + this.clearCache(); + } + this.cache[number] = promise; this.keys.push(number); if (this.keys.length > PAGE_LIMIT) { - delete this.cache[this.keys.shift()]; + const remove = this.keys.shift(); + + delete this.cache[remove]; + delete this.pageSizes[remove]; } return promise; @@ -107,17 +114,22 @@ function JobEventsApiService ($http, $q) { const { results, count } = data; const lastPage = this.getLastPage(count); - results.reverse(); - const shifted = results.splice(count % PAGE_SIZE); + if (count > PAGE_SIZE) { + results.splice(count % PAGE_SIZE); + } - this.state.results = shifted; + results.reverse(); + + this.state.results = results; this.state.count = count; this.state.page = lastPage; this.state.next = lastPage; this.state.last = lastPage; this.state.previous = Math.max(1, this.state.page - 1); - return { results: shifted, page: lastPage }; + this.clearCache(); + + return { results, page: lastPage }; }); return promise; diff --git a/awx/ui/client/features/output/engine.service.js b/awx/ui/client/features/output/engine.service.js index 2e6371520f..2d08b4ed6e 100644 --- a/awx/ui/client/features/output/engine.service.js +++ b/awx/ui/client/features/output/engine.service.js @@ -73,9 +73,8 @@ function JobEventEngine ($q) { this.buffer = data => { const pageAdded = this.page.addToBuffer(data); - this.pageCount++; - if (pageAdded) { + this.pageCount++; this.setBatchFrameCount(); if (this.isPausing()) { @@ -117,6 +116,9 @@ function JobEventEngine ($q) { this.chain = this.chain .then(() => { if (!this.isActive()) { + if (data.start_line < (this.lines.min)) { + return $q.resolve(); + } this.start(); } else if (data.event === JOB_END) { if (this.isPaused()) { @@ -146,6 +148,10 @@ function JobEventEngine ($q) { this.renderFrame = events => this.hooks.onEventFrame(events) .then(() => { + if (this.scroll.isLocked()) { + this.scroll.scrollToBottom(); + } + if (this.isEnding()) { const lastEvents = this.page.emptyBuffer(); diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 935d0a7c79..9663749557 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -50,8 +50,8 @@ function JobsIndexController ( // Stdout Navigation vm.scroll = { showBackToTop: false, - home: scrollHome, - end: scrollEnd, + home: scrollFirst, + end: scrollLast, down: scrollPageDown, up: scrollPageUp }; @@ -97,7 +97,14 @@ function init () { }); streaming = false; - return next().then(() => startListening()); + + if (status.state.running) { + return scrollLast().then(() => startListening()); + } else if (!status.state.finished) { + return scrollFirst().then(() => startListening()); + } + + return scrollLast(); } function stopListening () { @@ -117,30 +124,13 @@ function handleStatusEvent (data) { function handleJobEvent (data) { streaming = streaming || attachToRunningJob(); + streaming.then(() => { engine.pushJobEvent(data); status.pushJobEvent(data); }); } -function attachToRunningJob () { - if (!status.state.running) { - return $q.resolve(); - } - - return page.last() - .then(events => { - if (!events) { - return $q.resolve(); - } - - const minLine = 1 + Math.max(...events.map(event => event.end_line)); - - return render.clear() - .then(() => engine.setMinLine(minLine)); - }); -} - function next () { return page.next() .then(events => { @@ -217,8 +207,16 @@ function shift () { return render.shift(lines); } -function scrollHome () { - if (scroll.isPaused()) { +function scrollFirst () { + if (engine.isActive()) { + if (engine.isTransitioning()) { + return $q.resolve(); + } + + if (!engine.isPaused()) { + engine.pause(true); + } + } else if (scroll.isPaused()) { return $q.resolve(); } @@ -246,19 +244,57 @@ function scrollHome () { }); } -function scrollEnd () { +function scrollLast () { if (engine.isActive()) { if (engine.isTransitioning()) { return $q.resolve(); } if (engine.isPaused()) { - engine.resume(); - } else { - engine.pause(); + engine.resume(true); + } + } else if (scroll.isPaused()) { + return $q.resolve(); + } + + scroll.pause(); + + return render.clear() + .then(() => page.last()) + .then(events => { + if (!events) { + return $q.resolve(); + } + + const minLine = 1 + Math.max(...events.map(event => event.end_line)); + engine.setMinLine(minLine); + + return append(events); + }) + .then(() => { + if (!engine.isActive()) { + scroll.resume(); + } + scroll.setScrollPosition(scroll.getScrollHeight()); + }) + .then(() => { + if (!engine.isActive() && scroll.isMissing()) { + return previous(); + } + + return $q.resolve(); + }); +} + +function attachToRunningJob () { + if (engine.isActive()) { + if (engine.isTransitioning()) { + return $q.resolve(); } - return $q.resolve(); + if (engine.isPaused()) { + engine.resume(true); + } } else if (scroll.isPaused()) { return $q.resolve(); } @@ -271,12 +307,13 @@ function scrollEnd () { return $q.resolve(); } - return render.clear() - .then(() => append(events)); + const minLine = 1 + Math.max(...events.map(event => event.end_line)); + engine.setMinLine(minLine); + + return append(events); }) .then(() => { scroll.setScrollPosition(scroll.getScrollHeight()); - scroll.resume(); }); } diff --git a/awx/ui/client/features/output/page.service.js b/awx/ui/client/features/output/page.service.js index 854bda23b0..b8d9f96fb2 100644 --- a/awx/ui/client/features/output/page.service.js +++ b/awx/ui/client/features/output/page.service.js @@ -17,7 +17,7 @@ function JobPageService ($q) { this.bookmark = { pending: false, - set: false, + set: true, cache: [], state: { count: 0, diff --git a/awx/ui/client/features/output/status.service.js b/awx/ui/client/features/output/status.service.js index 29558c70df..704d87ed18 100644 --- a/awx/ui/client/features/output/status.service.js +++ b/awx/ui/client/features/output/status.service.js @@ -4,7 +4,9 @@ const PLAY_START = 'playbook_on_play_start'; const TASK_START = 'playbook_on_task_start'; const HOST_STATUS_KEYS = ['dark', 'failures', 'changed', 'ok', 'skipped']; -const FINISHED = ['successful', 'failed', 'error']; +const COMPLETE = ['successful', 'failed']; +const INCOMPLETE = ['canceled', 'error']; +const FINISHED = COMPLETE.concat(INCOMPLETE); function JobStatusService (moment, message) { this.dispatch = () => message.dispatch('status', this.state); @@ -105,8 +107,10 @@ function JobStatusService (moment, message) { } }; - this.isExpectingStatsEvent = () => (this.jobType === 'job') || - (this.jobType === 'project_update'); + this.isExpectingStatsEvent = () => ( + (this.jobType === 'job') || + (this.jobType === 'project_update')) && + (!_.includes(INCOMPLETE, this.state.status)); this.updateStats = () => { this.updateHostCounts();