From d48f69317fe108000e69e33e2199e17d2525a156 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Tue, 20 Feb 2018 16:44:51 -0500 Subject: [PATCH] Add scroll lock for real-time display --- awx/ui/client/features/jobs/_index.less | 9 + .../features/output/index.controller.js | 177 +++++++++--------- awx/ui/client/features/output/index.js | 91 +++++---- awx/ui/client/features/output/index.view.html | 24 +-- 4 files changed, 164 insertions(+), 137 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index a72c05a6be..623db07cc6 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -65,6 +65,15 @@ } } + &-menuIcon--active { + font-size: 22px; + line-height: 12px; + font-weight: bold; + padding: 10px; + cursor: pointer; + color: @at-blue; + } + &-toggle { color: @at-gray-848992; background-color: @at-gray-eb; diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index bad00f5542..48f223235e 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -3,8 +3,8 @@ import hasAnsi from 'has-ansi'; let vm; let ansi; +let model; let resource; -let related; let container; let $timeout; let $sce; @@ -13,11 +13,9 @@ let $scope; let $q; const record = {}; -const meta = { - scroll: {}, - page: {} -}; -const current = {}; + +let parent = null; +let cache = []; const PAGE_LIMIT = 3; const SCROLL_BUFFER = 250; @@ -27,6 +25,8 @@ const EVENT_START_PLAY = 'playbook_on_play_start'; const EVENT_STATS_PLAY = 'playbook_on_stats'; const ELEMENT_TBODY = '#atStdoutResultTable'; const ELEMENT_CONTAINER = '.at-Stdout-container'; +const JOB_START = 'playbook_on_start'; +const JOB_END = 'playbook_on_stats'; const EVENT_GROUPS = [ EVENT_START_TASK, @@ -48,55 +48,48 @@ function JobsIndexController ( _$compile_, _$q_ ) { + vm = this || {}; + $timeout = _$timeout_; $sce = _$sce_; $compile = _$compile_; $scope = _$scope_; $q = _$q_; resource = _resource_; + model = resource.model; ansi = new Ansi(); - related = getRelated(); - const events = resource.get(`related.${related}.results`); + const events = model.get(`related.${resource.related}.results`); const parsed = parseEvents(events); const html = $sce.trustAsHtml(parsed.html); - vm = this || {}; + cache.push({ page: 1, lines: parsed.lines }); - $scope.ns = 'jobs'; - $scope.jobs = { - modal: {} - }; - - vm.toggle = toggle; - vm.showHostDetails = showHostDetails; + // Development helper(s) vm.clear = clear; - $scope.$on(webSocketNamespace, processWebSocketEvents); - - vm.menu = { - scroll: { - display: false, - home: scrollHome, - end: scrollEnd, - down: scrollPageDown, - up: scrollPageUp - }, - top: { - expand, - isExpanded: true - }, - bottom: { - next - } + // Stdout Navigation + vm.scroll = { + lock: false, + display: false, + active: false, + home: scrollHome, + end: scrollEnd, + down: scrollPageDown, + up: scrollPageUp }; - meta.page.cache = [{ - page: 1, - lines: parsed.lines - }]; + // Expand/collapse + vm.toggle = toggle; + vm.expand = expand; + vm.isExpanded = true; + // Real-time (active between JOB_START and JOB_END events only) + $scope.$on(webSocketNamespace, processWebSocketEvents); + vm.stream = { + active: false + }; $timeout(() => { const table = $(ELEMENT_TBODY); @@ -117,43 +110,41 @@ function clear () { } function processWebSocketEvents (scope, data) { - meta.scroll.inProgress = true; + vm.scroll.active = true; + + if (data.event === JOB_START) { + vm.stream.active = true; + vm.scroll.lock = true; + } else if (data.event === JOB_END) { + vm.stream.active = false; + vm.scroll.lock = false; + } - console.log(data); append([data]) .then(() => { - container[0].scrollTop = container[0].scrollHeight; + if (vm.scroll.lock) { + container[0].scrollTop = container[0].scrollHeight; + } }); } -function getRelated () { - const name = resource.constructor.name; - - switch (name) { - case 'JobModel': - return 'job_events'; - default: - return 'events'; - } -} - function next () { const config = { - related, - page: meta.page.cache[meta.page.cache.length - 1].page + 1, + related: resource.related, + page: cache[cache.length - 1].page + 1, params: { order_by: 'start_line' } }; - console.log('[2] getting next page', config.page, meta.page.cache); - return resource.goToPage(config) + console.log('[2] getting next page', config.page, cache); + return model.goToPage(config) .then(data => { if (!data || !data.results) { return $q.resolve(); } - meta.page.cache.push({ + cache.push({ page: data.page }); @@ -166,21 +157,21 @@ function prev () { const container = $(ELEMENT_CONTAINER)[0]; const config = { - related, - page: meta.page.cache[0].page - 1, + related: resource.related, + page: cache[0].page - 1, params: { order_by: 'start_line' } }; - console.log('[2] getting previous page', config.page, meta.page.cache); - return resource.goToPage(config) + console.log('[2] getting previous page', config.page, cache); + return model.goToPage(config) .then(data => { if (!data || !data.results) { return $q.resolve(); } - meta.page.cache.unshift({ + cache.unshift({ page: data.page }); @@ -203,9 +194,9 @@ function append (events) { const parsed = parseEvents(events); const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html))); const table = $(ELEMENT_TBODY); - const index = meta.page.cache.length - 1; + const index = cache.length - 1; - meta.page.cache[index].lines = parsed.lines; + cache[index].lines = parsed.lines; table.append(rows); $compile(rows.contents())($scope); @@ -223,7 +214,7 @@ function prepend (events) { const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html))); const table = $(ELEMENT_TBODY); - meta.page.cache[0].lines = parsed.lines; + cache[0].lines = parsed.lines; table.prepend(rows); $compile(rows.contents())($scope); @@ -235,12 +226,12 @@ function prepend (events) { function pop () { console.log('[3] popping old page'); return $q(resolve => { - if (meta.page.cache.length <= PAGE_LIMIT) { + if (cache.length <= PAGE_LIMIT) { console.log('[3.1] nothing to pop'); return resolve(); } - const ejected = meta.page.cache.pop(); + const ejected = cache.pop(); console.log('[3.1] popping', ejected); const rows = $(ELEMENT_TBODY).children().slice(-ejected.lines); @@ -254,12 +245,12 @@ function pop () { function shift () { console.log('[3] shifting old page'); return $q(resolve => { - if (meta.page.cache.length <= PAGE_LIMIT) { + if (cache.length <= PAGE_LIMIT) { console.log('[3.1] nothing to shift'); return resolve(); } - const ejected = meta.page.cache.shift(); + const ejected = cache.shift(); console.log('[3.1] shifting', ejected); const rows = $(ELEMENT_TBODY).children().slice(0, ejected.lines); @@ -283,7 +274,7 @@ function clear () { } function expand () { - vm.toggle(meta.parent, true); + vm.toggle(parent, true); } function parseEvents (events) { @@ -375,7 +366,7 @@ function createRecord (ln, lines, event) { info.isParent = true; if (event.event_level === 1) { - meta.parent = event.uuid; + parent = event.uuid; } if (event.parent_uuid) { @@ -495,7 +486,7 @@ function toggle (uuid, menu) { let icon = $(`#${uuid} .at-Stdout-toggle > i`); if (menu || record[uuid].level === 1) { - vm.menu.top.isExpanded = !vm.menu.top.isExpanded; + vm.isExpanded = !vm.isExpanded; } if (record[uuid].children) { @@ -516,11 +507,11 @@ function toggle (uuid, menu) { } function onScroll () { - if (meta.scroll.inProgress) { + if (vm.scroll.active) { return; } - meta.scroll.inProgress = true; + vm.scroll.active = true; $timeout(() => { const top = container[0].scrollTop; @@ -528,17 +519,17 @@ function onScroll () { if (top <= SCROLL_BUFFER) { console.log('[1] scroll to top'); - vm.menu.scroll.display = false; + vm.scroll.display = false; prev() .then(() => { console.log('[5] scroll reset'); - meta.scroll.inProgress = false; + vm.scroll.active = false; }); return; } else { - vm.menu.scroll.display = true; + vm.scroll.display = true; if (bottom >= container[0].scrollHeight) { console.log('[1] scroll to bottom'); @@ -546,10 +537,10 @@ function onScroll () { next() .then(() => { console.log('[5] scroll reset'); - meta.scroll.inProgress = false; + vm.scroll.active = false; }); } else { - meta.scroll.inProgress = false; + vm.scroll.active = false; } } }, SCROLL_LOAD_DELAY); @@ -557,53 +548,59 @@ function onScroll () { function scrollHome () { const config = { - related, + related: resource.related, page: 'first', params: { order_by: 'start_line' } }; - meta.scroll.inProgress = true; + vm.scroll.active = true; - console.log('[2] getting first page', config.page, meta.page.cache); - return resource.goToPage(config) + console.log('[2] getting first page', config.page, cache); + return model.goToPage(config) .then(data => { if (!data || !data.results) { return $q.resolve(); } - meta.page.cache = [{ + cache = [{ page: data.page }] return clear() .then(() => prepend(data.results)) .then(() => { - meta.scroll.inProgress = false; + vm.scroll.active = false; }); }); } function scrollEnd () { + if (vm.scroll.lock) { + vm.scroll.lock = false; + + return; + } + const config = { - related, + related: resource.related, page: 'last', params: { order_by: 'start_line' } }; - meta.scroll.inProgress = true; + vm.scroll.active = true; - console.log('[2] getting last page', config.page, meta.page.cache); - return resource.goToPage(config) + console.log('[2] getting last page', config.page, cache); + return model.goToPage(config) .then(data => { if (!data || !data.results) { return $q.resolve(); } - meta.page.cache = [{ + cache = [{ page: data.page }] @@ -613,7 +610,7 @@ function scrollEnd () { const container = $(ELEMENT_CONTAINER)[0]; container.scrollTop = container.scrollHeight; - meta.scroll.inProgress = false; + vm.scroll.active = false; }); }); } diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index c67122d148..a23e8cbbc0 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -9,10 +9,12 @@ import IndexController from '~features/output/index.controller'; const indexTemplate = require('~features/output/index.view.html'); const MODULE_NAME = 'at.features.output'; +const PAGE_CACHE = true; +const PAGE_LIMIT = 3; +const PAGE_SIZE = 100; function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJob, $stateParams) { - const { id } = $stateParams; - const { type } = $stateParams; + const { id, type } = $stateParams; let Resource; let related = 'events'; @@ -40,52 +42,44 @@ function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJ } return new Resource('get', id) - .then(resource => resource.extend(related, { - pageCache: true, - pageLimit: 3, + .then(model => model.extend(related, { + pageCache: PAGE_CACHE, + pageLimit: PAGE_LIMIT, params: { - page_size: 100, + page_size: PAGE_SIZE, order_by: 'start_line' } - })); + })) + .then(model => { + return { + id, + type, + model, + related, + ws: getWebSocketResource(type), + page: { + cache: PAGE_CACHE, + limit: PAGE_LIMIT, + size: PAGE_SIZE + } + }; + }); } function resolveWebSocket (SocketService, $stateParams) { const { type, id } = $stateParams; const prefix = 'ws'; + const resource = getWebSocketResource(type); let name; let events; - switch (type) { - case 'system': - name = 'system_jobs'; - events = 'system_job_events'; - break; - case 'project': - name = 'project_updates'; - events = 'project_update_events'; - break; - case 'command': - name = 'ad_hoc_commands'; - events = 'ad_hoc_command_events'; - break; - case 'inventory': - name = 'inventory_updates'; - events = 'inventory_update_events'; - break; - case 'playbook': - name = 'jobs'; - events = 'job_events'; - break; - } - const state = { data: { socket: { groups: { - [name]: ['status_changed', 'summary'], - [events]: [] + [resource.name]: ['status_changed', 'summary'], + [resource.key]: [] } } } @@ -93,7 +87,7 @@ function resolveWebSocket (SocketService, $stateParams) { SocketService.addStateResolve(state, id); - return `${prefix}-${events}-${id}`; + return `${prefix}-${resource.key}-${id}`; } function resolveBreadcrumb (strings) { @@ -102,6 +96,37 @@ function resolveBreadcrumb (strings) { }; } +function getWebSocketResource (type) { + let name; + let key; + + switch (type) { + case 'system': + name = 'system_jobs'; + key = 'system_job_events'; + break; + case 'project': + name = 'project_updates'; + key = 'project_update_events'; + break; + case 'command': + name = 'ad_hoc_commands'; + key = 'ad_hoc_command_events'; + break; + case 'inventory': + name = 'inventory_updates'; + key = 'inventory_update_events'; + break; + case 'playbook': + name = 'jobs'; + key = 'job_events'; + break; + } + + return { name, key }; +} + + function JobsRun ($stateRegistry) { const state = { name: 'jobz', diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 11c903dfb0..71898ae4d1 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -8,21 +8,22 @@
-
+
+ ng-class="{ 'fa-minus': vm.isExpanded, 'fa-plus': !vm.isExpanded }">
-
- +
+
-
+
-
+
-
+
@@ -31,8 +32,8 @@
 
-
-
+
+

Back to Top

@@ -41,9 +42,4 @@
- - -
- -