From df84f822f6780d0b3cf062c1885ee0392f4de86e Mon Sep 17 00:00:00 2001 From: gconsidine Date: Tue, 27 Feb 2018 17:12:55 -0500 Subject: [PATCH] [WIP] Move page-related functionality into separate service --- .../features/{jobs => output}/_index.less | 0 .../features/output/index.controller.js | 115 +++++++------- awx/ui/client/features/output/index.js | 34 ++--- awx/ui/client/features/output/page.service.js | 140 ++++++++++++++++++ .../client/features/output/render.service.js | 0 5 files changed, 218 insertions(+), 71 deletions(-) rename awx/ui/client/features/{jobs => output}/_index.less (100%) create mode 100644 awx/ui/client/features/output/page.service.js create mode 100644 awx/ui/client/features/output/render.service.js diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/output/_index.less similarity index 100% rename from awx/ui/client/features/jobs/_index.less rename to awx/ui/client/features/output/_index.less diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index c6ceacea41..ba1bc2252e 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -5,6 +5,7 @@ let vm; let ansi; let model; let resource; +let page; let container; let $timeout; let $sce; @@ -41,7 +42,7 @@ const TIME_EVENTS = [ function JobsIndexController ( _resource_, - webSocketNamespace, + _page_, _$sce_, _$timeout_, _$scope_, @@ -56,6 +57,7 @@ function JobsIndexController ( $scope = _$scope_; $q = _$q_; resource = _resource_; + page = _page_; model = resource.model; ansi = new Ansi(); @@ -64,7 +66,9 @@ function JobsIndexController ( const parsed = parseEvents(events); const html = $sce.trustAsHtml(parsed.html); - cache.push({ page: 1, lines: parsed.lines }); + page.init(resource); + + page.add({ number: 1, lines: parsed.lines }); // Development helper(s) vm.clear = devClear; @@ -86,10 +90,12 @@ function JobsIndexController ( vm.isExpanded = true; // Real-time (active between JOB_START and JOB_END events only) - $scope.$on(webSocketNamespace, processWebSocketEvents); + $scope.$on(resource.ws.namespace, processWebSocketEvents); vm.stream = { isActive: false, isRendering: false, + isPaused: false, + buffered: 0, count: 0, page: 1 }; @@ -105,7 +111,13 @@ 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; + if (data.event === JOB_START) { vm.scroll.isActive = true; vm.stream.isActive = true; @@ -114,37 +126,28 @@ function processWebSocketEvents (scope, data) { vm.stream.isActive = false; } - // 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 + const pageAdded = page.addToBuffer(data); - if (vm.stream.count % resource.page.size === 0) { - cache.push({ page: vm.stream.page }); - - vm.stream.page++; - - if (buffer.length > (resource.page.resultLimit - resource.page.size)) { - buffer.splice(0, (buffer.length - resource.page.resultLimit) + resource.page.size); - } + if (pageAdded && !vm.scroll.isLocked) { + vm.stream.isPaused = true; } - vm.stream.count++; - buffer.push(data); + if (vm.stream.isPaused && vm.scroll.isLocked) { + vm.stream.isPaused = false; + } - if (vm.stream.isRendering || !vm.scroll.isLocked) { + if (vm.stream.isRendering || vm.stream.isPaused) { return; } - vm.stream.isRendering = true; - - const events = buffer.slice(0, buffer.length); - - buffer = []; + const events = page.emptyBuffer(); return render(events); } function render (events) { + vm.stream.isRendering = true; + return shift() .then(() => append(events)) .then(() => { @@ -154,16 +157,15 @@ function render (events) { } if (!vm.stream.isActive) { - if (buffer.length) { - events = buffer.slice(0, buffer.length); - buffer = []; + const buffer = page.emptyBuffer(); - return render(events); - } else { - vm.stream.isRendering = false; - vm.scroll.isLocked = false; - vm.scroll.isActive = false; + if (buffer.length) { + return render(buffer); } + + vm.stream.isRendering = false; + vm.scroll.isLocked = false; + vm.scroll.isActive = false; } else { vm.stream.isRendering = false; } @@ -172,13 +174,14 @@ function render (events) { function devClear () { cache = []; + page.init(resource); clear(); } function next () { const config = { related: resource.related, - page: cache[cache.length - 1].page + 1, + page: vm.scroll.lastPage + 1, params: { order_by: 'start_line' } @@ -191,9 +194,9 @@ function next () { return $q.resolve(); } - cache.push({ - page: data.page - }); + cache.push({ page: data.page, events: [] }); + + vm.scroll.lastPage = data.page; return shift() .then(() => append(data.results)); @@ -205,12 +208,13 @@ function prev () { const config = { related: resource.related, - page: cache[0].page - 1, + page: vm.scroll.firstPage - 1, params: { order_by: 'start_line' } }; + console.log(cache); // console.log('[2] getting previous page', config.page, cache); return model.goToPage(config) .then(data => { @@ -218,12 +222,13 @@ function prev () { return $q.resolve(); } - cache.unshift({ - page: data.page - }); + 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 => { @@ -241,13 +246,8 @@ function append (events) { const parsed = parseEvents(events); const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html))); const table = $(ELEMENT_TBODY); - const index = cache.length - 1; - if (cache[index].lines) { - cache[index].lines += parsed.lines; - } else { - cache[index].lines = parsed.lines; - } + page.updateLineCount('current', parsed.lines); table.append(rows); $compile(rows.contents())($scope); @@ -289,6 +289,8 @@ function pop () { // console.log('[3.1] popping', ejected); const rows = $(ELEMENT_TBODY).children().slice(-ejected.lines); + vm.scroll.firstPage = cache[0].page; + rows.empty(); rows.remove(); @@ -298,17 +300,19 @@ function pop () { } function shift () { - console.log('[3] shifting old page', cache.length); + // console.log('[3] shifting old page', cache.length); return $q(resolve => { - if (cache.length <= resource.page.pageLimit) { + if (!page.isOverCapacity()) { // console.log('[3.1] nothing to shift'); return resolve(); } window.requestAnimationFrame(() => { - const ejected = cache.shift(); - console.log('[3.1] shifting', ejected); - const rows = $(ELEMENT_TBODY).children().slice(0, ejected.lines); + const lines = page.trim(); + //console.log('[3.1] shifting', lines); + const rows = $(ELEMENT_TBODY).children().slice(0, lines); + + vm.scroll.firstPage = page.getPageNumber('first'); rows.empty(); rows.remove(); @@ -637,17 +641,20 @@ function scrollHome () { function scrollEnd () { if (vm.scroll.isLocked) { + // Make note of current page when unlocked -- keep buffered events for that page for + // continuity + + 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) { vm.scroll.isActive = true; + vm.scroll.isLocked = true; - return clear() - .then(() => { - vm.scroll.isLocked = true; - }); + return; } const config = { @@ -698,7 +705,7 @@ function scrollPageDown () { JobsIndexController.$inject = [ 'resource', - 'webSocketNamespace', + 'JobPageService', '$sce', '$timeout', '$scope', diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 7467c0b079..6d35022cf4 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -3,15 +3,17 @@ import IndexController from '~features/output/index.controller'; import atLibModels from '~models'; import atLibComponents from '~components'; -import JobsStrings from '~features/output/jobs.strings'; -import IndexController from '~features/output/index.controller'; +import Strings from '~features/output/jobs.strings'; +import Controller from '~features/output/index.controller'; +import PageService from '~features/output/page.service'; -const indexTemplate = require('~features/output/index.view.html'); +const Template = require('~features/output/index.view.html'); const MODULE_NAME = 'at.features.output'; const PAGE_CACHE = true; const PAGE_LIMIT = 3; const PAGE_SIZE = 100; +const WS_PREFIX = 'ws'; function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJob, $stateParams) { const { id, type } = $stateParams; @@ -56,20 +58,20 @@ function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJ type, model, related, - ws: getWebSocketResource(type), + ws: { + namespace: `${WS_PREFIX}-${getWebSocketResource(type).key}-${id}` + }, page: { cache: PAGE_CACHE, size: PAGE_SIZE, - pageLimit: PAGE_LIMIT, - resultLimit: PAGE_SIZE * PAGE_LIMIT + pageLimit: PAGE_LIMIT } }; }); } -function resolveWebSocket (SocketService, $stateParams) { +function resolveWebSocketConnection (SocketService, $stateParams) { const { type, id } = $stateParams; - const prefix = 'ws'; const resource = getWebSocketResource(type); let name; @@ -87,8 +89,6 @@ function resolveWebSocket (SocketService, $stateParams) { }; SocketService.addStateResolve(state, id); - - return `${prefix}-${resource.key}-${id}`; } function resolveBreadcrumb (strings) { @@ -139,8 +139,8 @@ function JobsRun ($stateRegistry) { }, views: { '@': { - templateUrl: indexTemplate, - controller: IndexController, + templateUrl: Template, + controller: Controller, controllerAs: 'vm' } }, @@ -155,13 +155,13 @@ function JobsRun ($stateRegistry) { resolveResource ], ncyBreadcrumb: [ - 'JobsStrings', + 'JobStrings', resolveBreadcrumb ], - webSocketNamespace: [ + webSocketConnection: [ 'SocketService', '$stateParams', - resolveWebSocket + resolveWebSocketConnection ] }, }; @@ -176,8 +176,8 @@ angular atLibModels, atLibComponents ]) - .controller('indexController', IndexController) - .service('JobsStrings', JobsStrings) + .service('JobStrings', Strings) + .service('JobPageService', PageService) .run(JobsRun); export default MODULE_NAME; diff --git a/awx/ui/client/features/output/page.service.js b/awx/ui/client/features/output/page.service.js new file mode 100644 index 0000000000..1d1227c68d --- /dev/null +++ b/awx/ui/client/features/output/page.service.js @@ -0,0 +1,140 @@ +function JobPageService () { + this.page = null; + this.resource = null; + this.result = null; + this.buffer = null; + this.cache = null; + + this.init = resource => { + this.resource = resource; + + this.page = { + limit: resource.page.pageLimit, + size: resource.page.size, + current: 0, + index: -1, + count: 0 + }; + + this.result = { + limit: this.page.limit * this.page.size, + count: 0 + }; + + this.buffer = { + count: 0 + }; + + this.cache = []; + }; + + this.add = (page, position) => { + page.events = page.events || []; + page.lines = page.lines || 0; + + if (!position) { + this.cache.push(page); + } + + this.page.count++; + }; + + this.addToBuffer = event => { + let pageAdded = false; + + if (this.result.count % this.page.size === 0) { + pageAdded = true; + + this.add({ number: this.page.count + 1, events: [event] }); + + this.trimBuffer(); + } else { + this.cache[this.cache.length - 1].events.push(event); + } + + this.buffer.count++; + this.result.count++; + + return pageAdded; + }; + + this.trimBuffer = () => { + const diff = this.cache.length - this.page.limit; + + if (diff <= 0) { + return; + } + + for (let i = 0; i < diff; i++) { + if (this.cache[i].events) { + this.buffer.count -= this.cache[i].events.length; + this.cache[i].events = []; + } + } + }; + + this.emptyBuffer = () => { + let data = []; + + for (let i = 0; i < this.cache.length; i++) { + const events = this.cache[i].events; + + if (events.length > 0) { + this.buffer.count -= events.length; + data = data.concat(this.cache[i].events.splice(0, events.length)); + } + } + + return data; + }; + + this.isOverCapacity = () => { + return (this.cache.length - this.page.limit) > 0; + }; + + this.trim = () => { + 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; + }; + + this.getPageNumber = (page) => { + let index; + + if (page === 'first') { + index = 0; + } + + return this.cache[index].number; + }; + + this.updateLineCount = (page, lines) => { + let index; + + if (page === 'current') { + index = this.cache.length - 1; + } + + if (this.cache[index].lines) { + this.cache[index].lines += lines; + } else { + this.cache[index].lines = lines; + } + } + + this.next = () => { + + }; + + this.prev = () => { + + }; + + this.current = () => { + return this.resource.model.get(`related.${this.resource.related}.results`); + }; +} + +export default JobPageService; diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js new file mode 100644 index 0000000000..e69de29bb2