From 212ab96a31ef07925b8d92649685b3c8a81ec063 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Thu, 2 Nov 2017 17:01:12 -0400 Subject: [PATCH 01/89] Add structure for sandboxed job results --- awx/ui/client/features/index.js | 5 ++- .../features/output/index.controller.js | 4 +++ awx/ui/client/features/output/index.js | 35 +++++++++++++++++++ awx/ui/client/features/output/index.view.html | 3 ++ awx/ui/client/features/output/jobs.strings.js | 14 ++++++++ awx/ui/client/lib/models/Jobs.js | 19 ++++++++++ 6 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 awx/ui/client/features/output/index.controller.js create mode 100644 awx/ui/client/features/output/index.js create mode 100644 awx/ui/client/features/output/index.view.html create mode 100644 awx/ui/client/features/output/jobs.strings.js create mode 100644 awx/ui/client/lib/models/Jobs.js diff --git a/awx/ui/client/features/index.js b/awx/ui/client/features/index.js index 763894c93c..0a6ec3864c 100644 --- a/awx/ui/client/features/index.js +++ b/awx/ui/client/features/index.js @@ -4,6 +4,7 @@ import atLibModels from '~models'; import atFeaturesApplications from '~features/applications'; import atFeaturesCredentials from '~features/credentials'; +import atFeaturesOutput from '~features/output'; import atFeaturesTemplates from '~features/templates'; import atFeaturesUsers from '~features/users'; import atFeaturesJobs from '~features/jobs'; @@ -18,7 +19,9 @@ angular.module(MODULE_NAME, [ atFeaturesCredentials, atFeaturesTemplates, atFeaturesUsers, - atFeaturesJobs + atFeaturesJobs, + atFeaturesOutput, + atFeaturesTemplates ]); export default MODULE_NAME; diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js new file mode 100644 index 0000000000..8a730a99c8 --- /dev/null +++ b/awx/ui/client/features/output/index.controller.js @@ -0,0 +1,4 @@ +function JobsIndexController () { +} + +module.exports = JobsIndexController; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js new file mode 100644 index 0000000000..03b596e00a --- /dev/null +++ b/awx/ui/client/features/output/index.js @@ -0,0 +1,35 @@ +import JobsStrings from '~features/output/jobs.strings'; +import IndexController from '~features/output/index.controller'; + +const indexTemplate = require('~features/output/index.view.html'); + +const MODULE_NAME = 'at.features.output'; + +function JobsRun ($stateExtender, strings) { + $stateExtender.addState({ + name: 'jobz', + route: '/jobz', + ncyBreadcrumb: { + label: strings.get('state.TITLE') + }, + data: { + activityStream: true, + activityStreamTarget: 'jobs' + }, + views: { + templateUrl: indexTemplate, + controller: IndexController, + controllerAs: 'vm' + } + }); +} + +JobsRun.$inject = ['$stateExtender', 'JobsStrings']; + +angular + .module(MODULE_NAME, []) + .controller('indexController', IndexController) + .service('JobsStrings', JobsStrings) + .run(JobsRun); + +export default MODULE_NAME; diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html new file mode 100644 index 0000000000..36bd84a02d --- /dev/null +++ b/awx/ui/client/features/output/index.view.html @@ -0,0 +1,3 @@ +

+ test +

diff --git a/awx/ui/client/features/output/jobs.strings.js b/awx/ui/client/features/output/jobs.strings.js new file mode 100644 index 0000000000..aa1afcdfaf --- /dev/null +++ b/awx/ui/client/features/output/jobs.strings.js @@ -0,0 +1,14 @@ +function JobsStrings (BaseString) { + BaseString.call(this, 'jobs'); + + const { t } = this; + const ns = this.jobs; + + ns.state = { + TITLE: t.s('JOBZ') + }; +} + +JobsStrings.$inject = ['BaseStringService']; + +export default JobsStrings; diff --git a/awx/ui/client/lib/models/Jobs.js b/awx/ui/client/lib/models/Jobs.js new file mode 100644 index 0000000000..e82b3e04af --- /dev/null +++ b/awx/ui/client/lib/models/Jobs.js @@ -0,0 +1,19 @@ +let BaseModel; + +function JobsModel (method, resource, config) { + BaseModel.call(this, 'jobs'); + + this.Constructor = JobsModel; + + return this.create(method, resource, config); +} + +function JobsModelLoader (_BaseModel_) { + BaseModel = _BaseModel_; + + return JobsModel; +} + +JobsModelLoader.$inject = ['BaseModel']; + +export default JobsModelLoader; From 5b8d2e76592fef1b35c1cfd68cbbf43122ae77fc Mon Sep 17 00:00:00 2001 From: gconsidine Date: Fri, 3 Nov 2017 13:44:26 -0400 Subject: [PATCH 02/89] Add model and resolve block to sandbox view --- awx/ui/build/webpack.watch.js | 8 +++++++- .../client/features/output/index.controller.js | 3 ++- awx/ui/client/features/output/index.js | 17 ++++++++++++++--- awx/ui/client/features/output/index.view.html | 15 ++++++++++++--- awx/ui/client/lib/models/index.js | 2 ++ 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/awx/ui/build/webpack.watch.js b/awx/ui/build/webpack.watch.js index d653707847..6ec25bed5a 100644 --- a/awx/ui/build/webpack.watch.js +++ b/awx/ui/build/webpack.watch.js @@ -26,7 +26,13 @@ const watch = { test: /\.js$/, enforce: 'pre', exclude: /node_modules/, - loader: 'eslint-loader' + loader: 'eslint-loader', + options: { + failOnWarning: false, + failOnError: false, + emitError: false, + emitWarning: false + } } ] }, diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 8a730a99c8..358750aefb 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -1,4 +1,5 @@ -function JobsIndexController () { +function JobsIndexController (resolved) { + console.log('test', resolved); } module.exports = JobsIndexController; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 03b596e00a..f7f0e9c768 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -17,9 +17,20 @@ function JobsRun ($stateExtender, strings) { activityStreamTarget: 'jobs' }, views: { - templateUrl: indexTemplate, - controller: IndexController, - controllerAs: 'vm' + '@': { + templateUrl: indexTemplate, + controller: IndexController, + controllerAs: 'vm' + } + }, + resolve: { + resolved: ['JobsModel', Jobs => { + const jobs = new Jobs(); + + return { + models: { jobs } + }; + }] } }); } diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 36bd84a02d..de0177058e 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -1,3 +1,12 @@ -

- test -

+
+
+ +

left

+
+
+
+ +

right

+
+
+
diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index fb902fb91c..0e5ab2c6b3 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -13,6 +13,7 @@ import InventoryScript from '~models/InventoryScript'; import InventorySource from '~models/InventorySource'; import Job from '~models/Job'; import JobTemplate from '~models/JobTemplate'; +import Jobs from '~models/Jobs'; import Me from '~models/Me'; import ModelsStrings from '~models/models.strings'; import NotificationTemplate from '~models/NotificationTemplate'; @@ -44,6 +45,7 @@ angular .service('InventorySourceModel', InventorySource) .service('JobModel', Job) .service('JobTemplateModel', JobTemplate) + .service('JobsModel', Jobs) .service('MeModel', Me) .service('ModelsStrings', ModelsStrings) .service('NotificationTemplate', NotificationTemplate) From 0de5301c23f6dcd9a069bf74c6dbce2a44ade68b Mon Sep 17 00:00:00 2001 From: gconsidine Date: Tue, 7 Nov 2017 11:45:23 -0500 Subject: [PATCH 03/89] Fix eslint errors/warnings emitted to browser --- awx/ui/build/webpack.watch.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/awx/ui/build/webpack.watch.js b/awx/ui/build/webpack.watch.js index 6ec25bed5a..0ae7d77ef5 100644 --- a/awx/ui/build/webpack.watch.js +++ b/awx/ui/build/webpack.watch.js @@ -26,13 +26,7 @@ const watch = { test: /\.js$/, enforce: 'pre', exclude: /node_modules/, - loader: 'eslint-loader', - options: { - failOnWarning: false, - failOnError: false, - emitError: false, - emitWarning: false - } + loader: 'eslint-loader' } ] }, @@ -60,6 +54,7 @@ const watch = { https: true, port: 3000, https: true, + clientLogLevel: 'none', proxy: { '/': { target: TARGET, From 946f3b5c9217bc709322f3a2eb6a53ffad0ad2ef Mon Sep 17 00:00:00 2001 From: gconsidine Date: Wed, 8 Nov 2017 16:08:37 -0500 Subject: [PATCH 04/89] Add ansi parsing libs --- awx/ui/client/features/_index.less | 1 + awx/ui/client/features/jobs/_index.less | 12 ++++++ .../features/output/index.controller.js | 39 ++++++++++++++++++- awx/ui/client/features/output/index.js | 10 ++--- awx/ui/client/features/output/index.view.html | 7 +++- 5 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 awx/ui/client/features/jobs/_index.less diff --git a/awx/ui/client/features/_index.less b/awx/ui/client/features/_index.less index e2339dc9e4..4a69120ed9 100644 --- a/awx/ui/client/features/_index.less +++ b/awx/ui/client/features/_index.less @@ -1,2 +1,3 @@ @import 'credentials/_index'; +@import 'jobs/_index'; @import 'users/tokens/_index'; diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less new file mode 100644 index 0000000000..9144f3c93c --- /dev/null +++ b/awx/ui/client/features/jobs/_index.less @@ -0,0 +1,12 @@ +.at-Stdout { + tr { + & > td:first-child { + padding-right: 5px; + border-right: 1px solid gray; + } + + & > td:last-child { + padding-left: 5px; + } + } +} diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 358750aefb..71c4bcf408 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -1,5 +1,40 @@ -function JobsIndexController (resolved) { - console.log('test', resolved); +import Ansi from 'ansi-to-html'; +import hasAnsi from 'has-ansi'; + +function JobsIndexController (job, $sce) { + const vm = this || {}; + const results = job.get('related.job_events.results'); + const ansi = new Ansi({}); + + /* + * const colors = []; + * + * for (let i = 0; i < 255; i++) { + * colors.push('#ababab'); + * } + * + */ + + let html = ''; + results.forEach((line, i) => { + if (!line.stdout) { + return; + } + + let output; + + if (hasAnsi(line.stdout)) { + output = ansi.toHtml(line.stdout); + } else { + output = line.stdout; // .replace(/(\n|\r)/g, ''); + } + + html += `${i}${output}`; + }); + + vm.html = $sce.trustAsHtml(html); } +JobsIndexController.$inject = ['job', '$sce']; + module.exports = JobsIndexController; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index f7f0e9c768..c5899b7964 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -24,13 +24,9 @@ function JobsRun ($stateExtender, strings) { } }, resolve: { - resolved: ['JobsModel', Jobs => { - const jobs = new Jobs(); - - return { - models: { jobs } - }; - }] + job: ['JobsModel', Jobs => new Jobs('get', 1002) + .then(job => job.extend('job_events')) + ] } }); } diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index de0177058e..92d2594851 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -6,7 +6,12 @@
-

right

+
+                
+                  
+                  
+                
+
From 30c472c4991e237e1edaf97f4aa0f21ae6fd0981 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Wed, 15 Nov 2017 15:44:20 -0500 Subject: [PATCH 05/89] Add basic event output in sandbox --- awx/ui/client/features/jobs/_index.less | 22 ++-- .../features/output/index.controller.js | 114 ++++++++++++++---- awx/ui/client/features/output/index.js | 11 +- awx/ui/package.json | 2 + 4 files changed, 111 insertions(+), 38 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index 9144f3c93c..50597f1b1a 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -1,12 +1,18 @@ .at-Stdout { - tr { - & > td:first-child { - padding-right: 5px; - border-right: 1px solid gray; - } + &-expand { + padding-right: 10px; + } - & > td:last-child { - padding-left: 5px; - } + &-lineNumber { + padding-right: 5px; + border-right: 1px solid gray; + } + + &-content { + padding-left: 5px; + } + + &-timestamp { + padding-left: 20px; } } diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 71c4bcf408..d94971ce7f 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -1,40 +1,102 @@ import Ansi from 'ansi-to-html'; import hasAnsi from 'has-ansi'; +let ansi; + +const EVENT_START_TASK = 'playbook_on_task_start'; +const EVENT_START_PLAY = 'playbook_on_play_start'; +const EVENT_STATS_PLAY = 'playbook_on_stats'; + +const EVENT_GROUPS = [ + EVENT_START_TASK, + EVENT_START_PLAY +]; + +const TIME_EVENTS = [ + EVENT_START_TASK, + EVENT_START_PLAY, + EVENT_STATS_PLAY +]; + function JobsIndexController (job, $sce) { const vm = this || {}; - const results = job.get('related.job_events.results'); - const ansi = new Ansi({}); + const events = job.get('related.job_events.results'); - /* - * const colors = []; - * - * for (let i = 0; i < 255; i++) { - * colors.push('#ababab'); - * } - * - */ + ansi = new Ansi(); - let html = ''; - results.forEach((line, i) => { - if (!line.stdout) { - return; - } - - let output; - - if (hasAnsi(line.stdout)) { - output = ansi.toHtml(line.stdout); - } else { - output = line.stdout; // .replace(/(\n|\r)/g, ''); - } - - html += `${i}${output}`; - }); + const html = parseEvents(events); vm.html = $sce.trustAsHtml(html); } +function parseEvents (events) { + events.sort((a, b) => a.start_line > b.start_line); + + return events.reduce((html, event) => `${html}${parseLine(event)}`, ''); +} + +function parseLine (event) { + if (!event || !event.stdout) { + return ''; + } + + const { stdout } = event; + const lines = stdout.split('\r\n'); + + let ln = event.start_line; + + return lines.reduce((html, line, i) => { + ln++; + + const time = getTime(event, i); + const group = getGroup(event, i); + + return `${html}${createRow(ln, line, time, group)}`; + }, ''); +} + +function createRow (ln, content, time, group) { + content = hasAnsi(content) ? ansi.toHtml(content) : content; + + let expand = ''; + if (group.parent) { + expand = ''; + } + + return ` + + ${expand} + ${ln} + ${content} + ${time} + `; +} + +function getGroup (event, i) { + const group = {}; + + if (EVENT_GROUPS.includes(event.event) && i === 1) { + group.parent = true; + group.classList = `parent parent-${event.event_level}`; + } else { + group.classList = ''; + } + + group.level = event.event_level; + + return group; +} + +function getTime (event, i) { + if (!TIME_EVENTS.includes(event.event) || i !== 1) { + return ''; + } + + const date = new Date(event.created); + + return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; +} + JobsIndexController.$inject = ['job', '$sce']; module.exports = JobsIndexController; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index c5899b7964..558a1e4714 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -8,7 +8,7 @@ const MODULE_NAME = 'at.features.output'; function JobsRun ($stateExtender, strings) { $stateExtender.addState({ name: 'jobz', - route: '/jobz', + route: '/jobz/:id', ncyBreadcrumb: { label: strings.get('state.TITLE') }, @@ -24,9 +24,12 @@ function JobsRun ($stateExtender, strings) { } }, resolve: { - job: ['JobsModel', Jobs => new Jobs('get', 1002) - .then(job => job.extend('job_events')) - ] + job: ['JobsModel', '$stateParams', (Jobs, $stateParams) => { + const { id } = $stateParams; + + return new Jobs('get', id) + .then(job => job.extend('job_events')); + }] } }); } diff --git a/awx/ui/package.json b/awx/ui/package.json index 2d9f32f488..5f8ff0ad5c 100644 --- a/awx/ui/package.json +++ b/awx/ui/package.json @@ -107,12 +107,14 @@ "angular-sanitize": "~1.6.6", "angular-scheduler": "git+https://git@github.com/ansible/angular-scheduler#v0.3.2", "angular-tz-extensions": "git+https://git@github.com/ansible/angular-tz-extensions#v0.5.2", + "ansi-to-html": "^0.6.3", "babel-polyfill": "^6.26.0", "bootstrap": "^3.3.7", "bootstrap-datepicker": "^1.7.1", "codemirror": "^5.17.0", "components-font-awesome": "^4.6.1", "d3": "~3.3.13", + "has-ansi": "^3.0.0", "javascript-detect-element-resize": "^0.5.3", "jquery": "~2.2.4", "jquery-ui": "^1.12.1", From 5c10ce3082f421536cc3b09895cff529a0bfb394 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Fri, 17 Nov 2017 16:14:53 -0500 Subject: [PATCH 06/89] Update job output styling --- awx/ui/client/features/jobs/_index.less | 30 ++++++++++++++++++- .../features/output/index.controller.js | 27 +++++++++++++++-- awx/ui/client/features/output/index.view.html | 13 +++++--- awx/ui/client/lib/theme/_utility.less | 4 +++ 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index 50597f1b1a..15acc8c90f 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -1,9 +1,27 @@ .at-Stdout { + font-family: monospace; + + &-controls { + border: 1px solid @at-gray-dark-2x; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom: none; + } + + &-controlIcon { + font-size: 16px; + padding: 10px; + } + &-expand { - padding-right: 10px; + background-color: @at-gray-light-2x; + + padding: 0 10px; } &-lineNumber { + background-color: @at-gray-light-2x; + padding-right: 5px; border-right: 1px solid gray; } @@ -15,4 +33,14 @@ &-timestamp { padding-left: 20px; } + + &-output { + font-size: 14px; + border: 1px solid @at-gray-dark-2x; + background-color: @at-gray-light-3x; + padding: 0; + margin: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + } } diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index d94971ce7f..01db5efeca 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -27,14 +27,27 @@ function JobsIndexController (job, $sce) { const html = parseEvents(events); vm.html = $sce.trustAsHtml(html); + vm.toggle = toggle; } function parseEvents (events) { - events.sort((a, b) => a.start_line > b.start_line); + events.sort(orderByLineNumber); return events.reduce((html, event) => `${html}${parseLine(event)}`, ''); } +function orderByLineNumber (a, b) { + if (a.start_line > b.start_line) { + return 1; + } + + if (a.start_line < b.start_line) { + return -1; + } + + return 0; +} + function parseLine (event) { if (!event || !event.stdout) { return ''; @@ -60,7 +73,7 @@ function createRow (ln, content, time, group) { let expand = ''; if (group.parent) { - expand = ''; + expand = ''; } return ` @@ -97,6 +110,16 @@ function getTime (event, i) { return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; } +function toggle (id) { + console.log(id); +} + +/* + *function addDynamic (start) { + * document.getElementsByClassName('parent') + *} + */ + JobsIndexController.$inject = ['job', '$sce']; module.exports = JobsIndexController; diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 92d2594851..8c43a4ae8a 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -5,11 +5,16 @@
- -
+        
+            
+
+
+
+
+ +
                 
-                  
-                  
+                  
diff --git a/awx/ui/client/lib/theme/_utility.less b/awx/ui/client/lib/theme/_utility.less index 1f47a481f3..8f95237fea 100644 --- a/awx/ui/client/lib/theme/_utility.less +++ b/awx/ui/client/lib/theme/_utility.less @@ -16,3 +16,7 @@ margin-left: 0; margin-right: 0; } + +.at-u-clear { + clear: both; +} From 3096a58272dfa5492fc4c3e518d8fb91a4fb87c8 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Thu, 30 Nov 2017 11:57:00 -0500 Subject: [PATCH 07/89] Update style to match latest mockup --- awx/ui/client/features/jobs/_index.less | 18 ++++++++----- .../features/output/index.controller.js | 18 ++++++++++++- awx/ui/client/features/output/index.view.html | 26 ++++++++++++++----- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index 15acc8c90f..020ee3f46e 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -2,6 +2,7 @@ font-family: monospace; &-controls { + color: @at-gray-dark-4x; border: 1px solid @at-gray-dark-2x; border-top-left-radius: 4px; border-top-right-radius: 4px; @@ -9,29 +10,34 @@ } &-controlIcon { - font-size: 16px; + font-size: 12px; padding: 10px; } &-expand { - background-color: @at-gray-light-2x; + color: @at-gray-dark-4x; + background-color: @at-gray-light; + font-size: 12px; - padding: 0 10px; + padding: 0 20px 0 10px; } &-lineNumber { - background-color: @at-gray-light-2x; + color: @at-gray-dark-4x; + background-color: @at-gray-light; padding-right: 5px; - border-right: 1px solid gray; + border-right: 1px solid @at-gray-dark; } &-content { - padding-left: 5px; + padding-left: 20px; } &-timestamp { padding-left: 20px; + font-size: 12px; + text-align: right; } &-output { diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 01db5efeca..fa61c0df6c 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -55,6 +55,7 @@ function parseLine (event) { const { stdout } = event; const lines = stdout.split('\r\n'); + const isTruncated = (event.end_line - event.start_line) > lines.length; let ln = event.start_line; @@ -63,17 +64,32 @@ function parseLine (event) { const time = getTime(event, i); const group = getGroup(event, i); + const isLastLine = i === lines.length - 1; + + if (isTruncated && isLastLine) { + return `${html}${createRow(ln, line, time, group)}${createTruncatedRow()}`; + } return `${html}${createRow(ln, line, time, group)}`; }, ''); } +function createTruncatedRow () { + return ` + + + + + + `; +} + function createRow (ln, content, time, group) { content = hasAnsi(content) ? ansi.toHtml(content) : content; let expand = ''; if (group.parent) { - expand = ''; + expand = ''; } return ` diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 8c43a4ae8a..6b51b5f548 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -7,16 +7,28 @@
-
-
+
+
-
-                
-                  
-                
-
+

+                
+                    
+                        
+                        
+                        
+                    
+                
+                
+                
+                    
+                        
+                        
+                        
+                        
+                    
+                
 
  Back to Top
From aaec3474b0cb861956d337f09aab62158c049cda Mon Sep 17 00:00:00 2001 From: gconsidine Date: Thu, 30 Nov 2017 17:25:53 -0500 Subject: [PATCH 08/89] Add support for params to BaseModel.extend --- awx/ui/client/features/jobs/_index.less | 29 ++++++++++++----- .../features/output/index.controller.js | 31 +++++++++++-------- awx/ui/client/features/output/index.js | 7 ++++- awx/ui/client/features/output/index.view.html | 20 ++++++------ awx/ui/client/lib/models/Base.js | 18 +++-------- 5 files changed, 60 insertions(+), 45 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index 020ee3f46e..5b66ff8366 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -1,7 +1,7 @@ .at-Stdout { font-family: monospace; - &-controls { + &-menu { color: @at-gray-dark-4x; border: 1px solid @at-gray-dark-2x; border-top-left-radius: 4px; @@ -9,12 +9,12 @@ border-bottom: none; } - &-controlIcon { + &-menuIcon { font-size: 12px; padding: 10px; } - &-expand { + &-toggle { color: @at-gray-dark-4x; background-color: @at-gray-light; font-size: 12px; @@ -22,25 +22,38 @@ padding: 0 20px 0 10px; } - &-lineNumber { + &-line { color: @at-gray-dark-4x; background-color: @at-gray-light; + text-align: right; + padding-right: 5px; border-right: 1px solid @at-gray-dark; } - &-content { - padding-left: 20px; + &-event { + padding-left: 2ch; + width: 83ch; } - &-timestamp { + &-time { padding-left: 20px; font-size: 12px; text-align: right; } - &-output { + &-container { + & > table { + table-layout: fixed; + + td { + vertical-align: top; + word-wrap: break-word; + white-space: pre-wrap; + } + } + font-size: 14px; border: 1px solid @at-gray-dark-2x; background-color: @at-gray-light-3x; diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index fa61c0df6c..c571062641 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -33,6 +33,7 @@ function JobsIndexController (job, $sce) { function parseEvents (events) { events.sort(orderByLineNumber); + console.log(events); return events.reduce((html, event) => `${html}${parseLine(event)}`, ''); } @@ -67,20 +68,20 @@ function parseLine (event) { const isLastLine = i === lines.length - 1; if (isTruncated && isLastLine) { - return `${html}${createRow(ln, line, time, group)}${createTruncatedRow()}`; + return `${html}${createRow(ln, line, time, group)}${createTruncatedRow(event.id)}`; } return `${html}${createRow(ln, line, time, group)}`; }, ''); } -function createTruncatedRow () { +function createTruncatedRow (id) { return ` - - - - - + + + ... + + `; } @@ -94,10 +95,10 @@ function createRow (ln, content, time, group) { return ` - ${expand} - ${ln} - ${content} - ${time} + ${expand} + ${ln} + ${content} + ${time} `; } @@ -129,13 +130,17 @@ function getTime (event, i) { function toggle (id) { console.log(id); } - /* + * + *function getTruncatedEvent () { + * + *} + * *function addDynamic (start) { * document.getElementsByClassName('parent') *} + * */ - JobsIndexController.$inject = ['job', '$sce']; module.exports = JobsIndexController; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 558a1e4714..abe9a5edec 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -28,7 +28,12 @@ function JobsRun ($stateExtender, strings) { const { id } = $stateParams; return new Jobs('get', id) - .then(job => job.extend('job_events')); + .then(job => job.extend('job_events', { + params: { + page_size: 200, + order_by: 'start_line' + } + })); }] } }); diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 6b51b5f548..5a585c7321 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -6,26 +6,26 @@
-
-
-
+
+
+
-

+            
- - - + + + - - - + + +
  
   Back to Top
diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index a2c202aa79..ee63b97076 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -327,25 +327,17 @@ function has (method, keys) { return value !== undefined && value !== null; } -function extend (method, related, config = {}) { - if (!related) { - related = method; - method = 'GET'; - } else { - method = method.toUpperCase(); - } +function extend (related, config) { + const req = this.parseRequestConfig('GET', config); - if (this.has(method, `related.${related}`)) { - const req = { - method, - url: this.get(`related.${related}`) - }; + if (this.has(req.method, `related.${related}`)) { + req.url = this.get(`related.${related}`); Object.assign(req, config); return $http(req) .then(({ data }) => { - this.set(method, `related.${related}`, data); + this.set(req.method, `related.${related}`, data); return this; }); From 0a66d1c3fc9a765df043e6fcab8ccc5a20f27c5d Mon Sep 17 00:00:00 2001 From: gconsidine Date: Mon, 4 Dec 2017 15:57:39 -0500 Subject: [PATCH 09/89] Add dynamic angular interaction after HTML insertion --- awx/ui/client/features/jobs/_index.less | 35 +++++++++------ .../features/output/index.controller.js | 45 ++++++++++--------- awx/ui/client/features/output/index.js | 2 +- awx/ui/client/features/output/index.view.html | 18 +------- 4 files changed, 48 insertions(+), 52 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index 5b66ff8366..8c3a7195e1 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -20,6 +20,7 @@ font-size: 12px; padding: 0 20px 0 10px; + user-select: none; } &-line { @@ -30,30 +31,24 @@ padding-right: 5px; border-right: 1px solid @at-gray-dark; + user-select: none; } &-event { - padding-left: 2ch; - width: 83ch; + padding: 0 10px; } &-time { - padding-left: 20px; + padding-right: 2ch; font-size: 12px; text-align: right; + user-select: none; + width: 11ch; + border-left: 1px dashed @at-gray-dark; } &-container { - & > table { - table-layout: fixed; - - td { - vertical-align: top; - word-wrap: break-word; - white-space: pre-wrap; - } - } - + max-height: 80vh; font-size: 14px; border: 1px solid @at-gray-dark-2x; background-color: @at-gray-light-3x; @@ -61,5 +56,19 @@ margin: 0; border-top-left-radius: 0; border-top-right-radius: 0; + + & > table { + table-layout: fixed; + + tr:hover > td { + background: white; + } + + td { + vertical-align: top; + word-wrap: break-word; + white-space: pre-wrap; + } + } } } diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index c571062641..d88d1e915f 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -2,6 +2,10 @@ import Ansi from 'ansi-to-html'; import hasAnsi from 'has-ansi'; let ansi; +let $timeout; +let $sce; +let $compile; +let $scope; const EVENT_START_TASK = 'playbook_on_task_start'; const EVENT_START_PLAY = 'playbook_on_play_start'; @@ -18,22 +22,30 @@ const TIME_EVENTS = [ EVENT_STATS_PLAY ]; -function JobsIndexController (job, $sce) { +function JobsIndexController (job, _$sce_, _$timeout_, _$scope_, _$compile_) { + ansi = new Ansi(); + $timeout = _$timeout_; + $sce = _$sce_; + $compile = _$compile_; + $scope = _$scope_; + const vm = this || {}; const events = job.get('related.job_events.results'); + const html = $sce.trustAsHtml(parseEvents(events)); - ansi = new Ansi(); - - const html = parseEvents(events); - - vm.html = $sce.trustAsHtml(html); vm.toggle = toggle; + + $timeout(() => { + const table = $('#result-table'); + + table.html($sce.getTrustedHtml(html)); + $compile(table.contents())($scope); + }); } function parseEvents (events) { events.sort(orderByLineNumber); - console.log(events); return events.reduce((html, event) => `${html}${parseLine(event)}`, ''); } @@ -90,12 +102,12 @@ function createRow (ln, content, time, group) { let expand = ''; if (group.parent) { - expand = ''; + expand = ''; } return ` - ${expand} + ${expand} ${ln} ${content} ${time} @@ -108,6 +120,7 @@ function getGroup (event, i) { if (EVENT_GROUPS.includes(event.event) && i === 1) { group.parent = true; group.classList = `parent parent-${event.event_level}`; + group.id = i; } else { group.classList = ''; } @@ -130,17 +143,7 @@ function getTime (event, i) { function toggle (id) { console.log(id); } -/* - * - *function getTruncatedEvent () { - * - *} - * - *function addDynamic (start) { - * document.getElementsByClassName('parent') - *} - * - */ -JobsIndexController.$inject = ['job', '$sce']; + +JobsIndexController.$inject = ['job', '$sce', '$timeout', '$scope', '$compile']; module.exports = JobsIndexController; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index abe9a5edec..aa98a2359a 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -30,7 +30,7 @@ function JobsRun ($stateExtender, strings) { return new Jobs('get', id) .then(job => job.extend('job_events', { params: { - page_size: 200, + page_size: 10000, order_by: 'start_line' } })); diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 5a585c7321..34fd7dc368 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -12,23 +12,7 @@
-

-                
-                    
-                        
-                        
-                        
-                    
-                
-                
-                
-                    
-                        
-                        
-                        
-                        
-                    
-                
 
  Back to Top
+
 
From 6f7841a9206a8c2cdcdc5dc79b6cbd3069960920 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Tue, 5 Dec 2017 16:50:00 -0500 Subject: [PATCH 10/89] Add record object to maintain event meta info --- .../features/output/index.controller.js | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index d88d1e915f..4b44fc2c0f 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -7,6 +7,8 @@ let $sce; let $compile; let $scope; +const record = {}; + const EVENT_START_TASK = 'playbook_on_task_start'; const EVENT_START_PLAY = 'playbook_on_play_start'; const EVENT_STATS_PLAY = 'playbook_on_stats'; @@ -41,6 +43,8 @@ function JobsIndexController (job, _$sce_, _$timeout_, _$scope_, _$compile_) { table.html($sce.getTrustedHtml(html)); $compile(table.contents())($scope); }); + + console.log(record); } function parseEvents (events) { @@ -68,22 +72,46 @@ function parseLine (event) { const { stdout } = event; const lines = stdout.split('\r\n'); - const isTruncated = (event.end_line - event.start_line) > lines.length; - let ln = event.start_line; + let eventLn = event.start_line; + let ln = event.start_line + 1; + + if (lines[0] === '') { + ln++; + } + + record[ln] = { + line: ln, + id: event.id, + uuid: event.uuid, + level: event.event_level, + start: event.start_line, + end: event.end_line, + isTruncated: (event.end_line - event.start_line) > lines.length + }; + + if (record[ln].isTruncated) { + record[ln].truncatedAt = event.start_line + lines.length; + } + + if (EVENT_GROUPS.includes(event.event)) { + record[ln].parent = true; + } + + const current = record[ln]; return lines.reduce((html, line, i) => { - ln++; + eventLn++; const time = getTime(event, i); const group = getGroup(event, i); const isLastLine = i === lines.length - 1; - if (isTruncated && isLastLine) { - return `${html}${createRow(ln, line, time, group)}${createTruncatedRow(event.id)}`; + if (current.isTruncated && isLastLine) { + return `${html}${createRow(eventLn, line, time, group)}${createTruncatedRow(event.id)}`; } - return `${html}${createRow(ln, line, time, group)}`; + return `${html}${createRow(eventLn, line, time, group)}`; }, ''); } From dbf1fd2d4f1387489f3d6b4df89f95b3c6c1fc0e Mon Sep 17 00:00:00 2001 From: gconsidine Date: Wed, 6 Dec 2017 13:10:43 -0500 Subject: [PATCH 11/89] Use event record with output template --- .../features/output/index.controller.js | 117 +++++++++--------- 1 file changed, 59 insertions(+), 58 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 4b44fc2c0f..76047c1078 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -25,11 +25,11 @@ const TIME_EVENTS = [ ]; function JobsIndexController (job, _$sce_, _$timeout_, _$scope_, _$compile_) { - ansi = new Ansi(); $timeout = _$timeout_; $sce = _$sce_; $compile = _$compile_; $scope = _$scope_; + ansi = new Ansi(); const vm = this || {}; const events = job.get('related.job_events.results'); @@ -73,103 +73,104 @@ function parseLine (event) { const { stdout } = event; const lines = stdout.split('\r\n'); - let eventLn = event.start_line; - let ln = event.start_line + 1; + let eventLine = event.start_line; + let displayLine = event.start_line + 1; if (lines[0] === '') { - ln++; + displayLine++; } - record[ln] = { - line: ln, + record[displayLine] = { + line: displayLine, id: event.id, uuid: event.uuid, level: event.event_level, start: event.start_line, end: event.end_line, - isTruncated: (event.end_line - event.start_line) > lines.length + isTruncated: (event.end_line - event.start_line) > lines.length, }; - if (record[ln].isTruncated) { - record[ln].truncatedAt = event.start_line + lines.length; + if (record[displayLine].isTruncated) { + record[displayLine].truncatedAt = event.start_line + lines.length; } if (EVENT_GROUPS.includes(event.event)) { - record[ln].parent = true; + record[displayLine].isParent = true; } - const current = record[ln]; + if (TIME_EVENTS.includes(event.event)) { + record[displayLine].time = getTime(event.created); + } + + const current = record[displayLine]; return lines.reduce((html, line, i) => { - eventLn++; + eventLine++; - const time = getTime(event, i); - const group = getGroup(event, i); const isLastLine = i === lines.length - 1; + let append = createRow(eventLine, line, current); if (current.isTruncated && isLastLine) { - return `${html}${createRow(eventLn, line, time, group)}${createTruncatedRow(event.id)}`; + append += createRow(); } - return `${html}${createRow(eventLn, line, time, group)}`; + return `${html}${append}`; }, ''); } -function createTruncatedRow (id) { - return ` - - - ... - - - `; -} - -function createRow (ln, content, time, group) { - content = hasAnsi(content) ? ansi.toHtml(content) : content; - +function createRow (ln, content, current) { let expand = ''; - if (group.parent) { - expand = ''; + let timestamp = ''; + let toggleRow = ''; + let classList = ''; + + content = content || ''; + + if (hasAnsi(content)) { + content = ansi.toHtml(content); + } + + if (current) { + if (current.line === ln) { + if (current.isParent) { + expand = ''; + toggleRow = `${expand}`; + } + + if (current.time) { + timestamp = current.time; + } + } else { + classList += `child-of-${current.line}`; + } + } + + if (!toggleRow) { + toggleRow = ''; + } + + if (!ln) { + ln = '...'; } return ` - - ${expand} + + ${toggleRow} ${ln} ${content} - ${time} + ${timestamp} `; } -function getGroup (event, i) { - const group = {}; - - if (EVENT_GROUPS.includes(event.event) && i === 1) { - group.parent = true; - group.classList = `parent parent-${event.event_level}`; - group.id = i; - } else { - group.classList = ''; - } - - group.level = event.event_level; - - return group; -} - -function getTime (event, i) { - if (!TIME_EVENTS.includes(event.event) || i !== 1) { - return ''; - } - - const date = new Date(event.created); +function getTime (created) { + const date = new Date(created); return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; } -function toggle (id) { - console.log(id); +function toggle (line) { + const lines = document.getElementsByClassName(`child-of-${line}`); + console.log(lines); } JobsIndexController.$inject = ['job', '$sce', '$timeout', '$scope', '$compile']; From d914b70bb61b80d0fe048590c226ca216f645abc Mon Sep 17 00:00:00 2001 From: gconsidine Date: Wed, 6 Dec 2017 15:41:52 -0500 Subject: [PATCH 12/89] Add expand/collapse to parent events --- awx/ui/client/features/jobs/_index.less | 13 +- .../features/output/index.controller.js | 118 +++++++++++------- 2 files changed, 77 insertions(+), 54 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index 8c3a7195e1..0763f0c42d 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -19,6 +19,10 @@ background-color: @at-gray-light; font-size: 12px; + & > i { + cursor: pointer; + } + padding: 0 20px 0 10px; user-select: none; } @@ -29,6 +33,7 @@ text-align: right; + vertical-align: top; padding-right: 5px; border-right: 1px solid @at-gray-dark; user-select: none; @@ -36,6 +41,8 @@ &-event { padding: 0 10px; + word-wrap: break-word; + white-space: pre-wrap; } &-time { @@ -63,12 +70,6 @@ tr:hover > td { background: white; } - - td { - vertical-align: top; - word-wrap: break-word; - white-space: pre-wrap; - } } } } diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 76047c1078..0c28ce968f 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -73,56 +73,66 @@ function parseLine (event) { const { stdout } = event; const lines = stdout.split('\r\n'); - let eventLine = event.start_line; - let displayLine = event.start_line + 1; + let ln = event.start_line; - if (lines[0] === '') { - displayLine++; - } - - record[displayLine] = { - line: displayLine, - id: event.id, - uuid: event.uuid, - level: event.event_level, - start: event.start_line, - end: event.end_line, - isTruncated: (event.end_line - event.start_line) > lines.length, - }; - - if (record[displayLine].isTruncated) { - record[displayLine].truncatedAt = event.start_line + lines.length; - } - - if (EVENT_GROUPS.includes(event.event)) { - record[displayLine].isParent = true; - } - - if (TIME_EVENTS.includes(event.event)) { - record[displayLine].time = getTime(event.created); - } - - const current = record[displayLine]; + const current = createRecord(ln, lines, event); return lines.reduce((html, line, i) => { - eventLine++; + ln++; const isLastLine = i === lines.length - 1; - let append = createRow(eventLine, line, current); + let append = createRow(current, ln, line); - if (current.isTruncated && isLastLine) { - append += createRow(); + if (current && current.isTruncated && isLastLine) { + append += createRow(current); } return `${html}${append}`; }, ''); } -function createRow (ln, content, current) { +function createRecord (ln, lines, event) { + if (!event.uuid) { + return null; + } + + const info = { + line: ln + 1, + uuid: event.uuid, + level: event.event_level, + start: event.start_line, + end: event.end_line, + isTruncated: (event.end_line - event.start_line) > lines.length + }; + + if (event.parent_uuid) { + info.childOf = event.parent_uuid; + } + + if (info.isTruncated) { + info.truncatedAt = event.start_line + lines.length; + } + + if (EVENT_GROUPS.includes(event.event)) { + info.isParent = true; + } + + if (TIME_EVENTS.includes(event.event)) { + info.time = getTime(event.created); + info.line++; + } + + record[event.uuid] = info; + + return info; +} + +function createRow (current, ln, content) { let expand = ''; let timestamp = ''; let toggleRow = ''; let classList = ''; + let id = ''; content = content || ''; @@ -131,17 +141,18 @@ function createRow (ln, content, current) { } if (current) { - if (current.line === ln) { - if (current.isParent) { - expand = ''; - toggleRow = `${expand}`; - } + if (current.isParent && current.line === ln) { + id = current.uuid; + expand = ''; + toggleRow = `${expand}`; + } - if (current.time) { - timestamp = current.time; - } - } else { - classList += `child-of-${current.line}`; + if (current.time && current.line === ln) { + timestamp = current.time; + } + + if (!classList) { + classList += `child-of-${current.childOf}`; } } @@ -154,7 +165,7 @@ function createRow (ln, content, current) { } return ` - + ${toggleRow} ${ln} ${content} @@ -168,9 +179,20 @@ function getTime (created) { return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; } -function toggle (line) { - const lines = document.getElementsByClassName(`child-of-${line}`); - console.log(lines); +function toggle (uuid) { + const i = $(`#${uuid} .at-Stdout-toggle > i`); + + if (i.hasClass('fa-chevron-down')) { + i.addClass('fa-chevron-right'); + i.removeClass('fa-chevron-down'); + + $(`.child-of-${uuid}`).addClass('hidden'); + } else { + i.addClass('fa-chevron-down'); + i.removeClass('fa-chevron-right'); + + $(`.child-of-${uuid}`).removeClass('hidden'); + } } JobsIndexController.$inject = ['job', '$sce', '$timeout', '$scope', '$compile']; From 56b6d7e85d6d33a256f7277870b5ba2b7d6aebfd Mon Sep 17 00:00:00 2001 From: gconsidine Date: Thu, 7 Dec 2017 15:31:57 -0500 Subject: [PATCH 13/89] Add scrollTo for top and bottom, add better expand/collapse --- awx/ui/client/features/jobs/_index.less | 15 +++- .../features/output/index.controller.js | 84 ++++++++++++++++--- awx/ui/client/features/output/index.view.html | 20 ++++- 3 files changed, 99 insertions(+), 20 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index 0763f0c42d..32ef05dbf7 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -1,7 +1,7 @@ .at-Stdout { font-family: monospace; - &-menu { + &-menuTop { color: @at-gray-dark-4x; border: 1px solid @at-gray-dark-2x; border-top-left-radius: 4px; @@ -9,9 +9,18 @@ border-bottom: none; } + &-menuBottom { + color: @at-gray-dark-4x; + border: 1px solid @at-gray-dark-2x; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-top: none; + } + &-menuIcon { font-size: 12px; padding: 10px; + cursor: pointer; } &-toggle { @@ -51,7 +60,6 @@ text-align: right; user-select: none; width: 11ch; - border-left: 1px dashed @at-gray-dark; } &-container { @@ -61,8 +69,7 @@ background-color: @at-gray-light-3x; padding: 0; margin: 0; - border-top-left-radius: 0; - border-top-right-radius: 0; + border-radius: 0; & > table { table-layout: fixed; diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 0c28ce968f..e6976a2ab1 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -1,6 +1,7 @@ import Ansi from 'ansi-to-html'; import hasAnsi from 'has-ansi'; +let vm; let ansi; let $timeout; let $sce; @@ -8,6 +9,7 @@ let $compile; let $scope; const record = {}; +const meta = {}; const EVENT_START_TASK = 'playbook_on_task_start'; const EVENT_START_PLAY = 'playbook_on_play_start'; @@ -31,11 +33,20 @@ function JobsIndexController (job, _$sce_, _$timeout_, _$scope_, _$compile_) { $scope = _$scope_; ansi = new Ansi(); - const vm = this || {}; + vm = this || {}; const events = job.get('related.job_events.results'); const html = $sce.trustAsHtml(parseEvents(events)); vm.toggle = toggle; + vm.menu = { + expand: menuExpand, + scrollToBottom: menuScrollToBottom, + scrollToTop: menuScrollToTop + }; + + vm.state = { + expand: true + }; $timeout(() => { const table = $('#result-table'); @@ -43,8 +54,23 @@ function JobsIndexController (job, _$sce_, _$timeout_, _$scope_, _$compile_) { table.html($sce.getTrustedHtml(html)); $compile(table.contents())($scope); }); +} - console.log(record); +function menuExpand () { + vm.state.expand = !vm.state.expand; + vm.toggle(meta.parent); +} + +function menuScrollToBottom () { + const container = $('.at-Stdout-container')[0]; + + container.scrollTo(0, container.scrollHeight); +} + +function menuScrollToTop () { + const container = $('.at-Stdout-container')[0]; + + container.scrollTo(0, 0); } function parseEvents (events) { @@ -106,7 +132,7 @@ function createRecord (ln, lines, event) { }; if (event.parent_uuid) { - info.childOf = event.parent_uuid; + info.parents = getParentEvents(event.parent_uuid); } if (info.isTruncated) { @@ -115,6 +141,19 @@ function createRecord (ln, lines, event) { if (EVENT_GROUPS.includes(event.event)) { info.isParent = true; + + if (event.event_level === 1) { + meta.parent = event.uuid; + } + + if (event.parent_uuid) { + if (record[event.parent_uuid].children && + !record[event.parent_uuid].children.includes(event.uuid)) { + record[event.parent_uuid].children.push(event.uuid); + } else { + record[event.parent_uuid].children = [event.uuid]; + } + } } if (TIME_EVENTS.includes(event.event)) { @@ -127,6 +166,20 @@ function createRecord (ln, lines, event) { return info; } +function getParentEvents (uuid, list) { + list = list || []; + + if (record[uuid]) { + list.push(uuid); + } + + if (record[uuid].parents) { + list = list.concat(record[uuid].parents); + } + + return list; +} + function createRow (current, ln, content) { let expand = ''; let timestamp = ''; @@ -151,8 +204,8 @@ function createRow (current, ln, content) { timestamp = current.time; } - if (!classList) { - classList += `child-of-${current.childOf}`; + if (current.parents) { + classList = current.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, ''); } } @@ -180,18 +233,23 @@ function getTime (created) { } function toggle (uuid) { - const i = $(`#${uuid} .at-Stdout-toggle > i`); + const lines = $(`.child-of-${uuid}`); + let icon = $(`#${uuid} .at-Stdout-toggle > i`); - if (i.hasClass('fa-chevron-down')) { - i.addClass('fa-chevron-right'); - i.removeClass('fa-chevron-down'); + if (record[uuid].children) { + icon = icon.add($(`#${record[uuid].children.join(', #')}`).find('.at-Stdout-toggle > i')); + } - $(`.child-of-${uuid}`).addClass('hidden'); + if (icon.hasClass('fa-chevron-down')) { + icon.addClass('fa-chevron-right'); + icon.removeClass('fa-chevron-down'); + + lines.addClass('hidden'); } else { - i.addClass('fa-chevron-down'); - i.removeClass('fa-chevron-right'); + icon.addClass('fa-chevron-down'); + icon.removeClass('fa-chevron-right'); - $(`.child-of-${uuid}`).removeClass('hidden'); + lines.removeClass('hidden'); } } diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 34fd7dc368..2da5753b03 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -6,13 +6,27 @@
-
-
-
+
+
+ +
+
+ +
+
 
+ +
+
+ +
+ +
+
From 21e74fc5eb8d9181a2cf479039084a80041e9678 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Mon, 11 Dec 2017 16:10:29 -0500 Subject: [PATCH 14/89] Add click to launch host event detail modal --- awx/ui/client/features/jobs/_index.less | 17 ++++- .../features/output/index.controller.js | 71 +++++++++++++------ awx/ui/client/features/output/index.view.html | 3 + .../lib/components/modal/modal.directive.js | 3 +- 4 files changed, 68 insertions(+), 26 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index 32ef05dbf7..78e1de6b33 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -49,9 +49,13 @@ } &-event { - padding: 0 10px; - word-wrap: break-word; - white-space: pre-wrap; + .at-mixin-event(); + } + + &-event--host { + .at-mixin-event(); + + cursor: pointer; } &-time { @@ -80,3 +84,10 @@ } } } + +.at-mixin-event() { + padding: 0 10px; + word-wrap: break-word; + white-space: pre-wrap; + +} diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index e6976a2ab1..3139c3a6f8 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -33,11 +33,19 @@ function JobsIndexController (job, _$sce_, _$timeout_, _$scope_, _$compile_) { $scope = _$scope_; ansi = new Ansi(); - vm = this || {}; const events = job.get('related.job_events.results'); const html = $sce.trustAsHtml(parseEvents(events)); + vm = this || {}; + + $scope.ns = 'jobs'; + $scope.jobs = { + modal: {} + }; + vm.toggle = toggle; + vm.showHostDetails = showHostDetails; + vm.menu = { expand: menuExpand, scrollToBottom: menuScrollToBottom, @@ -64,13 +72,13 @@ function menuExpand () { function menuScrollToBottom () { const container = $('.at-Stdout-container')[0]; - container.scrollTo(0, container.scrollHeight); + container.scrollTop = container.scrollHeight; } function menuScrollToTop () { const container = $('.at-Stdout-container')[0]; - container.scrollTo(0, 0); + container.scrollTop = 0; } function parseEvents (events) { @@ -123,14 +131,20 @@ function createRecord (ln, lines, event) { } const info = { + id: event.id, line: ln + 1, uuid: event.uuid, level: event.event_level, start: event.start_line, end: event.end_line, - isTruncated: (event.end_line - event.start_line) > lines.length + isTruncated: (event.end_line - event.start_line) > lines.length, + isHost: typeof event.host === 'number' }; + if (info.isHost) { + console.log(event); + } + if (event.parent_uuid) { info.parents = getParentEvents(event.parent_uuid); } @@ -147,11 +161,13 @@ function createRecord (ln, lines, event) { } if (event.parent_uuid) { - if (record[event.parent_uuid].children && - !record[event.parent_uuid].children.includes(event.uuid)) { - record[event.parent_uuid].children.push(event.uuid); - } else { - record[event.parent_uuid].children = [event.uuid]; + if (record[event.parent_uuid]) { + if (record[event.parent_uuid].children && + !record[event.parent_uuid].children.includes(event.uuid)) { + record[event.parent_uuid].children.push(event.uuid); + } else { + record[event.parent_uuid].children = [event.uuid]; + } } } } @@ -171,21 +187,21 @@ function getParentEvents (uuid, list) { if (record[uuid]) { list.push(uuid); - } - if (record[uuid].parents) { - list = list.concat(record[uuid].parents); + if (record[uuid].parents) { + list = list.concat(record[uuid].parents); + } } return list; } function createRow (current, ln, content) { - let expand = ''; - let timestamp = ''; - let toggleRow = ''; - let classList = ''; let id = ''; + let timestamp = ''; + let tdToggle = ''; + let tdEvent = ''; + let classList = ''; content = content || ''; @@ -196,8 +212,11 @@ function createRow (current, ln, content) { if (current) { if (current.isParent && current.line === ln) { id = current.uuid; - expand = ''; - toggleRow = `${expand}`; + tdToggle = ``; + } + + if (current.isHost) { + tdEvent = `${content}`; } if (current.time && current.line === ln) { @@ -209,8 +228,12 @@ function createRow (current, ln, content) { } } - if (!toggleRow) { - toggleRow = ''; + if (!tdEvent) { + tdEvent = `${content}`; + } + + if (!tdToggle) { + tdToggle = ''; } if (!ln) { @@ -219,9 +242,9 @@ function createRow (current, ln, content) { return ` - ${toggleRow} + ${tdToggle} ${ln} - ${content} + ${tdEvent} ${timestamp} `; } @@ -232,6 +255,10 @@ function getTime (created) { return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; } +function showHostDetails (id) { + $scope.jobs.modal.show('title', `test${id}`); +} + function toggle (uuid) { const lines = $(`.child-of-${uuid}`); let icon = $(`#${uuid} .at-Stdout-toggle > i`); diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 2da5753b03..ba1256c5cc 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -4,6 +4,7 @@

left

+
@@ -29,4 +30,6 @@
+ + diff --git a/awx/ui/client/lib/components/modal/modal.directive.js b/awx/ui/client/lib/components/modal/modal.directive.js index 302ff92a03..f3def99885 100644 --- a/awx/ui/client/lib/components/modal/modal.directive.js +++ b/awx/ui/client/lib/components/modal/modal.directive.js @@ -12,7 +12,7 @@ function atModalLink (scope, el, attrs, controllers) { }); } -function AtModalController (eventService, strings) { +function AtModalController ($timeout, eventService, strings) { const vm = this; let overlay; @@ -58,6 +58,7 @@ function AtModalController (eventService, strings) { } AtModalController.$inject = [ + '$timeout', 'EventService', 'ComponentsStrings' ]; From a7f29aac3af98ca841f91696d27ba350115e02c0 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Tue, 12 Dec 2017 16:26:12 -0500 Subject: [PATCH 15/89] Add component-based stdout for host modal --- awx/ui/client/features/_index.less | 2 +- .../features/output/index.controller.js | 23 +++- awx/ui/client/features/output/index.js | 12 +- awx/ui/client/features/output/index.view.html | 7 +- awx/ui/client/lib/components/index.js | 2 + .../lib/components/output/stdout.directive.js | 105 ++++++++++++++++++ .../lib/components/output/stdout.partial.html | 19 ++++ awx/ui/client/lib/models/Job.js | 8 +- awx/ui/client/lib/models/JobEvent.js | 19 ++++ awx/ui/client/lib/models/Jobs.js | 19 ---- awx/ui/client/lib/models/index.js | 10 +- 11 files changed, 189 insertions(+), 37 deletions(-) create mode 100644 awx/ui/client/lib/components/output/stdout.directive.js create mode 100644 awx/ui/client/lib/components/output/stdout.partial.html create mode 100644 awx/ui/client/lib/models/JobEvent.js delete mode 100644 awx/ui/client/lib/models/Jobs.js diff --git a/awx/ui/client/features/_index.less b/awx/ui/client/features/_index.less index 4a69120ed9..59e8e4630b 100644 --- a/awx/ui/client/features/_index.less +++ b/awx/ui/client/features/_index.less @@ -1,3 +1,3 @@ @import 'credentials/_index'; -@import 'jobs/_index'; +@import 'output/_index'; @import 'users/tokens/_index'; diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 3139c3a6f8..238adc11f4 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -3,6 +3,7 @@ import hasAnsi from 'has-ansi'; let vm; let ansi; +let jobEvent; let $timeout; let $sce; let $compile; @@ -26,19 +27,19 @@ const TIME_EVENTS = [ EVENT_STATS_PLAY ]; -function JobsIndexController (job, _$sce_, _$timeout_, _$scope_, _$compile_) { +function JobsIndexController (job, JobEventModel, _$sce_, _$timeout_, _$scope_, _$compile_) { $timeout = _$timeout_; $sce = _$sce_; $compile = _$compile_; $scope = _$scope_; + ansi = new Ansi(); + jobEvent = new JobEventModel(); const events = job.get('related.job_events.results'); const html = $sce.trustAsHtml(parseEvents(events)); - vm = this || {}; - - $scope.ns = 'jobs'; + vm = this || {}; $scope.ns = 'jobs'; $scope.jobs = { modal: {} }; @@ -256,7 +257,17 @@ function getTime (created) { } function showHostDetails (id) { - $scope.jobs.modal.show('title', `test${id}`); + jobEvent.request('get', id) + .then(() => { + const title = jobEvent.get('host_name'); + + vm.host = { + menu: true, + stdout: jobEvent.get('stdout') + }; + + $scope.jobs.modal.show(title); + }); } function toggle (uuid) { @@ -280,6 +291,6 @@ function toggle (uuid) { } } -JobsIndexController.$inject = ['job', '$sce', '$timeout', '$scope', '$compile']; +JobsIndexController.$inject = ['job', 'JobEventModel', '$sce', '$timeout', '$scope', '$compile']; module.exports = JobsIndexController; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index aa98a2359a..e989329f45 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -1,3 +1,8 @@ +import JobsStrings from '~features/output/jobs.strings'; +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'; @@ -24,7 +29,7 @@ function JobsRun ($stateExtender, strings) { } }, resolve: { - job: ['JobsModel', '$stateParams', (Jobs, $stateParams) => { + job: ['JobModel', '$stateParams', (Jobs, $stateParams) => { const { id } = $stateParams; return new Jobs('get', id) @@ -42,7 +47,10 @@ function JobsRun ($stateExtender, strings) { JobsRun.$inject = ['$stateExtender', 'JobsStrings']; angular - .module(MODULE_NAME, []) + .module(MODULE_NAME, [ + atLibModels, + atLibComponents + ]) .controller('indexController', IndexController) .service('JobsStrings', JobsStrings) .run(JobsRun); diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index ba1256c5cc..0d92977ee5 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -1,7 +1,7 @@
-

left

+

@@ -31,5 +31,8 @@
- + +
+ +
diff --git a/awx/ui/client/lib/components/index.js b/awx/ui/client/lib/components/index.js index 9ac933628c..339747ad67 100644 --- a/awx/ui/client/lib/components/index.js +++ b/awx/ui/client/lib/components/index.js @@ -20,6 +20,7 @@ import launchTemplate from '~components/launchTemplateButton/launchTemplateButto import layout from '~components/layout/layout.directive'; import list from '~components/list/list.directive'; import modal from '~components/modal/modal.directive'; +import outputStdout from '~components/output/stdout.directive'; import panel from '~components/panel/panel.directive'; import panelBody from '~components/panel/body.directive'; import panelHeading from '~components/panel/heading.directive'; @@ -68,6 +69,7 @@ angular .directive('atRowItem', rowItem) .directive('atRowAction', rowAction) .directive('atModal', modal) + .directive('atOutputStdout', outputStdout) .directive('atPanel', panel) .directive('atPanelBody', panelBody) .directive('atPanelHeading', panelHeading) diff --git a/awx/ui/client/lib/components/output/stdout.directive.js b/awx/ui/client/lib/components/output/stdout.directive.js new file mode 100644 index 0000000000..880e2c6eb6 --- /dev/null +++ b/awx/ui/client/lib/components/output/stdout.directive.js @@ -0,0 +1,105 @@ +import Ansi from 'ansi-to-html'; +import hasAnsi from 'has-ansi'; + +const templateUrl = require('~components/output/stdout.partial.html'); + +let $sce; +let $timeout; +let ansi; + +function atOutputStdoutLink (scope, element, attrs, controller) { + controller.init(scope, element); +} + +function AtOutputStdoutController (_$sce_, _$timeout_) { + const vm = this || {}; + + $timeout = _$timeout_; + $sce = _$sce_; + ansi = new Ansi(); + + let scope; + let element; + + vm.init = (_scope_, _element_) => { + scope = _scope_; + element = _element_; + + scope.$watch('state.stdout', curr => { + if (!curr) { + return; + } + + render(scope.state.stdout); + }); + }; + + vm.scroll = position => { + const container = element.find('.at-Stdout-container')[0]; + + if (position === 'bottom') { + container.scrollTop = container.scrollHeight; + } else { + container.scrollTop = 0; + } + }; +} + +AtOutputStdoutController.$inject = [ + '$sce', + '$timeout', +]; + +function render (stdout) { + console.log('render'); + const html = $sce.trustAsHtml(parseStdout(stdout)); + + $timeout(() => { + const table = $('#atStdoutTBody'); + + table.html($sce.getTrustedHtml(html)); + }); +} + +function parseStdout (stdout) { + const lines = stdout.split('\r\n'); + + let ln = 0; + + return lines.reduce((html, line) => { + ln++; + + return `${html}${createRow(ln, line)}`; + }, ''); +} + +function createRow (ln, content) { + content = content || ''; + + if (hasAnsi(content)) { + content = ansi.toHtml(content); + } + + return ` + + ${ln} + ${content} + `; +} +function atOutputStdout () { + return { + restrict: 'E', + transclude: true, + replace: true, + require: 'atOutputStdout', + templateUrl, + controller: AtOutputStdoutController, + controllerAs: 'vm', + link: atOutputStdoutLink, + scope: { + state: '=', + } + }; +} + +export default atOutputStdout; diff --git a/awx/ui/client/lib/components/output/stdout.partial.html b/awx/ui/client/lib/components/output/stdout.partial.html new file mode 100644 index 0000000000..ca38d736f4 --- /dev/null +++ b/awx/ui/client/lib/components/output/stdout.partial.html @@ -0,0 +1,19 @@ +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
diff --git a/awx/ui/client/lib/models/Job.js b/awx/ui/client/lib/models/Job.js index 7d87f82330..ef80c5dafb 100644 --- a/awx/ui/client/lib/models/Job.js +++ b/awx/ui/client/lib/models/Job.js @@ -1,5 +1,5 @@ -let Base; let $http; +let BaseModel; function getRelaunch (params) { const req = { @@ -24,7 +24,7 @@ function postRelaunch (params) { } function JobModel (method, resource, config) { - Base.call(this, 'jobs'); + BaseModel.call(this, 'jobs'); this.Constructor = JobModel; this.postRelaunch = postRelaunch.bind(this); @@ -33,8 +33,8 @@ function JobModel (method, resource, config) { return this.create(method, resource, config); } -function JobModelLoader (BaseModel, _$http_) { - Base = BaseModel; +function JobModelLoader (_BaseModel_, _$http_) { + BaseModel = _BaseModel_; $http = _$http_; return JobModel; diff --git a/awx/ui/client/lib/models/JobEvent.js b/awx/ui/client/lib/models/JobEvent.js new file mode 100644 index 0000000000..1c71ba9c54 --- /dev/null +++ b/awx/ui/client/lib/models/JobEvent.js @@ -0,0 +1,19 @@ +let BaseModel; + +function JobEventModel (method, resource, config) { + BaseModel.call(this, 'job_events'); + + this.Constructor = JobEventModel; + + return this.create(method, resource, config); +} + +function JobEventModelLoader (_BaseModel_) { + BaseModel = _BaseModel_; + + return JobEventModel; +} + +JobEventModel.$inject = ['BaseModel']; + +export default JobEventModelLoader; diff --git a/awx/ui/client/lib/models/Jobs.js b/awx/ui/client/lib/models/Jobs.js deleted file mode 100644 index e82b3e04af..0000000000 --- a/awx/ui/client/lib/models/Jobs.js +++ /dev/null @@ -1,19 +0,0 @@ -let BaseModel; - -function JobsModel (method, resource, config) { - BaseModel.call(this, 'jobs'); - - this.Constructor = JobsModel; - - return this.create(method, resource, config); -} - -function JobsModelLoader (_BaseModel_) { - BaseModel = _BaseModel_; - - return JobsModel; -} - -JobsModelLoader.$inject = ['BaseModel']; - -export default JobsModelLoader; diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index 0e5ab2c6b3..91eb3742ee 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -12,10 +12,10 @@ import Inventory from '~models/Inventory'; import InventoryScript from '~models/InventoryScript'; import InventorySource from '~models/InventorySource'; import Job from '~models/Job'; +import JobEvent from '~models/JobEvent'; import JobTemplate from '~models/JobTemplate'; import Jobs from '~models/Jobs'; import Me from '~models/Me'; -import ModelsStrings from '~models/models.strings'; import NotificationTemplate from '~models/NotificationTemplate'; import Organization from '~models/Organization'; import Project from '~models/Project'; @@ -26,6 +26,8 @@ import WorkflowJobTemplate from '~models/WorkflowJobTemplate'; import WorkflowJobTemplateNode from '~models/WorkflowJobTemplateNode'; import UnifiedJob from '~models/UnifiedJob'; +import ModelsStrings from '~models/models.strings'; + const MODULE_NAME = 'at.lib.models'; angular @@ -43,11 +45,11 @@ angular .service('InventoryModel', Inventory) .service('InventoryScriptModel', InventoryScript) .service('InventorySourceModel', InventorySource) + .service('JobEventModel', JobEvent) .service('JobModel', Job) .service('JobTemplateModel', JobTemplate) .service('JobsModel', Jobs) .service('MeModel', Me) - .service('ModelsStrings', ModelsStrings) .service('NotificationTemplate', NotificationTemplate) .service('OrganizationModel', Organization) .service('ProjectModel', Project) @@ -56,6 +58,8 @@ angular .service('UnifiedJobTemplateModel', UnifiedJobTemplate) .service('WorkflowJobModel', WorkflowJob) .service('WorkflowJobTemplateModel', WorkflowJobTemplate) - .service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode); + .service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode) + .service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode) + .service('ModelsStrings', ModelsStrings); export default MODULE_NAME; From 81dac1d1b89347a1861920a9f27c9813a4729ff7 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Wed, 13 Dec 2017 13:02:42 -0500 Subject: [PATCH 16/89] Update code/output components --- .../features/output/index.controller.js | 24 +--- awx/ui/client/features/output/index.view.html | 12 +- .../lib/components/code/events.directive.js | 104 ++++++++++++++++++ .../lib/components/code/events.partial.html | 0 .../components/code/menu-bottom.directive.js | 41 +++++++ .../components/code/menu-bottom.partial.html | 7 ++ .../lib/components/code/menu-top.directive.js | 52 +++++++++ .../lib/components/code/menu-top.partial.html | 12 ++ .../{output => code}/stdout.directive.js | 3 +- .../{output => code}/stdout.partial.html | 0 awx/ui/client/lib/components/index.js | 10 +- 11 files changed, 230 insertions(+), 35 deletions(-) create mode 100644 awx/ui/client/lib/components/code/events.directive.js create mode 100644 awx/ui/client/lib/components/code/events.partial.html create mode 100644 awx/ui/client/lib/components/code/menu-bottom.directive.js create mode 100644 awx/ui/client/lib/components/code/menu-bottom.partial.html create mode 100644 awx/ui/client/lib/components/code/menu-top.directive.js create mode 100644 awx/ui/client/lib/components/code/menu-top.partial.html rename awx/ui/client/lib/components/{output => code}/stdout.directive.js (95%) rename awx/ui/client/lib/components/{output => code}/stdout.partial.html (100%) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 238adc11f4..0b87c47552 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -48,13 +48,10 @@ function JobsIndexController (job, JobEventModel, _$sce_, _$timeout_, _$scope_, vm.showHostDetails = showHostDetails; vm.menu = { - expand: menuExpand, - scrollToBottom: menuScrollToBottom, - scrollToTop: menuScrollToTop - }; - - vm.state = { - expand: true + top: { + expand: menuExpand, + isExpanded: false + } }; $timeout(() => { @@ -66,22 +63,9 @@ function JobsIndexController (job, JobEventModel, _$sce_, _$timeout_, _$scope_, } function menuExpand () { - vm.state.expand = !vm.state.expand; vm.toggle(meta.parent); } -function menuScrollToBottom () { - const container = $('.at-Stdout-container')[0]; - - container.scrollTop = container.scrollHeight; -} - -function menuScrollToTop () { - const container = $('.at-Stdout-container')[0]; - - container.scrollTop = 0; -} - function parseEvents (events) { events.sort(orderByLineNumber); diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 0d92977ee5..7850adacb3 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -7,17 +7,7 @@
-
-
- -
-
- -
- -
-
+
 
diff --git a/awx/ui/client/lib/components/code/events.directive.js b/awx/ui/client/lib/components/code/events.directive.js new file mode 100644 index 0000000000..8426bf3ae6 --- /dev/null +++ b/awx/ui/client/lib/components/code/events.directive.js @@ -0,0 +1,104 @@ +import Ansi from 'ansi-to-html'; +import hasAnsi from 'has-ansi'; + +const templateUrl = require('~components/code/events.partial.html'); + +let $sce; +let $timeout; +let ansi; + +function atOutputEventLink (scope, element, attrs, controller) { + controller.init(scope, element); +} + +function AtOutputEventController (_$sce_, _$timeout_) { + const vm = this || {}; + + $timeout = _$timeout_; + $sce = _$sce_; + ansi = new Ansi(); + + let scope; + let element; + + vm.init = (_scope_, _element_) => { + scope = _scope_; + element = _element_; + + scope.$watch('state.stdout', curr => { + if (!curr) { + return; + } + + render(scope.state.stdout); + }); + }; + + vm.scroll = position => { + const container = element.find('.at-Stdout-container')[0]; + + if (position === 'bottom') { + container.scrollTop = container.scrollHeight; + } else { + container.scrollTop = 0; + } + }; +} + +AtOutputEventController.$inject = [ + '$sce', + '$timeout', +]; + +function render (stdout) { + const html = $sce.trustAsHtml(parseStdout(stdout)); + + $timeout(() => { + const table = $('#atStdoutTBody'); + + table.html($sce.getTrustedHtml(html)); + }); +} + +function parseStdout (stdout) { + const lines = stdout.split('\r\n'); + + let ln = 0; + + return lines.reduce((html, line) => { + ln++; + + return `${html}${createRow(ln, line)}`; + }, ''); +} + +function createRow (ln, content) { + content = content || ''; + + if (hasAnsi(content)) { + content = ansi.toHtml(content); + } + + return ` + + ${ln} + ${content} + `; +} +function atOutputEvent () { + return { + restrict: 'E', + transclude: true, + replace: true, + require: 'atOutputEvent', + templateUrl, + controller: AtOutputEventController, + controllerAs: 'vm', + link: atOutputEventLink, + scope: { + state: '=', + } + }; +} + +export default atOutputEvent; diff --git a/awx/ui/client/lib/components/code/events.partial.html b/awx/ui/client/lib/components/code/events.partial.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/awx/ui/client/lib/components/code/menu-bottom.directive.js b/awx/ui/client/lib/components/code/menu-bottom.directive.js new file mode 100644 index 0000000000..5b15a7bc10 --- /dev/null +++ b/awx/ui/client/lib/components/code/menu-bottom.directive.js @@ -0,0 +1,41 @@ +const templateUrl = require('~components/code/menu-bottom.partial.html'); + +function atCodeMenuBottomLink (scope, element, attrs, controller) { + controller.init(scope, element); +} + +function AtCodeMenuBottomController () { + const vm = this || {}; + + let element; + + vm.init = (_scope_, _element_) => { + element = _element_; + }; + + vm.scroll = () => { + const container = element.find('.at-Stdout-container')[0]; + + container.scrollTop = container.scrollHeight; + }; +} + +AtCodeMenuBottomController.$inject = []; + +function atCodeMenuBottom () { + return { + restrict: 'E', + transclude: true, + replace: true, + require: 'atCodeMenuBottom', + templateUrl, + controller: AtCodeMenuBottomController, + controllerAs: 'vm', + link: atCodeMenuBottomLink, + scope: { + state: '=', + } + }; +} + +export default atCodeMenuBottom; diff --git a/awx/ui/client/lib/components/code/menu-bottom.partial.html b/awx/ui/client/lib/components/code/menu-bottom.partial.html new file mode 100644 index 0000000000..1a0717a360 --- /dev/null +++ b/awx/ui/client/lib/components/code/menu-bottom.partial.html @@ -0,0 +1,7 @@ +
+
+ +
+ +
+
diff --git a/awx/ui/client/lib/components/code/menu-top.directive.js b/awx/ui/client/lib/components/code/menu-top.directive.js new file mode 100644 index 0000000000..824e24123f --- /dev/null +++ b/awx/ui/client/lib/components/code/menu-top.directive.js @@ -0,0 +1,52 @@ +const templateUrl = require('~components/code/menu-top.partial.html'); + +function atCodeMenuTopLink (scope, element, attrs, controller) { + controller.init(scope, element); +} + +function AtCodeMenuTopController () { + const vm = this || {}; + + let element; + let scope; + + vm.init = (_scope_, _element_) => { + scope = _scope_; + element = _element_; + + scope.state.isExpanded = scope.state.isExpanded || false; + }; + + vm.scroll = () => { + const container = element.parent().find('.at-Stdout-container')[0]; + + console.log(container); + + container.scrollTop = 0; + }; + + vm.expand = () => { + scope.state.isExpanded = !scope.state.isExpanded; + scope.state.expand(); + }; +} + +AtCodeMenuTopController.$inject = []; + +function atCodeMenuTop () { + return { + restrict: 'E', + transclude: true, + replace: true, + require: 'atCodeMenuTop', + templateUrl, + controller: AtCodeMenuTopController, + controllerAs: 'vm', + link: atCodeMenuTopLink, + scope: { + state: '=', + } + }; +} + +export default atCodeMenuTop; diff --git a/awx/ui/client/lib/components/code/menu-top.partial.html b/awx/ui/client/lib/components/code/menu-top.partial.html new file mode 100644 index 0000000000..2e21dc1b8b --- /dev/null +++ b/awx/ui/client/lib/components/code/menu-top.partial.html @@ -0,0 +1,12 @@ +
+
+ +
+ +
+ +
+ +
+
diff --git a/awx/ui/client/lib/components/output/stdout.directive.js b/awx/ui/client/lib/components/code/stdout.directive.js similarity index 95% rename from awx/ui/client/lib/components/output/stdout.directive.js rename to awx/ui/client/lib/components/code/stdout.directive.js index 880e2c6eb6..1f32b5ca7f 100644 --- a/awx/ui/client/lib/components/output/stdout.directive.js +++ b/awx/ui/client/lib/components/code/stdout.directive.js @@ -1,7 +1,7 @@ import Ansi from 'ansi-to-html'; import hasAnsi from 'has-ansi'; -const templateUrl = require('~components/output/stdout.partial.html'); +const templateUrl = require('~components/code/stdout.partial.html'); let $sce; let $timeout; @@ -51,7 +51,6 @@ AtOutputStdoutController.$inject = [ ]; function render (stdout) { - console.log('render'); const html = $sce.trustAsHtml(parseStdout(stdout)); $timeout(() => { diff --git a/awx/ui/client/lib/components/output/stdout.partial.html b/awx/ui/client/lib/components/code/stdout.partial.html similarity index 100% rename from awx/ui/client/lib/components/output/stdout.partial.html rename to awx/ui/client/lib/components/code/stdout.partial.html diff --git a/awx/ui/client/lib/components/index.js b/awx/ui/client/lib/components/index.js index 339747ad67..bf45c63a6d 100644 --- a/awx/ui/client/lib/components/index.js +++ b/awx/ui/client/lib/components/index.js @@ -1,6 +1,10 @@ import atLibServices from '~services'; import actionGroup from '~components/action/action-group.directive'; +import codeMenuBottom from '~components/code/menu-bottom.directive'; +import codeMenuTop from '~components/code/menu-top.directive'; +import codeEvents from '~components/code/events.directive'; +import codeStdout from '~components/code/stdout.directive'; import divider from '~components/utility/divider.directive'; import form from '~components/form/form.directive'; import formAction from '~components/form/action.directive'; @@ -20,7 +24,6 @@ import launchTemplate from '~components/launchTemplateButton/launchTemplateButto import layout from '~components/layout/layout.directive'; import list from '~components/list/list.directive'; import modal from '~components/modal/modal.directive'; -import outputStdout from '~components/output/stdout.directive'; import panel from '~components/panel/panel.directive'; import panelBody from '~components/panel/body.directive'; import panelHeading from '~components/panel/heading.directive'; @@ -46,6 +49,10 @@ angular atLibServices ]) .directive('atActionGroup', actionGroup) + .directive('atCodeEvents', codeEvents) + .directive('atCodeMenuBottom', codeMenuBottom) + .directive('atCodeMenuTop', codeMenuTop) + .directive('atCodeStdout', codeStdout) .directive('atDivider', divider) .directive('atForm', form) .directive('atFormAction', formAction) @@ -69,7 +76,6 @@ angular .directive('atRowItem', rowItem) .directive('atRowAction', rowAction) .directive('atModal', modal) - .directive('atOutputStdout', outputStdout) .directive('atPanel', panel) .directive('atPanelBody', panelBody) .directive('atPanelHeading', panelHeading) From e26c977b36586d9806c079121aadf1501f42973b Mon Sep 17 00:00:00 2001 From: gconsidine Date: Thu, 4 Jan 2018 11:06:54 -0500 Subject: [PATCH 17/89] Add partial implementation of model.next --- .../features/output/index.controller.js | 33 ++++++++++++++++--- awx/ui/client/features/output/index.js | 2 +- awx/ui/client/features/output/index.view.html | 21 ++++++++++-- .../lib/components/code/menu-top.directive.js | 2 -- .../lib/components/code/stdout.directive.js | 17 +++++----- awx/ui/client/lib/models/Base.js | 19 +++++++++++ 6 files changed, 76 insertions(+), 18 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 0b87c47552..c94660d298 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -3,6 +3,7 @@ import hasAnsi from 'has-ansi'; let vm; let ansi; +let job; let jobEvent; let $timeout; let $sce; @@ -27,11 +28,12 @@ const TIME_EVENTS = [ EVENT_STATS_PLAY ]; -function JobsIndexController (job, JobEventModel, _$sce_, _$timeout_, _$scope_, _$compile_) { +function JobsIndexController (_job_, JobEventModel, _$sce_, _$timeout_, _$scope_, _$compile_) { $timeout = _$timeout_; $sce = _$sce_; $compile = _$compile_; $scope = _$scope_; + job = _job_; ansi = new Ansi(); jobEvent = new JobEventModel(); @@ -39,7 +41,9 @@ function JobsIndexController (job, JobEventModel, _$sce_, _$timeout_, _$scope_, const events = job.get('related.job_events.results'); const html = $sce.trustAsHtml(parseEvents(events)); - vm = this || {}; $scope.ns = 'jobs'; + vm = this || {}; + + $scope.ns = 'jobs'; $scope.jobs = { modal: {} }; @@ -48,9 +52,13 @@ function JobsIndexController (job, JobEventModel, _$sce_, _$timeout_, _$scope_, vm.showHostDetails = showHostDetails; vm.menu = { + scroll, top: { - expand: menuExpand, + expand, isExpanded: false + }, + bottom: { + next } }; @@ -62,10 +70,27 @@ function JobsIndexController (job, JobEventModel, _$sce_, _$timeout_, _$scope_, }); } -function menuExpand () { +function next () { + job.next('job_events') + .then(data => { + console.log(data); + }); +} + +function expand () { vm.toggle(meta.parent); } +function scroll (direction) { + const container = $('.at-Stdout-container')[0]; + + if (direction === 'top') { + container.scrollTop = 0; + } else { + container.scrollTop = container.scrollHeight; + } +} + function parseEvents (events) { events.sort(orderByLineNumber); diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index e989329f45..6ff616c6ba 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -35,7 +35,7 @@ function JobsRun ($stateExtender, strings) { return new Jobs('get', id) .then(job => job.extend('job_events', { params: { - page_size: 10000, + page_size: 10, order_by: 'start_line' } })); diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 7850adacb3..89862f9cba 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -7,12 +7,27 @@
- +
+
+ +
+ +
+ +
+ +
+
 
-
+
+ +
+ +
@@ -23,6 +38,6 @@
- +
diff --git a/awx/ui/client/lib/components/code/menu-top.directive.js b/awx/ui/client/lib/components/code/menu-top.directive.js index 824e24123f..e78ead1bae 100644 --- a/awx/ui/client/lib/components/code/menu-top.directive.js +++ b/awx/ui/client/lib/components/code/menu-top.directive.js @@ -20,8 +20,6 @@ function AtCodeMenuTopController () { vm.scroll = () => { const container = element.parent().find('.at-Stdout-container')[0]; - console.log(container); - container.scrollTop = 0; }; diff --git a/awx/ui/client/lib/components/code/stdout.directive.js b/awx/ui/client/lib/components/code/stdout.directive.js index 1f32b5ca7f..c73e90b7e8 100644 --- a/awx/ui/client/lib/components/code/stdout.directive.js +++ b/awx/ui/client/lib/components/code/stdout.directive.js @@ -7,11 +7,11 @@ let $sce; let $timeout; let ansi; -function atOutputStdoutLink (scope, element, attrs, controller) { +function atCodeStdoutLink (scope, element, attrs, controller) { controller.init(scope, element); } -function AtOutputStdoutController (_$sce_, _$timeout_) { +function AtCodeStdoutController (_$sce_, _$timeout_) { const vm = this || {}; $timeout = _$timeout_; @@ -45,7 +45,7 @@ function AtOutputStdoutController (_$sce_, _$timeout_) { }; } -AtOutputStdoutController.$inject = [ +AtCodeStdoutController.$inject = [ '$sce', '$timeout', ]; @@ -85,20 +85,21 @@ function createRow (ln, content) { ${content} `; } -function atOutputStdout () { + +function atCodeStdout () { return { restrict: 'E', transclude: true, replace: true, - require: 'atOutputStdout', + require: 'atCodeStdout', templateUrl, - controller: AtOutputStdoutController, + controller: AtCodeStdoutController, controllerAs: 'vm', - link: atOutputStdoutLink, + link: atCodeStdoutLink, scope: { state: '=', } }; } -export default atOutputStdout; +export default atCodeStdout; diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index ee63b97076..cfd84c6a1f 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -346,6 +346,24 @@ function extend (related, config) { return Promise.reject(new Error(`No related property, ${related}, exists`)); } +function next (related) { + related = related || this.resource; + + if (!this.has(`related.${related}.next`)) { + return Promise.resolve(null); + } + + const req = { + method: 'GET', + url: this.get(`related.${related}.next`) + }; + + return $http(req) + .then(({ data }) => { + console.log(data); + }); +} + function normalizePath (resource) { const version = '/api/v2/'; @@ -523,6 +541,7 @@ function BaseModel (resource, settings) { this.isCacheable = isCacheable; this.isCreatable = isCreatable; this.match = match; + this.next = next; this.normalizePath = normalizePath; this.options = options; this.parseRequestConfig = parseRequestConfig; From 3006caffe17fd816d5435c59a8de7e2076e1390d Mon Sep 17 00:00:00 2001 From: gconsidine Date: Fri, 5 Jan 2018 15:14:19 -0500 Subject: [PATCH 18/89] Update style to be inline with mockups --- awx/ui/client/features/jobs/_index.less | 50 ++++++++--- .../features/output/index.controller.js | 86 +++++++++++++++---- awx/ui/client/features/output/index.js | 2 +- awx/ui/client/features/output/index.view.html | 13 ++- awx/ui/client/lib/models/Base.js | 43 ++++++++-- 5 files changed, 150 insertions(+), 44 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index 78e1de6b33..1ebec50483 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -11,10 +11,28 @@ &-menuBottom { color: @at-gray-dark-4x; - border: 1px solid @at-gray-dark-2x; - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; - border-top: none; + font-size: 10px; + text-transform: uppercase; + font-weight: bold; + position: absolute; + right: 60px; + bottom: 24px; + cursor: pointer; + } + + &-menuIconGroup { + & > p { + margin: 0; + } + + & > p:first-child { + font-size: 20px; + margin-right: 8px; + } + + & > p:last-child { + margin-top: 9px; + } } &-menuIcon { @@ -26,25 +44,24 @@ &-toggle { color: @at-gray-dark-4x; background-color: @at-gray-light; - font-size: 12px; + font-size: 18px; + line-height: 12px; & > i { cursor: pointer; } - padding: 0 20px 0 10px; + padding: 0 10px 0 10px; user-select: none; } &-line { - color: @at-gray-dark-4x; + color: @at-gray-dark-6x; background-color: @at-gray-light; - text-align: right; - vertical-align: top; padding-right: 5px; - border-right: 1px solid @at-gray-dark; + border-right: 1px solid @at-gray-dark-2x; user-select: none; } @@ -64,16 +81,25 @@ text-align: right; user-select: none; width: 11ch; + + & > span { + background-color: white; + border-radius: 4px; + padding: 1px 2px; + } } &-container { max-height: 80vh; - font-size: 14px; + font-size: 15px; border: 1px solid @at-gray-dark-2x; - background-color: @at-gray-light-3x; + background-color: @at-gray-light-2x; + color: @at-gray-dark-6x; padding: 0; margin: 0; border-radius: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; & > table { table-layout: fixed; diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index c94660d298..0926846e42 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -5,13 +5,16 @@ let vm; let ansi; let job; let jobEvent; +let container; let $timeout; let $sce; let $compile; let $scope; const record = {}; -const meta = {}; +const meta = { + scroll: {} +}; const EVENT_START_TASK = 'playbook_on_task_start'; const EVENT_START_PLAY = 'playbook_on_play_start'; @@ -52,10 +55,13 @@ function JobsIndexController (_job_, JobEventModel, _$sce_, _$timeout_, _$scope_ vm.showHostDetails = showHostDetails; vm.menu = { - scroll, + scroll: { + display: false, + to: scrollTo + }, top: { expand, - isExpanded: false + isExpanded: true }, bottom: { next @@ -64,9 +70,15 @@ function JobsIndexController (_job_, JobEventModel, _$sce_, _$timeout_, _$scope_ $timeout(() => { const table = $('#result-table'); + container = $('.at-Stdout-container'); table.html($sce.getTrustedHtml(html)); $compile(table.contents())($scope); + + meta.scroll.height = container[0].scrollHeight; + meta.scroll.buffer = 100; + + container.scroll(onScroll); }); } @@ -78,16 +90,14 @@ function next () { } function expand () { - vm.toggle(meta.parent); + vm.toggle(meta.parent, true); } -function scroll (direction) { - const container = $('.at-Stdout-container')[0]; - +function scrollTo (direction) { if (direction === 'top') { - container.scrollTop = 0; + container[0].scrollTop = 0; } else { - container.scrollTop = container.scrollHeight; + container[0].scrollTop = container[0].scrollHeight; } } @@ -222,7 +232,7 @@ function createRow (current, ln, content) { if (current) { if (current.isParent && current.line === ln) { id = current.uuid; - tdToggle = ``; + tdToggle = ``; } if (current.isHost) { @@ -230,7 +240,7 @@ function createRow (current, ln, content) { } if (current.time && current.line === ln) { - timestamp = current.time; + timestamp = `${current.time}`; } if (current.parents) { @@ -261,8 +271,11 @@ function createRow (current, ln, content) { function getTime (created) { const date = new Date(created); + const hour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours(); + const minute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes(); + const second = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds(); - return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; + return `${hour}:${minute}:${second}`; } function showHostDetails (id) { @@ -279,27 +292,64 @@ function showHostDetails (id) { }); } -function toggle (uuid) { +function toggle (uuid, menu) { const lines = $(`.child-of-${uuid}`); let icon = $(`#${uuid} .at-Stdout-toggle > i`); + if (menu || record[uuid].level === 1) { + vm.menu.top.isExpanded = !vm.menu.top.isExpanded; + } + if (record[uuid].children) { icon = icon.add($(`#${record[uuid].children.join(', #')}`).find('.at-Stdout-toggle > i')); } - if (icon.hasClass('fa-chevron-down')) { - icon.addClass('fa-chevron-right'); - icon.removeClass('fa-chevron-down'); + if (icon.hasClass('fa-angle-down')) { + icon.addClass('fa-angle-right'); + icon.removeClass('fa-angle-down'); lines.addClass('hidden'); } else { - icon.addClass('fa-chevron-down'); - icon.removeClass('fa-chevron-right'); + icon.addClass('fa-angle-down'); + icon.removeClass('fa-angle-right'); lines.removeClass('hidden'); } } +function onScroll () { + if (meta.scroll.inProgress) { + return; + } + + meta.scroll.inProgress = true; + + $timeout(() => { + const top = container[0].scrollTop; + const bottom = top + meta.scroll.buffer + container[0].offsetHeight; + + meta.scroll.inProgress = false; + + if (top === 0) { + vm.menu.scroll.display = false; + + return; + } + + vm.menu.scroll.display = true; + + if (bottom >= meta.scroll.height) { + // fetch more lines + } + }, 500); +} + +/* + *function approximateLineNumber () { + * + *} + */ + JobsIndexController.$inject = ['job', 'JobEventModel', '$sce', '$timeout', '$scope', '$compile']; module.exports = JobsIndexController; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 6ff616c6ba..62e7309115 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -35,7 +35,7 @@ function JobsRun ($stateExtender, strings) { return new Jobs('get', id) .then(job => job.extend('job_events', { params: { - page_size: 10, + page_size: 1000, order_by: 'start_line' } })); diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 89862f9cba..e45303ccec 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -13,7 +13,7 @@ ng-class="{ 'fa-minus': vm.menu.top.isExpanded, 'fa-plus': !vm.menu.top.isExpanded }">
-
+
@@ -22,13 +22,10 @@
 
-
-
- -
- -
- +
+
+

+

Back to Top

diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index cfd84c6a1f..0efa02b329 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -346,21 +346,53 @@ function extend (related, config) { return Promise.reject(new Error(`No related property, ${related}, exists`)); } -function next (related) { - related = related || this.resource; +function next (related, config = {}) { + const url = this.get(`related.${related}.next`); - if (!this.has(`related.${related}.next`)) { + if (!url) { return Promise.resolve(null); } const req = { method: 'GET', - url: this.get(`related.${related}.next`) + url }; return $http(req) .then(({ data }) => { - console.log(data); + const results = this.get(`related.${related}.results`) || []; + + data.results = results.concat(data.results); + this.set('get', `related.${related}`, data); + + if (config.limit < results.length) { + console.log(results); + } + }); +} + +function prev (related, config = {}) { + const url = this.get(`related.${related}.previous`); + + if (!url) { + return Promise.resolve(null); + } + + const req = { + method: 'GET', + url + }; + + return $http(req) + .then(({ data }) => { + const results = this.get(`related.${related}.results`) || []; + + data.results = data.results.concat(results); + this.set('get', `related.${related}`, data); + + if (config.limit < results.length) { + console.log(results); + } }); } @@ -545,6 +577,7 @@ function BaseModel (resource, settings) { this.normalizePath = normalizePath; this.options = options; this.parseRequestConfig = parseRequestConfig; + this.prev = prev; this.request = request; this.requestWithCache = requestWithCache; this.search = search; From b88ad50a75a3f65dd8a4563567a48fd96281b458 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Tue, 9 Jan 2018 09:15:54 -0500 Subject: [PATCH 19/89] Update style of stdout container --- awx/ui/client/features/jobs/_index.less | 3 ++- awx/ui/client/features/output/index.controller.js | 14 ++++++++++---- awx/ui/client/features/output/index.js | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index 1ebec50483..66725e86eb 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -90,7 +90,8 @@ } &-container { - max-height: 80vh; + height: calc(~"100vh - 240px"); + overflow-y: scroll; font-size: 15px; border: 1px solid @at-gray-dark-2x; background-color: @at-gray-light-2x; diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 0926846e42..b00404b398 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -77,6 +77,9 @@ function JobsIndexController (_job_, JobEventModel, _$sce_, _$timeout_, _$scope_ meta.scroll.height = container[0].scrollHeight; meta.scroll.buffer = 100; + meta.next = job.get('related.job_events.next'); + meta.prev = job.get('related.job_events.previous'); + meta.cursor = job.get('related.job_events.results').length - 1; container.scroll(onScroll); }); @@ -84,8 +87,11 @@ function JobsIndexController (_job_, JobEventModel, _$sce_, _$timeout_, _$scope_ function next () { job.next('job_events') - .then(data => { - console.log(data); + .then(() => { + meta.next = job.get('related.job_events.next'); + meta.prev = job.get('related.job_events.previous'); + + console.log(job.get('related.job_events.results')); }); } @@ -338,8 +344,8 @@ function onScroll () { vm.menu.scroll.display = true; - if (bottom >= meta.scroll.height) { - // fetch more lines + if (bottom >= meta.scroll.height && meta.next) { + next(); } }, 500); } diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 62e7309115..630d84a442 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -35,7 +35,7 @@ function JobsRun ($stateExtender, strings) { return new Jobs('get', id) .then(job => job.extend('job_events', { params: { - page_size: 1000, + page_size: 13, order_by: 'start_line' } })); From 5a75059c865b4bda8c8e1c18cc25ebf42234b2bb Mon Sep 17 00:00:00 2001 From: gconsidine Date: Tue, 9 Jan 2018 16:01:20 -0500 Subject: [PATCH 20/89] Add load on scroll and max results to base model --- .../features/output/index.controller.js | 27 ++++++++------- awx/ui/client/lib/models/Base.js | 34 +++++++++++++++---- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index b00404b398..6aae149d69 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -79,22 +79,31 @@ function JobsIndexController (_job_, JobEventModel, _$sce_, _$timeout_, _$scope_ meta.scroll.buffer = 100; meta.next = job.get('related.job_events.next'); meta.prev = job.get('related.job_events.previous'); - meta.cursor = job.get('related.job_events.results').length - 1; + meta.cursor = job.get('related.job_events.results').length; container.scroll(onScroll); }); } function next () { - job.next('job_events') + job.next({ related: 'job_events', limit: 5 }) .then(() => { meta.next = job.get('related.job_events.next'); meta.prev = job.get('related.job_events.previous'); - console.log(job.get('related.job_events.results')); + append(); }); } +function append () { + const events = job.get('related.job_events.results').slice(meta.cursor); + const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parseEvents(events)))); + const table = $('#result-table'); + + table.append(rows); + $compile(rows.contents())($scope); +} + function expand () { vm.toggle(meta.parent, true); } @@ -141,13 +150,13 @@ function parseLine (event) { ln++; const isLastLine = i === lines.length - 1; - let append = createRow(current, ln, line); + let row = createRow(current, ln, line); if (current && current.isTruncated && isLastLine) { - append += createRow(current); + row += createRow(current); } - return `${html}${append}`; + return `${html}${row}`; }, ''); } @@ -350,12 +359,6 @@ function onScroll () { }, 500); } -/* - *function approximateLineNumber () { - * - *} - */ - JobsIndexController.$inject = ['job', 'JobEventModel', '$sce', '$timeout', '$scope', '$compile']; module.exports = JobsIndexController; diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index 0efa02b329..20746ed8aa 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -104,6 +104,10 @@ function httpGet (config = {}) { if (config.params) { req.params = config.params; + + if (config.params.page_size) { + this.pageSize = config.params.page_size; + } } if (typeof config.resource === 'object') { @@ -330,6 +334,10 @@ function has (method, keys) { function extend (related, config) { const req = this.parseRequestConfig('GET', config); + if (config.params.page_size) { + this.pageSize = config.params.page_size; + } + if (this.has(req.method, `related.${related}`)) { req.url = this.get(`related.${related}`); @@ -346,8 +354,17 @@ function extend (related, config) { return Promise.reject(new Error(`No related property, ${related}, exists`)); } -function next (related, config = {}) { - const url = this.get(`related.${related}.next`); +function next (config = {}) { + let url; + let results; + + if (config.related) { + url = this.get(`related.${config.related}.next`); + results = this.get(`related.${config.related}.results`) || []; + } else { + url = this.get('next'); + results = this.get('results'); + } if (!url) { return Promise.resolve(null); @@ -360,13 +377,18 @@ function next (related, config = {}) { return $http(req) .then(({ data }) => { - const results = this.get(`related.${related}.results`) || []; + results = results || []; data.results = results.concat(data.results); - this.set('get', `related.${related}`, data); - if (config.limit < results.length) { - console.log(results); + if ((config.limit * this.pageSize) < data.results.length) { + data.results.splice(-config.limit * this.pageSize); + } + + if (config.related) { + this.set('get', `related.${config.related}`, data); + } else { + this.set('get', data); } }); } From c08538b8f09cbd1db6c4462f524d041680eb77f6 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Wed, 10 Jan 2018 13:29:32 -0500 Subject: [PATCH 21/89] Fix model pagination behavior, limit, and cache --- .../features/output/index.controller.js | 23 ++++------ awx/ui/client/features/output/index.js | 6 ++- awx/ui/client/lib/models/Base.js | 45 +++++++------------ 3 files changed, 29 insertions(+), 45 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 6aae149d69..380aed1a95 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -16,6 +16,7 @@ const meta = { scroll: {} }; +const SCROLL_BUFFER = 250; const EVENT_START_TASK = 'playbook_on_task_start'; const EVENT_START_PLAY = 'playbook_on_play_start'; const EVENT_STATS_PLAY = 'playbook_on_stats'; @@ -75,8 +76,6 @@ function JobsIndexController (_job_, JobEventModel, _$sce_, _$timeout_, _$scope_ table.html($sce.getTrustedHtml(html)); $compile(table.contents())($scope); - meta.scroll.height = container[0].scrollHeight; - meta.scroll.buffer = 100; meta.next = job.get('related.job_events.next'); meta.prev = job.get('related.job_events.previous'); meta.cursor = job.get('related.job_events.results').length; @@ -86,17 +85,17 @@ function JobsIndexController (_job_, JobEventModel, _$sce_, _$timeout_, _$scope_ } function next () { - job.next({ related: 'job_events', limit: 5 }) - .then(() => { + job.next({ related: 'job_events' }) + .then(cursor => { meta.next = job.get('related.job_events.next'); meta.prev = job.get('related.job_events.previous'); - append(); + append(cursor); }); } -function append () { - const events = job.get('related.job_events.results').slice(meta.cursor); +function append (cursor) { + const events = job.get('related.job_events.results').slice(cursor); const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parseEvents(events)))); const table = $('#result-table'); @@ -176,10 +175,6 @@ function createRecord (ln, lines, event) { isHost: typeof event.host === 'number' }; - if (info.isHost) { - console.log(event); - } - if (event.parent_uuid) { info.parents = getParentEvents(event.parent_uuid); } @@ -341,7 +336,7 @@ function onScroll () { $timeout(() => { const top = container[0].scrollTop; - const bottom = top + meta.scroll.buffer + container[0].offsetHeight; + const bottom = top + SCROLL_BUFFER + container[0].offsetHeight; meta.scroll.inProgress = false; @@ -353,10 +348,10 @@ function onScroll () { vm.menu.scroll.display = true; - if (bottom >= meta.scroll.height && meta.next) { + if (bottom >= container[0].scrollHeight && meta.next) { next(); } - }, 500); + }, 250); } JobsIndexController.$inject = ['job', 'JobEventModel', '$sce', '$timeout', '$scope', '$compile']; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 630d84a442..b29adebb48 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -34,10 +34,12 @@ function JobsRun ($stateExtender, strings) { return new Jobs('get', id) .then(job => job.extend('job_events', { + pageCache: true, + pageLimit: 2, params: { - page_size: 13, + page_size: 25, order_by: 'start_line' - } + }, })); }] } diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index 20746ed8aa..ac0f44081f 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -107,6 +107,8 @@ function httpGet (config = {}) { if (config.params.page_size) { this.pageSize = config.params.page_size; + this.pageLimit = config.pageLimit || false; + this.pageCache = config.pageCache || false; } } @@ -336,6 +338,8 @@ function extend (related, config) { if (config.params.page_size) { this.pageSize = config.params.page_size; + this.pageLimit = config.pageLimit || false; + this.pageCache = config.pageCache || false; } if (this.has(req.method, `related.${related}`)) { @@ -377,12 +381,19 @@ function next (config = {}) { return $http(req) .then(({ data }) => { - results = results || []; + let cursor = 0; - data.results = results.concat(data.results); + if (this.pageCache) { + results = results || []; + data.results = results.concat(data.results); + cursor = results.length; - if ((config.limit * this.pageSize) < data.results.length) { - data.results.splice(-config.limit * this.pageSize); + if (this.pageLimit && this.pageLimit * this.pageSize < data.results.length) { + const removeCount = data.results.length - this.pageLimit * this.pageSize; + + data.results.splice(0, removeCount); + cursor -= removeCount; + } } if (config.related) { @@ -390,31 +401,8 @@ function next (config = {}) { } else { this.set('get', data); } - }); -} -function prev (related, config = {}) { - const url = this.get(`related.${related}.previous`); - - if (!url) { - return Promise.resolve(null); - } - - const req = { - method: 'GET', - url - }; - - return $http(req) - .then(({ data }) => { - const results = this.get(`related.${related}.results`) || []; - - data.results = data.results.concat(results); - this.set('get', `related.${related}`, data); - - if (config.limit < results.length) { - console.log(results); - } + return cursor <= 0 ? 0 : cursor; }); } @@ -599,7 +587,6 @@ function BaseModel (resource, settings) { this.normalizePath = normalizePath; this.options = options; this.parseRequestConfig = parseRequestConfig; - this.prev = prev; this.request = request; this.requestWithCache = requestWithCache; this.search = search; From fa59f46f2bae3a6306ffd64792985be237bb223c Mon Sep 17 00:00:00 2001 From: gconsidine Date: Mon, 15 Jan 2018 09:36:59 -0500 Subject: [PATCH 22/89] Update less variable names --- awx/ui/client/features/jobs/_index.less | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index 66725e86eb..356ad91076 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -2,15 +2,15 @@ font-family: monospace; &-menuTop { - color: @at-gray-dark-4x; - border: 1px solid @at-gray-dark-2x; + color: @at-gray-848992; + border: 1px solid @at-gray-f2; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom: none; } &-menuBottom { - color: @at-gray-dark-4x; + color: @at-gray-848992; font-size: 10px; text-transform: uppercase; font-weight: bold; @@ -42,8 +42,8 @@ } &-toggle { - color: @at-gray-dark-4x; - background-color: @at-gray-light; + color: @at-gray-848992; + background-color: @at-gray-eb; font-size: 18px; line-height: 12px; @@ -56,12 +56,12 @@ } &-line { - color: @at-gray-dark-6x; - background-color: @at-gray-light; + color: @at-gray-161b1f; + background-color: @at-gray-eb; text-align: right; vertical-align: top; padding-right: 5px; - border-right: 1px solid @at-gray-dark-2x; + border-right: 1px solid @at-gray-b7; user-select: none; } @@ -93,9 +93,9 @@ height: calc(~"100vh - 240px"); overflow-y: scroll; font-size: 15px; - border: 1px solid @at-gray-dark-2x; - background-color: @at-gray-light-2x; - color: @at-gray-dark-6x; + border: 1px solid @at-gray-b7; + background-color: @at-gray-f2; + color: @at-gray-161b1f; padding: 0; margin: 0; border-radius: 0; From ab8651eab645223ef840ce6dd48597586df681ec Mon Sep 17 00:00:00 2001 From: gconsidine Date: Wed, 17 Jan 2018 15:32:06 -0500 Subject: [PATCH 23/89] Add functions to calc number of rows in view --- awx/ui/client/features/jobs/_index.less | 2 +- .../features/output/index.controller.js | 63 +++++++++++++++++-- awx/ui/client/features/output/index.js | 2 +- awx/ui/client/features/output/index.view.html | 2 +- 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index 356ad91076..750491edff 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -3,7 +3,7 @@ &-menuTop { color: @at-gray-848992; - border: 1px solid @at-gray-f2; + border: 1px solid @at-gray-b7; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom: none; diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 380aed1a95..8b1f2b84f4 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -16,10 +16,14 @@ const meta = { scroll: {} }; +const ROW_LIMIT = 200; const SCROLL_BUFFER = 250; +const SCROLL_LOAD_DELAY = 100; const EVENT_START_TASK = 'playbook_on_task_start'; 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 EVENT_GROUPS = [ EVENT_START_TASK, @@ -70,8 +74,8 @@ function JobsIndexController (_job_, JobEventModel, _$sce_, _$timeout_, _$scope_ }; $timeout(() => { - const table = $('#result-table'); - container = $('.at-Stdout-container'); + const table = $(ELEMENT_TBODY); + container = $(ELEMENT_CONTAINER); table.html($sce.getTrustedHtml(html)); $compile(table.contents())($scope); @@ -90,14 +94,65 @@ function next () { meta.next = job.get('related.job_events.next'); meta.prev = job.get('related.job_events.previous'); + console.log(ROW_LIMIT); + console.log(getRowCount()); + console.log(getRowHeight()); + console.log(getRowsInView()); + console.log(getScrollPosition()); + + console.log('above:', getRowsAbove()); + console.log('below:', getRowsBelow()); append(cursor); }); } +function getRowCount () { + return $(ELEMENT_TBODY).children().length; +} + +function getRowHeight () { + return $(ELEMENT_TBODY).children()[0].offsetHeight; +} + +function getViewHeight () { + return $(ELEMENT_CONTAINER)[0].offsetHeight; +} + +function getScrollPosition () { + return $(ELEMENT_CONTAINER)[0].scrollTop; +} + +function getScrollHeight () { + return $(ELEMENT_CONTAINER)[0].scrollHeight; +} + +function getRowsAbove () { + const top = getScrollPosition(); + + if (top === 0) { + return 0; + } + + return Math.floor(top / getRowHeight()); +} + +function getRowsBelow () { + const bottom = getScrollPosition() + getViewHeight(); + + return Math.floor((getScrollHeight() - bottom) / getRowHeight()); +} + +function getRowsInView () { + const rowHeight = getRowHeight(); + const viewHeight = getViewHeight(); + + return Math.floor(viewHeight / rowHeight); +} + function append (cursor) { const events = job.get('related.job_events.results').slice(cursor); const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parseEvents(events)))); - const table = $('#result-table'); + const table = $(ELEMENT_TBODY); table.append(rows); $compile(rows.contents())($scope); @@ -351,7 +406,7 @@ function onScroll () { if (bottom >= container[0].scrollHeight && meta.next) { next(); } - }, 250); + }, SCROLL_LOAD_DELAY); } JobsIndexController.$inject = ['job', 'JobEventModel', '$sce', '$timeout', '$scope', '$compile']; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index b29adebb48..c3049bdfb7 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -37,7 +37,7 @@ function JobsRun ($stateExtender, strings) { pageCache: true, pageLimit: 2, params: { - page_size: 25, + page_size: 1000, order_by: 'start_line' }, })); diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index e45303ccec..2ebf9f3285 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -20,7 +20,7 @@
-
 
+
 
From 4b81d8d494db499de7e2a2509725b25fcf212b02 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Fri, 19 Jan 2018 15:07:25 -0500 Subject: [PATCH 24/89] Add WIP implementation of pagination with cache --- .../features/output/index.controller.js | 38 +++- awx/ui/client/lib/models/Base.js | 182 ++++++++++++++---- 2 files changed, 181 insertions(+), 39 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 8b1f2b84f4..2b0919ea4b 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -89,20 +89,36 @@ function JobsIndexController (_job_, JobEventModel, _$sce_, _$timeout_, _$scope_ } function next () { - job.next({ related: 'job_events' }) + const config = { + related: 'job_events', + params: { + order_by: 'start_line' + } + }; + + job.next(config) .then(cursor => { meta.next = job.get('related.job_events.next'); meta.prev = job.get('related.job_events.previous'); console.log(ROW_LIMIT); console.log(getRowCount()); - console.log(getRowHeight()); console.log(getRowsInView()); console.log(getScrollPosition()); console.log('above:', getRowsAbove()); console.log('below:', getRowsBelow()); append(cursor); + shift(); + + debugger; + }); +} + +function prev () { + job.prev({ related: 'job_events' }) + .then(cursor => { + console.log(cursor); }); } @@ -158,6 +174,22 @@ function append (cursor) { $compile(rows.contents())($scope); } +/* + *function prepend (cursor) { + * + *} + * + */ +function shift () { + const count = getRowCount() - ROW_LIMIT; + const rows = $(ELEMENT_TBODY).children().slice(0, count); + + console.log(count, rows); + + rows.empty(); + rows.remove(); +} + function expand () { vm.toggle(meta.parent, true); } @@ -398,6 +430,8 @@ function onScroll () { if (top === 0) { vm.menu.scroll.display = false; + prev(); + return; } diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index ac0f44081f..def0deff11 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -106,9 +106,10 @@ function httpGet (config = {}) { req.params = config.params; if (config.params.page_size) { - this.pageSize = config.params.page_size; - this.pageLimit = config.pageLimit || false; - this.pageCache = config.pageCache || false; + this.page.size = config.params.page_size; + this.page.limit = config.pageLimit || false; + this.page.cache = config.pageCache || false; + this.page.current = 1; } } @@ -120,6 +121,7 @@ function httpGet (config = {}) { req.url = `${this.path}${config.resource}/`; } + console.log(req, this.path); return $http(req) .then(res => { this.model.GET = res.data; @@ -337,9 +339,11 @@ function extend (related, config) { const req = this.parseRequestConfig('GET', config); if (config.params.page_size) { - this.pageSize = config.params.page_size; - this.pageLimit = config.pageLimit || false; - this.pageCache = config.pageCache || false; + this.page.size = config.params.page_size; + this.page.limit = config.pageLimit || false; + this.page.cache = config.pageCache || false; + this.page.current = 1; + this.page.cursor = 0; } if (this.has(req.method, `related.${related}`)) { @@ -358,54 +362,146 @@ function extend (related, config) { return Promise.reject(new Error(`No related property, ${related}, exists`)); } -function next (config = {}) { +function goToPage (config, page) { + const params = config.params || {}; + let url; - let results; + // let results; + let cursor; + let pageNumber; if (config.related) { - url = this.get(`related.${config.related}.next`); - results = this.get(`related.${config.related}.results`) || []; + // results = this.get(`related.${config.related}.results`) || []; + url = `${this.endpoint}${config.related}/`; } else { - url = this.get('next'); - results = this.get('results'); + // results = this.get('results'); + url = this.endpoint; } - if (!url) { - return Promise.resolve(null); + params.page_size = this.page.size; + + if (page === 'next') { + // if (at max) + + pageNumber = this.page.current + 1; + cursor = this.page.cursor + this.page.size; + } else if (page === 'prev') { + // if (at min) + + pageNumber = this.page.current - 1; + cursor = this.page.cursor - this.page.size; + } else { + pageNumber = page; + cursor = this.page.size * (pageNumber - 1); } + if (cursor !== 0 && cursor !== (this.page.limit * this.page.size)) { + this.page.cursor = cursor; + this.page.current = pageNumber; + + return Promise.resolve(cursor); + } + + params.page_size = this.page.size; + params.page = this.page.current; + const req = { method: 'GET', - url + url, + params }; return $http(req) .then(({ data }) => { - let cursor = 0; - - if (this.pageCache) { - results = results || []; - data.results = results.concat(data.results); - cursor = results.length; - - if (this.pageLimit && this.pageLimit * this.pageSize < data.results.length) { - const removeCount = data.results.length - this.pageLimit * this.pageSize; - - data.results.splice(0, removeCount); - cursor -= removeCount; - } - } - - if (config.related) { - this.set('get', `related.${config.related}`, data); - } else { - this.set('get', data); - } - - return cursor <= 0 ? 0 : cursor; + console.log(data); }); } +function next (config = {}) { + return this.goToPage(config, 'next'); +/* + * .then(({ data }) => { + * let cursor = 0; + * + * if (this.pageCache) { + * results = results || []; + * data.results = results.concat(data.results); + * cursor = results.length; + * + * if (this.pageLimit && this.pageLimit * this.pageSize < data.results.length) { + * const removeCount = data.results.length - this.pageLimit * this.pageSize; + * + * data.results.splice(0, removeCount); + * cursor -= removeCount; + * } + * } + * + * if (config.related) { + * this.set('get', `related.${config.related}`, data); + * } else { + * this.set('get', data); + * } + * + * this.page.current++; + * + * return cursor <= 0 ? 0 : cursor; + * }); + */ +} + +function prev (config = {}) { + return this.goToPage(config, 'next'); +} +/* + * let url; + * let results; + * + * console.log(config, config.cursor) + * if (config.related) { + * url = this.get(`related.${config.related}.next`); + * results = this.get(`related.${config.related}.results`) || []; + * } else { + * url = this.get('next'); + * results = this.get('results'); + * } + * + * if (!url) { + * return Promise.resolve(null); + * } + * + * const req = { + * method: 'GET', + * url + * }; + * + * return $http(req) + * .then(({ data }) => { + * let cursor = 0; + * + * if (this.pageCache) { + * results = results || []; + * data.results = results.concat(data.results); + * cursor = results.length; + * + * if (this.pageLimit && this.pageLimit * this.pageSize < data.results.length) { + * const removeCount = data.results.length - this.pageLimit * this.pageSize; + * + * data.results.splice(0, removeCount); + * cursor -= removeCount; + * } + * } + * + * if (config.related) { + * this.set('get', `related.${config.related}`, data); + * } else { + * this.set('get', data); + * } + * + * return cursor <= 0 ? 0 : cursor; + * }); + *} + */ + function normalizePath (resource) { const version = '/api/v2/'; @@ -515,6 +611,10 @@ function create (method, resource, config) { return this; } + if (req.resource) { + this.setEndpoint(req.resource); + } + this.promise = this.request(req); if (req.graft) { @@ -525,6 +625,10 @@ function create (method, resource, config) { .then(() => this); } +function setEndpoint (resource) { + this.endpoint = `${this.path}${resource}/`; +} + function parseRequestConfig (method, resource, config) { if (!method) { return null; @@ -577,6 +681,7 @@ function BaseModel (resource, settings) { this.create = create; this.find = find; this.get = get; + this.goToPage = goToPage; this.graft = graft; this.has = has; this.isEditable = isEditable; @@ -587,10 +692,12 @@ function BaseModel (resource, settings) { this.normalizePath = normalizePath; this.options = options; this.parseRequestConfig = parseRequestConfig; + this.prev = prev; this.request = request; this.requestWithCache = requestWithCache; this.search = search; this.set = set; + this.setEndpoint = setEndpoint; this.unset = unset; this.extend = extend; this.copy = copy; @@ -605,6 +712,7 @@ function BaseModel (resource, settings) { delete: httpDelete.bind(this) }; + this.page = {}; this.model = {}; this.path = this.normalizePath(resource); this.label = strings.get(`${resource}.LABEL`); From 745e547e346146364a34343004f7fdebfd39d2c9 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Mon, 22 Jan 2018 15:01:11 -0500 Subject: [PATCH 25/89] Add nested page cache --- .../features/output/index.controller.js | 2 - awx/ui/client/lib/models/Base.js | 71 ++++++++++++------- 2 files changed, 47 insertions(+), 26 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 2b0919ea4b..dcfabe4d7a 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -110,8 +110,6 @@ function next () { console.log('below:', getRowsBelow()); append(cursor); shift(); - - debugger; }); } diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index def0deff11..77400855ff 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -107,9 +107,15 @@ function httpGet (config = {}) { if (config.params.page_size) { this.page.size = config.params.page_size; - this.page.limit = config.pageLimit || false; - this.page.cache = config.pageCache || false; this.page.current = 1; + + if (config.pageCache) { + this.page.cache = { + root: {} + }; + + this.page.limit = config.pageLimit || false; + } } } @@ -121,11 +127,14 @@ function httpGet (config = {}) { req.url = `${this.path}${config.resource}/`; } - console.log(req, this.path); return $http(req) .then(res => { this.model.GET = res.data; + if (config.pageCache) { + this.page.cache[this.page.current] = res.data.results; + } + return res; }); } @@ -340,10 +349,12 @@ function extend (related, config) { if (config.params.page_size) { this.page.size = config.params.page_size; - this.page.limit = config.pageLimit || false; - this.page.cache = config.pageCache || false; this.page.current = 1; - this.page.cursor = 0; + + if (config.pageCache) { + this.page.cache = this.page.cache || {}; + this.page.limit = config.pageLimit || false; + } } if (this.has(req.method, `related.${related}`)) { @@ -355,6 +366,12 @@ function extend (related, config) { .then(({ data }) => { this.set(req.method, `related.${related}`, data); + if (config.pageCache) { + const key = `related.${related}.${this.page.current}`; + + _.set(this.page.cache, key, data.results); + } + return this; }); } @@ -366,40 +383,42 @@ function goToPage (config, page) { const params = config.params || {}; let url; - // let results; - let cursor; + let count; + let key; let pageNumber; if (config.related) { - // results = this.get(`related.${config.related}.results`) || []; + count = this.get(`related.${config.related}.count`); url = `${this.endpoint}${config.related}/`; + key = `related.${config.related}`; } else { - // results = this.get('results'); + count = this.get('count'); url = this.endpoint; + key = 'root'; } params.page_size = this.page.size; if (page === 'next') { // if (at max) - pageNumber = this.page.current + 1; - cursor = this.page.cursor + this.page.size; - } else if (page === 'prev') { + } else if (page === 'previous') { + // if (at min) + pageNumber = this.page.current - 1; + } else if (page === 'first') { // if (at min) - pageNumber = this.page.current - 1; - cursor = this.page.cursor - this.page.size; - } else { + pageNumber = 1; + } else if (page === 'last') { + // if (at min) + + pageNumber = Math.floor(count / this.page.size); + } else if (typeof pageNumber === 'number') { pageNumber = page; - cursor = this.page.size * (pageNumber - 1); } - if (cursor !== 0 && cursor !== (this.page.limit * this.page.size)) { - this.page.cursor = cursor; - this.page.current = pageNumber; - - return Promise.resolve(cursor); + if (this.page.cache && this.page.cache[pageNumber]) { + return Promise.resolve(this.page.cache[pageNumber]); } params.page_size = this.page.size; @@ -413,7 +432,11 @@ function goToPage (config, page) { return $http(req) .then(({ data }) => { - console.log(data); + if (this.page.cache) { + _.set(this.page.cache, `${key}.${pageNumber}`, data.results); + } + + console.log('cache', this.page.cache); }); } @@ -450,7 +473,7 @@ function next (config = {}) { } function prev (config = {}) { - return this.goToPage(config, 'next'); + return this.goToPage(config, 'prev'); } /* * let url; From cc36ee6bedd03fac1814b1fbbc361d905b37817c Mon Sep 17 00:00:00 2001 From: gconsidine Date: Tue, 23 Jan 2018 17:05:59 -0500 Subject: [PATCH 26/89] Add WIP prepend/previous on scroll --- .../features/output/index.controller.js | 161 +++++++++++++----- awx/ui/client/features/output/index.js | 4 +- awx/ui/client/lib/models/Base.js | 159 ++++++----------- 3 files changed, 172 insertions(+), 152 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index dcfabe4d7a..3fa2686c08 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -10,6 +10,7 @@ let $timeout; let $sce; let $compile; let $scope; +let $q; const record = {}; const meta = { @@ -18,7 +19,7 @@ const meta = { const ROW_LIMIT = 200; const SCROLL_BUFFER = 250; -const SCROLL_LOAD_DELAY = 100; +// const SCROLL_LOAD_DELAY = 100; const EVENT_START_TASK = 'playbook_on_task_start'; const EVENT_START_PLAY = 'playbook_on_play_start'; const EVENT_STATS_PLAY = 'playbook_on_stats'; @@ -36,11 +37,20 @@ const TIME_EVENTS = [ EVENT_STATS_PLAY ]; -function JobsIndexController (_job_, JobEventModel, _$sce_, _$timeout_, _$scope_, _$compile_) { +function JobsIndexController ( + _job_, + JobEventModel, + _$sce_, + _$timeout_, + _$scope_, + _$compile_, + _$q_ +) { $timeout = _$timeout_; $sce = _$sce_; $compile = _$compile_; $scope = _$scope_; + $q = _$q_; job = _job_; ansi = new Ansi(); @@ -80,10 +90,6 @@ function JobsIndexController (_job_, JobEventModel, _$sce_, _$timeout_, _$scope_ table.html($sce.getTrustedHtml(html)); $compile(table.contents())($scope); - meta.next = job.get('related.job_events.next'); - meta.prev = job.get('related.job_events.previous'); - meta.cursor = job.get('related.job_events.results').length; - container.scroll(onScroll); }); } @@ -96,10 +102,13 @@ function next () { } }; - job.next(config) - .then(cursor => { - meta.next = job.get('related.job_events.next'); - meta.prev = job.get('related.job_events.previous'); + console.log('[2] getting next page'); + return job.next(config) + .then(events => { + console.log(events); + if (!events) { + return $q.resolve(); + } console.log(ROW_LIMIT); console.log(getRowCount()); @@ -108,15 +117,21 @@ function next () { console.log('above:', getRowsAbove()); console.log('below:', getRowsBelow()); - append(cursor); - shift(); + return shift() + .then(() => append(events)); }); } function prev () { - job.prev({ related: 'job_events' }) - .then(cursor => { - console.log(cursor); + console.log('[2] getting previous page'); + return job.prev({ related: 'job_events' }) + .then(events => { + if (!events) { + return $q.resolve(); + } + + return pop() + .then(() => prepend(events)); }); } @@ -163,29 +178,66 @@ function getRowsInView () { return Math.floor(viewHeight / rowHeight); } -function append (cursor) { - const events = job.get('related.job_events.results').slice(cursor); - const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parseEvents(events)))); - const table = $(ELEMENT_TBODY); +function append (events) { + console.log('[4] appending next page'); - table.append(rows); - $compile(rows.contents())($scope); + return $q(resolve => { + const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parseEvents(events)))); + const table = $(ELEMENT_TBODY); + + table.append(rows); + $compile(rows.contents())($scope); + + $timeout(() => { + resolve(); + }); + }); +} + +function prepend (events) { + console.log('[4] prepending next page'); + + return $q(resolve => { + const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parseEvents(events)))); + const table = $(ELEMENT_TBODY); + + table.prepend(rows); + $compile(rows.contents())($scope); + + $timeout(() => { + resolve(); + }); + }); +} + +function pop () { + console.log('[3] popping old page'); + return $q(resolve => { + const count = getRowCount() - ROW_LIMIT; + const rows = $(ELEMENT_TBODY).children().slice(-count); + + rows.empty(); + rows.remove(); + + $timeout(() => { + resolve(); + }); + }); } -/* - *function prepend (cursor) { - * - *} - * - */ function shift () { - const count = getRowCount() - ROW_LIMIT; - const rows = $(ELEMENT_TBODY).children().slice(0, count); + console.log('[3] shifting old page'); + return $q(resolve => { + const count = getRowCount() - ROW_LIMIT; + const rows = $(ELEMENT_TBODY).children().slice(0, count); - console.log(count, rows); + rows.empty(); + rows.remove(); - rows.empty(); - rows.remove(); + $timeout(() => { + resolve(); + }); + }); } function expand () { @@ -419,28 +471,43 @@ function onScroll () { meta.scroll.inProgress = true; - $timeout(() => { - const top = container[0].scrollTop; - const bottom = top + SCROLL_BUFFER + container[0].offsetHeight; + const top = container[0].scrollTop; + const bottom = top + SCROLL_BUFFER + container[0].offsetHeight; - meta.scroll.inProgress = false; + if (top === 0) { + console.log('[1] scroll to top'); + vm.menu.scroll.display = false; - if (top === 0) { - vm.menu.scroll.display = false; + prev() + .then(() => { + console.log('[5] scroll reset'); + meta.scroll.inProgress = false; + }); - prev(); + return; + } - return; - } + vm.menu.scroll.display = true; - vm.menu.scroll.display = true; + if (bottom >= container[0].scrollHeight) { + console.log('[1] scroll to bottom'); - if (bottom >= container[0].scrollHeight && meta.next) { - next(); - } - }, SCROLL_LOAD_DELAY); + next() + .then(() => { + console.log('[5] scroll reset'); + meta.scroll.inProgress = false; + }); + } } -JobsIndexController.$inject = ['job', 'JobEventModel', '$sce', '$timeout', '$scope', '$compile']; +JobsIndexController.$inject = [ + 'job', + 'JobEventModel', + '$sce', + '$timeout', + '$scope', + '$compile', + '$q' +]; module.exports = JobsIndexController; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index c3049bdfb7..1e8fc4e23e 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -35,9 +35,9 @@ function JobsRun ($stateExtender, strings) { return new Jobs('get', id) .then(job => job.extend('job_events', { pageCache: true, - pageLimit: 2, + pageLimit: 4, params: { - page_size: 1000, + page_size: 100, order_by: 'start_line' }, })); diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index 77400855ff..e9723655f4 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -110,11 +110,17 @@ function httpGet (config = {}) { this.page.current = 1; if (config.pageCache) { - this.page.cache = { - root: {} - }; - + this.page.cachedPages = this.page.cachedPages || {}; + this.page.cache = this.page.cache || {}; this.page.limit = config.pageLimit || false; + + if (!_.has(this.page.cachedPages, 'root')) { + this.page.cachedPages.root = []; + } + + if (!_.has(this.page.cache, 'root')) { + this.page.cache.root = {}; + } } } } @@ -132,7 +138,10 @@ function httpGet (config = {}) { this.model.GET = res.data; if (config.pageCache) { - this.page.cache[this.page.current] = res.data.results; + this.page.cache.root[this.page.current] = res.data.results; + this.page.cachedPages.root.push(this.page.current); + this.page.count = res.data.count; + this.page.last = Math.ceil(res.data.count / this.page.size); } return res; @@ -352,8 +361,17 @@ function extend (related, config) { this.page.current = 1; if (config.pageCache) { + this.page.cachedPages = this.page.cachedPages || {}; this.page.cache = this.page.cache || {}; this.page.limit = config.pageLimit || false; + + if (!_.has(this.page.cachedPages, `related.${related}`)) { + _.set(this.page.cachedPages, `related.${related}`, []); + } + + if (!_.has(this.page.cache, `related.${related}`)) { + _.set(this.page.cache, `related.${related}`, []); + } } } @@ -367,9 +385,10 @@ function extend (related, config) { this.set(req.method, `related.${related}`, data); if (config.pageCache) { - const key = `related.${related}.${this.page.current}`; - - _.set(this.page.cache, key, data.results); + this.page.cache.related[related][this.page.current] = data.results; + this.page.cachedPages.related[related].push(this.page.current); + this.page.count = data.count; + this.page.last = Math.ceil(data.count / this.page.size); } return this; @@ -383,16 +402,15 @@ function goToPage (config, page) { const params = config.params || {}; let url; - let count; let key; let pageNumber; + let pageCache; + let pagesInCache; if (config.related) { - count = this.get(`related.${config.related}.count`); url = `${this.endpoint}${config.related}/`; key = `related.${config.related}`; } else { - count = this.get('count'); url = this.endpoint; key = 'root'; } @@ -400,29 +418,34 @@ function goToPage (config, page) { params.page_size = this.page.size; if (page === 'next') { - // if (at max) pageNumber = this.page.current + 1; } else if (page === 'previous') { - // if (at min) pageNumber = this.page.current - 1; } else if (page === 'first') { - // if (at min) - pageNumber = 1; } else if (page === 'last') { - // if (at min) - - pageNumber = Math.floor(count / this.page.size); - } else if (typeof pageNumber === 'number') { + pageNumber = this.page.last; + } else { pageNumber = page; } - if (this.page.cache && this.page.cache[pageNumber]) { - return Promise.resolve(this.page.cache[pageNumber]); + this.page.current = pageNumber; + + if (pageNumber < 1 || pageNumber > this.page.last) { + return Promise.resolve(null); + } + + if (this.page.cache) { + pageCache = _.get(this.page.cache, key); + pagesInCache = _.get(this.page.cachedPages, key); + + if (_.has(pageCache, pageNumber)) { + return Promise.resolve(pageCache[pageNumber]); + } } params.page_size = this.page.size; - params.page = this.page.current; + params.page = pageNumber; const req = { method: 'GET', @@ -432,98 +455,28 @@ function goToPage (config, page) { return $http(req) .then(({ data }) => { - if (this.page.cache) { - _.set(this.page.cache, `${key}.${pageNumber}`, data.results); + if (pageCache) { + pageCache[pageNumber] = data.results; + pagesInCache.push(pageNumber); + + if (pagesInCache.length > this.page.limit) { + const pageToDelete = pagesInCache.shift(); + + delete pageCache[pageToDelete]; + } } - console.log('cache', this.page.cache); + return data.results; }); } function next (config = {}) { return this.goToPage(config, 'next'); -/* - * .then(({ data }) => { - * let cursor = 0; - * - * if (this.pageCache) { - * results = results || []; - * data.results = results.concat(data.results); - * cursor = results.length; - * - * if (this.pageLimit && this.pageLimit * this.pageSize < data.results.length) { - * const removeCount = data.results.length - this.pageLimit * this.pageSize; - * - * data.results.splice(0, removeCount); - * cursor -= removeCount; - * } - * } - * - * if (config.related) { - * this.set('get', `related.${config.related}`, data); - * } else { - * this.set('get', data); - * } - * - * this.page.current++; - * - * return cursor <= 0 ? 0 : cursor; - * }); - */ } function prev (config = {}) { - return this.goToPage(config, 'prev'); + return this.goToPage(config, 'previous'); } -/* - * let url; - * let results; - * - * console.log(config, config.cursor) - * if (config.related) { - * url = this.get(`related.${config.related}.next`); - * results = this.get(`related.${config.related}.results`) || []; - * } else { - * url = this.get('next'); - * results = this.get('results'); - * } - * - * if (!url) { - * return Promise.resolve(null); - * } - * - * const req = { - * method: 'GET', - * url - * }; - * - * return $http(req) - * .then(({ data }) => { - * let cursor = 0; - * - * if (this.pageCache) { - * results = results || []; - * data.results = results.concat(data.results); - * cursor = results.length; - * - * if (this.pageLimit && this.pageLimit * this.pageSize < data.results.length) { - * const removeCount = data.results.length - this.pageLimit * this.pageSize; - * - * data.results.splice(0, removeCount); - * cursor -= removeCount; - * } - * } - * - * if (config.related) { - * this.set('get', `related.${config.related}`, data); - * } else { - * this.set('get', data); - * } - * - * return cursor <= 0 ? 0 : cursor; - * }); - *} - */ function normalizePath (resource) { const version = '/api/v2/'; From e5187e4ac8bd1567111d25e47d37c22691e34a10 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Wed, 24 Jan 2018 15:52:31 -0500 Subject: [PATCH 27/89] Adjust pagination/scrolling --- awx/ui/build/webpack.watch.js | 22 +-- .../features/output/index.controller.js | 139 +++++++++++------- awx/ui/client/features/output/index.js | 2 +- awx/ui/client/lib/models/Base.js | 28 +++- 4 files changed, 123 insertions(+), 68 deletions(-) diff --git a/awx/ui/build/webpack.watch.js b/awx/ui/build/webpack.watch.js index 0ae7d77ef5..a68c2eee25 100644 --- a/awx/ui/build/webpack.watch.js +++ b/awx/ui/build/webpack.watch.js @@ -20,16 +20,18 @@ const watch = { output: { filename: OUTPUT }, - module: { - rules: [ - { - test: /\.js$/, - enforce: 'pre', - exclude: /node_modules/, - loader: 'eslint-loader' - } - ] - }, + /* + *module: { + * rules: [ + * { + * test: /\.js$/, + * enforce: 'pre', + * exclude: /node_modules/, + * loader: 'eslint-loader' + * } + * ] + *}, + */ plugins: [ new HtmlWebpackHarddiskPlugin(), new HardSourceWebpackPlugin({ diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 3fa2686c08..9021108a30 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -14,12 +14,14 @@ let $q; const record = {}; const meta = { - scroll: {} + scroll: {}, + page: {} }; const ROW_LIMIT = 200; +const PAGE_LIMIT = 3; const SCROLL_BUFFER = 250; -// const SCROLL_LOAD_DELAY = 100; +const SCROLL_LOAD_DELAY = 250; const EVENT_START_TASK = 'playbook_on_task_start'; const EVENT_START_PLAY = 'playbook_on_play_start'; const EVENT_STATS_PLAY = 'playbook_on_stats'; @@ -83,6 +85,11 @@ function JobsIndexController ( } }; + meta.page.cache = [{ + page: 1, + count: events.length + }]; + $timeout(() => { const table = $(ELEMENT_TBODY); container = $(ELEMENT_CONTAINER); @@ -97,41 +104,54 @@ function JobsIndexController ( function next () { const config = { related: 'job_events', + page: meta.page.cache[meta.page.cache.length - 1].page + 1, params: { order_by: 'start_line' } }; - console.log('[2] getting next page'); - return job.next(config) - .then(events => { - console.log(events); - if (!events) { + console.log('[2] getting next page', config.page, meta.page.cache); + return job.goToPage(config) + .then(data => { + if (!data || !data.results) { return $q.resolve(); } - console.log(ROW_LIMIT); - console.log(getRowCount()); - console.log(getRowsInView()); - console.log(getScrollPosition()); + meta.page.cache.push({ + page: data.page, + count: data.results.length + }); - console.log('above:', getRowsAbove()); - console.log('below:', getRowsBelow()); + console.log(data.results); return shift() - .then(() => append(events)); + .then(() => append(data.results)); }); } function prev () { - console.log('[2] getting previous page'); - return job.prev({ related: 'job_events' }) - .then(events => { - if (!events) { + const config = { + related: 'job_events', + page: meta.page.cache[0].page - 1, + params: { + order_by: 'start_line' + } + }; + + console.log('[2] getting previous page', config.page, meta.page.cache); + + return job.goToPage(config) + .then(data => { + if (!data || !data.results) { return $q.resolve(); } + meta.page.cache.unshift({ + page: data.page, + count: data.results.length + }); + return pop() - .then(() => prepend(events)); + .then(() => prepend(data.results)); }); } @@ -187,7 +207,6 @@ function append (events) { table.append(rows); $compile(rows.contents())($scope); - $timeout(() => { resolve(); }); @@ -201,6 +220,10 @@ function prepend (events) { const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parseEvents(events)))); const table = $(ELEMENT_TBODY); + // Set row count added + // pop number of rows (not number of events) + // meta.page.cache[0].rows = rows.length; + table.prepend(rows); $compile(rows.contents())($scope); @@ -213,14 +236,20 @@ function prepend (events) { function pop () { console.log('[3] popping old page'); return $q(resolve => { - const count = getRowCount() - ROW_LIMIT; - const rows = $(ELEMENT_TBODY).children().slice(-count); + if (meta.page.cache.length <= PAGE_LIMIT) { + console.log('[3.1] nothing to pop'); + return resolve(); + } + + const ejected = meta.page.cache.pop(); + console.log('[3.1] popping', ejected); + const rows = $(ELEMENT_TBODY).children().slice(-ejected.count); rows.empty(); rows.remove(); $timeout(() => { - resolve(); + return resolve(); }); }); } @@ -228,14 +257,20 @@ function pop () { function shift () { console.log('[3] shifting old page'); return $q(resolve => { - const count = getRowCount() - ROW_LIMIT; - const rows = $(ELEMENT_TBODY).children().slice(0, count); + if (meta.page.cache.length <= PAGE_LIMIT) { + console.log('[3.1] nothing to shift'); + return resolve(); + } + + const ejected = meta.page.cache.shift(); + console.log('[3.1] shifting', ejected); + const rows = $(ELEMENT_TBODY).children().slice(0, ejected.count); rows.empty(); rows.remove(); $timeout(() => { - resolve(); + return resolve(); }); }); } @@ -471,33 +506,37 @@ function onScroll () { meta.scroll.inProgress = true; - const top = container[0].scrollTop; - const bottom = top + SCROLL_BUFFER + container[0].offsetHeight; + $timeout(() => { + const top = container[0].scrollTop; + const bottom = top + SCROLL_BUFFER + container[0].offsetHeight; - if (top === 0) { - console.log('[1] scroll to top'); - vm.menu.scroll.display = false; + if (top <= SCROLL_BUFFER) { + console.log('[1] scroll to top'); + vm.menu.scroll.display = false; - prev() - .then(() => { - console.log('[5] scroll reset'); + prev() + .then(() => { + console.log('[5] scroll reset'); + meta.scroll.inProgress = false; + }); + + return; + } else { + vm.menu.scroll.display = true; + + if (bottom >= container[0].scrollHeight) { + console.log('[1] scroll to bottom'); + + next() + .then(() => { + console.log('[5] scroll reset'); + meta.scroll.inProgress = false; + }); + } else { meta.scroll.inProgress = false; - }); - - return; - } - - vm.menu.scroll.display = true; - - if (bottom >= container[0].scrollHeight) { - console.log('[1] scroll to bottom'); - - next() - .then(() => { - console.log('[5] scroll reset'); - meta.scroll.inProgress = false; - }); - } + } + } + }, SCROLL_LOAD_DELAY); } JobsIndexController.$inject = [ diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 1e8fc4e23e..e8daf39557 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -35,7 +35,7 @@ function JobsRun ($stateExtender, strings) { return new Jobs('get', id) .then(job => job.extend('job_events', { pageCache: true, - pageLimit: 4, + pageLimit: 3, params: { page_size: 100, order_by: 'start_line' diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index e9723655f4..0c04964a5f 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -398,8 +398,9 @@ function extend (related, config) { return Promise.reject(new Error(`No related property, ${related}, exists`)); } -function goToPage (config, page) { +function goToPage (config) { const params = config.params || {}; + const page = config.page; let url; let key; @@ -429,18 +430,22 @@ function goToPage (config, page) { pageNumber = page; } - this.page.current = pageNumber; - + console.log('pageNumber', page, pageNumber); if (pageNumber < 1 || pageNumber > this.page.last) { return Promise.resolve(null); } + this.page.current = pageNumber; + if (this.page.cache) { pageCache = _.get(this.page.cache, key); pagesInCache = _.get(this.page.cachedPages, key); if (_.has(pageCache, pageNumber)) { - return Promise.resolve(pageCache[pageNumber]); + return Promise.resolve({ + results: pageCache[pageNumber], + page: pageNumber + }); } } @@ -455,6 +460,8 @@ function goToPage (config, page) { return $http(req) .then(({ data }) => { + let ejected; + if (pageCache) { pageCache[pageNumber] = data.results; pagesInCache.push(pageNumber); @@ -466,16 +473,23 @@ function goToPage (config, page) { } } - return data.results; + return { + results: data.results, + page: pageNumber + } }); } function next (config = {}) { - return this.goToPage(config, 'next'); + config.page = 'next'; + + return this.goToPage(config); } function prev (config = {}) { - return this.goToPage(config, 'previous'); + config.page = 'previous'; + + return this.goToPage(config); } function normalizePath (resource) { From 52a6cca20650e0ca0557df6b61926b31ad4e9091 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Thu, 1 Feb 2018 09:59:10 -0500 Subject: [PATCH 28/89] Add models for remaining event types --- .../features/output/index.controller.js | 58 ++++++++++------- awx/ui/client/features/output/index.js | 62 ++++++++++++++----- awx/ui/client/lib/models/AdHocCommand.js | 10 +-- awx/ui/client/lib/models/Base.js | 4 +- awx/ui/client/lib/models/ProjectUpdate.js | 19 ++++++ awx/ui/client/lib/models/SystemJob.js | 19 ++++++ awx/ui/client/lib/models/index.js | 7 ++- 7 files changed, 132 insertions(+), 47 deletions(-) create mode 100644 awx/ui/client/lib/models/ProjectUpdate.js create mode 100644 awx/ui/client/lib/models/SystemJob.js diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 9021108a30..d826fdbcc2 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -4,7 +4,6 @@ import hasAnsi from 'has-ansi'; let vm; let ansi; let job; -let jobEvent; let container; let $timeout; let $sce; @@ -18,7 +17,6 @@ const meta = { page: {} }; -const ROW_LIMIT = 200; const PAGE_LIMIT = 3; const SCROLL_BUFFER = 250; const SCROLL_LOAD_DELAY = 250; @@ -41,7 +39,6 @@ const TIME_EVENTS = [ function JobsIndexController ( _job_, - JobEventModel, _$sce_, _$timeout_, _$scope_, @@ -56,10 +53,10 @@ function JobsIndexController ( job = _job_; ansi = new Ansi(); - jobEvent = new JobEventModel(); const events = job.get('related.job_events.results'); - const html = $sce.trustAsHtml(parseEvents(events)); + const parsed = parseEvents(events); + const html = $sce.trustAsHtml(parsed.html); vm = this || {}; @@ -87,7 +84,7 @@ function JobsIndexController ( meta.page.cache = [{ page: 1, - count: events.length + lines: parsed.lines }]; $timeout(() => { @@ -118,11 +115,9 @@ function next () { } meta.page.cache.push({ - page: data.page, - count: data.results.length + page: data.page }); - console.log(data.results); return shift() .then(() => append(data.results)); }); @@ -138,7 +133,6 @@ function prev () { }; console.log('[2] getting previous page', config.page, meta.page.cache); - return job.goToPage(config) .then(data => { if (!data || !data.results) { @@ -146,8 +140,7 @@ function prev () { } meta.page.cache.unshift({ - page: data.page, - count: data.results.length + page: data.page }); return pop() @@ -202,8 +195,12 @@ function append (events) { console.log('[4] appending next page'); return $q(resolve => { - const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parseEvents(events)))); + const parsed = parseEvents(events); + const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html))); const table = $(ELEMENT_TBODY); + const index = meta.page.cache.length - 1; + + meta.page.cache[index].lines = parsed.lines; table.append(rows); $compile(rows.contents())($scope); @@ -217,12 +214,11 @@ function prepend (events) { console.log('[4] prepending next page'); return $q(resolve => { - const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parseEvents(events)))); + const parsed = parseEvents(events); + const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html))); const table = $(ELEMENT_TBODY); - // Set row count added - // pop number of rows (not number of events) - // meta.page.cache[0].rows = rows.length; + meta.page.cache[0].lines = parsed.lines; table.prepend(rows); $compile(rows.contents())($scope); @@ -243,7 +239,7 @@ function pop () { const ejected = meta.page.cache.pop(); console.log('[3.1] popping', ejected); - const rows = $(ELEMENT_TBODY).children().slice(-ejected.count); + const rows = $(ELEMENT_TBODY).children().slice(-ejected.lines); rows.empty(); rows.remove(); @@ -264,7 +260,7 @@ function shift () { const ejected = meta.page.cache.shift(); console.log('[3.1] shifting', ejected); - const rows = $(ELEMENT_TBODY).children().slice(0, ejected.count); + const rows = $(ELEMENT_TBODY).children().slice(0, ejected.lines); rows.empty(); rows.remove(); @@ -288,9 +284,22 @@ function scrollTo (direction) { } function parseEvents (events) { + let lines = 0; + let html = ''; + events.sort(orderByLineNumber); - return events.reduce((html, event) => `${html}${parseLine(event)}`, ''); + events.forEach(event => { + const line = parseLine(event); + + html += line.html; + lines += line.count; + }); + + return { + html, + lines + }; } function orderByLineNumber (a, b) { @@ -307,17 +316,18 @@ function orderByLineNumber (a, b) { function parseLine (event) { if (!event || !event.stdout) { - return ''; + return { html: '', count: 0 }; } const { stdout } = event; const lines = stdout.split('\r\n'); + let count = lines.length; let ln = event.start_line; const current = createRecord(ln, lines, event); - return lines.reduce((html, line, i) => { + const html = lines.reduce((html, line, i) => { ln++; const isLastLine = i === lines.length - 1; @@ -325,10 +335,13 @@ function parseLine (event) { if (current && current.isTruncated && isLastLine) { row += createRow(current); + count++; } return `${html}${row}`; }, ''); + + return { html, count }; } function createRecord (ln, lines, event) { @@ -541,7 +554,6 @@ function onScroll () { JobsIndexController.$inject = [ 'job', - 'JobEventModel', '$sce', '$timeout', '$scope', diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index e8daf39557..d447ed2817 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -10,10 +10,48 @@ const indexTemplate = require('~features/output/index.view.html'); const MODULE_NAME = 'at.features.output'; +function resolveJob (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJob, $stateParams) { + const { id } = $stateParams; + const { type } = $stateParams; + + let Resource; + + switch (type) { + case 'project': + Resource = ProjectUpdate; + break; + case 'playbook': + Resource = Job; + break; + case 'command': + Resource = AdHocCommand; + break; + case 'system': + Resource = SystemJob; + break; + case 'workflow': + Resource = WorkflowJob; + break; + default: + // Redirect + return null; + } + + return new Resource('get', id) + .then(resource => resource.extend('job_events', { + pageCache: true, + pageLimit: 3, + params: { + page_size: 100, + order_by: 'start_line' + } + })); +} + function JobsRun ($stateExtender, strings) { $stateExtender.addState({ name: 'jobz', - route: '/jobz/:id', + route: '/jobz/:type/:id', ncyBreadcrumb: { label: strings.get('state.TITLE') }, @@ -29,19 +67,15 @@ function JobsRun ($stateExtender, strings) { } }, resolve: { - job: ['JobModel', '$stateParams', (Jobs, $stateParams) => { - const { id } = $stateParams; - - return new Jobs('get', id) - .then(job => job.extend('job_events', { - pageCache: true, - pageLimit: 3, - params: { - page_size: 100, - order_by: 'start_line' - }, - })); - }] + job: [ + 'JobModel', + 'ProjectUpdateModel', + 'AdHocCommandModel', + 'SystemJobModel', + 'WorkflowJobModel', + '$stateParams', + resolveJob + ] } }); } diff --git a/awx/ui/client/lib/models/AdHocCommand.js b/awx/ui/client/lib/models/AdHocCommand.js index c398219531..9f259a929a 100644 --- a/awx/ui/client/lib/models/AdHocCommand.js +++ b/awx/ui/client/lib/models/AdHocCommand.js @@ -1,5 +1,5 @@ -let Base; let $http; +let BaseModel; function getRelaunch (params) { const req = { @@ -20,7 +20,7 @@ function postRelaunch (params) { } function AdHocCommandModel (method, resource, config) { - Base.call(this, 'ad_hoc_commands'); + BaseModel.call(this, 'ad_hoc_commands'); this.Constructor = AdHocCommandModel; this.postRelaunch = postRelaunch.bind(this); @@ -29,16 +29,16 @@ function AdHocCommandModel (method, resource, config) { return this.create(method, resource, config); } -function AdHocCommandModelLoader (BaseModel, _$http_) { - Base = BaseModel; +function AdHocCommandModelLoader (_$http_, _BaseModel_) { $http = _$http_; + BaseModel = _BaseModel_; return AdHocCommandModel; } AdHocCommandModelLoader.$inject = [ + '$http', 'BaseModel', - '$http' ]; export default AdHocCommandModelLoader; diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index 0c04964a5f..9985de8799 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -460,8 +460,6 @@ function goToPage (config) { return $http(req) .then(({ data }) => { - let ejected; - if (pageCache) { pageCache[pageNumber] = data.results; pagesInCache.push(pageNumber); @@ -469,7 +467,9 @@ function goToPage (config) { if (pagesInCache.length > this.page.limit) { const pageToDelete = pagesInCache.shift(); + console.log(pageCache); delete pageCache[pageToDelete]; + console.log(this.page.cache); } } diff --git a/awx/ui/client/lib/models/ProjectUpdate.js b/awx/ui/client/lib/models/ProjectUpdate.js new file mode 100644 index 0000000000..3de76790df --- /dev/null +++ b/awx/ui/client/lib/models/ProjectUpdate.js @@ -0,0 +1,19 @@ +let BaseModel; + +function ProjectUpdateModel (method, resource, config) { + BaseModel.call(this, 'jobs'); + + this.Constructor = ProjectUpdateModel; + + return this.create(method, resource, config); +} + +function ProjectUpdateModelLoader (_BaseModel_) { + BaseModel = _BaseModel_; + + return ProjectUpdateModel; +} + +ProjectUpdateModelLoader.$inject = ['BaseModel']; + +export default ProjectUpdateModelLoader; diff --git a/awx/ui/client/lib/models/SystemJob.js b/awx/ui/client/lib/models/SystemJob.js new file mode 100644 index 0000000000..ec41941046 --- /dev/null +++ b/awx/ui/client/lib/models/SystemJob.js @@ -0,0 +1,19 @@ +let BaseModel; + +function SystemJobModel (method, resource, config) { + BaseModel.call(this, 'jobs'); + + this.Constructor = SystemJobModel; + + return this.create(method, resource, config); +} + +function SystemJobModelLoader (_BaseModel_) { + BaseModel = _BaseModel_; + + return SystemJobModel; +} + +SystemJobModelLoader.$inject = ['BaseModel']; + +export default SystemJobModelLoader; diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index 91eb3742ee..3875975d4d 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -14,12 +14,13 @@ import InventorySource from '~models/InventorySource'; import Job from '~models/Job'; import JobEvent from '~models/JobEvent'; import JobTemplate from '~models/JobTemplate'; -import Jobs from '~models/Jobs'; import Me from '~models/Me'; import NotificationTemplate from '~models/NotificationTemplate'; import Organization from '~models/Organization'; import Project from '~models/Project'; import Schedule from '~models/Schedule'; +import ProjectUpdate from '~models/ProjectUpdate'; +import SystemJob from '~models/SystemJob'; import UnifiedJobTemplate from '~models/UnifiedJobTemplate'; import WorkflowJob from '~models/WorkflowJob'; import WorkflowJobTemplate from '~models/WorkflowJobTemplate'; @@ -48,18 +49,18 @@ angular .service('JobEventModel', JobEvent) .service('JobModel', Job) .service('JobTemplateModel', JobTemplate) - .service('JobsModel', Jobs) .service('MeModel', Me) .service('NotificationTemplate', NotificationTemplate) .service('OrganizationModel', Organization) .service('ProjectModel', Project) .service('ScheduleModel', Schedule) .service('UnifiedJobModel', UnifiedJob) + .service('ProjectUpdateModel', ProjectUpdate) + .service('SystemJobModel', SystemJob) .service('UnifiedJobTemplateModel', UnifiedJobTemplate) .service('WorkflowJobModel', WorkflowJob) .service('WorkflowJobTemplateModel', WorkflowJobTemplate) .service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode) - .service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode) .service('ModelsStrings', ModelsStrings); export default MODULE_NAME; From 41d3d29ae84a03541d2c866852261a8a0c120ceb Mon Sep 17 00:00:00 2001 From: gconsidine Date: Fri, 2 Feb 2018 10:43:43 -0500 Subject: [PATCH 29/89] Fix project update model --- .../client/features/output/index.controller.js | 16 ++++++++++++++++ awx/ui/client/features/output/index.js | 10 ++++++++-- awx/ui/client/lib/models/ProjectUpdate.js | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index d826fdbcc2..14d086ff4b 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -473,6 +473,22 @@ function getTime (created) { return `${hour}:${minute}:${second}`; } +function pageUp () { + +} + +function pageDown () { + +} + +function jumpToStart () { + +} + +function jumpToEnd () { + +} + function showHostDetails (id) { jobEvent.request('get', id) .then(() => { diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index d447ed2817..bb515e226b 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -15,13 +15,16 @@ function resolveJob (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJob, $ const { type } = $stateParams; let Resource; + let related; switch (type) { case 'project': Resource = ProjectUpdate; + related = 'events'; break; case 'playbook': Resource = Job; + related = 'job_events'; break; case 'command': Resource = AdHocCommand; @@ -38,14 +41,17 @@ function resolveJob (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJob, $ } return new Resource('get', id) - .then(resource => resource.extend('job_events', { + .then(resource => resource.extend(related, { pageCache: true, pageLimit: 3, params: { page_size: 100, order_by: 'start_line' } - })); + })) + .catch(err => { + console.error(err); + }); } function JobsRun ($stateExtender, strings) { diff --git a/awx/ui/client/lib/models/ProjectUpdate.js b/awx/ui/client/lib/models/ProjectUpdate.js index 3de76790df..84fae23f50 100644 --- a/awx/ui/client/lib/models/ProjectUpdate.js +++ b/awx/ui/client/lib/models/ProjectUpdate.js @@ -1,7 +1,7 @@ let BaseModel; function ProjectUpdateModel (method, resource, config) { - BaseModel.call(this, 'jobs'); + BaseModel.call(this, 'project_updates'); this.Constructor = ProjectUpdateModel; From 07ff25a241090b025bf4c69860f0e19898947830 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Tue, 6 Feb 2018 15:55:44 -0500 Subject: [PATCH 30/89] Add generalized resource to job results view --- .../features/output/index.controller.js | 31 +++++++++++++------ awx/ui/client/features/output/index.js | 6 ++-- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 14d086ff4b..d2bdedeb6d 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -3,7 +3,8 @@ import hasAnsi from 'has-ansi'; let vm; let ansi; -let job; +let resource; +let related; let container; let $timeout; let $sce; @@ -38,7 +39,7 @@ const TIME_EVENTS = [ ]; function JobsIndexController ( - _job_, + _resource_, _$sce_, _$timeout_, _$scope_, @@ -50,11 +51,12 @@ function JobsIndexController ( $compile = _$compile_; $scope = _$scope_; $q = _$q_; - job = _job_; + resource = _resource_; ansi = new Ansi(); + related = getRelated(); - const events = job.get('related.job_events.results'); + const events = resource.get(`related.${related}.results`); const parsed = parseEvents(events); const html = $sce.trustAsHtml(parsed.html); @@ -98,9 +100,20 @@ function JobsIndexController ( }); } +function getRelated () { + const name = resource.constructor.name; + + switch (name) { + case 'ProjectUpdateModel': + return 'events'; + case 'JobModel': + return 'job_events'; + } +} + function next () { const config = { - related: 'job_events', + related, page: meta.page.cache[meta.page.cache.length - 1].page + 1, params: { order_by: 'start_line' @@ -108,7 +121,7 @@ function next () { }; console.log('[2] getting next page', config.page, meta.page.cache); - return job.goToPage(config) + return resource.goToPage(config) .then(data => { if (!data || !data.results) { return $q.resolve(); @@ -125,7 +138,7 @@ function next () { function prev () { const config = { - related: 'job_events', + related, page: meta.page.cache[0].page - 1, params: { order_by: 'start_line' @@ -133,7 +146,7 @@ function prev () { }; console.log('[2] getting previous page', config.page, meta.page.cache); - return job.goToPage(config) + return resource.goToPage(config) .then(data => { if (!data || !data.results) { return $q.resolve(); @@ -569,7 +582,7 @@ function onScroll () { } JobsIndexController.$inject = [ - 'job', + 'resource', '$sce', '$timeout', '$scope', diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index bb515e226b..7c082c1c0e 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -10,7 +10,7 @@ const indexTemplate = require('~features/output/index.view.html'); const MODULE_NAME = 'at.features.output'; -function resolveJob (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJob, $stateParams) { +function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJob, $stateParams) { const { id } = $stateParams; const { type } = $stateParams; @@ -73,14 +73,14 @@ function JobsRun ($stateExtender, strings) { } }, resolve: { - job: [ + resource: [ 'JobModel', 'ProjectUpdateModel', 'AdHocCommandModel', 'SystemJobModel', 'WorkflowJobModel', '$stateParams', - resolveJob + resolveResource ] } }); From 2e07fee39f97a06282795c08e95994d8b1a64b71 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Wed, 7 Feb 2018 16:08:11 -0500 Subject: [PATCH 31/89] Add more robust stdout navigation --- awx/ui/client/features/jobs/_index.less | 8 + .../features/output/index.controller.js | 145 +++++++++++++----- awx/ui/client/features/output/index.view.html | 17 +- 3 files changed, 131 insertions(+), 39 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index 750491edff..abd2352fb1 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -41,6 +41,14 @@ cursor: pointer; } + &-menuIcon--lg { + font-size: 22px; + line-height: 12px; + font-weight: bold; + padding: 10px; + cursor: pointer; + } + &-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 d2bdedeb6d..dd0c28f845 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -73,7 +73,10 @@ function JobsIndexController ( vm.menu = { scroll: { display: false, - to: scrollTo + home: scrollHome, + end: scrollEnd, + down: scrollPageDown, + up: scrollPageUp }, top: { expand, @@ -137,6 +140,8 @@ function next () { } function prev () { + const container = $(ELEMENT_CONTAINER)[0]; + const config = { related, page: meta.page.cache[0].page - 1, @@ -157,7 +162,10 @@ function prev () { }); return pop() - .then(() => prepend(data.results)); + .then(() => prepend(data.results)) + .then(lines => { + container.scrollTop = (getRowHeight() * lines) + }); }); } @@ -236,9 +244,7 @@ function prepend (events) { table.prepend(rows); $compile(rows.contents())($scope); - $timeout(() => { - resolve(); - }); + $timeout(() => resolve(parsed.lines)); }); } @@ -257,9 +263,7 @@ function pop () { rows.empty(); rows.remove(); - $timeout(() => { - return resolve(); - }); + $timeout(() => resolve(ejected)); }); } @@ -278,9 +282,19 @@ function shift () { rows.empty(); rows.remove(); - $timeout(() => { - return resolve(); - }); + $timeout(() => resolve()); + }); +} + +function clear () { + console.log('[3] clearing pages'); + return $q(resolve => { + const rows = $(ELEMENT_TBODY).children(); + + rows.empty(); + rows.remove(); + + $timeout(() => resolve()); }); } @@ -288,14 +302,6 @@ function expand () { vm.toggle(meta.parent, true); } -function scrollTo (direction) { - if (direction === 'top') { - container[0].scrollTop = 0; - } else { - container[0].scrollTop = container[0].scrollHeight; - } -} - function parseEvents (events) { let lines = 0; let html = ''; @@ -486,22 +492,6 @@ function getTime (created) { return `${hour}:${minute}:${second}`; } -function pageUp () { - -} - -function pageDown () { - -} - -function jumpToStart () { - -} - -function jumpToEnd () { - -} - function showHostDetails (id) { jobEvent.request('get', id) .then(() => { @@ -581,6 +571,91 @@ function onScroll () { }, SCROLL_LOAD_DELAY); } +function scrollHome () { + const config = { + related, + page: 'first', + params: { + order_by: 'start_line' + } + }; + + meta.scroll.inProgress = true; + + console.log('[2] getting first page', config.page, meta.page.cache); + return resource.goToPage(config) + .then(data => { + if (!data || !data.results) { + return $q.resolve(); + } + + meta.page.cache = [{ + page: data.page + }] + + return clear() + .then(() => prepend(data.results)) + .then(() => { + meta.scroll.inProgress = false; + }); + }); +} + +function scrollEnd () { + const config = { + related, + page: 'last', + params: { + order_by: 'start_line' + } + }; + + meta.scroll.inProgress = true; + + console.log('[2] getting last page', config.page, meta.page.cache); + return resource.goToPage(config) + .then(data => { + if (!data || !data.results) { + return $q.resolve(); + } + + meta.page.cache = [{ + page: data.page + }] + + return clear() + .then(() => append(data.results)) + .then(() => { + const container = $(ELEMENT_CONTAINER)[0]; + + container.scrollTop = getScrollHeight(); + meta.scroll.inProgress = false; + }); + }); +} + +function scrollPageUp () { + const container = $(ELEMENT_CONTAINER)[0]; + const jump = container.scrollTop - container.offsetHeight; + + if (jump < 0) { + container.scrollTop = 0; + } else { + container.scrollTop = jump; + } +} + +function scrollPageDown () { + const container = $(ELEMENT_CONTAINER)[0]; + const jump = container.scrollTop + container.offsetHeight; + + if (jump > container.scrollHeight) { + container.scrollTop = container.scrollHeight; + } else { + container.scrollTop = jump; + } +} + JobsIndexController.$inject = [ 'resource', '$sce', diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 2ebf9f3285..dd938739b0 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -13,8 +13,17 @@ ng-class="{ 'fa-minus': vm.menu.top.isExpanded, 'fa-plus': !vm.menu.top.isExpanded }">
-
- +
+ +
+
+ +
+
+ +
+
+
@@ -23,8 +32,8 @@
 
-
-

+
+

Back to Top

From a6ee7b6aac3235af77e18bc973788cb78690081a Mon Sep 17 00:00:00 2001 From: gconsidine Date: Thu, 8 Feb 2018 13:58:31 -0500 Subject: [PATCH 32/89] Remove unused functionality from controller --- awx/ui/client/features/jobs/_index.less | 16 +++++ .../features/output/index.controller.js | 65 +++---------------- awx/ui/client/features/output/index.view.html | 6 +- 3 files changed, 28 insertions(+), 59 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index abd2352fb1..a72c05a6be 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -7,6 +7,10 @@ border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom: none; + + & > div { + user-select: none; + } } &-menuBottom { @@ -18,6 +22,10 @@ right: 60px; bottom: 24px; cursor: pointer; + + &:hover { + color: @at-blue; + } } &-menuIconGroup { @@ -39,6 +47,10 @@ font-size: 12px; padding: 10px; cursor: pointer; + + &:hover { + color: @at-blue; + } } &-menuIcon--lg { @@ -47,6 +59,10 @@ font-weight: bold; padding: 10px; cursor: pointer; + + &:hover { + color: @at-blue; + } } &-toggle { diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index dd0c28f845..305f0dd8d9 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -20,7 +20,7 @@ const meta = { const PAGE_LIMIT = 3; const SCROLL_BUFFER = 250; -const SCROLL_LOAD_DELAY = 250; +const SCROLL_LOAD_DELAY = 50; const EVENT_START_TASK = 'playbook_on_task_start'; const EVENT_START_PLAY = 'playbook_on_play_start'; const EVENT_STATS_PLAY = 'playbook_on_stats'; @@ -161,57 +161,18 @@ function prev () { page: data.page }); + const previousHeight = container.scrollHeight; + return pop() .then(() => prepend(data.results)) .then(lines => { - container.scrollTop = (getRowHeight() * lines) + const currentHeight = container.scrollHeight; + + container.scrollTop = currentHeight - previousHeight; }); }); } -function getRowCount () { - return $(ELEMENT_TBODY).children().length; -} - -function getRowHeight () { - return $(ELEMENT_TBODY).children()[0].offsetHeight; -} - -function getViewHeight () { - return $(ELEMENT_CONTAINER)[0].offsetHeight; -} - -function getScrollPosition () { - return $(ELEMENT_CONTAINER)[0].scrollTop; -} - -function getScrollHeight () { - return $(ELEMENT_CONTAINER)[0].scrollHeight; -} - -function getRowsAbove () { - const top = getScrollPosition(); - - if (top === 0) { - return 0; - } - - return Math.floor(top / getRowHeight()); -} - -function getRowsBelow () { - const bottom = getScrollPosition() + getViewHeight(); - - return Math.floor((getScrollHeight() - bottom) / getRowHeight()); -} - -function getRowsInView () { - const rowHeight = getRowHeight(); - const viewHeight = getViewHeight(); - - return Math.floor(viewHeight / rowHeight); -} - function append (events) { console.log('[4] appending next page'); @@ -628,7 +589,7 @@ function scrollEnd () { .then(() => { const container = $(ELEMENT_CONTAINER)[0]; - container.scrollTop = getScrollHeight(); + container.scrollTop = container.scrollHeight; meta.scroll.inProgress = false; }); }); @@ -638,22 +599,14 @@ function scrollPageUp () { const container = $(ELEMENT_CONTAINER)[0]; const jump = container.scrollTop - container.offsetHeight; - if (jump < 0) { - container.scrollTop = 0; - } else { - container.scrollTop = jump; - } + container.scrollTop = jump; } function scrollPageDown () { const container = $(ELEMENT_CONTAINER)[0]; const jump = container.scrollTop + container.offsetHeight; - if (jump > container.scrollHeight) { - container.scrollTop = container.scrollHeight; - } else { - container.scrollTop = jump; - } + container.scrollTop = jump; } JobsIndexController.$inject = [ diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index dd938739b0..4b460e4bc1 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -16,15 +16,15 @@
+
+ +
-
- -
From 2eef16632512f72fc47a2a2169b0fdfb05ad7085 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Fri, 9 Feb 2018 14:30:17 -0500 Subject: [PATCH 33/89] Remove git merge conflict artifacts --- awx/ui/client/lib/models/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index 3875975d4d..809b6a72ba 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -60,7 +60,6 @@ angular .service('UnifiedJobTemplateModel', UnifiedJobTemplate) .service('WorkflowJobModel', WorkflowJob) .service('WorkflowJobTemplateModel', WorkflowJobTemplate) - .service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode) - .service('ModelsStrings', ModelsStrings); + .service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode); export default MODULE_NAME; From ad1764c7f234e1357b59b08b388021c7ac105e55 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Fri, 9 Feb 2018 16:01:29 -0500 Subject: [PATCH 34/89] Add ws subscription to job results --- .../features/output/index.controller.js | 4 ++ awx/ui/client/features/output/index.js | 52 ++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 305f0dd8d9..0e1a76b65a 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 resource; let related; +let socket; let container; let $timeout; let $sce; @@ -40,6 +41,7 @@ const TIME_EVENTS = [ function JobsIndexController ( _resource_, + _socket_, _$sce_, _$timeout_, _$scope_, @@ -52,6 +54,7 @@ function JobsIndexController ( $scope = _$scope_; $q = _$q_; resource = _resource_; + socket = _socket_; ansi = new Ansi(); related = getRelated(); @@ -611,6 +614,7 @@ function scrollPageDown () { JobsIndexController.$inject = [ 'resource', + 'socket', '$sce', '$timeout', '$scope', diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 7c082c1c0e..4d395b17a9 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -54,13 +54,38 @@ function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJ }); } -function JobsRun ($stateExtender, strings) { - $stateExtender.addState({ +function resolveSocket (SocketService, $stateParams) { + const { id } = $stateParams; + const { type } = $stateParams; + + // TODO: accommodate other result types (management, scm_update, etc) + const state = { + data: { + socket: { + groups: { + jobs: ['status_changed', 'summary'], + job_events: [] + } + } + } + }; + + SocketService.addStateResolve(state, id); + + return SocketService; +} + +function resolveBreadcrumb (strings) { + return { + label: strings.get('state.TITLE') + }; +} + +function JobsRun ($stateRegistry) { + const state = { name: 'jobz', + url: '/jobz/:type/:id', route: '/jobz/:type/:id', - ncyBreadcrumb: { - label: strings.get('state.TITLE') - }, data: { activityStream: true, activityStreamTarget: 'jobs' @@ -81,12 +106,23 @@ function JobsRun ($stateExtender, strings) { 'WorkflowJobModel', '$stateParams', resolveResource + ], + ncyBreadcrumb: [ + 'JobsStrings', + resolveBreadcrumb + ], + socket: [ + 'SocketService', + '$stateParams', + resolveSocket ] - } - }); + }, + }; + + $stateRegistry.register(state); } -JobsRun.$inject = ['$stateExtender', 'JobsStrings']; +JobsRun.$inject = ['$stateRegistry']; angular .module(MODULE_NAME, [ From 3d02ef820903de5d197c91cb2ef621588ddafc67 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Wed, 14 Feb 2018 14:23:14 -0500 Subject: [PATCH 35/89] Add basic (no optimization) real-time implementation --- .../features/output/index.controller.js | 27 ++++++++++++++++--- awx/ui/client/features/output/index.js | 22 ++++++++++----- awx/ui/client/features/output/index.view.html | 2 +- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 0e1a76b65a..2d95f596e1 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -5,7 +5,6 @@ let vm; let ansi; let resource; let related; -let socket; let container; let $timeout; let $sce; @@ -18,6 +17,7 @@ const meta = { scroll: {}, page: {} }; +const current = {}; const PAGE_LIMIT = 3; const SCROLL_BUFFER = 250; @@ -41,7 +41,7 @@ const TIME_EVENTS = [ function JobsIndexController ( _resource_, - _socket_, + webSocketNamespace, _$sce_, _$timeout_, _$scope_, @@ -54,7 +54,6 @@ function JobsIndexController ( $scope = _$scope_; $q = _$q_; resource = _resource_; - socket = _socket_; ansi = new Ansi(); related = getRelated(); @@ -72,6 +71,9 @@ function JobsIndexController ( vm.toggle = toggle; vm.showHostDetails = showHostDetails; + vm.clear = clear; + + $scope.$on(webSocketNamespace, processWebSocketEvents); vm.menu = { scroll: { @@ -95,6 +97,7 @@ function JobsIndexController ( lines: parsed.lines }]; + $timeout(() => { const table = $(ELEMENT_TBODY); container = $(ELEMENT_CONTAINER); @@ -106,6 +109,22 @@ function JobsIndexController ( }); } +function clear () { + const rows = $(ELEMENT_TBODY).children(); + + rows.empty(); + rows.remove(); +} + +function processWebSocketEvents (scope, data) { + meta.scroll.inProgress = true; + + append([data]) + .then(() => { + container[0].scrollTop = container[0].scrollHeight; + }); +} + function getRelated () { const name = resource.constructor.name; @@ -614,7 +633,7 @@ function scrollPageDown () { JobsIndexController.$inject = [ 'resource', - 'socket', + 'webSocketNamespace', '$sce', '$timeout', '$scope', diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 4d395b17a9..6ff6e61012 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -54,17 +54,27 @@ function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJ }); } -function resolveSocket (SocketService, $stateParams) { +function resolveWebSocket (SocketService, $stateParams) { + const prefix = 'ws'; const { id } = $stateParams; const { type } = $stateParams; - // TODO: accommodate other result types (management, scm_update, etc) + let name; + + switch (type) { + case 'playbook': + name = 'job_events'; + break; + default: + name = 'events'; + } + const state = { data: { socket: { groups: { jobs: ['status_changed', 'summary'], - job_events: [] + [name]: [] } } } @@ -72,7 +82,7 @@ function resolveSocket (SocketService, $stateParams) { SocketService.addStateResolve(state, id); - return SocketService; + return `${prefix}-${name}-${id}`; } function resolveBreadcrumb (strings) { @@ -111,10 +121,10 @@ function JobsRun ($stateRegistry) { 'JobsStrings', resolveBreadcrumb ], - socket: [ + webSocketNamespace: [ 'SocketService', '$stateParams', - resolveSocket + resolveWebSocket ] }, }; diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 4b460e4bc1..11c903dfb0 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -1,7 +1,7 @@
-

+

From d6e705894759e9847e80782c437a3cec1ff5c793 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Thu, 15 Feb 2018 13:31:56 -0500 Subject: [PATCH 36/89] Remove extraneous model import --- awx/ui/client/features/output/index.controller.js | 4 ++-- awx/ui/client/lib/models/index.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 2d95f596e1..a0e9b6bd1d 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -129,10 +129,10 @@ function getRelated () { const name = resource.constructor.name; switch (name) { - case 'ProjectUpdateModel': - return 'events'; case 'JobModel': return 'job_events'; + default: + return 'events'; } } diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index 809b6a72ba..3875975d4d 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -60,6 +60,7 @@ angular .service('UnifiedJobTemplateModel', UnifiedJobTemplate) .service('WorkflowJobModel', WorkflowJob) .service('WorkflowJobTemplateModel', WorkflowJobTemplate) - .service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode); + .service('WorkflowJobTemplateNodeModel', WorkflowJobTemplateNode) + .service('ModelsStrings', ModelsStrings); export default MODULE_NAME; From e143698484c8ad25d9b9f97b6742dcb46bbb2097 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Mon, 19 Feb 2018 10:03:06 -0500 Subject: [PATCH 37/89] Fix resource references in models --- awx/ui/client/features/output/index.js | 3 +-- awx/ui/client/lib/models/SystemJob.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 6ff6e61012..927217f350 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -15,12 +15,11 @@ function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJ const { type } = $stateParams; let Resource; - let related; + let related = 'events'; switch (type) { case 'project': Resource = ProjectUpdate; - related = 'events'; break; case 'playbook': Resource = Job; diff --git a/awx/ui/client/lib/models/SystemJob.js b/awx/ui/client/lib/models/SystemJob.js index ec41941046..cc092ff8f4 100644 --- a/awx/ui/client/lib/models/SystemJob.js +++ b/awx/ui/client/lib/models/SystemJob.js @@ -1,7 +1,7 @@ let BaseModel; function SystemJobModel (method, resource, config) { - BaseModel.call(this, 'jobs'); + BaseModel.call(this, 'system_jobs'); this.Constructor = SystemJobModel; From 83897d43a75e89a4c7385466157f2c04409416af Mon Sep 17 00:00:00 2001 From: gconsidine Date: Mon, 19 Feb 2018 13:33:40 -0500 Subject: [PATCH 38/89] Add websocket connection info for remaining job types --- .../features/output/index.controller.js | 1 + awx/ui/client/features/output/index.js | 38 ++++++++++++------- .../src/shared/socket/socket.service.js | 12 ++++-- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index a0e9b6bd1d..bad00f5542 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -119,6 +119,7 @@ function clear () { function processWebSocketEvents (scope, data) { meta.scroll.inProgress = true; + console.log(data); append([data]) .then(() => { container[0].scrollTop = container[0].scrollHeight; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 927217f350..c67122d148 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -47,33 +47,45 @@ function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJ page_size: 100, order_by: 'start_line' } - })) - .catch(err => { - console.error(err); - }); + })); } function resolveWebSocket (SocketService, $stateParams) { + const { type, id } = $stateParams; const prefix = 'ws'; - const { id } = $stateParams; - const { type } = $stateParams; let name; + let events; switch (type) { - case 'playbook': - name = 'job_events'; + 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; - default: - name = 'events'; } const state = { data: { socket: { groups: { - jobs: ['status_changed', 'summary'], - [name]: [] + [name]: ['status_changed', 'summary'], + [events]: [] } } } @@ -81,7 +93,7 @@ function resolveWebSocket (SocketService, $stateParams) { SocketService.addStateResolve(state, id); - return `${prefix}-${name}-${id}`; + return `${prefix}-${events}-${id}`; } function resolveBreadcrumb (strings) { diff --git a/awx/ui/client/src/shared/socket/socket.service.js b/awx/ui/client/src/shared/socket/socket.service.js index 7c390d0cba..b26dae58d7 100644 --- a/awx/ui/client/src/shared/socket/socket.service.js +++ b/awx/ui/client/src/shared/socket/socket.service.js @@ -90,12 +90,18 @@ export default // ex: 'ws-jobs-' str = `ws-${data.group_name}-${data.job}`; } + else if(data.group_name==="project_update_events"){ + str = `ws-${data.group_name}-${data.project_update}`; + } else if(data.group_name==="ad_hoc_command_events"){ - // The naming scheme is "ws" then a - // dash (-) and the group_name, then the job ID - // ex: 'ws-jobs-' str = `ws-${data.group_name}-${data.ad_hoc_command}`; } + else if(data.group_name==="system_job_events"){ + str = `ws-${data.group_name}-${data.system_job}`; + } + else if(data.group_name==="inventory_update_events"){ + str = `ws-${data.group_name}-${data.inventory_update}`; + } else if(data.group_name==="control"){ // As of v. 3.1.0, there is only 1 "control" // message, which is for expiring the session if the From d48f69317fe108000e69e33e2199e17d2525a156 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Tue, 20 Feb 2018 16:44:51 -0500 Subject: [PATCH 39/89] 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 @@
- - -
- -
From 5c3cf83d081a4e64c6c70c762c74fd5bac33156d Mon Sep 17 00:00:00 2001 From: gconsidine Date: Wed, 21 Feb 2018 15:54:08 -0500 Subject: [PATCH 40/89] Implement memory max (NodeList ejection) in real-time mode --- .../features/output/index.controller.js | 225 +++++++++++------- awx/ui/client/features/output/index.view.html | 4 +- awx/ui/client/lib/models/Base.js | 1 - 3 files changed, 142 insertions(+), 88 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 48f223235e..71db68d5b4 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -16,8 +16,8 @@ const record = {}; let parent = null; let cache = []; +let buffer = []; -const PAGE_LIMIT = 3; const SCROLL_BUFFER = 250; const SCROLL_LOAD_DELAY = 50; const EVENT_START_TASK = 'playbook_on_task_start'; @@ -67,13 +67,13 @@ function JobsIndexController ( cache.push({ page: 1, lines: parsed.lines }); // Development helper(s) - vm.clear = clear; + vm.clear = devClear; // Stdout Navigation vm.scroll = { - lock: false, - display: false, - active: false, + isLocked: false, + showBackToTop: false, + isActive: false, home: scrollHome, end: scrollEnd, down: scrollPageDown, @@ -88,10 +88,13 @@ function JobsIndexController ( // Real-time (active between JOB_START and JOB_END events only) $scope.$on(webSocketNamespace, processWebSocketEvents); vm.stream = { - active: false + isActive: false, + isRendering: false, + count: 0, + page: 1 }; - $timeout(() => { + window.requestAnimationFrame(() => { const table = $(ELEMENT_TBODY); container = $(ELEMENT_CONTAINER); @@ -102,32 +105,72 @@ function JobsIndexController ( }); } -function clear () { - const rows = $(ELEMENT_TBODY).children(); - - rows.empty(); - rows.remove(); -} - function processWebSocketEvents (scope, data) { - vm.scroll.active = true; + vm.scroll.isActive = true; if (data.event === JOB_START) { - vm.stream.active = true; - vm.scroll.lock = true; + vm.stream.isActive = true; + vm.scroll.isLocked = true; } else if (data.event === JOB_END) { - vm.stream.active = false; - vm.scroll.lock = false; + vm.stream.isActive = false; } - append([data]) + if (vm.stream.count % resource.page.size === 0) { + cache.push({ + page: vm.stream.page + }); + + vm.stream.page++; + } + + vm.stream.count++; + buffer.push(data); + + if (vm.stream.isRendering) { + return; + } + + vm.stream.isRendering = true; + + const events = buffer.slice(0, buffer.length); + + buffer = []; + + return render(events); +} + +function render (events) { + return shift() + .then(() => append(events)) .then(() => { - if (vm.scroll.lock) { - container[0].scrollTop = container[0].scrollHeight; + if (vm.scroll.isLocked) { + const height = container[0].scrollHeight; + container[0].scrollTop = height; + } + + if (!vm.stream.isActive) { + if (buffer.length) { + events = buffer.slice(0, buffer.length); + buffer = []; + + return render(events) + .then(() => { + vm.stream.isRendering = false; + vm.scroll.isLocked = false; + vm.scroll.isActive = false; + }); + } + } else { + vm.stream.isRendering = false; } }); } +function devClear () { + cache = []; + clear(); +} + function next () { const config = { related: resource.related, @@ -137,7 +180,7 @@ function next () { } }; - console.log('[2] getting next page', config.page, cache); + // console.log('[2] getting next page', config.page, cache); return model.goToPage(config) .then(data => { if (!data || !data.results) { @@ -164,7 +207,7 @@ function prev () { } }; - console.log('[2] getting previous page', config.page, cache); + // console.log('[2] getting previous page', config.page, cache); return model.goToPage(config) .then(data => { if (!data || !data.results) { @@ -188,88 +231,100 @@ function prev () { } function append (events) { - console.log('[4] appending next page'); - + // console.log('[4] appending next page'); return $q(resolve => { - const parsed = parseEvents(events); - const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html))); - const table = $(ELEMENT_TBODY); - const index = cache.length - 1; + window.requestAnimationFrame(() => { + const parsed = parseEvents(events); + const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html))); + const table = $(ELEMENT_TBODY); + const index = cache.length - 1; - cache[index].lines = parsed.lines; + if (cache[index].lines) { + cache[index].lines += parsed.lines; + } else { + cache[index].lines = parsed.lines; + } - table.append(rows); - $compile(rows.contents())($scope); - $timeout(() => { - resolve(); + table.append(rows); + $compile(rows.contents())($scope); + + return resolve(); }); }); } function prepend (events) { - console.log('[4] prepending next page'); + // console.log('[4] prepending next page'); return $q(resolve => { - const parsed = parseEvents(events); - const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html))); - const table = $(ELEMENT_TBODY); + window.requestAnimationFrame(() => { + const parsed = parseEvents(events); + const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html))); + const table = $(ELEMENT_TBODY); - cache[0].lines = parsed.lines; + cache[0].lines = parsed.lines; - table.prepend(rows); - $compile(rows.contents())($scope); + table.prepend(rows); + $compile(rows.contents())($scope); - $timeout(() => resolve(parsed.lines)); + return resolve(parsed.lines); + }); }); } function pop () { - console.log('[3] popping old page'); + // console.log('[3] popping old page'); return $q(resolve => { - if (cache.length <= PAGE_LIMIT) { - console.log('[3.1] nothing to pop'); + if (cache.length <= resource.page.limit) { + // console.log('[3.1] nothing to pop'); return resolve(); } - const ejected = cache.pop(); - console.log('[3.1] popping', ejected); - const rows = $(ELEMENT_TBODY).children().slice(-ejected.lines); + window.requestAnimationFrame(() => { + const ejected = cache.pop(); + // console.log('[3.1] popping', ejected); + const rows = $(ELEMENT_TBODY).children().slice(-ejected.lines); - rows.empty(); - rows.remove(); + rows.empty(); + rows.remove(); - $timeout(() => resolve(ejected)); + return resolve(ejected); + }); }); } function shift () { - console.log('[3] shifting old page'); + // console.log('[3] shifting old page'); return $q(resolve => { - if (cache.length <= PAGE_LIMIT) { - console.log('[3.1] nothing to shift'); + if (cache.length <= resource.page.limit) { + // console.log('[3.1] nothing to shift'); return resolve(); } - const ejected = cache.shift(); - console.log('[3.1] shifting', ejected); - const rows = $(ELEMENT_TBODY).children().slice(0, ejected.lines); + window.requestAnimationFrame(() => { + const ejected = cache.shift(); + // console.log('[3.1] shifting', ejected); + const rows = $(ELEMENT_TBODY).children().slice(0, ejected.lines); - rows.empty(); - rows.remove(); + rows.empty(); + rows.remove(); - $timeout(() => resolve()); + return resolve(); + }); }); } function clear () { - console.log('[3] clearing pages'); + // console.log('[3] clearing pages'); return $q(resolve => { - const rows = $(ELEMENT_TBODY).children(); + window.requestAnimationFrame(() => { + const rows = $(ELEMENT_TBODY).children(); - rows.empty(); - rows.remove(); + rows.empty(); + rows.remove(); - $timeout(() => resolve()); + return resolve(); + }); }); } @@ -507,40 +562,40 @@ function toggle (uuid, menu) { } function onScroll () { - if (vm.scroll.active) { + if (vm.scroll.isActive) { return; } - vm.scroll.active = true; + vm.scroll.isActive = true; $timeout(() => { const top = container[0].scrollTop; const bottom = top + SCROLL_BUFFER + container[0].offsetHeight; if (top <= SCROLL_BUFFER) { - console.log('[1] scroll to top'); - vm.scroll.display = false; + // console.log('[1] scroll to top'); + vm.scroll.showBackToTop = false; prev() .then(() => { - console.log('[5] scroll reset'); - vm.scroll.active = false; + // console.log('[5] scroll reset'); + vm.scroll.isActive = false; }); return; } else { - vm.scroll.display = true; + vm.scroll.showBackToTop = true; if (bottom >= container[0].scrollHeight) { - console.log('[1] scroll to bottom'); + // console.log('[1] scroll to bottom'); next() .then(() => { - console.log('[5] scroll reset'); - vm.scroll.active = false; + // console.log('[5] scroll reset'); + vm.scroll.isActive = false; }); } else { - vm.scroll.active = false; + vm.scroll.isActive = false; } } }, SCROLL_LOAD_DELAY); @@ -555,9 +610,9 @@ function scrollHome () { } }; - vm.scroll.active = true; + vm.scroll.isActive = true; - console.log('[2] getting first page', config.page, cache); + // console.log('[2] getting first page', config.page, cache); return model.goToPage(config) .then(data => { if (!data || !data.results) { @@ -571,14 +626,14 @@ function scrollHome () { return clear() .then(() => prepend(data.results)) .then(() => { - vm.scroll.active = false; + vm.scroll.isActive = false; }); }); } function scrollEnd () { - if (vm.scroll.lock) { - vm.scroll.lock = false; + if (vm.scroll.isLocked) { + vm.scroll.isLocked = false; return; } @@ -591,9 +646,9 @@ function scrollEnd () { } }; - vm.scroll.active = true; + vm.scroll.isActive = true; - console.log('[2] getting last page', config.page, cache); + // console.log('[2] getting last page', config.page, cache); return model.goToPage(config) .then(data => { if (!data || !data.results) { @@ -610,7 +665,7 @@ function scrollEnd () { const container = $(ELEMENT_CONTAINER)[0]; container.scrollTop = container.scrollHeight; - vm.scroll.active = false; + vm.scroll.isActive = false; }); }); } diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 71898ae4d1..3c1a276826 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -15,7 +15,7 @@
+ ng-class=" { 'at-Stdout-menuIcon--active': vm.scroll.isLocked }">
@@ -32,7 +32,7 @@
 
-
+

Back to Top

diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index 9985de8799..b2e91f97df 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -430,7 +430,6 @@ function goToPage (config) { pageNumber = page; } - console.log('pageNumber', page, pageNumber); if (pageNumber < 1 || pageNumber > this.page.last) { return Promise.resolve(null); } From 60a43015e2d5452af92001a44ce530403d7455ed Mon Sep 17 00:00:00 2001 From: gconsidine Date: Thu, 22 Feb 2018 15:45:09 -0500 Subject: [PATCH 41/89] Update when scroll,stream flags are flipped --- awx/ui/client/features/jobs/_index.less | 3 +-- .../client/features/output/index.controller.js | 17 +++++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/awx/ui/client/features/jobs/_index.less b/awx/ui/client/features/jobs/_index.less index 623db07cc6..d661afae97 100644 --- a/awx/ui/client/features/jobs/_index.less +++ b/awx/ui/client/features/jobs/_index.less @@ -1,6 +1,4 @@ .at-Stdout { - font-family: monospace; - &-menuTop { color: @at-gray-848992; border: 1px solid @at-gray-b7; @@ -123,6 +121,7 @@ } &-container { + font-family: monospace; height: calc(~"100vh - 240px"); overflow-y: scroll; font-size: 15px; diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 71db68d5b4..66f3171dd7 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -115,6 +115,12 @@ function processWebSocketEvents (scope, data) { vm.stream.isActive = false; } + if (!vm.scroll.isLocked) { + vm.scroll.isActive = false; + + return; + } + if (vm.stream.count % resource.page.size === 0) { cache.push({ page: vm.stream.page @@ -153,12 +159,11 @@ function render (events) { events = buffer.slice(0, buffer.length); buffer = []; - return render(events) - .then(() => { - vm.stream.isRendering = false; - vm.scroll.isLocked = false; - vm.scroll.isActive = false; - }); + return render(events); + } else { + vm.stream.isRendering = false; + vm.scroll.isLocked = false; + vm.scroll.isActive = false; } } else { vm.stream.isRendering = false; From a5bd905f18ca6cea99f2cb4cd902ed130c03e8b7 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Mon, 26 Feb 2018 11:18:02 -0500 Subject: [PATCH 42/89] [WIP] Add event buffering on scroll/resume --- .../features/output/index.controller.js | 37 +++++++++++-------- awx/ui/client/features/output/index.js | 5 ++- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 66f3171dd7..c6ceacea41 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -106,33 +106,32 @@ function JobsIndexController ( } function processWebSocketEvents (scope, data) { - vm.scroll.isActive = true; - if (data.event === JOB_START) { + vm.scroll.isActive = true; vm.stream.isActive = true; vm.scroll.isLocked = true; } else if (data.event === JOB_END) { vm.stream.isActive = false; } - if (!vm.scroll.isLocked) { - vm.scroll.isActive = false; - - return; - } + // 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 if (vm.stream.count % resource.page.size === 0) { - cache.push({ - page: vm.stream.page - }); + 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); + } } vm.stream.count++; buffer.push(data); - if (vm.stream.isRendering) { + if (vm.stream.isRendering || !vm.scroll.isLocked) { return; } @@ -280,7 +279,7 @@ function prepend (events) { function pop () { // console.log('[3] popping old page'); return $q(resolve => { - if (cache.length <= resource.page.limit) { + if (cache.length <= resource.page.pageLimit) { // console.log('[3.1] nothing to pop'); return resolve(); } @@ -299,16 +298,16 @@ function pop () { } function shift () { - // console.log('[3] shifting old page'); + console.log('[3] shifting old page', cache.length); return $q(resolve => { - if (cache.length <= resource.page.limit) { + if (cache.length <= resource.page.pageLimit) { // console.log('[3.1] nothing to shift'); return resolve(); } window.requestAnimationFrame(() => { const ejected = cache.shift(); - // console.log('[3.1] shifting', ejected); + console.log('[3.1] shifting', ejected); const rows = $(ELEMENT_TBODY).children().slice(0, ejected.lines); rows.empty(); @@ -639,8 +638,16 @@ function scrollHome () { function scrollEnd () { if (vm.scroll.isLocked) { vm.scroll.isLocked = false; + vm.scroll.isActive = false; return; + } else if (!vm.scroll.isLocked && vm.stream.isActive) { + vm.scroll.isActive = true; + + return clear() + .then(() => { + vm.scroll.isLocked = true; + }); } const config = { diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index a23e8cbbc0..7467c0b079 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -59,8 +59,9 @@ function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJ ws: getWebSocketResource(type), page: { cache: PAGE_CACHE, - limit: PAGE_LIMIT, - size: PAGE_SIZE + size: PAGE_SIZE, + pageLimit: PAGE_LIMIT, + resultLimit: PAGE_SIZE * PAGE_LIMIT } }; }); From df84f822f6780d0b3cf062c1885ee0392f4de86e Mon Sep 17 00:00:00 2001 From: gconsidine Date: Tue, 27 Feb 2018 17:12:55 -0500 Subject: [PATCH 43/89] [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 From b16d9a89e38041a897c7a614d9d909f8ac314486 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Thu, 1 Mar 2018 09:15:43 -0500 Subject: [PATCH 44/89] Refactor page handling --- .../features/output/index.controller.js | 232 +++++++----------- awx/ui/client/features/output/page.service.js | 164 +++++++++++-- awx/ui/client/lib/models/Base.js | 2 - 3 files changed, 238 insertions(+), 160 deletions(-) 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); } } From 0c09447f2d2cb2cd77bf181ab425a331c76b1619 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Thu, 1 Mar 2018 16:29:32 -0500 Subject: [PATCH 45/89] Refactor scroll handling into independent service --- .../features/output/index.controller.js | 553 ++++-------------- awx/ui/client/features/output/index.js | 4 +- awx/ui/client/features/output/page.service.js | 13 +- .../client/features/output/render.service.js | 297 ++++++++++ .../client/features/output/scroll.service.js | 172 ++++++ 5 files changed, 578 insertions(+), 461 deletions(-) create mode 100644 awx/ui/client/features/output/scroll.service.js diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index ce8a047e7f..25ebb9b531 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -1,83 +1,41 @@ -import Ansi from 'ansi-to-html'; -import hasAnsi from 'has-ansi'; - -let vm; -let ansi; -let model; -let resource; -let page; -let container; -let $timeout; -let $sce; -let $compile; -let $scope; -let $q; - -const record = {}; - -let parent = null; - -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'; -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, - EVENT_START_PLAY -]; - -const TIME_EVENTS = [ - EVENT_START_TASK, - EVENT_START_PLAY, - EVENT_STATS_PLAY -]; +let vm; +let $compile; +let $scope; +let $q; +let page; +let render; +let scroll; +let resource; function JobsIndexController ( _resource_, _page_, - _$sce_, - _$timeout_, + _scroll_, + _render_, _$scope_, _$compile_, _$q_ ) { vm = this || {}; - $timeout = _$timeout_; - $sce = _$sce_; $compile = _$compile_; $scope = _$scope_; $q = _$q_; resource = _resource_; + page = _page_; - model = resource.model; - - ansi = new Ansi(); - - const events = model.get(`related.${resource.related}.results`); - const parsed = parseEvents(events); - const html = $sce.trustAsHtml(parsed.html); - - page.init(resource); - - page.add({ number: 1, lines: parsed.lines }); + scroll = _scroll_; + render = _render_; // Development helper(s) vm.clear = devClear; // Stdout Navigation vm.scroll = { - isLocked: false, showBackToTop: false, - isActive: false, - position: 0, - time: 0, home: scrollHome, end: scrollEnd, down: scrollPageDown, @@ -90,87 +48,93 @@ function JobsIndexController ( vm.isExpanded = true; // Real-time (active between JOB_START and JOB_END events only) - $scope.$on(resource.ws.namespace, processWebSocketEvents); vm.stream = { - isActive: false, - isRendering: false, - isPaused: false, - buffered: 0, - count: 0, - page: 1 + active: false, + rendering: false, + paused: false }; - window.requestAnimationFrame(() => { - const table = $(ELEMENT_TBODY); - container = $(ELEMENT_CONTAINER); + const stream = false; // TODO: Set in route - table.html($sce.getTrustedHtml(html)); - $compile(table.contents())($scope); - - container.scroll(onScroll); - }); + render.requestAnimationFrame(() => init()); } -function processWebSocketEvents (scope, data) { - let done; +function init (stream) { + page.init(resource); + render.init({ + get: () => resource.model.get(`related.${resource.related}.results`), + compile: html => $compile(html)($scope) + }); + + scroll.init({ + isAtRest: scrollIsAtRest, + previous, + next + }); + + if (stream) { + $scope.$on(resource.ws.namespace, process); + } else { + next(); + } +} + +function process (scope, data) { if (data.event === JOB_START) { - vm.scroll.isActive = true; - vm.stream.isActive = true; - vm.scroll.isLocked = true; + vm.stream.active = true; + scroll.lock(); } else if (data.event === JOB_END) { - vm.stream.isActive = false; + vm.stream.active = false; } const pageAdded = page.addToBuffer(data); - if (pageAdded && !vm.scroll.isLocked) { - vm.stream.isPaused = true; + if (pageAdded && !scroll.isLocked()) { + vm.stream.paused = true; } - if (vm.stream.isPaused && vm.scroll.isLocked) { - vm.stream.isPaused = false; + if (vm.stream.paused && scroll.isLocked()) { + vm.stream.paused = false; } - if (vm.stream.isRendering || vm.stream.isPaused) { + if (vm.stream.rendering || vm.stream.paused) { return; } const events = page.emptyBuffer(); - return render(events); + return renderStream(events); } -function render (events) { - vm.stream.isRendering = true; +function renderStream (events) { + vm.stream.rendering = true; return shift() .then(() => append(events)) .then(() => { - if (vm.scroll.isLocked) { - const height = container[0].scrollHeight; - container[0].scrollTop = height; + if (scroll.isLocked()) { + scroll.setScrollPosition(scroll.getScrollHeight()); } - if (!vm.stream.isActive) { + if (!vm.stream.active) { const buffer = page.emptyBuffer(); if (buffer.length) { - return render(buffer); + return renderStream(buffer); + } else { + vm.stream.rendering = false; + scroll.unlock(); } - - vm.stream.isRendering = false; - vm.scroll.isLocked = false; - vm.scroll.isActive = false; } else { - vm.stream.isRendering = false; + vm.stream.rendering = false; } }); } function devClear () { - page.init(resource); - clear(); + init(true); + render.clear(); } function next () { @@ -182,13 +146,12 @@ function next () { return shift() .then(() => append(events)); - }) + }); } function previous () { - const container = $(ELEMENT_CONTAINER)[0]; - - let previousHeight; + let initialPosition = scroll.getScrollPosition(); + let postPopHeight; return page.previous() .then(events => { @@ -198,296 +161,56 @@ function previous () { return pop() .then(() => { - previousHeight = container.scrollHeight; + postPopHeight = scroll.getScrollHeight(); return prepend(events); }) .then(() => { - const currentHeight = container.scrollHeight; - container.scrollTop = currentHeight - previousHeight; + const currentHeight = scroll.getScrollHeight(); + + scroll.setScrollPosition(currentHeight - postPopHeight + initialPosition); }); }); } function append (events) { - return $q(resolve => { - window.requestAnimationFrame(() => { - const parsed = parseEvents(events); - const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html))); - const table = $(ELEMENT_TBODY); - - page.updateLineCount('current', parsed.lines); - - table.append(rows); - $compile(rows.contents())($scope); - - return resolve(); + return render.append(events) + .then(count => { + page.updateLineCount('current', count); }); - }); } function prepend (events) { - return $q(resolve => { - window.requestAnimationFrame(() => { - const parsed = parseEvents(events); - const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html))); - const table = $(ELEMENT_TBODY); - - page.updateLineCount('current', parsed.lines); - - table.prepend(rows); - $compile(rows.contents())($scope); - - $scope.$apply(() => { - return resolve(parsed.lines); - }); + return render.prepend(events) + .then(count => { + page.updateLineCount('current', count); }); - }); } function pop () { - return $q(resolve => { - if (!page.isOverCapacity()) { - return resolve(); - } + if (!page.isOverCapacity()) { + return $q.resolve(); + } - window.requestAnimationFrame(() => { - const lines = page.trim('right'); - const rows = $(ELEMENT_TBODY).children().slice(-lines); + const lines = page.trim('right'); - rows.empty(); - rows.remove(); - - return resolve(); - }); - }); + return render.pop(lines); } function shift () { - return $q(resolve => { - if (!page.isOverCapacity()) { - return resolve(); - } + if (!page.isOverCapacity()) { + return $q.resolve(); + } - window.requestAnimationFrame(() => { - const lines = page.trim('left'); - const rows = $(ELEMENT_TBODY).children().slice(0, lines); + const lines = page.trim('left'); - rows.empty(); - rows.remove(); - - return resolve(); - }); - }); -} - -function clear () { - return $q(resolve => { - window.requestAnimationFrame(() => { - const rows = $(ELEMENT_TBODY).children(); - - rows.empty(); - rows.remove(); - - return resolve(); - }); - }); + return render.shift(lines); } function expand () { vm.toggle(parent, true); } -function parseEvents (events) { - let lines = 0; - let html = ''; - - events.sort(orderByLineNumber); - - events.forEach(event => { - const line = parseLine(event); - - html += line.html; - lines += line.count; - }); - - return { - html, - lines - }; -} - -function orderByLineNumber (a, b) { - if (a.start_line > b.start_line) { - return 1; - } - - if (a.start_line < b.start_line) { - return -1; - } - - return 0; -} - -function parseLine (event) { - if (!event || !event.stdout) { - return { html: '', count: 0 }; - } - - const { stdout } = event; - const lines = stdout.split('\r\n'); - - let count = lines.length; - let ln = event.start_line; - - const current = createRecord(ln, lines, event); - - const html = lines.reduce((html, line, i) => { - ln++; - - const isLastLine = i === lines.length - 1; - let row = createRow(current, ln, line); - - if (current && current.isTruncated && isLastLine) { - row += createRow(current); - count++; - } - - return `${html}${row}`; - }, ''); - - return { html, count }; -} - -function createRecord (ln, lines, event) { - if (!event.uuid) { - return null; - } - - const info = { - id: event.id, - line: ln + 1, - uuid: event.uuid, - level: event.event_level, - start: event.start_line, - end: event.end_line, - isTruncated: (event.end_line - event.start_line) > lines.length, - isHost: typeof event.host === 'number' - }; - - if (event.parent_uuid) { - info.parents = getParentEvents(event.parent_uuid); - } - - if (info.isTruncated) { - info.truncatedAt = event.start_line + lines.length; - } - - if (EVENT_GROUPS.includes(event.event)) { - info.isParent = true; - - if (event.event_level === 1) { - parent = event.uuid; - } - - if (event.parent_uuid) { - if (record[event.parent_uuid]) { - if (record[event.parent_uuid].children && - !record[event.parent_uuid].children.includes(event.uuid)) { - record[event.parent_uuid].children.push(event.uuid); - } else { - record[event.parent_uuid].children = [event.uuid]; - } - } - } - } - - if (TIME_EVENTS.includes(event.event)) { - info.time = getTime(event.created); - info.line++; - } - - record[event.uuid] = info; - - return info; -} - -function getParentEvents (uuid, list) { - list = list || []; - - if (record[uuid]) { - list.push(uuid); - - if (record[uuid].parents) { - list = list.concat(record[uuid].parents); - } - } - - return list; -} - -function createRow (current, ln, content) { - let id = ''; - let timestamp = ''; - let tdToggle = ''; - let tdEvent = ''; - let classList = ''; - - content = content || ''; - - if (hasAnsi(content)) { - content = ansi.toHtml(content); - } - - if (current) { - if (current.isParent && current.line === ln) { - id = current.uuid; - tdToggle = ``; - } - - if (current.isHost) { - tdEvent = `${content}`; - } - - if (current.time && current.line === ln) { - timestamp = `${current.time}`; - } - - if (current.parents) { - classList = current.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, ''); - } - } - - if (!tdEvent) { - tdEvent = `${content}`; - } - - if (!tdToggle) { - tdToggle = ''; - } - - if (!ln) { - ln = '...'; - } - - return ` - - ${tdToggle} - ${ln} - ${tdEvent} - ${timestamp} - `; -} - -function getTime (created) { - const date = new Date(created); - const hour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours(); - const minute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes(); - const second = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds(); - - return `${hour}:${minute}:${second}`; -} - function showHostDetails (id) { jobEvent.request('get', id) .then(() => { @@ -527,100 +250,38 @@ function toggle (uuid, menu) { } } -function onScroll () { - if (vm.scroll.isActive) { - return; - } - - if (vm.scroll.register) { - $timeout.cancel(vm.scroll.register); - } - - vm.scroll.register = $timeout(registerScrollEvent, SCROLL_DELAY); -} - -function registerScrollEvent () { - vm.scroll.isActive = true; - - const position = container[0].scrollTop; - const height = container[0].offsetHeight; - const downward = position > vm.scroll.position; - - let promise; - - if (position !== 0 ) { - vm.scroll.showBackToTop = true; - } else { - vm.scroll.showBackToTop = false; - } - - - console.log('downward', downward); - if (downward) { - if (((height - position) / height) < SCROLL_THRESHOLD) { - promise = next; - } - } 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 () { + scroll.pause(); + return page.first() .then(events => { if (!events) { return; } - return clear() - .then(() => prepend(events)) + return render.clear() + .then(() => render.prepend(events)) .then(() => { - vm.scroll.isActive = false; + scroll.setScrollPosition(0); + scroll.resume(); }); }); } function scrollEnd () { - if (vm.scroll.isLocked) { + if (scroll.isLocked()) { page.bookmark(); - - vm.scroll.isLocked = false; - vm.scroll.isActive = false; + scroll.unlock(); return; - } else if (!vm.scroll.isLocked && vm.stream.isActive) { + } else if (!scroll.isLocked() && vm.stream.active) { page.bookmark(); - - vm.scroll.isActive = true; - vm.scroll.isLocked = true; + scroll.lock(); return; } - vm.scroll.isActive = true; + scroll.pause(); return page.last() .then(events => { @@ -628,36 +289,32 @@ function scrollEnd () { return; } - return clear() - .then(() => append(events)) + return render.clear() + .then(() => render.append(events)) .then(() => { - const container = $(ELEMENT_CONTAINER)[0]; - - container.scrollTop = container.scrollHeight; - vm.scroll.isActive = false; + scroll.setScrollPosition(scroll.getScrollHeight()); + scroll.resume(); }); }); } function scrollPageUp () { - const container = $(ELEMENT_CONTAINER)[0]; - const jump = container.scrollTop - container.offsetHeight; - - container.scrollTop = jump; + scroll.pageUp(); } function scrollPageDown () { - const container = $(ELEMENT_CONTAINER)[0]; - const jump = container.scrollTop + container.offsetHeight; + scroll.pageDown(); +} - container.scrollTop = jump; +function scrollIsAtRest (isAtRest) { + vm.scroll.showBackToTop = !isAtRest; } JobsIndexController.$inject = [ 'resource', 'JobPageService', - '$sce', - '$timeout', + 'JobScrollService', + 'JobRenderService', '$scope', '$compile', '$q' diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 6d35022cf4..a0e0b70123 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -1,11 +1,10 @@ -import JobsStrings from '~features/output/jobs.strings'; -import IndexController from '~features/output/index.controller'; import atLibModels from '~models'; import atLibComponents from '~components'; import Strings from '~features/output/jobs.strings'; import Controller from '~features/output/index.controller'; import PageService from '~features/output/page.service'; +import ScrollService from '~features/output/scroll.service'; const Template = require('~features/output/index.view.html'); @@ -178,6 +177,7 @@ angular ]) .service('JobStrings', Strings) .service('JobPageService', PageService) + .service('JobScrollService', ScrollService) .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 index b82eba4705..e91b80191e 100644 --- a/awx/ui/client/features/output/page.service.js +++ b/awx/ui/client/features/output/page.service.js @@ -1,16 +1,10 @@ function JobPageService ($q) { - 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, + limit: this.resource.page.pageLimit, + size: this.resource.page.size, current: 0, index: -1, count: 0, @@ -153,13 +147,10 @@ function JobPageService ($q) { } 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; diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js index e69de29bb2..235de94198 100644 --- a/awx/ui/client/features/output/render.service.js +++ b/awx/ui/client/features/output/render.service.js @@ -0,0 +1,297 @@ +import Ansi from 'ansi-to-html'; +import hasAnsi from 'has-ansi'; + +const ELEMENT_TBODY = '#atStdoutResultTable'; +const EVENT_START_TASK = 'playbook_on_task_start'; +const EVENT_START_PLAY = 'playbook_on_play_start'; +const EVENT_STATS_PLAY = 'playbook_on_stats'; +const JOB_START = 'playbook_on_start'; +const JOB_END = 'playbook_on_stats'; + +const EVENT_GROUPS = [ + EVENT_START_TASK, + EVENT_START_PLAY +]; + +const TIME_EVENTS = [ + EVENT_START_TASK, + EVENT_START_PLAY, + EVENT_STATS_PLAY +]; + +const ansi = new Ansi(); + +function JobRenderService ($q, $sce, $window) { + this.init = ({ compile, apply, get }) => { + this.parent = null; + this.record = {}; + this.el = $(ELEMENT_TBODY); + this.hooks = { get, compile, apply }; + }; + + this.sortByLineNumber = (a, b) => { + if (a.start_line > b.start_line) { + return 1; + } + + if (a.start_line < b.start_line) { + return -1; + } + + return 0; + }; + + this.transformEventGroup = events => { + let lines = 0; + let html = ''; + + events.sort(this.sortByLineNumber); + + events.forEach(event => { + const line = this.transformEvent(event); + + html += line.html; + lines += line.count; + }); + + return { html, lines }; + }; + + this.transformEvent = event => { + if (!event || !event.stdout) { + return { html: '', count: 0 }; + } + + const { stdout } = event; + const lines = stdout.split('\r\n'); + + let count = lines.length; + let ln = event.start_line; + + const current = this.createRecord(ln, lines, event); + + const html = lines.reduce((html, line, i) => { + ln++; + + const isLastLine = i === lines.length - 1; + let row = this.createRow(current, ln, line); + + if (current && current.isTruncated && isLastLine) { + row += this.createRow(current); + count++; + } + + return `${html}${row}`; + }, ''); + + return { html, count }; + }; + + this.createRecord = event => { + if (!event.uuid) { + return null; + } + + const info = { + id: event.id, + line: ln + 1, + uuid: event.uuid, + level: event.event_level, + start: event.start_line, + end: event.end_line, + isTruncated: (event.end_line - event.start_line) > lines.length, + isHost: typeof event.host === 'number' + }; + + if (event.parent_uuid) { + info.parents = getParentEvents(event.parent_uuid); + } + + if (info.isTruncated) { + info.truncatedAt = event.start_line + lines.length; + } + + if (EVENT_GROUPS.includes(event.event)) { + info.isParent = true; + + if (event.event_level === 1) { + this.parent = event.uuid; + } + + if (event.parent_uuid) { + if (this.record[event.parent_uuid]) { + if (this.record[event.parent_uuid].children && + !this.record[event.parent_uuid].children.includes(event.uuid)) { + this.record[event.parent_uuid].children.push(event.uuid); + } else { + this.record[event.parent_uuid].children = [event.uuid]; + } + } + } + } + + if (TIME_EVENTS.includes(event.event)) { + info.time = this.getTimestamp(event.created); + info.line++; + } + + this.record[event.uuid] = info; + + return info; + }; + + this.createRow = (current, ln, content) => { + let id = ''; + let timestamp = ''; + let tdToggle = ''; + let tdEvent = ''; + let classList = ''; + + content = content || ''; + + if (hasAnsi(content)) { + content = ansi.toHtml(content); + } + + if (current) { + if (current.isParent && current.line === ln) { + id = current.uuid; + tdToggle = ``; + } + + if (current.isHost) { + tdEvent = `${content}`; + } + + if (current.time && current.line === ln) { + timestamp = `${current.time}`; + } + + if (current.parents) { + classList = current.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, ''); + } + } + + if (!tdEvent) { + tdEvent = `${content}`; + } + + if (!tdToggle) { + tdToggle = ''; + } + + if (!ln) { + ln = '...'; + } + + return ` + + ${tdToggle} + ${ln} + ${tdEvent} + ${timestamp} + `; + } + + this.getTimestamp = (created) => { + const date = new Date(created); + const hour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours(); + const minute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes(); + const second = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds(); + + return `${hour}:${minute}:${second}`; + } + + this.getParentEvents = (uuid, list) => { + list = list || []; + + if (this.record[uuid]) { + list.push(uuid); + + if (this.record[uuid].parents) { + list = list.concat(record[uuid].parents); + } + } + + return list; + }; + + this.getEvents = () => { + return this.hooks.get(); + }; + + this.insert = (events, insert) => { + const result = this.transformEventGroup(events); + const html = this.sanitize(result.html); + + return this.requestAnimationFrame(() => insert(html)) + .then(() => this.compile(html)) + .then(() => result.lines); + }; + + this.remove = elements => { + return this.requestAnimationFrame(() => { + elements.empty(); + elements.remove(); + }); + }; + + this.requestAnimationFrame = fn => { + return $q(resolve => { + $window.requestAnimationFrame(() => { + if (fn) { + fn(); + } + + return resolve(); + }); + }); + }; + + this.compile = html => { + this.hooks.compile(html); + + return this.requestAnimationFrame(); + }; + + this.build = () => { + + }; + + this.clear = () => { + const elements = this.el.children(); + + return this.remove(elements); + }; + + this.shift = lines => { + const elements = this.el.children().slice(0, lines); + + return this.remove(elements); + }; + + this.pop = lines => { + const elements = this.el.children().slice(-lines); + + return this.remove(elements); + }; + + this.prepend = events => { + return this.insert(events, html => this.el.prepend(html)) + }; + + this.append = events => { + return this.insert(events, html => this.el.append(html)) + }; + + // TODO: stdout from the API should not be trusted. + this.sanitize = html => { + html = $sce.trustAsHtml(html); + + return $sce.getTrustedHtml(html); + }; +} + +JobRenderService.$inject = ['$q', '$sce', '$window']; + +export default JobRenderService; diff --git a/awx/ui/client/features/output/scroll.service.js b/awx/ui/client/features/output/scroll.service.js new file mode 100644 index 0000000000..489871e354 --- /dev/null +++ b/awx/ui/client/features/output/scroll.service.js @@ -0,0 +1,172 @@ +const ELEMENT_CONTAINER = '.at-Stdout-container'; +const DELAY = 100; +const THRESHOLD = 0.1; + +function JobScrollService ($q, $timeout) { + this.init = (hooks) => { + this.el = $(ELEMENT_CONTAINER); + this.timer = null; + + this.position = { + previous: 0, + current: 0 + }; + + this.hooks = { + isAtRest: hooks.isAtRest, + next: hooks.next, + previous: hooks.previous + }; + + this.state = { + locked: false, + paused: false, + top: true + }; + + this.el.scroll(this.listen); + }; + + this.listen = () => { + if (this.isPaused()) { + return; + } + + if (this.timer) { + $timeout.cancel(this.timer); + } + + this.timer = $timeout(this.register, DELAY); + }; + + this.register = () => { + this.pause(); + + const height = this.getScrollHeight(); + const current = this.getScrollPosition(); + const downward = current > this.position.previous; + + let promise; + + if (downward && this.isBeyondThreshold(downward, current)) { + promise = this.hooks.next; + } else if (!downward && this.isBeyondThreshold(downward, current)) { + promise = this.hooks.previous; + } + + if (!promise) { + this.setScrollPosition(current); + this.isAtRest(); + this.resume(); + + return $q.resolve(); + } + + return promise() + .then(() => { + this.setScrollPosition(this.getScrollPosition()); + this.isAtRest(); + this.resume(); + }); + }; + + this.isBeyondThreshold = (downward, current) => { + const previous = this.position.previous; + const height = this.getScrollHeight(); + + if (downward) { + current += this.getViewableHeight(); + + if (current >= height || ((height - current) / height) < THRESHOLD) { + return true; + } + } else { + if (current <= 0 || (current / height) < THRESHOLD) { + return true; + } + } + + return false; + }; + + this.pageUp = () => { + if (this.isPaused()) { + return; + } + + const top = this.getScrollPosition(); + const height = this.getViewableHeight(); + + this.setScrollPosition(top - height); + }; + + this.pageDown = () => { + if (this.isPaused()) { + return; + } + + const top = this.getScrollPosition(); + const height = this.getViewableHeight(); + + this.setScrollPosition(top + height); + }; + + this.getScrollHeight = () => { + return this.el[0].scrollHeight; + }; + + this.getViewableHeight = () => { + return this.el[0].offsetHeight; + }; + + this.getScrollPosition = () => { + return this.el[0].scrollTop; + }; + + this.setScrollPosition = position => { + this.position.previous = this.position.current; + this.position.current = position; + this.el[0].scrollTop = position; + this.isAtRest(); + }; + + 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; + }; + + this.pause = () => { + this.state.paused = true; + }; + + this.isPaused = () => { + return this.state.paused; + }; + + this.lock = () => { + this.state.locked = true; + this.state.paused = true; + }; + + this.unlock = () => { + this.state.locked = false; + this.state.paused = false; + }; + + this.isLocked = () => { + return this.state.locked; + }; +} + +JobScrollService.$inject = ['$q', '$timeout']; + +export default JobScrollService; From 3705169de0e25ba25ec0e62544279f2be0c21cec Mon Sep 17 00:00:00 2001 From: gconsidine Date: Fri, 2 Mar 2018 16:23:44 -0500 Subject: [PATCH 46/89] Remove stream service from job index includes --- awx/ui/client/features/output/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index a0e0b70123..bc73ca83c7 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -4,6 +4,7 @@ import atLibComponents from '~components'; import Strings from '~features/output/jobs.strings'; import Controller from '~features/output/index.controller'; import PageService from '~features/output/page.service'; +import RenderService from '~features/output/render.service'; import ScrollService from '~features/output/scroll.service'; const Template = require('~features/output/index.view.html'); From 60a19246ae44c2bf038abf9dc97d1fb102b8a6da Mon Sep 17 00:00:00 2001 From: gconsidine Date: Tue, 6 Mar 2018 17:47:22 -0500 Subject: [PATCH 47/89] Add promise-based event processesing for consistency --- .../features/output/index.controller.js | 180 +++++++------ awx/ui/client/features/output/page.service.js | 245 ++++++++++-------- .../client/features/output/scroll.service.js | 7 + 3 files changed, 244 insertions(+), 188 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 25ebb9b531..e2aeae2d90 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -10,6 +10,8 @@ let render; let scroll; let resource; +let chain; + function JobsIndexController ( _resource_, _page_, @@ -56,6 +58,7 @@ function JobsIndexController ( const stream = false; // TODO: Set in route + chain = $q.resolve(); render.requestAnimationFrame(() => init()); } @@ -81,37 +84,39 @@ function init (stream) { } function process (scope, data) { - if (data.event === JOB_START) { - vm.stream.active = true; - scroll.lock(); - } else if (data.event === JOB_END) { - vm.stream.active = false; - } + chain = chain.then(() => { + if (data.event === JOB_START) { + vm.stream.active = true; + scroll.lock(); + } else if (data.event === JOB_END) { + vm.stream.active = false; + } - const pageAdded = page.addToBuffer(data); + const pageAdded = page.addToBuffer(data); - if (pageAdded && !scroll.isLocked()) { - vm.stream.paused = true; - } + if (pageAdded && !scroll.isLocked()) { + vm.stream.paused = true; + } - if (vm.stream.paused && scroll.isLocked()) { - vm.stream.paused = false; - } + if (vm.stream.paused && scroll.isLocked()) { + vm.stream.paused = false; + } - if (vm.stream.rendering || vm.stream.paused) { - return; - } + if (vm.stream.rendering || vm.stream.paused) { + return; + } - const events = page.emptyBuffer(); + const events = page.emptyBuffer(); - return renderStream(events); + return renderStream(events); + }) } function renderStream (events) { vm.stream.rendering = true; return shift() - .then(() => append(events)) + .then(() => append(events, true)) .then(() => { if (scroll.isLocked()) { scroll.setScrollPosition(scroll.getScrollHeight()); @@ -135,6 +140,12 @@ function renderStream (events) { function devClear () { init(true); render.clear(); + + vm.stream = { + active: false, + rendering: false, + paused: false + }; } function next () { @@ -167,23 +178,22 @@ function previous () { }) .then(() => { const currentHeight = scroll.getScrollHeight(); - scroll.setScrollPosition(currentHeight - postPopHeight + initialPosition); }); }); } -function append (events) { +function append (events, stream) { return render.append(events) .then(count => { - page.updateLineCount('current', count); + page.updateLineCount(count, stream); }); } function prepend (events) { return render.prepend(events) .then(count => { - page.updateLineCount('current', count); + page.updateLineCount(count); }); } @@ -192,7 +202,7 @@ function pop () { return $q.resolve(); } - const lines = page.trim('right'); + const lines = page.trim(); return render.pop(lines); } @@ -202,11 +212,71 @@ function shift () { return $q.resolve(); } - const lines = page.trim('left'); + const lines = page.trim(true); return render.shift(lines); } +function scrollHome () { + scroll.pause(); + + return page.first() + .then(events => { + if (!events) { + return; + } + + return render.clear() + .then(() => prepend(events)) + .then(() => { + scroll.resetScrollPosition(); + scroll.resume(); + }); + }); +} + +function scrollEnd () { + if (scroll.isLocked()) { + page.setBookmark(); + scroll.unlock(); + + return; + } else if (!scroll.isLocked() && vm.stream.active) { + page.removeBookmark(); + scroll.lock(); + + return; + } + + scroll.pause(); + + return page.last() + .then(events => { + if (!events) { + return; + } + + return render.clear() + .then(() => append(events)) + .then(() => { + scroll.setScrollPosition(scroll.getScrollHeight()); + scroll.resume(); + }); + }); +} + +function scrollPageUp () { + scroll.pageUp(); +} + +function scrollPageDown () { + scroll.pageDown(); +} + +function scrollIsAtRest (isAtRest) { + vm.scroll.showBackToTop = !isAtRest; +} + function expand () { vm.toggle(parent, true); } @@ -250,66 +320,6 @@ function toggle (uuid, menu) { } } -function scrollHome () { - scroll.pause(); - - return page.first() - .then(events => { - if (!events) { - return; - } - - return render.clear() - .then(() => render.prepend(events)) - .then(() => { - scroll.setScrollPosition(0); - scroll.resume(); - }); - }); -} - -function scrollEnd () { - if (scroll.isLocked()) { - page.bookmark(); - scroll.unlock(); - - return; - } else if (!scroll.isLocked() && vm.stream.active) { - page.bookmark(); - scroll.lock(); - - return; - } - - scroll.pause(); - - return page.last() - .then(events => { - if (!events) { - return; - } - - return render.clear() - .then(() => render.append(events)) - .then(() => { - scroll.setScrollPosition(scroll.getScrollHeight()); - scroll.resume(); - }); - }); -} - -function scrollPageUp () { - scroll.pageUp(); -} - -function scrollPageDown () { - scroll.pageDown(); -} - -function scrollIsAtRest (isAtRest) { - vm.scroll.showBackToTop = !isAtRest; -} - JobsIndexController.$inject = [ 'resource', 'JobPageService', diff --git a/awx/ui/client/features/output/page.service.js b/awx/ui/client/features/output/page.service.js index e91b80191e..a89d56d6b8 100644 --- a/awx/ui/client/features/output/page.service.js +++ b/awx/ui/client/features/output/page.service.js @@ -5,12 +5,21 @@ function JobPageService ($q) { this.page = { limit: this.resource.page.pageLimit, size: this.resource.page.size, - current: 0, - index: -1, - count: 0, - first: 0, - last: 0, - bookmark: { + cache: [], + state: { + count: 0, + current: 0, + first: 0, + last: 0 + } + }; + + this.bookmark = { + pending: false, + set: false, + cache: [], + state: { + count: 0, first: 0, last: 0, current: 0 @@ -25,43 +34,45 @@ function JobPageService ($q) { this.buffer = { count: 0 }; - - this.cache = []; }; - this.add = (page, position, bookmark) => { - page.events = page.events || []; - page.lines = page.lines || 0; + this.addPage = (number, events, push, reference) => { + const page = { number, events, lines: 0 }; + reference = reference || this.getActiveReference(); - if (position === 'first') { - this.cache.unshift(page); - this.page.first = page.number; - this.page.last = this.cache[this.cache.length -1].number; + if (push) { + reference.cache.push(page); + reference.state.last = page.number; + reference.state.first = reference.cache[0].number; } else { - this.cache.push(page); - this.page.last = page.number; - this.page.first = this.cache[0].number; + reference.cache.unshift(page); + reference.state.first = page.number; + reference.state.last = reference.cache[reference.cache.length -1].number; } - if (bookmark) { - this.page.bookmark.current = page.number; - } + reference.state.current = page.number; + reference.state.count++; + }; - this.page.current = page.number; - this.page.count++; + this.addToPageCache = (index, event, reference) => { + reference.cache[index].events.push(event); }; this.addToBuffer = event => { + const reference = this.getReference(); let pageAdded = false; if (this.result.count % this.page.size === 0) { - pageAdded = true; + this.addPage(reference.state.current + 1, [event], true, reference); - this.add({ number: this.page.count + 1, events: [event] }); + if (this.isBookmarkPending()) { + this.setBookmark(); + } this.trimBuffer(); + pageAdded = true; } else { - this.cache[this.cache.length - 1].events.push(event); + this.addToPageCache(reference.cache.length - 1, event, reference); } this.buffer.count++; @@ -71,104 +82,127 @@ function JobPageService ($q) { }; this.trimBuffer = () => { - const diff = this.cache.length - this.page.limit; + const reference = this.getReference(); + const diff = reference.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 = []; + if (reference.cache[i].events) { + this.buffer.count -= reference.cache[i].events.length; + reference.cache[i].events.splice(0, reference.cache[i].events.length); } } }; this.emptyBuffer = () => { + const reference = this.getReference(); let data = []; - for (let i = 0; i < this.cache.length; i++) { - const events = this.cache[i].events; + for (let i = 0; i < reference.cache.length; i++) { + const count = reference.cache[i].events.length; - if (events.length > 0) { - this.buffer.count -= events.length; - data = data.concat(this.cache[i].events.splice(0, events.length)); + if (count > 0) { + this.buffer.count -= count; + data = data.concat(reference.cache[i].events.splice(0, count)); } } return data; }; - this.emptyCache = () => { - this.page.first = this.page.current; - this.page.last = this.page.current; - this.cache = []; + this.emptyCache = number => { + const reference = this.getActiveReference(); + + number = number || reference.state.current; + + reference.state.first = number; + reference.state.last = number; + reference.state.current = number; + reference.cache.splice(0, reference.cache.length); }; this.isOverCapacity = () => { - return (this.cache.length - this.page.limit) > 0; + const reference = this.getActiveReference(); + + return (reference.cache.length - this.page.limit) > 0; }; - this.trim = side => { - const count = this.cache.length - this.page.limit; - + this.trim = left => { + let reference = this.getActiveReference(); + let excess = reference.cache.length - this.page.limit; let ejected; - if (side === 'left') { - ejected = this.cache.splice(0, count); - this.page.first = this.cache[0].number; + if (left) { + ejected = reference.cache.splice(0, excess); + reference.state.first = reference.cache[0].number; } else { - ejected = this.cache.splice(-count); - this.page.last = this.cache[this.cache.length - 1].number; + ejected = reference.cache.splice(-excess); + reference.state.last = reference.cache[reference.cache.length - 1].number; } return ejected.reduce((total, page) => total + page.lines, 0); }; - this.getPageNumber = page => { - let index; - - if (page === 'first') { - index = 0; - } - - return this.cache[index].number; + this.isPageBookmarked = number => { + return number >= this.page.bookmark.first && number <= this.page.bookmark.last; }; - this.updateLineCount = (page, lines) => { - let index; + this.updateLineCount = (lines, stream) => { + let reference; - if (page === 'current') { - index = this.cache.findIndex(item => item.number === this.page.current); + if (stream) { + reference = this.getReference(); + } else { + reference = this.getActiveReference(); } - this.cache[index].lines += lines; + const index = reference.cache.findIndex(item => item.number === reference.state.current); + + reference.cache[index].lines += lines; } - this.bookmark = () => { - 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; - this.page.bookmark.active = true; - } else { - this.page.bookmark.active = false; + this.isBookmarkPending = () => { + return this.bookmark.pending; + }; + + this.isBookmarkSet = () => { + return this.bookmark.set; + }; + + this.setBookmark = () => { + if (this.isBookmarkSet()) { + return; } + + if (!this.isBookmarkPending()) { + this.bookmark.pending = true; + + return; + } + + this.bookmark.state.first = this.page.state.first; + this.bookmark.state.last = this.page.state.last; + this.bookmark.state.current = this.page.state.current; + this.bookmark.cache = JSON.parse(JSON.stringify(this.page.cache)); + this.bookmark.set = true; + this.bookmark.pending = false; + }; + + this.removeBookmark = () => { + this.bookmark.set = false; + this.bookmark.pending = false; + this.bookmark.cache.splice(0, this.bookmark.cache.length); + this.bookmark.state.first = 0; + this.bookmark.state.last = 0; + this.bookmark.state.current = 0; }; 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); + const reference = this.getActiveReference(); + const config = this.buildRequestConfig(reference.state.last + 1); return this.resource.model.goToPage(config) .then(data => { @@ -176,24 +210,15 @@ function JobPageService ($q) { return $q.resolve(); } - this.add({ number: data.page, events: [], lines: 0 }, 'last', bookmark); + this.addPage(data.page, [], true); 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); + const reference = this.getActiveReference(); + const config = this.buildRequestConfig(reference.state.first - 1); return this.resource.model.goToPage(config) .then(data => { @@ -201,7 +226,7 @@ function JobPageService ($q) { return $q.resolve(); } - this.add({ number: data.page, events: [], lines: 0 }, 'first', bookmark); + this.addPage(data.page, [], false); return data.results; }); @@ -210,15 +235,14 @@ function JobPageService ($q) { 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'); + this.emptyCache(data.page); + this.addPage(data.page, [], true); return data.results; }); @@ -227,23 +251,22 @@ function JobPageService ($q) { 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'); + this.emptyCache(data.page); + this.addPage(data.page, [], false); return data.results; }); }; - this.buildRequestConfig = (page) => { + this.buildRequestConfig = number => { return { - page, + page: number, related: this.resource.related, params: { order_by: 'start_line' @@ -251,8 +274,24 @@ function JobPageService ($q) { }; }; - this.current = () => { - return this.resource.model.get(`related.${this.resource.related}.results`); + this.getActiveReference = () => { + return this.isBookmarkSet() ? this.getReference(true) : this.getReference(); + }; + + this.getReference = (bookmark) => { + if (bookmark) { + return { + bookmark: true, + cache: this.bookmark.cache, + state: this.bookmark.state + }; + } + + return { + bookmark: false, + cache: this.page.cache, + state: this.page.state + }; }; } diff --git a/awx/ui/client/features/output/scroll.service.js b/awx/ui/client/features/output/scroll.service.js index 489871e354..ef80655bf5 100644 --- a/awx/ui/client/features/output/scroll.service.js +++ b/awx/ui/client/features/output/scroll.service.js @@ -130,6 +130,13 @@ function JobScrollService ($q, $timeout) { this.isAtRest(); }; + this.resetScrollPosition = () => { + this.position.previous = 0; + this.position.current = 0; + this.el[0].scrollTop = 0; + this.isAtRest(); + }; + this.isAtRest = () => { if (this.position.current === 0 && !this.state.top) { this.state.top = true; From 1cc7d5535e6c284eb7e433c69321d970e414ce7e Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Sun, 4 Mar 2018 16:04:16 -0500 Subject: [PATCH 48/89] hacking together some basic bootstrapping for job results view - not yet an actual test --- awx/ui/test/e2e/tests/test-jobz.js | 118 +++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 awx/ui/test/e2e/tests/test-jobz.js diff --git a/awx/ui/test/e2e/tests/test-jobz.js b/awx/ui/test/e2e/tests/test-jobz.js new file mode 100644 index 0000000000..9f76fc7bec --- /dev/null +++ b/awx/ui/test/e2e/tests/test-jobz.js @@ -0,0 +1,118 @@ +import uuid from 'uuid'; + +import { + get, + post, +} from '../api'; +import { + getAdminMachineCredential, + getInventory, + getOrCreate, + getOrganization, + waitForJob, +} from '../fixtures'; + +// AWX_E2E_URL='https://localhost:3000' npm --prefix awx/ui run e2e -- --filter="*jobz*" + +const session = `e2e-${uuid().substr(0, 8)}`; + +const SCM_URL = 'https://github.com/jakemcdermott/ansible-playbooks'; +const PLAYBOOK = 'setfact_50.yml'; +const PARAMS = '?job_event_search=page_size:200;order_by:start_line;not__event__in:playbook_on_start,playbook_on_play_start,playbook_on_task_start,playbook_on_stats;task:set'; + +let data; + +const waitForJobz = endpoint => { + const interval = 2000; + const statuses = ['successful', 'failed', 'error', 'canceled']; + + let attempts = 20; + + return new Promise((resolve, reject) => { + (function pollStatus () { + get(endpoint).then(update => { + const completed = statuses.indexOf(update.data.status) > -1; + + if (completed) { + return resolve(update.data); + } + + if (--attempts <= 0) { + return reject(new Error('Retry limit exceeded.')); + } + + return setTimeout(pollStatus, interval); + }); + }()); + }); +}; + +const getProject = (namespace = session) => getOrganization(namespace) + .then(organization => getOrCreate('/projects/', { + name: `${namespace}-project`, + description: namespace, + organization: organization.id, + scm_url: SCM_URL, + scm_type: 'git' + }) + .then(project => { + if (project.related.current_update) { + return waitForJobz(project.related.current_update) + .then(() => project); + } + return project; + })); + +const getJobTemplate = (namespace = session) => { + const promises = [ + getInventory(namespace), + getAdminMachineCredential(namespace), + getProject(namespace) + ]; + + return Promise.all(promises) + .then(([inventory, credential, project]) => getOrCreate('/job_templates/', { + name: `${namespace}-job-template`, + description: namespace, + inventory: inventory.id, + credential: credential.id, + project: project.id, + playbook: PLAYBOOK, + })); +}; + +const getJob = (namespace = session) => getJobTemplate(namespace) + .then(template => { + if (template.related.last_job) { + return waitForJobz(template.related.last_job); + } + + return post(template.related.launch, {}) + .then(res => waitForJobz(res.data.url)); + }); + +module.exports = { + before: (client, done) => { + getJob() + .then(job => { + data = { job }; + done(); + }) + }, + 'test jobz': client => { + const location = `${client.globals.launch_url}/#/jobz/playbook/${data.job.id}`; + const templates = client.page.templates(); + + client.useCss(); + client.resizeWindow(1200, 800); + client.login(); + client.waitForAngular(); + + // client.url(location); + client.url(`${location}${PARAMS}`); + + client.pause(); + + client.end(); + }, +}; From c12173233b74cea537c77328ab1b8af7130c767d Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Sun, 4 Mar 2018 16:11:45 -0500 Subject: [PATCH 49/89] add initial test and sanity check for search tags --- awx/ui/test/e2e/tests/test-searchez.js | 125 +++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 awx/ui/test/e2e/tests/test-searchez.js diff --git a/awx/ui/test/e2e/tests/test-searchez.js b/awx/ui/test/e2e/tests/test-searchez.js new file mode 100644 index 0000000000..9442ac66c0 --- /dev/null +++ b/awx/ui/test/e2e/tests/test-searchez.js @@ -0,0 +1,125 @@ +import { range } from 'lodash'; + +import { getAdminMachineCredential } from '../fixtures'; + +// AWX_E2E_URL='https://localhost:3000' npm --prefix awx/ui run e2e -- --filter="*jobz*" + +const spinny = 'div.spinny'; +const searchInput = 'smart-search input'; +const searchSubmit = 'smart-search i[class*="search"]'; +const searchTags = 'smart-search .SmartSearch-tagContainer'; +const searchClearAll = 'smart-search .SmartSearch-clearAll'; +const searchTagDelete = 'i[class*="fa-times"]'; + +const createTagSelector = n => `${searchTags}:nth-of-type(${n})`; +const createTagDeleteSelector = n => `${searchTags}:nth-of-type(${n}) ${searchTagDelete}`; + +const checkTags = (client, tags) => { + const strategy = 'css selector'; + + const countReached = createTagSelector(tags.length); + const countExceeded = createTagSelector(tags.length + 1); + + if (tags.length > 0) { + client.waitForElementVisible(countReached); + client.waitForElementNotPresent(countExceeded); + } + + client.elements(strategy, searchTags, tagElements => { + client.assert.equal(tagElements.value.length, tags.length); + + let n = -1; + tagElements.value.map(o => o.ELEMENT).forEach(id => { + client.elementIdText(id, ({ value }) => { + client.assert.equal(value, tags[++n]); + }); + }); + }); +}; + +module.exports = { + before: (client, done) => { + const resources = range(25).map(n => getAdminMachineCredential(`test-search-${n}`)); + Promise.all(resources).then(done); + }, + 'add and remove search tags': client => { + const credentials = client.page.credentials(); + + client.login(); + client.waitForAngular(); + + credentials.section.navigation.waitForElementVisible('@credentials'); + credentials.section.navigation.click('@credentials'); + + client.waitForElementVisible(spinny); + client.waitForElementNotVisible(spinny); + + client.waitForElementVisible(searchInput); + client.waitForElementVisible(searchSubmit); + + client.expect.element(searchInput).enabled; + client.expect.element(searchSubmit).enabled; + + checkTags(client, []); + + client.setValue(searchInput, 'foo'); + client.click(searchSubmit); + client.waitForElementVisible(spinny); + client.waitForElementNotVisible(spinny); + + checkTags(client, ['foo']); + + client.setValue(searchInput, 'bar e2e'); + client.click(searchSubmit); + client.waitForElementVisible(spinny); + client.waitForElementNotVisible(spinny); + + checkTags(client, ['foo', 'bar', 'e2e']); + + client.click(searchClearAll); + client.waitForElementVisible(spinny); + client.waitForElementNotVisible(spinny); + + checkTags(client, []); + + client.setValue(searchInput, 'fiz name:foo'); + client.click(searchSubmit); + client.waitForElementVisible(spinny); + client.waitForElementNotVisible(spinny); + + checkTags(client, ['fiz', 'name:foo']); + + client.click(searchClearAll); + client.waitForElementVisible(spinny); + client.waitForElementNotVisible(spinny); + + checkTags(client, []); + + client.setValue(searchInput, 'hello name:world fiz'); + client.click(searchSubmit); + client.waitForElementVisible(spinny); + client.waitForElementNotVisible(spinny); + + checkTags(client, ['hello', 'fiz', 'name:world']); + + client.click(createTagDeleteSelector(2)); + client.waitForElementVisible(spinny); + client.waitForElementNotVisible(spinny); + + checkTags(client, ['hello', 'name:world']); + + client.click(createTagDeleteSelector(1)); + client.waitForElementVisible(spinny); + client.waitForElementNotVisible(spinny); + + checkTags(client, ['name:world']); + + client.click(createTagDeleteSelector(1)); + client.waitForElementVisible(spinny); + client.waitForElementNotVisible(spinny); + + checkTags(client, []); + + client.end(); + }, +}; \ No newline at end of file From 0adf671de4b5f2fa82e334ed2ef1fb34cd137d85 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Sun, 4 Mar 2018 16:25:10 -0500 Subject: [PATCH 50/89] refactor, lint, separate data transformation logic from display logic --- .../shared/smart-search/queryset.service.js | 54 +- .../smart-search/smart-search.controller.js | 768 +++++++++--------- .../smart-search/smart-search.partial.html | 4 +- 3 files changed, 424 insertions(+), 402 deletions(-) diff --git a/awx/ui/client/src/shared/smart-search/queryset.service.js b/awx/ui/client/src/shared/smart-search/queryset.service.js index 943f63818b..7d07139c4c 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -44,19 +44,16 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear key = this.replaceDefaultFlags(key); if (!Array.isArray(values)) { - values = this.replaceEncodedTokens(values); - - return `${key}=${values}`; + values = [values]; } return values .map(value => { value = this.replaceDefaultFlags(value); value = this.replaceEncodedTokens(value); - - return `${key}=${value}`; + return [key, value] }) - .join('&'); + }, // encodes ui-router params from {operand__key__comparator: value} pairs to API-consumable URL encodeQueryset(params) { @@ -69,15 +66,33 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear result += '&'; } - return result += this.encodeTerms(value, key); + const encodedTermString = this.encodeTerms(value, key) + .map(([key, value]) => `${key}=${value}`) + .join('&'); + + return result += encodedTermString; }, '?'); }, + // like encodeQueryset, but return an actual unstringified API-consumable http param object + encodeQuerysetObject(params) { + return _.reduce(params, (obj, value, key) => { + const encodedTerms = this.encodeTerms(value, key); + + for (let encodedIndex in encodedTerms) { + const [encodedKey, encodedValue] = encodedTerms[encodedIndex]; + obj[encodedKey] = obj[encodedKey] || []; + obj[encodedKey].push(encodedValue) + } + + return obj; + }, {}); + }, // encodes a ui smart-search param to a django-friendly param // operand:key:comparator:value => {operand__key__comparator: value} - encodeParam(params){ + encodeParam({ term, relatedSearchTerm, searchTerm, singleSearchParam }){ // Assumption here is that we have a key and a value so the length // of the paramParts array will be 2. [0] is the key and [1] the value - let paramParts = SmartSearchService.splitTermIntoParts(params.term); + let paramParts = SmartSearchService.splitTermIntoParts(term); let keySplit = paramParts[0].split('.'); let exclude = false; let lessThanGreaterThan = paramParts[1].match(/^(>|<).*$/) ? true : false; @@ -88,16 +103,16 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear let paramString = exclude ? "not__" : ""; let valueString = paramParts[1]; if(keySplit.length === 1) { - if(params.searchTerm && !lessThanGreaterThan) { - if(params.singleSearchParam) { + if(searchTerm && !lessThanGreaterThan) { + if(singleSearchParam) { paramString += keySplit[0] + '__icontains'; } else { paramString += keySplit[0] + '__icontains_DEFAULT'; } } - else if(params.relatedSearchTerm) { - if(params.singleSearchParam) { + else if(relatedSearchTerm) { + if(singleSearchParam) { paramString += keySplit[0]; } else { @@ -131,8 +146,8 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear } } - if(params.singleSearchParam) { - return {[params.singleSearchParam]: paramString + "=" + valueString}; + if(singleSearchParam) { + return {[singleSearchParam]: paramString + "=" + valueString}; } else { return {[paramString] : encodeURIComponent(valueString)}; @@ -189,7 +204,14 @@ export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSear return decodeParamString(value); } }, - + convertToSearchTags(obj) { + const tags = []; + for (let key in obj) { + const value = obj[key]; + tags.push(this.decodeParam(value, key)); + } + return tags; + }, // encodes a django queryset for ui-router's URLMatcherFactory // {operand__key__comparator: value, } => 'operand:key:comparator:value;...' // value.isArray expands to: diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js index e7dd435307..5a59bbde81 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js +++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js @@ -1,422 +1,422 @@ -export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', 'SmartSearchService', 'i18n', 'ConfigService', '$transitions', - function($stateParams, $scope, $state, GetBasePath, qs, SmartSearchService, i18n, configService, $transitions) { +function SmartSearchController ( + $scope, + $state, + $stateParams, + $transitions, + configService, + GetBasePath, + i18n, + qs, + SmartSearchService +) { + const searchKey = `${$scope.iterator}_search`; + const optionsKey = `${$scope.list.iterator}_options`; - let path, - defaults, - queryset, - transitionSuccessListener; + let path; + let defaults; + let queryset; + let transitionSuccessListener; - configService.getConfig() - .then(config => init(config)); + configService.getConfig() + .then(config => init(config)); - function init(config) { - let version; + function init (config) { + let version; - try { - version = config.version.split('-')[0]; - } catch (err) { - version = 'latest'; - } + try { + [version] = config.version.split('-'); + } catch (err) { + version = 'latest'; + } - $scope.documentationLink = `http://docs.ansible.com/ansible-tower/${version}/html/userguide/search_sort.html`; + $scope.documentationLink = `http://docs.ansible.com/ansible-tower/${version}/html/userguide/search_sort.html`; + $scope.searchPlaceholder = i18n._('Search'); - if($scope.defaultParams) { - defaults = $scope.defaultParams; - } - else { - // steps through the current tree of $state configurations, grabs default search params - defaults = _.find($state.$current.path, (step) => { - if(step && step.params && step.params.hasOwnProperty(`${$scope.iterator}_search`)){ - return step.params.hasOwnProperty(`${$scope.iterator}_search`); - } - }).params[`${$scope.iterator}_search`].config.value; - } + if ($scope.defaultParams) { + defaults = $scope.defaultParams; + } else { + // steps through the current tree of $state configurations, grabs default search params + const stateConfig = _.find($state.$current.path, step => _.has(step, `params.${searchKey}`)); + defaults = stateConfig.params[searchKey].config.value; + } - if($scope.querySet) { - queryset = _.cloneDeep($scope.querySet); - } - else { - queryset = $state.params[`${$scope.iterator}_search`]; - } + if ($scope.querySet) { + queryset = _.cloneDeep($scope.querySet); + } else { + queryset = $state.params[searchKey]; + } - path = GetBasePath($scope.basePath) || $scope.basePath; - generateSearchTags(); - qs.initFieldset(path, $scope.djangoModel).then((data) => { + path = GetBasePath($scope.basePath) || $scope.basePath; + generateSearchTags(); + + qs.initFieldset(path, $scope.djangoModel) + .then((data) => { $scope.models = data.models; $scope.options = data.options.data; if ($scope.list) { - $scope.$emit(`${$scope.list.iterator}_options`, data.options); - } - }); - $scope.searchPlaceholder = $scope.disableSearch ? i18n._('Cannot search running job') : i18n._('Search'); - - function compareParams(a, b) { - for (let key in a) { - if (!(key in b) || a[key].toString() !== b[key].toString()) { - return false; - } - } - for (let key in b) { - if (!(key in a)) { - return false; - } - } - return true; - } - - if(transitionSuccessListener) { - transitionSuccessListener(); - } - - transitionSuccessListener = $transitions.onSuccess({}, function(trans) { - // State has changed - check to see if this is a param change - if(trans.from().name === trans.to().name) { - if(!compareParams(trans.params('from')[`${$scope.iterator}_search`], trans.params('to')[`${$scope.iterator}_search`])) { - // Params are not the same - we need to update the search. This should only happen when the user - // hits the forward/back navigation buttons in their browser. - queryset = trans.params('to')[`${$scope.iterator}_search`]; - qs.search(path, queryset).then((res) => { - $scope.dataset = res.data; - $scope.collection = res.data.results; - $scope.$emit('updateDataset', res.data); - }); - - $scope.searchTerm = null; - generateSearchTags(); - } + $scope.$emit(optionsKey, data.options); } }); - $scope.$on('$destroy', transitionSuccessListener); + function compareParams(a, b) { + for (let key in a) { + if (!(key in b) || a[key].toString() !== b[key].toString()) { + return false; + } + } + for (let key in b) { + if (!(key in a)) { + return false; + } + } + return true; + } - $scope.$watch('disableSearch', function(disableSearch){ - if(disableSearch) { - $scope.searchPlaceholder = i18n._('Cannot search running job'); - } - else { - $scope.searchPlaceholder = i18n._('Search'); + if (transitionSuccessListener) { + transitionSuccessListener(); + } + + transitionSuccessListener = $transitions.onSuccess({}, trans => { + // State has changed - check to see if this is a param change + if (trans.from().name === trans.to().name) { + if (!compareParams(trans.params('from')[searchKey], trans.params('to')[searchKey])) { + // Params are not the same - we need to update the search. This should only + // happen when the user hits the forward/back browser navigation buttons. + queryset = trans.params('to')[searchKey]; + qs.search(path, queryset).then((res) => { + $scope.dataset = res.data; + $scope.collection = res.data.results; + $scope.$emit('updateDataset', res.data); + }); + + $scope.searchTerm = null; + generateSearchTags(); } + } + }); + + $scope.$on('$destroy', transitionSuccessListener); + $scope.$watch('disableSearch', disableSearch => { + if (disableSearch) { + $scope.searchPlaceholder = i18n._('Cannot search running job'); + } else { + $scope.searchPlaceholder = i18n._('Search'); + } + }); + } + + function generateSearchTags () { + $scope.searchTags = []; + + const querysetCopy = angular.copy(queryset); + + if ($scope.singleSearchParam && querysetCopy[$scope.singleSearchParam]) { + const searchParam = querysetCopy[$scope.singleSearchParam].split('%20and%20'); + delete querysetCopy[$scope.singleSearchParam]; + + $.each(searchParam, (index, param) => { + const paramParts = decodeURIComponent(param).split(/=(.+)/); + const reconstructedSearchString = qs.decodeParam(paramParts[1], paramParts[0]); + $scope.searchTags.push(reconstructedSearchString); }); } - function generateSearchTags() { - $scope.searchTags = []; + $scope.searchTags = $scope.searchTags.concat(qs.stripDefaultParams(querysetCopy, defaults)); + } - let querysetCopy = angular.copy(queryset); - - if($scope.singleSearchParam && querysetCopy[$scope.singleSearchParam]) { - let searchParam = querysetCopy[$scope.singleSearchParam].split('%20and%20'); - delete querysetCopy[$scope.singleSearchParam]; - - $.each(searchParam, function(index, param) { - let paramParts = decodeURIComponent(param).split(/=(.+)/); - let reconstructedSearchString = qs.decodeParam(paramParts[1], paramParts[0]); - $scope.searchTags.push(reconstructedSearchString); - }); + function revertSearch (queryToBeRestored) { + queryset = queryToBeRestored; + // https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic + // This transition will not reload controllers/resolves/views + // but will register new $stateParams[$scope.iterator + '_search'] terms + if (!$scope.querySet) { + $state.go('.', { [searchKey]: queryset }); + } + qs.search(path, queryset).then((res) => { + if ($scope.querySet) { + $scope.querySet = queryset; } + $scope.dataset = res.data; + $scope.collection = res.data.results; + }); - $scope.searchTags = $scope.searchTags.concat(qs.stripDefaultParams(querysetCopy, defaults)); + $scope.searchTerm = null; + + generateSearchTags(); + } + + $scope.toggleKeyPane = () => { + $scope.showKeyPane = !$scope.showKeyPane; + }; + + function searchWithoutKey (term, singleSearchParam = null) { + if (singleSearchParam) { + return { [singleSearchParam]: `search=${encodeURIComponent(term)}` }; + } + return { search: encodeURIComponent(term) }; + } + + function isAnsibleFactSearchTerm (termParts) { + const rootField = termParts[0].split('.')[0].replace(/^-/, ''); + return rootField === 'ansible_facts'; + } + + function isRelatedField (termParts) { + const rootField = termParts[0].split('.')[0].replace(/^-/, ''); + const listName = $scope.list.name; + const baseRelatedTypePath = `models.${listName}.base.${rootField}.type`; + + const isRelatedSearchTermField = (_.contains($scope.models[listName].related, rootField)); + const isBaseModelRelatedSearchTermField = (_.get($scope, baseRelatedTypePath) === 'field'); + + return (isRelatedSearchTermField || isBaseModelRelatedSearchTermField); + } + + function getSearchInputQueryset ({ terms, singleSearchParam }) { + let params = {}; + + // remove leading/trailing whitespace + terms = (terms) ? terms.trim() : ''; + let splitTerms; + + if (singleSearchParam === 'host_filter') { + splitTerms = SmartSearchService.splitFilterIntoTerms(terms); + } else { + splitTerms = SmartSearchService.splitSearchIntoTerms(terms); } - function revertSearch(queryToBeRestored) { - queryset = queryToBeRestored; - // https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic - // This transition will not reload controllers/resolves/views - // but will register new $stateParams[$scope.iterator + '_search'] terms - if(!$scope.querySet) { - $state.go('.', { - [$scope.iterator + '_search']: queryset }); + const combineSameSearches = (a, b) => { + if (!a) { + return undefined; } - qs.search(path, queryset).then((res) => { - if($scope.querySet) { - $scope.querySet = queryset; - } - $scope.dataset = res.data; - $scope.collection = res.data.results; - }); - $scope.searchTerm = null; - - generateSearchTags(); - } - - function searchWithoutKey(term) { - if($scope.singleSearchParam) { - return { - [$scope.singleSearchParam]: "search=" + encodeURIComponent(term) - }; + if (_.isArray(a)) { + return a.concat(b); } - return { - search: encodeURIComponent(term) - }; - } - $scope.toggleKeyPane = function() { - $scope.showKeyPane = !$scope.showKeyPane; + if (singleSearchParam) { + return `${a}%20and%20${b}`; + } + + return [a, b]; }; - // add a search tag, merge new queryset, $state.go() - $scope.addTerm = function(terms) { - let params = {}, - origQueryset = _.clone(queryset); - - // Remove leading/trailing whitespace if there is any - terms = (terms) ? terms.trim() : ""; - - if(terms && terms !== '') { - let splitTerms; - - if ($scope.singleSearchParam === 'host_filter') { - splitTerms = SmartSearchService.splitFilterIntoTerms(terms); - } else { - splitTerms = SmartSearchService.splitSearchIntoTerms(terms); - } - - _.forEach(splitTerms, (term) => { - let termParts = SmartSearchService.splitTermIntoParts(term); - - function combineSameSearches(a,b){ - if (_.isArray(a)) { - return a.concat(b); - } - else { - if(a) { - if($scope.singleSearchParam) { - return a + "%20and%20" + b; - } - else { - return [a,b]; - } - } - } - } - - if($scope.singleSearchParam) { - if (termParts.length === 1) { - params = _.merge(params, searchWithoutKey(term), combineSameSearches); - } - else { - let root = termParts[0].split(".")[0].replace(/^-/, ''); - if(_.has($scope.models[$scope.list.name].base, root) || root === "ansible_facts") { - if(_.has($scope.models[$scope.list.name].base[root], "type") && $scope.models[$scope.list.name].base[root].type === 'field'){ - // Intent is to land here for searching on the base model. - params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true, singleSearchParam: $scope.singleSearchParam ? $scope.singleSearchParam : false}), combineSameSearches); - } - else { - // Intent is to land here when performing ansible_facts searches - params = _.merge(params, qs.encodeParam({term: term, searchTerm: true, singleSearchParam: $scope.singleSearchParam ? $scope.singleSearchParam : false}), combineSameSearches); - } - } - else if(_.contains($scope.models[$scope.list.name].related, root)) { - // Intent is to land here for related searches - params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true, singleSearchParam: $scope.singleSearchParam ? $scope.singleSearchParam : false}), combineSameSearches); - } - // Its not a search term or a related search term - treat it as a string - else { - params = _.merge(params, searchWithoutKey(term), combineSameSearches); - } - } - } - - else { - // if only a value is provided, search using default keys - if (termParts.length === 1) { - params = _.merge(params, searchWithoutKey(term), combineSameSearches); - } else { - // Figure out if this is a search term - let root = termParts[0].split(".")[0].replace(/^-/, ''); - if(_.has($scope.models[$scope.list.name].base, root)) { - if($scope.models[$scope.list.name].base[root].type && $scope.models[$scope.list.name].base[root].type === 'field') { - params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true}), combineSameSearches); - } - else { - params = _.merge(params, qs.encodeParam({term: term, searchTerm: true}), combineSameSearches); - } - } - // The related fields need to also be checked for related searches. - // The related fields for the search are retrieved from the API - // options endpoint, and are stored in the $scope.model. FYI, the - // Django search model is what sets the related fields on the model. - else if(_.contains($scope.models[$scope.list.name].related, root)) { - params = _.merge(params, qs.encodeParam({term: term, relatedSearchTerm: true}), combineSameSearches); - } - // Its not a search term or a related search term - treat it as a string - else { - params = _.merge(params, searchWithoutKey(term), combineSameSearches); - } - - } - } - }); - - queryset = _.merge({}, queryset, params, (objectValue, sourceValue, key, object) => { - if (object[key] && object[key] !== sourceValue){ - if(_.isArray(object[key])) { - // Add the new value to the array and return - object[key].push(sourceValue); - return object[key]; - } - else { - if($scope.singleSearchParam) { - if(!object[key]) { - return sourceValue; - } - else { - let singleSearchParamKeys = object[key].split("%20and%20"); - - if(_.includes(singleSearchParamKeys, sourceValue)) { - return object[key]; - } - else { - return object[key] + "%20and%20" + sourceValue; - } - } - } - // Start the array of keys - return [object[key], sourceValue]; - } - } - else { - // // https://lodash.com/docs/3.10.1#merge - // If customizer fn returns undefined merging is handled by default _.merge algorithm - return undefined; - } - }); - - // Go back to the first page after a new search - delete queryset.page; - - // https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic - // This transition will not reload controllers/resolves/views - // but will register new $stateParams[$scope.iterator + '_search'] terms - if(!$scope.querySet) { - $state.go('.', {[$scope.iterator + '_search']:queryset }).then(function(){ - // ISSUE: same as above in $scope.remove. For some reason deleting the page - // from the queryset works for all lists except lists in modals. - delete $stateParams[$scope.iterator + '_search'].page; - }); - } - qs.search(path, queryset).then((res) => { - if($scope.querySet) { - $scope.querySet = queryset; - } - $scope.dataset = res.data; - $scope.collection = res.data.results; - }) - .catch(function() { - revertSearch(origQueryset); - }); - - $scope.searchTerm = null; - - generateSearchTags(); - } - }; - - // remove tag, merge new queryset, $state.go - $scope.removeTerm = function(index) { - let tagToRemove = $scope.searchTags.splice(index, 1)[0], - termParts = SmartSearchService.splitTermIntoParts(tagToRemove), - removed; - - let removeFromQuerySet = function(set) { - _.each(removed, (value, key) => { - if (Array.isArray(set[key])){ - _.remove(set[key], (item) => item === value); - // If the array is now empty, remove that key - if (set[key].length === 0) { - delete set[key]; - } - } else { - if ($scope.singleSearchParam && set[$scope.singleSearchParam] && set[$scope.singleSearchParam].includes("%20and%20")) { - let searchParamParts = set[$scope.singleSearchParam].split("%20and%20"); - // The value side of each paramPart might have been encoded in SmartSearch.splitFilterIntoTerms - _.each(searchParamParts, (paramPart, paramPartIndex) => { - searchParamParts[paramPartIndex] = decodeURIComponent(paramPart); - }); - var index = searchParamParts.indexOf(value); - if (index !== -1) { - searchParamParts.splice(index, 1); - } - set[$scope.singleSearchParam] = searchParamParts.join("%20and%20"); - } else { - delete set[key]; - } - } - }); - }; + _.each(splitTerms, term => { + const termParts = SmartSearchService.splitTermIntoParts(term); + let termParams; if (termParts.length === 1) { - removed = searchWithoutKey(tagToRemove); + termParams = searchWithoutKey(term, singleSearchParam); + } else if (isAnsibleFactSearchTerm(termParts)) { + termParams = qs.encodeParam({ term, singleSearchParam }); + } else if (isRelatedField(termParts)) { + termParams = qs.encodeParam({ term, singleSearchParam, related: true }); } else { - let root = termParts[0].split(".")[0].replace(/^-/, ''); - let encodeParams = { - term: tagToRemove, - singleSearchParam: $scope.singleSearchParam ? $scope.singleSearchParam : false - }; - if($scope.models[$scope.list.name]) { - if($scope.singleSearchParam) { - removed = qs.encodeParam(encodeParams); - } - else if(_.has($scope.models[$scope.list.name].base, root)) { - if($scope.models[$scope.list.name].base[root].type && $scope.models[$scope.list.name].base[root].type === 'field') { - encodeParams.relatedSearchTerm = true; - } - else { - encodeParams.searchTerm = true; - } - removed = qs.encodeParam(encodeParams); - } - else if(_.contains($scope.models[$scope.list.name].related, root)) { - encodeParams.relatedSearchTerm = true; - removed = qs.encodeParam(encodeParams); - } - else { - removed = searchWithoutKey(termParts[termParts.length-1]); - } - } - else { - removed = searchWithoutKey(termParts[termParts.length-1]); - } + termParams = qs.encodeParam({ term, singleSearchParam }); } - removeFromQuerySet(queryset); - if (!$scope.querySet) { - $state.go('.', { - [$scope.iterator + '_search']: queryset }).then(function(){ - // ISSUE: for some reason deleting a tag from a list in a modal does not - // remove the param from $stateParams. Here we'll manually check to make sure - // that that happened and remove it if it didn't. - removeFromQuerySet($stateParams[`${$scope.iterator}_search`]); - }); - } - qs.search(path, queryset).then((res) => { - if($scope.querySet) { - $scope.querySet = queryset; - } + params = _.merge(params, termParams, combineSameSearches); + }); - $scope.dataset = res.data; - $scope.collection = res.data.results; - - generateSearchTags(); - }); - }; - - $scope.clearAllTerms = function(){ - let cleared = _.cloneDeep(defaults); - delete cleared.page; - queryset = cleared; - if(!$scope.querySet) { - $state.go('.', {[$scope.iterator + '_search']: queryset}); - } - qs.search(path, queryset).then((res) => { - if($scope.querySet) { - $scope.querySet = queryset; - } - $scope.dataset = res.data; - $scope.collection = res.data.results; - }); - $scope.searchTags = qs.stripDefaultParams(queryset, defaults); - }; + return params; } + + function mergeQueryset (qset, additional, singleSearchParam) { + const merged = _.merge({}, qset, additional, (objectValue, sourceValue, key, object) => { + if (!(object[key] && object[key] !== sourceValue)) { + // // https://lodash.com/docs/3.10.1#each + // If this returns undefined merging is handled by default _.merge algorithm + return undefined; + } + + if (_.isArray(object[key])) { + object[key].push(sourceValue); + return object[key]; + } + + if (singleSearchParam) { + if (!object[key]) { + return sourceValue; + } + + const singleSearchParamKeys = object[key].split('%20and%20'); + + if (_.includes(singleSearchParamKeys, sourceValue)) { + return object[key]; + } + + return `${object[key]}%20and%20${sourceValue}`; + } + + // Start the array of keys + return [object[key], sourceValue]; + }); + + return merged; + } + + $scope.addTerms = terms => { + const { singleSearchParam } = $scope; + const origQueryset = _.clone(queryset); + + // Remove leading/trailing whitespace if there is any + terms = (terms) ? terms.trim() : ''; + + if (!(terms && terms !== '')) { + return; + } + + const searchInputQueryset = getSearchInputQueryset({ terms, singleSearchParam }); + queryset = mergeQueryset(queryset, searchInputQueryset, singleSearchParam); + + // Go back to the first page after a new search + delete queryset.page; + + // https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic + // This transition will not reload controllers/resolves/views but will register new + // $stateParams[searchKey] terms. + if (!$scope.querySet) { + $state.go('.', { [searchKey]: queryset }) + .then(() => { + // same as above in $scope.remove. For some reason deleting the page + // from the queryset works for all lists except lists in modals. + delete $stateParams[searchKey].page; + }); + } + + qs.search(path, queryset) + .then(({ data }) => { + if ($scope.querySet) { + $scope.querySet = queryset; + } + $scope.dataset = data; + $scope.collection = data.results; + }) + .catch(() => revertSearch(origQueryset)); + + $scope.searchTerm = null; + + generateSearchTags(); + }; + + function removeTermsFromQueryset(qset, term, singleSearchParam = null) { + const returnedQueryset = _.cloneDeep(qset); + + const removeSingleTermFromQueryset = (value, key) => { + const space = '%20and%20'; + + if (Array.isArray(returnedQueryset[key])) { + returnedQueryset[key] = returnedQueryset[key].filter(item => item !== value); + if (returnedQueryset[key].length < 1) { + delete returnedQueryset[key]; + } + } else if (singleSearchParam && _.get(returnedQueryset, singleSearchParam, []).includes(space)) { + const searchParamParts = returnedQueryset[singleSearchParam].split(space); + // The value side of each paramPart might have been encoded in + // SmartSearch.splitFilterIntoTerms + _.each(searchParamParts, (paramPart, paramPartIndex) => { + searchParamParts[paramPartIndex] = decodeURIComponent(paramPart); + }); + + const paramPartIndex = searchParamParts.indexOf(value); + + if (paramPartIndex !== -1) { + searchParamParts.splice(paramPartIndex, 1); + } + + returnedQueryset[singleSearchParam] = searchParamParts.join(space); + + } else { + delete returnedQueryset[key]; + } + }; + + const termParts = SmartSearchService.splitTermIntoParts(term); + + let removed; + + if (termParts.length === 1) { + removed = searchWithoutKey(term, singleSearchParam); + } else if (isRelatedField(termParts)) { + removed = qs.encodeParam({ term, singleSearchParam, related: true }); + } else { + removed = qs.encodeParam({ term, singleSearchParam }); + } + + if (!removed) { + removed = searchWithoutKey(termParts[termParts.length - 1], singleSearchParam); + } + + _.each(removed, removeSingleTermFromQueryset); + + return returnedQueryset; + } + + // remove tag, merge new queryset, $state.go + $scope.removeTerm = index => { + const { singleSearchParam } = $scope; + const [term] = $scope.searchTags.splice(index, 1); + + const modifiedQueryset = removeTermsFromQueryset(queryset, term, singleSearchParam); + + if (!$scope.querySet) { + $state.go('.', { [searchKey]: modifiedQueryset }) + .then(() => { + // for some reason deleting a tag from a list in a modal does not + // remove the param from $stateParams. Here we'll manually check to make sure + // that that happened and remove it if it didn't. + const clearedParams = removeTermsFromQueryset( + $stateParams[searchKey], term, singleSearchParam); + $stateParams[searchKey] = clearedParams; + }); + } + + qs.search(path, queryset) + .then(({ data }) => { + if ($scope.querySet) { + $scope.querySet = queryset; + } + $scope.dataset = data; + $scope.collection = data.results; + }); + + generateSearchTags(); + }; + + $scope.clearAllTerms = () => { + const cleared = _.cloneDeep(defaults); + + delete cleared.page; + + queryset = cleared; + + if (!$scope.querySet) { + $state.go('.', { [searchKey]: queryset }); + } + + qs.search(path, queryset) + .then(({ data }) => { + if ($scope.querySet) { + $scope.querySet = queryset; + } + $scope.dataset = data; + $scope.collection = data.results; + }); + + $scope.searchTags = qs.stripDefaultParams(queryset, defaults); + }; +} + +SmartSearchController.$inject = [ + '$scope', + '$state', + '$stateParams', + '$transitions', + 'ConfigService', + 'GetBasePath', + 'i18n', + 'QuerySet', + 'SmartSearchService', ]; + +export default SmartSearchController; diff --git a/awx/ui/client/src/shared/smart-search/smart-search.partial.html b/awx/ui/client/src/shared/smart-search/smart-search.partial.html index 1f31adcf9e..ce52fea759 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.partial.html +++ b/awx/ui/client/src/shared/smart-search/smart-search.partial.html @@ -3,11 +3,11 @@
-
+
-
+
From 13162ca33ab877d7b52e5fa08b03bef46b864521 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Sun, 4 Mar 2018 16:29:41 -0500 Subject: [PATCH 51/89] move data transformation logic into a service so it can be reused --- .../shared/smart-search/queryset.service.js | 782 +++++++++++------- .../smart-search/smart-search.controller.js | 196 +---- 2 files changed, 492 insertions(+), 486 deletions(-) diff --git a/awx/ui/client/src/shared/smart-search/queryset.service.js b/awx/ui/client/src/shared/smart-search/queryset.service.js index 7d07139c4c..780900d32f 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -1,333 +1,507 @@ -export default ['$q', 'Rest', 'ProcessErrors', '$rootScope', 'Wait', 'DjangoSearchModel', 'SmartSearchService', - function($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, SmartSearchService) { - return { - // kick off building a model for a specific endpoint - // this is usually a list's basePath - // unified_jobs is the exception, where we need to fetch many subclass OPTIONS and summary_fields - initFieldset(path, name) { - let defer = $q.defer(); - defer.resolve(this.getCommonModelOptions(path, name)); - return defer.promise; - }, +function searchWithoutKey (term, singleSearchParam = null) { + if (singleSearchParam) { + return { [singleSearchParam]: `search=${encodeURIComponent(term)}` }; + } + return { search: encodeURIComponent(term) }; +} - getCommonModelOptions(path, name) { - let resolve, base, - defer = $q.defer(); +function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearchModel, SmartSearchService) { + return { + // kick off building a model for a specific endpoint + // this is usually a list's basePath + // unified_jobs is the exception, where we need to fetch many subclass OPTIONS and summary_fields + initFieldset(path, name) { + let defer = $q.defer(); + defer.resolve(this.getCommonModelOptions(path, name)); + return defer.promise; + }, + getCommonModelOptions(path, name) { + let resolve, base, + defer = $q.defer(); - this.url = path; - resolve = this.options(path) - .then((res) => { - base = res.data.actions.GET; - let relatedSearchFields = res.data.related_search_fields; - defer.resolve({ - models: { - [name]: new DjangoSearchModel(name, base, relatedSearchFields) - }, - options: res - }); + this.url = path; + resolve = this.options(path) + .then((res) => { + base = res.data.actions.GET; + let relatedSearchFields = res.data.related_search_fields; + defer.resolve({ + models: { + [name]: new DjangoSearchModel(name, base, relatedSearchFields) + }, + options: res }); - return defer.promise; - }, + }); + return defer.promise; + }, + replaceDefaultFlags (value) { + value = value.toString().replace(/__icontains_DEFAULT/g, "__icontains"); + value = value.toString().replace(/__search_DEFAULT/g, "__search"); - replaceDefaultFlags (value) { - value = value.toString().replace(/__icontains_DEFAULT/g, "__icontains"); - value = value.toString().replace(/__search_DEFAULT/g, "__search"); + return value; + }, + replaceEncodedTokens(value) { + return decodeURIComponent(value).replace(/"|'/g, ""); + }, + encodeTerms (values, key) { + key = this.replaceDefaultFlags(key); - return value; - }, + if (!Array.isArray(values)) { + values = [values]; + } - replaceEncodedTokens(value) { - return decodeURIComponent(value).replace(/"|'/g, ""); - }, + return values + .map(value => { + value = this.replaceDefaultFlags(value); + value = this.replaceEncodedTokens(value); + return [key, value] + }) - encodeTerms (values, key) { - key = this.replaceDefaultFlags(key); + }, + // encodes ui-router params from {operand__key__comparator: value} pairs to API-consumable URL + encodeQueryset(params) { + if (typeof params !== 'object') { + return ''; + } - if (!Array.isArray(values)) { - values = [values]; + return _.reduce(params, (result, value, key) => { + if (result !== '?') { + result += '&'; } - return values - .map(value => { - value = this.replaceDefaultFlags(value); - value = this.replaceEncodedTokens(value); - return [key, value] - }) + const encodedTermString = this.encodeTerms(value, key) + .map(([key, value]) => `${key}=${value}`) + .join('&'); - }, - // encodes ui-router params from {operand__key__comparator: value} pairs to API-consumable URL - encodeQueryset(params) { - if (typeof params !== 'object') { - return ''; + return result += encodedTermString; + }, '?'); + }, + // like encodeQueryset, but return an actual unstringified API-consumable http param object + encodeQuerysetObject(params) { + return _.reduce(params, (obj, value, key) => { + const encodedTerms = this.encodeTerms(value, key); + + for (let encodedIndex in encodedTerms) { + const [encodedKey, encodedValue] = encodedTerms[encodedIndex]; + obj[encodedKey] = obj[encodedKey] || []; + obj[encodedKey].push(encodedValue) } - return _.reduce(params, (result, value, key) => { - if (result !== '?') { - result += '&'; - } - - const encodedTermString = this.encodeTerms(value, key) - .map(([key, value]) => `${key}=${value}`) - .join('&'); - - return result += encodedTermString; - }, '?'); - }, - // like encodeQueryset, but return an actual unstringified API-consumable http param object - encodeQuerysetObject(params) { - return _.reduce(params, (obj, value, key) => { - const encodedTerms = this.encodeTerms(value, key); - - for (let encodedIndex in encodedTerms) { - const [encodedKey, encodedValue] = encodedTerms[encodedIndex]; - obj[encodedKey] = obj[encodedKey] || []; - obj[encodedKey].push(encodedValue) - } - - return obj; - }, {}); - }, - // encodes a ui smart-search param to a django-friendly param - // operand:key:comparator:value => {operand__key__comparator: value} - encodeParam({ term, relatedSearchTerm, searchTerm, singleSearchParam }){ - // Assumption here is that we have a key and a value so the length - // of the paramParts array will be 2. [0] is the key and [1] the value - let paramParts = SmartSearchService.splitTermIntoParts(term); - let keySplit = paramParts[0].split('.'); - let exclude = false; - let lessThanGreaterThan = paramParts[1].match(/^(>|<).*$/) ? true : false; - if(keySplit[0].match(/^-/g)) { - exclude = true; - keySplit[0] = keySplit[0].replace(/^-/, ''); - } - let paramString = exclude ? "not__" : ""; - let valueString = paramParts[1]; - if(keySplit.length === 1) { - if(searchTerm && !lessThanGreaterThan) { - if(singleSearchParam) { - paramString += keySplit[0] + '__icontains'; - } - else { - paramString += keySplit[0] + '__icontains_DEFAULT'; - } - } - else if(relatedSearchTerm) { - if(singleSearchParam) { - paramString += keySplit[0]; - } - else { - paramString += keySplit[0] + '__search_DEFAULT'; - } + return obj; + }, {}); + }, + // encodes a ui smart-search param to a django-friendly param + // operand:key:comparator:value => {operand__key__comparator: value} + encodeParam({ term, relatedSearchTerm, searchTerm, singleSearchParam }){ + // Assumption here is that we have a key and a value so the length + // of the paramParts array will be 2. [0] is the key and [1] the value + let paramParts = SmartSearchService.splitTermIntoParts(term); + let keySplit = paramParts[0].split('.'); + let exclude = false; + let lessThanGreaterThan = paramParts[1].match(/^(>|<).*$/) ? true : false; + if(keySplit[0].match(/^-/g)) { + exclude = true; + keySplit[0] = keySplit[0].replace(/^-/, ''); + } + let paramString = exclude ? "not__" : ""; + let valueString = paramParts[1]; + if(keySplit.length === 1) { + if(searchTerm && !lessThanGreaterThan) { + if(singleSearchParam) { + paramString += keySplit[0] + '__icontains'; } else { + paramString += keySplit[0] + '__icontains_DEFAULT'; + } + } + else if(relatedSearchTerm) { + if(singleSearchParam) { paramString += keySplit[0]; } - } - else { - paramString += keySplit.join('__'); - } - - if(lessThanGreaterThan) { - if(paramParts[1].match(/^>=.*$/)) { - paramString += '__gte'; - valueString = valueString.replace(/^(>=)/,""); - } - else if(paramParts[1].match(/^<=.*$/)) { - paramString += '__lte'; - valueString = valueString.replace(/^(<=)/,""); - } - else if(paramParts[1].match(/^<.*$/)) { - paramString += '__lt'; - valueString = valueString.replace(/^(<)/,""); - } - else if(paramParts[1].match(/^>.*$/)) { - paramString += '__gt'; - valueString = valueString.replace(/^(>)/,""); - } - } - - if(singleSearchParam) { - return {[singleSearchParam]: paramString + "=" + valueString}; - } - else { - return {[paramString] : encodeURIComponent(valueString)}; - } - }, - // decodes a django queryset param into a ui smart-search tag or set of tags - decodeParam(value, key){ - - let decodeParamString = function(searchString) { - if(key === 'search') { - // Don't include 'search:' in the search tag - return decodeURIComponent(`${searchString}`); - } else { - key = key.toString().replace(/__icontains_DEFAULT/g, ""); - key = key.toString().replace(/__search_DEFAULT/g, ""); - let split = key.split('__'); - let decodedParam = searchString; - let exclude = false; - if(key.startsWith('not__')) { - exclude = true; - split = split.splice(1, split.length); - } - if(key.endsWith('__gt')) { - decodedParam = '>' + decodedParam; - split = split.splice(0, split.length-1); - } - else if(key.endsWith('__lt')) { - decodedParam = '<' + decodedParam; - split = split.splice(0, split.length-1); - } - else if(key.endsWith('__gte')) { - decodedParam = '>=' + decodedParam; - split = split.splice(0, split.length-1); - } - else if(key.endsWith('__lte')) { - decodedParam = '<=' + decodedParam; - split = split.splice(0, split.length-1); - } - - let uriDecodedParam = decodeURIComponent(decodedParam); - - return exclude ? `-${split.join('.')}:${uriDecodedParam}` : `${split.join('.')}:${uriDecodedParam}`; + paramString += keySplit[0] + '__search_DEFAULT'; } - }; + } + else { + paramString += keySplit[0]; + } + } + else { + paramString += keySplit.join('__'); + } + if(lessThanGreaterThan) { + if(paramParts[1].match(/^>=.*$/)) { + paramString += '__gte'; + valueString = valueString.replace(/^(>=)/,""); + } + else if(paramParts[1].match(/^<=.*$/)) { + paramString += '__lte'; + valueString = valueString.replace(/^(<=)/,""); + } + else if(paramParts[1].match(/^<.*$/)) { + paramString += '__lt'; + valueString = valueString.replace(/^(<)/,""); + } + else if(paramParts[1].match(/^>.*$/)) { + paramString += '__gt'; + valueString = valueString.replace(/^(>)/,""); + } + } + + if(singleSearchParam) { + return {[singleSearchParam]: paramString + "=" + valueString}; + } + else { + return {[paramString] : encodeURIComponent(valueString)}; + } + }, + // decodes a django queryset param into a ui smart-search tag or set of tags + decodeParam(value, key){ + + let decodeParamString = function(searchString) { + if(key === 'search') { + // Don't include 'search:' in the search tag + return decodeURIComponent(`${searchString}`); + } + else { + key = key.toString().replace(/__icontains_DEFAULT/g, ""); + key = key.toString().replace(/__search_DEFAULT/g, ""); + let split = key.split('__'); + let decodedParam = searchString; + let exclude = false; + if(key.startsWith('not__')) { + exclude = true; + split = split.splice(1, split.length); + } + if(key.endsWith('__gt')) { + decodedParam = '>' + decodedParam; + split = split.splice(0, split.length-1); + } + else if(key.endsWith('__lt')) { + decodedParam = '<' + decodedParam; + split = split.splice(0, split.length-1); + } + else if(key.endsWith('__gte')) { + decodedParam = '>=' + decodedParam; + split = split.splice(0, split.length-1); + } + else if(key.endsWith('__lte')) { + decodedParam = '<=' + decodedParam; + split = split.splice(0, split.length-1); + } + + let uriDecodedParam = decodeURIComponent(decodedParam); + + return exclude ? `-${split.join('.')}:${uriDecodedParam}` : `${split.join('.')}:${uriDecodedParam}`; + } + }; + + if (Array.isArray(value)){ + value = _.uniq(_.flattenDeep(value)); + return _.map(value, (item) => { + return decodeParamString(item); + }); + } + else { + return decodeParamString(value); + } + }, + // encodes a django queryset for ui-router's URLMatcherFactory + // {operand__key__comparator: value, } => 'operand:key:comparator:value;...' + // value.isArray expands to: + // {operand__key__comparator: [value1, value2], } => 'operand:key:comparator:value1;operand:key:comparator:value1...' + encodeArr(params) { + let url; + url = _.reduce(params, (result, value, key) => { + return result.concat(encodeUrlString(value, key)); + }, []); + + return url.join(';'); + + // {key:'value'} => 'key:value' + // {key: [value1, value2, ...]} => ['key:value1', 'key:value2'] + function encodeUrlString(value, key){ if (Array.isArray(value)){ value = _.uniq(_.flattenDeep(value)); return _.map(value, (item) => { - return decodeParamString(item); + return `${key}:${item}`; }); } else { - return decodeParamString(value); - } - }, - convertToSearchTags(obj) { - const tags = []; - for (let key in obj) { - const value = obj[key]; - tags.push(this.decodeParam(value, key)); - } - return tags; - }, - // encodes a django queryset for ui-router's URLMatcherFactory - // {operand__key__comparator: value, } => 'operand:key:comparator:value;...' - // value.isArray expands to: - // {operand__key__comparator: [value1, value2], } => 'operand:key:comparator:value1;operand:key:comparator:value1...' - encodeArr(params) { - let url; - url = _.reduce(params, (result, value, key) => { - return result.concat(encodeUrlString(value, key)); - }, []); - - return url.join(';'); - - // {key:'value'} => 'key:value' - // {key: [value1, value2, ...]} => ['key:value1', 'key:value2'] - function encodeUrlString(value, key){ - if (Array.isArray(value)){ - value = _.uniq(_.flattenDeep(value)); - return _.map(value, (item) => { - return `${key}:${item}`; - }); - } - else { - return `${key}:${value}`; - } - } - }, - - // decodes a django queryset for ui-router's URLMatcherFactory - // 'operand:key:comparator:value,...' => {operand__key__comparator: value, } - decodeArr(arr) { - let params = {}; - _.forEach(arr.split(';'), (item) => { - let key = item.split(':')[0], - value = item.split(':')[1]; - if(!params[key]){ - params[key] = value; - } - else if (Array.isArray(params[key])){ - params[key] = _.uniq(_.flattenDeep(params[key])); - params[key].push(value); - } - else { - params[key] = [params[key], value]; - } - }); - return params; - }, - // REST utilities - options(endpoint) { - Rest.setUrl(endpoint); - return Rest.options(endpoint); - }, - search(endpoint, params) { - Wait('start'); - this.url = `${endpoint}${this.encodeQueryset(params)}`; - Rest.setUrl(this.url); - - return Rest.get() - .then(function(response) { - Wait('stop'); - - if (response - .headers('X-UI-Max-Events') !== null) { - response.data.maxEvents = response. - headers('X-UI-Max-Events'); - } - - return response; - }) - .catch(function(response) { - Wait('stop'); - - this.error(response.data, response.status); - - throw response; - }.bind(this)); - }, - error(data, status) { - if(data && data.detail){ - let error = typeof data.detail === "string" ? data.detail : JSON.parse(data.detail); - - if(_.isArray(error)){ - data.detail = error[0]; - } - } - ProcessErrors($rootScope, data, status, null, { - hdr: 'Error!', - msg: `Invalid search term entered. GET returned: ${status}` - }); - }, - // Removes state definition defaults and pagination terms - stripDefaultParams(params, defaults) { - if(defaults) { - let stripped =_.pick(params, (value, key) => { - // setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value - return defaults[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaults[key] !== null; - }); - let strippedCopy = _.cloneDeep(stripped); - if(_.keys(_.pick(defaults, _.keys(strippedCopy))).length > 0){ - for (var key in strippedCopy) { - if (strippedCopy.hasOwnProperty(key)) { - let value = strippedCopy[key]; - if(_.isArray(value)){ - let index = _.indexOf(value, defaults[key]); - value = value.splice(index, 1)[0]; - } - } - } - stripped = strippedCopy; - } - return _(strippedCopy).map(this.decodeParam).flatten().value(); - } - else { - return _(params).map(this.decodeParam).flatten().value(); + return `${key}:${value}`; } } - }; - } + }, + // decodes a django queryset for ui-router's URLMatcherFactory + // 'operand:key:comparator:value,...' => {operand__key__comparator: value, } + decodeArr(arr) { + let params = {}; + + if (!arr) { + return params; + } + + _.forEach(arr.split(';'), (item) => { + let key = item.split(':')[0], + value = item.split(':')[1]; + if(!params[key]){ + params[key] = value; + } + else if (Array.isArray(params[key])){ + params[key] = _.uniq(_.flattenDeep(params[key])); + params[key].push(value); + } + else { + params[key] = [params[key], value]; + } + }); + return params; + }, + // REST utilities + options(endpoint) { + Rest.setUrl(endpoint); + return Rest.options(endpoint); + }, + search(endpoint, params) { + Wait('start'); + this.url = `${endpoint}${this.encodeQueryset(params)}`; + Rest.setUrl(this.url); + + return Rest.get() + .then(function(response) { + Wait('stop'); + + if (response + .headers('X-UI-Max-Events') !== null) { + response.data.maxEvents = response. + headers('X-UI-Max-Events'); + } + + return response; + }) + .catch(function(response) { + Wait('stop'); + + this.error(response.data, response.status); + + throw response; + }.bind(this)); + }, + error(data, status) { + if(data && data.detail){ + let error = typeof data.detail === "string" ? data.detail : JSON.parse(data.detail); + + if(_.isArray(error)){ + data.detail = error[0]; + } + } + ProcessErrors($rootScope, data, status, null, { + hdr: 'Error!', + msg: `Invalid search term entered. GET returned: ${status}` + }); + }, + // Removes state definition defaults and pagination terms + stripDefaultParams(params, defaultParams) { + if (!params) { + return []; + } + if(defaultParams) { + let stripped =_.pick(params, (value, key) => { + // setting the default value of a term to null in a state definition is a very explicit way to ensure it will NEVER generate a search tag, even with a non-default value + return defaultParams[key] !== value && key !== 'order_by' && key !== 'page' && key !== 'page_size' && defaultParams[key] !== null; + }); + let strippedCopy = _.cloneDeep(stripped); + if(_.keys(_.pick(defaultParams, _.keys(strippedCopy))).length > 0){ + for (var key in strippedCopy) { + if (strippedCopy.hasOwnProperty(key)) { + let value = strippedCopy[key]; + if(_.isArray(value)){ + let index = _.indexOf(value, defaultParams[key]); + value = value.splice(index, 1)[0]; + } + } + } + stripped = strippedCopy; + } + return _(strippedCopy).map(this.decodeParam).flatten().value(); + } + else { + return _(params).map(this.decodeParam).flatten().value(); + } + }, + mergeQueryset (queryset, additional, singleSearchParam) { + const space = '%20and%20'; + + const merged = _.merge({}, queryset, additional, (objectValue, sourceValue, key, object) => { + if (!(object[key] && object[key] !== sourceValue)) { + // // https://lodash.com/docs/3.10.1#each + // If this returns undefined merging is handled by default _.merge algorithm + return undefined; + } + + if (_.isArray(object[key])) { + object[key].push(sourceValue); + return object[key]; + } + + if (singleSearchParam) { + if (!object[key]) { + return sourceValue; + } + + const singleSearchParamKeys = object[key].split(space); + + if (_.includes(singleSearchParamKeys, sourceValue)) { + return object[key]; + } + + return `${object[key]}${space}${sourceValue}`; + } + + // Start the array of keys + return [object[key], sourceValue]; + }); + + return merged; + }, + getSearchInputQueryset (searchInput, isRelatedField = null, isAnsibleFactField = null, singleSearchParam = null) { + // XXX Should find a better approach than passing in the two 'is...Field' callbacks XXX + const space = '%20and%20'; + let params = {}; + + // Remove leading/trailing whitespace if there is any + const terms = (searchInput) ? searchInput.trim() : ''; + + if (!(terms && terms !== '')) { + return; + } + + let splitTerms; + + if (singleSearchParam === 'host_filter') { + splitTerms = SmartSearchService.splitFilterIntoTerms(terms); + } else { + splitTerms = SmartSearchService.splitSearchIntoTerms(terms); + } + + const combineSameSearches = (a, b) => { + if (!a) { + return undefined; + } + + if (_.isArray(a)) { + return a.concat(b); + } + + if (singleSearchParam) { + return `${a}${space}${b}`; + } + + return [a, b]; + }; + + _.each(splitTerms, term => { + const termParts = SmartSearchService.splitTermIntoParts(term); + let termParams; + + if (termParts.length === 1) { + termParams = searchWithoutKey(term, singleSearchParam); + } else if (isAnsibleFactField && isAnsibleFactField(termParts)) { + termParams = this.encodeParam({ term, singleSearchParam }); + } else if (isRelatedField && isRelatedField(termParts)) { + termParams = this.encodeParam({ term, singleSearchParam, related: true }); + } else { + termParams = this.encodeParam({ term, singleSearchParam }); + } + + params = _.merge(params, termParams, combineSameSearches); + }); + + return params; + }, + removeTermsFromQueryset(queryset, term, isRelatedField = null, singleSearchParam = null) { + const modifiedQueryset = _.cloneDeep(queryset); + + const removeSingleTermFromQueryset = (value, key) => { + const space = '%20and%20'; + + if (Array.isArray(modifiedQueryset[key])) { + modifiedQueryset[key] = modifiedQueryset[key].filter(item => item !== value); + if (modifiedQueryset[key].length < 1) { + delete modifiedQueryset[key]; + } + } else if (singleSearchParam && _.get(modifiedQueryset, singleSearchParam, []).includes(space)) { + const searchParamParts = modifiedQueryset[singleSearchParam].split(space); + // The value side of each paramPart might have been encoded in + // SmartSearch.splitFilterIntoTerms + _.each(searchParamParts, (paramPart, paramPartIndex) => { + searchParamParts[paramPartIndex] = decodeURIComponent(paramPart); + }); + + const paramPartIndex = searchParamParts.indexOf(value); + + if (paramPartIndex !== -1) { + searchParamParts.splice(paramPartIndex, 1); + } + + modifiedQueryset[singleSearchParam] = searchParamParts.join(space); + + } else { + delete modifiedQueryset[key]; + } + }; + + const termParts = SmartSearchService.splitTermIntoParts(term); + + let removed; + + if (termParts.length === 1) { + removed = searchWithoutKey(term, singleSearchParam); + } else if (isRelatedField && isRelatedField(termParts)) { + removed = this.encodeParam({ term, singleSearchParam, related: true }); + } else { + removed = this.encodeParam({ term, singleSearchParam }); + } + + if (!removed) { + removed = searchWithoutKey(termParts[termParts.length - 1], singleSearchParam); + } + + _.each(removed, removeSingleTermFromQueryset); + + return modifiedQueryset; + }, + createSearchTagsFromQueryset(queryset, defaultParams = null, singleSearchParam = null) { + const space = '%20and%20'; + const modifiedQueryset = angular.copy(queryset); + + let searchTags = []; + + if (singleSearchParam && modifiedQueryset[singleSearchParam]) { + const searchParam = modifiedQueryset[singleSearchParam].split(space); + delete modifiedQueryset[singleSearchParam]; + + $.each(searchParam, (index, param) => { + const paramParts = decodeURIComponent(param).split(/=(.+)/); + const reconstructedSearchString = this.decodeParam(paramParts[1], paramParts[0]); + + searchTags.push(reconstructedSearchString); + }); + } + + return searchTags.concat(this.stripDefaultParams(modifiedQueryset, defaultParams)); + } + }; +} + +QuerysetService.$inject = [ + '$q', + 'Rest', + 'ProcessErrors', + '$rootScope', + 'Wait', + 'DjangoSearchModel', + 'SmartSearchService', ]; + +export default QuerysetService; diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js index 5a59bbde81..414a0763db 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js +++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js @@ -7,7 +7,6 @@ function SmartSearchController ( GetBasePath, i18n, qs, - SmartSearchService ) { const searchKey = `${$scope.iterator}_search`; const optionsKey = `${$scope.list.iterator}_options`; @@ -58,7 +57,7 @@ function SmartSearchController ( } }); - function compareParams(a, b) { + function compareParams (a, b) { for (let key in a) { if (!(key in b) || a[key].toString() !== b[key].toString()) { return false; @@ -106,22 +105,8 @@ function SmartSearchController ( } function generateSearchTags () { - $scope.searchTags = []; - - const querysetCopy = angular.copy(queryset); - - if ($scope.singleSearchParam && querysetCopy[$scope.singleSearchParam]) { - const searchParam = querysetCopy[$scope.singleSearchParam].split('%20and%20'); - delete querysetCopy[$scope.singleSearchParam]; - - $.each(searchParam, (index, param) => { - const paramParts = decodeURIComponent(param).split(/=(.+)/); - const reconstructedSearchString = qs.decodeParam(paramParts[1], paramParts[0]); - $scope.searchTags.push(reconstructedSearchString); - }); - } - - $scope.searchTags = $scope.searchTags.concat(qs.stripDefaultParams(querysetCopy, defaults)); + const { singleSearchParam } = $scope; + $scope.searchTags = qs.createSearchTagsFromQueryset(queryset, defaults, singleSearchParam); } function revertSearch (queryToBeRestored) { @@ -149,14 +134,7 @@ function SmartSearchController ( $scope.showKeyPane = !$scope.showKeyPane; }; - function searchWithoutKey (term, singleSearchParam = null) { - if (singleSearchParam) { - return { [singleSearchParam]: `search=${encodeURIComponent(term)}` }; - } - return { search: encodeURIComponent(term) }; - } - - function isAnsibleFactSearchTerm (termParts) { + function isAnsibleFactField (termParts) { const rootField = termParts[0].split('.')[0].replace(/^-/, ''); return rootField === 'ansible_facts'; } @@ -172,111 +150,21 @@ function SmartSearchController ( return (isRelatedSearchTermField || isBaseModelRelatedSearchTermField); } - function getSearchInputQueryset ({ terms, singleSearchParam }) { - let params = {}; - - // remove leading/trailing whitespace - terms = (terms) ? terms.trim() : ''; - let splitTerms; - - if (singleSearchParam === 'host_filter') { - splitTerms = SmartSearchService.splitFilterIntoTerms(terms); - } else { - splitTerms = SmartSearchService.splitSearchIntoTerms(terms); - } - - const combineSameSearches = (a, b) => { - if (!a) { - return undefined; - } - - if (_.isArray(a)) { - return a.concat(b); - } - - if (singleSearchParam) { - return `${a}%20and%20${b}`; - } - - return [a, b]; - }; - - _.each(splitTerms, term => { - const termParts = SmartSearchService.splitTermIntoParts(term); - let termParams; - - if (termParts.length === 1) { - termParams = searchWithoutKey(term, singleSearchParam); - } else if (isAnsibleFactSearchTerm(termParts)) { - termParams = qs.encodeParam({ term, singleSearchParam }); - } else if (isRelatedField(termParts)) { - termParams = qs.encodeParam({ term, singleSearchParam, related: true }); - } else { - termParams = qs.encodeParam({ term, singleSearchParam }); - } - - params = _.merge(params, termParams, combineSameSearches); - }); - - return params; - } - - function mergeQueryset (qset, additional, singleSearchParam) { - const merged = _.merge({}, qset, additional, (objectValue, sourceValue, key, object) => { - if (!(object[key] && object[key] !== sourceValue)) { - // // https://lodash.com/docs/3.10.1#each - // If this returns undefined merging is handled by default _.merge algorithm - return undefined; - } - - if (_.isArray(object[key])) { - object[key].push(sourceValue); - return object[key]; - } - - if (singleSearchParam) { - if (!object[key]) { - return sourceValue; - } - - const singleSearchParamKeys = object[key].split('%20and%20'); - - if (_.includes(singleSearchParamKeys, sourceValue)) { - return object[key]; - } - - return `${object[key]}%20and%20${sourceValue}`; - } - - // Start the array of keys - return [object[key], sourceValue]; - }); - - return merged; - } - $scope.addTerms = terms => { const { singleSearchParam } = $scope; - const origQueryset = _.clone(queryset); + const unmodifiedQueryset = _.clone(queryset); - // Remove leading/trailing whitespace if there is any - terms = (terms) ? terms.trim() : ''; - - if (!(terms && terms !== '')) { - return; - } - - const searchInputQueryset = getSearchInputQueryset({ terms, singleSearchParam }); - queryset = mergeQueryset(queryset, searchInputQueryset, singleSearchParam); + const searchInputQueryset = qs.getSearchInputQueryset(terms, isRelatedField, isAnsibleFactField, singleSearchParam); + const modifiedQueryset = qs.mergeQueryset(queryset, searchInputQueryset, singleSearchParam); // Go back to the first page after a new search - delete queryset.page; + delete modifiedQueryset.page; // https://ui-router.github.io/docs/latest/interfaces/params.paramdeclaration.html#dynamic // This transition will not reload controllers/resolves/views but will register new // $stateParams[searchKey] terms. if (!$scope.querySet) { - $state.go('.', { [searchKey]: queryset }) + $state.go('.', { [searchKey]: modifiedQueryset }) .then(() => { // same as above in $scope.remove. For some reason deleting the page // from the queryset works for all lists except lists in modals. @@ -284,80 +172,26 @@ function SmartSearchController ( }); } - qs.search(path, queryset) + qs.search(path, modifiedQueryset) .then(({ data }) => { if ($scope.querySet) { - $scope.querySet = queryset; + $scope.querySet = modifiedQueryset; } $scope.dataset = data; $scope.collection = data.results; }) - .catch(() => revertSearch(origQueryset)); + .catch(() => revertSearch(unmodifiedQueryset)); $scope.searchTerm = null; generateSearchTags(); }; - - function removeTermsFromQueryset(qset, term, singleSearchParam = null) { - const returnedQueryset = _.cloneDeep(qset); - - const removeSingleTermFromQueryset = (value, key) => { - const space = '%20and%20'; - - if (Array.isArray(returnedQueryset[key])) { - returnedQueryset[key] = returnedQueryset[key].filter(item => item !== value); - if (returnedQueryset[key].length < 1) { - delete returnedQueryset[key]; - } - } else if (singleSearchParam && _.get(returnedQueryset, singleSearchParam, []).includes(space)) { - const searchParamParts = returnedQueryset[singleSearchParam].split(space); - // The value side of each paramPart might have been encoded in - // SmartSearch.splitFilterIntoTerms - _.each(searchParamParts, (paramPart, paramPartIndex) => { - searchParamParts[paramPartIndex] = decodeURIComponent(paramPart); - }); - - const paramPartIndex = searchParamParts.indexOf(value); - - if (paramPartIndex !== -1) { - searchParamParts.splice(paramPartIndex, 1); - } - - returnedQueryset[singleSearchParam] = searchParamParts.join(space); - - } else { - delete returnedQueryset[key]; - } - }; - - const termParts = SmartSearchService.splitTermIntoParts(term); - - let removed; - - if (termParts.length === 1) { - removed = searchWithoutKey(term, singleSearchParam); - } else if (isRelatedField(termParts)) { - removed = qs.encodeParam({ term, singleSearchParam, related: true }); - } else { - removed = qs.encodeParam({ term, singleSearchParam }); - } - - if (!removed) { - removed = searchWithoutKey(termParts[termParts.length - 1], singleSearchParam); - } - - _.each(removed, removeSingleTermFromQueryset); - - return returnedQueryset; - } - // remove tag, merge new queryset, $state.go $scope.removeTerm = index => { const { singleSearchParam } = $scope; const [term] = $scope.searchTags.splice(index, 1); - const modifiedQueryset = removeTermsFromQueryset(queryset, term, singleSearchParam); + const modifiedQueryset = qs.removeTermsFromQueryset(queryset, term, isRelatedField, singleSearchParam); if (!$scope.querySet) { $state.go('.', { [searchKey]: modifiedQueryset }) @@ -365,8 +199,7 @@ function SmartSearchController ( // for some reason deleting a tag from a list in a modal does not // remove the param from $stateParams. Here we'll manually check to make sure // that that happened and remove it if it didn't. - const clearedParams = removeTermsFromQueryset( - $stateParams[searchKey], term, singleSearchParam); + const clearedParams = qs.removeTermsFromQueryset($stateParams[searchKey], term, isRelatedField, singleSearchParam); $stateParams[searchKey] = clearedParams; }); } @@ -416,7 +249,6 @@ SmartSearchController.$inject = [ 'GetBasePath', 'i18n', 'QuerySet', - 'SmartSearchService', ]; export default SmartSearchController; From 7acc99cf15041e7122193107ebbe78b2b0b02650 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Sun, 4 Mar 2018 16:51:03 -0500 Subject: [PATCH 52/89] initial search integration --- awx/ui/client/features/output/_index.less | 49 +++++++++++ .../features/output/index.controller.js | 83 ++++++++++++++++++- awx/ui/client/features/output/index.js | 30 +++++-- awx/ui/client/features/output/index.view.html | 45 ++++++++++ .../features/output/search-key.directive.js | 38 +++++++++ .../features/output/search-key.partial.html | 20 +++++ 6 files changed, 254 insertions(+), 11 deletions(-) create mode 100644 awx/ui/client/features/output/search-key.directive.js create mode 100644 awx/ui/client/features/output/search-key.partial.html diff --git a/awx/ui/client/features/output/_index.less b/awx/ui/client/features/output/_index.less index d661afae97..989b532ab6 100644 --- a/awx/ui/client/features/output/_index.less +++ b/awx/ui/client/features/output/_index.less @@ -150,3 +150,52 @@ white-space: pre-wrap; } + +// Search --------------------------------------------------------------------------------- +@at-jobz-top-search-key: @at-space-2x; +@at-jobz-bottom-search-key: @at-space-3x; + +.jobz-searchKeyPaneContainer { + margin-top: @at-jobz-top-search-key; + margin-bottom: @at-jobz-bottom-search-key; +} + +.jobz-searchKeyPane { + // background-color: @at-gray-f6; + background-color: @login-notice-bg; + color: @login-notice-text; + border-radius: @at-border-radius; + border: 1px solid @at-gray-b7; + // color: @at-gray-848992; + padding: 6px @at-padding-input 6px @at-padding-input; +} + +.jobz-searchClearAll { + font-size: 10px; + padding-bottom: @at-space; +} + +.jobz-Button-searchKey { + .at-mixin-Button(); + + background-color: @at-blue; + border-color: at-color-button-border-default; + color: @at-white; + + &:hover, &:active { + color: @at-white; + background-color: @at-blue-hover; + box-shadow: none; + } + + &:focus { + color: @at-white; + } +} + +.jobz-tagz { + margin-top: @at-space; + display: flex; + width: 100%; + flex-wrap: wrap; +} diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index e2aeae2d90..7e524a26f4 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -9,6 +9,8 @@ let page; let render; let scroll; let resource; +let $state; +let qs; let chain; @@ -19,7 +21,9 @@ function JobsIndexController ( _render_, _$scope_, _$compile_, - _$q_ + _$q_, + _$state_, + _qs_, ) { vm = this || {}; @@ -59,6 +63,23 @@ function JobsIndexController ( const stream = false; // TODO: Set in route chain = $q.resolve(); + + // search + $state = _$state_; + qs = _qs_; + + vm.searchValue = ''; + vm.searchRejected = null; + vm.searchKey = false; + vm.searchKeyExamples = searchKeyExamples; + vm.searchKeyFields = searchKeyFields; + + vm.clearSearch = clearSearch; + vm.search = search; + vm.toggleSearchKey = toggleSearchKey; + vm.removeSearchTag = removeSearchTag; + vm.searchTags = getSearchTags(getCurrentQueryset()); + render.requestAnimationFrame(() => init()); } @@ -318,6 +339,62 @@ function toggle (uuid, menu) { lines.removeClass('hidden'); } + +// +// Search +// + +const searchReloadOptions = { reload: true, inherit: false }; +const searchKeyExamples = ['id:>1', 'task:set', 'created:>=2000-01-01']; +const searchKeyFields = ['changed', 'failed', 'host_name', 'stdout', 'task', 'role', 'playbook', 'play']; + +function toggleSearchKey () { + vm.searchKey = !vm.searchKey; +} + +function getCurrentQueryset() { + const { job_event_search } = $state.params; + + return qs.decodeArr(job_event_search); +} + +function getSearchTags (queryset) { + return qs.createSearchTagsFromQueryset(queryset) + .filter(tag => !tag.startsWith('event')) + .filter(tag => !tag.startsWith('-event')) + .filter(tag => !tag.startsWith('page_size')) + .filter(tag => !tag.startsWith('order_by')); +} + +function removeSearchTag (index) { + const searchTerm = vm.searchTags[index]; + + const currentQueryset = getCurrentQueryset(); + const modifiedQueryset = qs.removeTermsFromQueryset(currentQueryset, searchTerm); + + vm.searchTags = getSearchTags(modifiedQueryset); + + $state.params.job_event_search = qs.encodeArr(modifiedQueryset); + $state.transitionTo($state.current, $state.params, searchReloadOptions); +} + +function search () { + const searchInputQueryset = qs.getSearchInputQueryset(vm.searchValue); + + const currentQueryset = getCurrentQueryset(); + const modifiedQueryset = qs.mergeQueryset(currentQueryset, searchInputQueryset); + + vm.searchTags = getSearchTags(modifiedQueryset); + + $state.params.job_event_search = qs.encodeArr(modifiedQueryset); + $state.transitionTo($state.current, $state.params, searchReloadOptions); +} + +function clearSearch () { + vm.searchTags = []; + + $state.params.job_event_search = ''; + $state.transitionTo($state.current, $state.params, searchReloadOptions); } JobsIndexController.$inject = [ @@ -327,7 +404,9 @@ JobsIndexController.$inject = [ 'JobRenderService', '$scope', '$compile', - '$q' + '$q', + '$state', + 'QuerySet', ]; module.exports = JobsIndexController; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index bc73ca83c7..593b7b1097 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -6,6 +6,7 @@ import Controller from '~features/output/index.controller'; import PageService from '~features/output/page.service'; import RenderService from '~features/output/render.service'; import ScrollService from '~features/output/scroll.service'; +import SearchKeyDirective from '~features/output/search-key.directive'; const Template = require('~features/output/index.view.html'); @@ -15,8 +16,8 @@ const PAGE_LIMIT = 3; const PAGE_SIZE = 100; const WS_PREFIX = 'ws'; -function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJob, $stateParams) { - const { id, type } = $stateParams; +function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJob, $stateParams, qs, Wait) { + const { id, type, job_event_search } = $stateParams; let Resource; let related = 'events'; @@ -43,14 +44,20 @@ function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJ return null; } + const params = { page_size: PAGE_SIZE, order_by: 'start_line' }; + + if (job_event_search) { + const searchParams = qs.encodeQuerysetObject(qs.decodeArr(job_event_search)); + + Object.assign(params, searchParams); + } + + Wait('start'); return new Resource('get', id) .then(model => model.extend(related, { pageCache: PAGE_CACHE, pageLimit: PAGE_LIMIT, - params: { - page_size: PAGE_SIZE, - order_by: 'start_line' - } + params, })) .then(model => { return { @@ -67,7 +74,9 @@ function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJ pageLimit: PAGE_LIMIT } }; - }); + }) + .catch(({ data, status }) => qs.error(data, status)) + .finally(() => Wait('stop')); } function resolveWebSocketConnection (SocketService, $stateParams) { @@ -131,8 +140,8 @@ function getWebSocketResource (type) { function JobsRun ($stateRegistry) { const state = { name: 'jobz', - url: '/jobz/:type/:id', - route: '/jobz/:type/:id', + url: '/jobz/:type/:id?job_event_search', + route: '/jobz/:type/:id?job_event_search', data: { activityStream: true, activityStreamTarget: 'jobs' @@ -152,6 +161,8 @@ function JobsRun ($stateRegistry) { 'SystemJobModel', 'WorkflowJobModel', '$stateParams', + 'QuerySet', + 'Wait', resolveResource ], ncyBreadcrumb: [ @@ -179,6 +190,7 @@ angular .service('JobStrings', Strings) .service('JobPageService', PageService) .service('JobScrollService', ScrollService) + .directive('atSearchKey', SearchKeyDirective) .run(JobsRun); export default MODULE_NAME; diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 3c1a276826..2516ede6c7 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -7,6 +7,51 @@
+ +
+
+ + + + + + +
+
+ +
+
+
{{ tag }}
+
+ +
+
+ +
+ + + +
{ + vm.examples = scope.examples || []; + vm.fields = scope.fields || []; + vm.relatedFields = scope.relatedFields || []; + } +} + +AtSearchKeyController.$inject = ['$scope']; + + +function atSearchKey () { + return { + templateUrl, + restrict: 'E', + require: ['atSearchKey'], + controllerAs: 'vm', + link: atSearchKeyLink, + controller: AtSearchKeyController, + scope: { + examples: '=', + fields: '=', + relatedFields: '=', + }, + }; +} + +export default atSearchKey; diff --git a/awx/ui/client/features/output/search-key.partial.html b/awx/ui/client/features/output/search-key.partial.html new file mode 100644 index 0000000000..d2790d285f --- /dev/null +++ b/awx/ui/client/features/output/search-key.partial.html @@ -0,0 +1,20 @@ + +
+
+
+
+
EXAMPLES:
+ +
+
+
+ FIELDS: + {{ field }}, +
+
+ ADDITIONAL INFORMATION: + For additional information on advanced search search syntax please see the Ansible Tower + documentation. +
+
+
From 189963ae83b9152c5f322091e7b9934355e9ec18 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Thu, 8 Mar 2018 16:59:30 -0500 Subject: [PATCH 53/89] Add independent stream service --- .../features/output/index.controller.js | 118 +++--------- awx/ui/client/features/output/index.js | 2 + awx/ui/client/features/output/index.view.html | 3 +- awx/ui/client/features/output/page.service.js | 24 ++- .../client/features/output/stream.service.js | 182 ++++++++++++++++++ 5 files changed, 230 insertions(+), 99 deletions(-) create mode 100644 awx/ui/client/features/output/stream.service.js diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 7e524a26f4..aeb615c22f 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -1,6 +1,3 @@ -const JOB_START = 'playbook_on_start'; -const JOB_END = 'playbook_on_stats'; - let vm; let $compile; let $scope; @@ -8,17 +5,20 @@ let $q; let page; let render; let scroll; +let stream; let resource; let $state; let qs; let chain; +let chainLength; function JobsIndexController ( _resource_, _page_, _scroll_, _render_, + _stream_, _$scope_, _$compile_, _$q_, @@ -35,6 +35,7 @@ function JobsIndexController ( page = _page_; scroll = _scroll_; render = _render_; + stream = _stream_; // Development helper(s) vm.clear = devClear; @@ -53,17 +54,6 @@ function JobsIndexController ( vm.expand = expand; vm.isExpanded = true; - // Real-time (active between JOB_START and JOB_END events only) - vm.stream = { - active: false, - rendering: false, - paused: false - }; - - const stream = false; // TODO: Set in route - - chain = $q.resolve(); - // search $state = _$state_; qs = _qs_; @@ -83,8 +73,10 @@ function JobsIndexController ( render.requestAnimationFrame(() => init()); } -function init (stream) { - page.init(resource); +function init (pageMode) { + page.init({ + resource + }); render.init({ get: () => resource.model.get(`related.${resource.related}.results`), @@ -97,76 +89,24 @@ function init (stream) { next }); - if (stream) { - $scope.$on(resource.ws.namespace, process); - } else { + stream.init({ + page, + scroll, + resource, + render: events => shift().then(() => append(events, true)), + listen: (namespace, listener) => { + $scope.$on(namespace, (scope, data) => listener(data)); + } + }); + + if (pageMode) { next(); } } -function process (scope, data) { - chain = chain.then(() => { - if (data.event === JOB_START) { - vm.stream.active = true; - scroll.lock(); - } else if (data.event === JOB_END) { - vm.stream.active = false; - } - - const pageAdded = page.addToBuffer(data); - - if (pageAdded && !scroll.isLocked()) { - vm.stream.paused = true; - } - - if (vm.stream.paused && scroll.isLocked()) { - vm.stream.paused = false; - } - - if (vm.stream.rendering || vm.stream.paused) { - return; - } - - const events = page.emptyBuffer(); - - return renderStream(events); - }) -} - -function renderStream (events) { - vm.stream.rendering = true; - - return shift() - .then(() => append(events, true)) - .then(() => { - if (scroll.isLocked()) { - scroll.setScrollPosition(scroll.getScrollHeight()); - } - - if (!vm.stream.active) { - const buffer = page.emptyBuffer(); - - if (buffer.length) { - return renderStream(buffer); - } else { - vm.stream.rendering = false; - scroll.unlock(); - } - } else { - vm.stream.rendering = false; - } - }); -} - -function devClear () { - init(true); +function devClear (pageMode) { + init(pageMode); render.clear(); - - vm.stream = { - active: false, - rendering: false, - paused: false - }; } function next () { @@ -257,14 +197,12 @@ function scrollHome () { } function scrollEnd () { - if (scroll.isLocked()) { - page.setBookmark(); - scroll.unlock(); - - return; - } else if (!scroll.isLocked() && vm.stream.active) { - page.removeBookmark(); - scroll.lock(); + if (stream.isActive()) { + if (stream.isPaused()) { + stream.resume(); + } else { + stream.pause(); + } return; } @@ -339,6 +277,7 @@ function toggle (uuid, menu) { lines.removeClass('hidden'); } +} // // Search @@ -402,6 +341,7 @@ JobsIndexController.$inject = [ 'JobPageService', 'JobScrollService', 'JobRenderService', + 'JobStreamService', '$scope', '$compile', '$q', diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 593b7b1097..64819e9ca3 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -7,6 +7,7 @@ import PageService from '~features/output/page.service'; import RenderService from '~features/output/render.service'; import ScrollService from '~features/output/scroll.service'; import SearchKeyDirective from '~features/output/search-key.directive'; +import StreamService from '~features/output/stream.service'; const Template = require('~features/output/index.view.html'); @@ -190,6 +191,7 @@ angular .service('JobStrings', Strings) .service('JobPageService', PageService) .service('JobScrollService', ScrollService) + .service('JobStreamService', StreamService) .directive('atSearchKey', SearchKeyDirective) .run(JobsRun); diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 2516ede6c7..53b69a43b2 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -1,7 +1,8 @@
-

+

+

diff --git a/awx/ui/client/features/output/page.service.js b/awx/ui/client/features/output/page.service.js index a89d56d6b8..64f0542fe0 100644 --- a/awx/ui/client/features/output/page.service.js +++ b/awx/ui/client/features/output/page.service.js @@ -1,5 +1,5 @@ function JobPageService ($q) { - this.init = resource => { + this.init = ({ resource }) => { this.resource = resource; this.page = { @@ -54,12 +54,9 @@ function JobPageService ($q) { reference.state.count++; }; - this.addToPageCache = (index, event, reference) => { - reference.cache[index].events.push(event); - }; - this.addToBuffer = event => { const reference = this.getReference(); + const index = reference.cache.length - 1; let pageAdded = false; if (this.result.count % this.page.size === 0) { @@ -70,9 +67,10 @@ function JobPageService ($q) { } this.trimBuffer(); + pageAdded = true; } else { - this.addToPageCache(reference.cache.length - 1, event, reference); + reference.cache[index].events.push(event); } this.buffer.count++; @@ -97,6 +95,14 @@ function JobPageService ($q) { } }; + this.isBufferFull = () => { + if (this.buffer.count === 2) { + return true; + } + + return false; + } + this.emptyBuffer = () => { const reference = this.getReference(); let data = []; @@ -183,9 +189,9 @@ function JobPageService ($q) { return; } - this.bookmark.state.first = this.page.state.first; - this.bookmark.state.last = this.page.state.last; - this.bookmark.state.current = this.page.state.current; + this.bookmark.state.first = this.page.state.first - 1; + this.bookmark.state.last = this.page.state.last - 1; + this.bookmark.state.current = this.page.state.current - 1; this.bookmark.cache = JSON.parse(JSON.stringify(this.page.cache)); this.bookmark.set = true; this.bookmark.pending = false; diff --git a/awx/ui/client/features/output/stream.service.js b/awx/ui/client/features/output/stream.service.js new file mode 100644 index 0000000000..557337adc3 --- /dev/null +++ b/awx/ui/client/features/output/stream.service.js @@ -0,0 +1,182 @@ +const JOB_START = 'playbook_on_start'; +const JOB_END = 'playbook_on_stats'; +const MAX_LAG = 120; + +function JobStreamService ($q) { + this.init = ({ resource, scroll, page, render, listen }) => { + this.resource = resource; + this.scroll = scroll; + this.page = page; + + this.lag = 0; + this.count = 0; + this.pageCount = 0; + this.chain = $q.resolve(); + this.factors = this.getBatchFactors(this.resource.page.size); + this.state = { + started: false, + paused: false, + pausing: false, + resuming: false, + ending: false, + ended: false + }; + + this.hooks = { + render, + listen + }; + + this.hooks.listen(resource.ws.namespace, this.listen); + }; + + this.getBatchFactors = size => { + const factors = [1]; + + for (let i = 2; i <= size / 2; i++) { + if (size % i === 0) { + factors.push(i); + } + } + + factors.push(size); + + return factors; + }; + + this.getBatchFactorIndex = () => { + const index = Math.floor((this.lag / MAX_LAG) * this.factors.length); + + return index > this.factors.length - 1 ? this.factors.length - 1 : index; + }; + + this.setBatchFrameCount = () => { + const index = this.getBatchFactorIndex(); + + this.framesPerRender = this.factors[index]; + }; + + this.buffer = data => { + const pageAdded = this.page.addToBuffer(data); + + this.pageCount++; + + if (pageAdded) { + this.setBatchFrameCount(); + + if (this.isPausing()) { + this.pause(true); + } else if (this.isResuming()) { + this.resume(true); + } + } + }; + + this.listen = data => { + this.lag++; + + this.chain = this.chain + .then(() => { + if (data.event === JOB_START) { + this.start(); + } else if (data.event === JOB_END) { + if (this.isPaused()) { + this.end(true); + } else { + this.end(); + } + } + + this.buffer(data); + this.count++; + + if (this.isPaused() || !this.isBatchFull()) { + return $q.resolve(); + } + + const events = this.page.emptyBuffer(); + this.count -= events.length; + + return this.renderFrame(events); + }) + .then(() => --this.lag); + }; + + this.renderFrame = events => { + return this.hooks.render(events) + .then(() => { + if (this.scroll.isLocked()) { + this.scroll.setScrollPosition(this.scroll.getScrollHeight()); + } + + if (this.isEnding()) { + const lastEvents = this.page.emptyBuffer(); + + if (lastEvents.length) { + return this.renderFrame(lastEvents); + } + + this.end(true); + } + + return $q.resolve(); + }); + }; + + this.resume = done => { + if (done) { + this.state.resuming = false; + this.state.paused = false; + + return; + } + + this.scroll.lock(); + this.state.resuming = true; + this.page.removeBookmark(); + }; + + this.pause = done => { + if (done) { + this.state.pausing = false; + this.state.paused = true; + this.scroll.resume(); + + return; + } + + this.scroll.unlock(); + this.scroll.pause(); + this.state.pausing = true; + this.page.setBookmark(); + }; + + this.start = () => { + this.state.started = true; + this.scroll.lock(); + }; + + this.end = done => { + if (done) { + this.state.ending = false; + this.state.ended = true; + this.scroll.unlock(); + + return; + } + + this.state.ending = true; + }; + + this.isBatchFull = () => this.count % this.framesPerRender === 0; + this.isPaused = () => this.state.paused; + this.isPausing = () => this.state.pausing; + this.isResuming = () => this.state.resuming; + this.isActive = () => this.state.started && !this.state.ended; + this.isEnding = () => this.state.ending; + this.isDone = () => this.state.ended; +} + +JobStreamService.$inject = ['$q']; + +export default JobStreamService; From 57ea582898da3e65ce55b700b1b9ac251b23de89 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Fri, 9 Mar 2018 12:35:19 -0500 Subject: [PATCH 54/89] Fix stream pause/resume transitions --- .../features/output/index.controller.js | 21 ++++++++++++-- awx/ui/client/features/output/index.view.html | 1 - awx/ui/client/features/output/page.service.js | 3 +- .../client/features/output/render.service.js | 11 ++----- .../client/features/output/scroll.service.js | 6 ++-- .../client/features/output/stream.service.js | 29 ++++++++++--------- 6 files changed, 42 insertions(+), 29 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index aeb615c22f..4f84cfbf8d 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -10,9 +10,6 @@ let resource; let $state; let qs; -let chain; -let chainLength; - function JobsIndexController ( _resource_, _page_, @@ -179,6 +176,10 @@ function shift () { } function scrollHome () { + if (scroll.isPaused()) { + return; + } + scroll.pause(); return page.first() @@ -198,12 +199,18 @@ function scrollHome () { function scrollEnd () { if (stream.isActive()) { + if (stream.isTransitioning()) { + return; + } + if (stream.isPaused()) { stream.resume(); } else { stream.pause(); } + return; + } else if (scroll.isPaused()) { return; } @@ -225,10 +232,18 @@ function scrollEnd () { } function scrollPageUp () { + if (scroll.isPaused()) { + return; + } + scroll.pageUp(); } function scrollPageDown () { + if (scroll.isPaused()) { + return; + } + scroll.pageDown(); } diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 53b69a43b2..dfb6790d78 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -1,7 +1,6 @@
-

diff --git a/awx/ui/client/features/output/page.service.js b/awx/ui/client/features/output/page.service.js index 64f0542fe0..3c5eca95e8 100644 --- a/awx/ui/client/features/output/page.service.js +++ b/awx/ui/client/features/output/page.service.js @@ -189,12 +189,13 @@ function JobPageService ($q) { return; } - this.bookmark.state.first = this.page.state.first - 1; + this.bookmark.state.first = this.page.state.first; this.bookmark.state.last = this.page.state.last - 1; this.bookmark.state.current = this.page.state.current - 1; this.bookmark.cache = JSON.parse(JSON.stringify(this.page.cache)); this.bookmark.set = true; this.bookmark.pending = false; + console.log('applied', JSON.stringify(this.bookmark.state, 0, 2)); }; this.removeBookmark = () => { diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js index 235de94198..9f883a126c 100644 --- a/awx/ui/client/features/output/render.service.js +++ b/awx/ui/client/features/output/render.service.js @@ -87,7 +87,7 @@ function JobRenderService ($q, $sce, $window) { return { html, count }; }; - this.createRecord = event => { + this.createRecord = (ln, lines, event) => { if (!event.uuid) { return null; } @@ -104,7 +104,7 @@ function JobRenderService ($q, $sce, $window) { }; if (event.parent_uuid) { - info.parents = getParentEvents(event.parent_uuid); + info.parents = this.getParentEvents(event.parent_uuid); } if (info.isTruncated) { @@ -209,7 +209,7 @@ function JobRenderService ($q, $sce, $window) { list.push(uuid); if (this.record[uuid].parents) { - list = list.concat(record[uuid].parents); + list = list.concat(this.record[uuid].parents); } } @@ -231,7 +231,6 @@ function JobRenderService ($q, $sce, $window) { this.remove = elements => { return this.requestAnimationFrame(() => { - elements.empty(); elements.remove(); }); }; @@ -254,10 +253,6 @@ function JobRenderService ($q, $sce, $window) { return this.requestAnimationFrame(); }; - this.build = () => { - - }; - this.clear = () => { const elements = this.el.children(); diff --git a/awx/ui/client/features/output/scroll.service.js b/awx/ui/client/features/output/scroll.service.js index ef80655bf5..791d495796 100644 --- a/awx/ui/client/features/output/scroll.service.js +++ b/awx/ui/client/features/output/scroll.service.js @@ -137,6 +137,10 @@ function JobScrollService ($q, $timeout) { this.isAtRest(); }; + this.scrollToBottom = () => { + this.setScrollPosition(this.getScrollHeight()); + }; + this.isAtRest = () => { if (this.position.current === 0 && !this.state.top) { this.state.top = true; @@ -161,12 +165,10 @@ function JobScrollService ($q, $timeout) { this.lock = () => { this.state.locked = true; - this.state.paused = true; }; this.unlock = () => { this.state.locked = false; - this.state.paused = false; }; this.isLocked = () => { diff --git a/awx/ui/client/features/output/stream.service.js b/awx/ui/client/features/output/stream.service.js index 557337adc3..05603d103a 100644 --- a/awx/ui/client/features/output/stream.service.js +++ b/awx/ui/client/features/output/stream.service.js @@ -106,7 +106,7 @@ function JobStreamService ($q) { return this.hooks.render(events) .then(() => { if (this.scroll.isLocked()) { - this.scroll.setScrollPosition(this.scroll.getScrollHeight()); + this.scroll.scrollToBottom(); } if (this.isEnding()) { @@ -127,13 +127,13 @@ function JobStreamService ($q) { if (done) { this.state.resuming = false; this.state.paused = false; - - return; + } else if (!this.isTransitioning()) { + this.scroll.pause(); + this.scroll.lock(); + this.scroll.scrollToBottom(); + this.state.resuming = true; + this.page.removeBookmark(); } - - this.scroll.lock(); - this.state.resuming = true; - this.page.removeBookmark(); }; this.pause = done => { @@ -141,18 +141,17 @@ function JobStreamService ($q) { this.state.pausing = false; this.state.paused = true; this.scroll.resume(); - - return; + } else if (!this.isTransitioning()) { + this.scroll.pause(); + this.scroll.unlock(); + this.state.pausing = true; + this.page.setBookmark(); } - - this.scroll.unlock(); - this.scroll.pause(); - this.state.pausing = true; - this.page.setBookmark(); }; this.start = () => { this.state.started = true; + this.scroll.pause(); this.scroll.lock(); }; @@ -161,6 +160,7 @@ function JobStreamService ($q) { this.state.ending = false; this.state.ended = true; this.scroll.unlock(); + this.scroll.resume(); return; } @@ -172,6 +172,7 @@ function JobStreamService ($q) { this.isPaused = () => this.state.paused; this.isPausing = () => this.state.pausing; this.isResuming = () => this.state.resuming; + this.isTransitioning = () => this.isActive() && (this.state.pausing || this.state.resuming); this.isActive = () => this.state.started && !this.state.ended; this.isEnding = () => this.state.ending; this.isDone = () => this.state.ended; From c9612b8c75e376a89a887951b6d28007743d4783 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Fri, 9 Mar 2018 13:09:46 -0500 Subject: [PATCH 55/89] Fix (most) lint errors --- .../features/output/index.controller.js | 22 ++++----- awx/ui/client/features/output/index.js | 47 ++++++++++-------- awx/ui/client/features/output/page.service.js | 47 +++++++----------- .../client/features/output/render.service.js | 49 +++++++------------ .../client/features/output/scroll.service.js | 34 ++++--------- .../features/output/search-key.directive.js | 3 +- .../client/features/output/stream.service.js | 32 ++++++------ awx/ui/client/lib/models/Base.js | 4 +- 8 files changed, 101 insertions(+), 137 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 4f84cfbf8d..b65cb52a68 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -110,7 +110,7 @@ function next () { return page.next() .then(events => { if (!events) { - return; + return $q.resolve(); } return shift() @@ -119,13 +119,13 @@ function next () { } function previous () { - let initialPosition = scroll.getScrollPosition(); + const initialPosition = scroll.getScrollPosition(); let postPopHeight; return page.previous() .then(events => { if (!events) { - return; + return $q.resolve(); } return pop() @@ -134,7 +134,7 @@ function previous () { return prepend(events); }) - .then(() => { + .then(() => { const currentHeight = scroll.getScrollHeight(); scroll.setScrollPosition(currentHeight - postPopHeight + initialPosition); }); @@ -177,7 +177,7 @@ function shift () { function scrollHome () { if (scroll.isPaused()) { - return; + return $q.resolve(); } scroll.pause(); @@ -185,7 +185,7 @@ function scrollHome () { return page.first() .then(events => { if (!events) { - return; + return $q.resolve(); } return render.clear() @@ -200,7 +200,7 @@ function scrollHome () { function scrollEnd () { if (stream.isActive()) { if (stream.isTransitioning()) { - return; + return $q.resolve(); } if (stream.isPaused()) { @@ -209,9 +209,9 @@ function scrollEnd () { stream.pause(); } - return; + return $q.resolve(); } else if (scroll.isPaused()) { - return; + return $q.resolve(); } scroll.pause(); @@ -219,7 +219,7 @@ function scrollEnd () { return page.last() .then(events => { if (!events) { - return; + return $q.resolve(); } return render.clear() @@ -306,7 +306,7 @@ function toggleSearchKey () { vm.searchKey = !vm.searchKey; } -function getCurrentQueryset() { +function getCurrentQueryset () { const { job_event_search } = $state.params; return qs.decodeArr(job_event_search); diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 64819e9ca3..ebf69f5243 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -17,7 +17,16 @@ const PAGE_LIMIT = 3; const PAGE_SIZE = 100; const WS_PREFIX = 'ws'; -function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJob, $stateParams, qs, Wait) { +function resolveResource ( + Job, + ProjectUpdate, + AdHocCommand, + SystemJob, + WorkflowJob, + $stateParams, + qs, + Wait +) { const { id, type, job_event_search } = $stateParams; let Resource; @@ -60,22 +69,20 @@ function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJ pageLimit: PAGE_LIMIT, params, })) - .then(model => { - return { - id, - type, - model, - related, - ws: { - namespace: `${WS_PREFIX}-${getWebSocketResource(type).key}-${id}` - }, - page: { - cache: PAGE_CACHE, - size: PAGE_SIZE, - pageLimit: PAGE_LIMIT - } - }; - }) + .then(model => ({ + id, + type, + model, + related, + ws: { + namespace: `${WS_PREFIX}-${getWebSocketResource(type).key}-${id}` + }, + page: { + cache: PAGE_CACHE, + size: PAGE_SIZE, + pageLimit: PAGE_LIMIT + } + })) .catch(({ data, status }) => qs.error(data, status)) .finally(() => Wait('stop')); } @@ -84,9 +91,6 @@ function resolveWebSocketConnection (SocketService, $stateParams) { const { type, id } = $stateParams; const resource = getWebSocketResource(type); - let name; - let events; - const state = { data: { socket: { @@ -132,12 +136,13 @@ function getWebSocketResource (type) { name = 'jobs'; key = 'job_events'; break; + default: + throw new Error('Unsupported WebSocket type'); } return { name, key }; } - function JobsRun ($stateRegistry) { const state = { name: 'jobz', diff --git a/awx/ui/client/features/output/page.service.js b/awx/ui/client/features/output/page.service.js index 3c5eca95e8..3f6461345e 100644 --- a/awx/ui/client/features/output/page.service.js +++ b/awx/ui/client/features/output/page.service.js @@ -47,7 +47,7 @@ function JobPageService ($q) { } else { reference.cache.unshift(page); reference.state.first = page.number; - reference.state.last = reference.cache[reference.cache.length -1].number; + reference.state.last = reference.cache[reference.cache.length - 1].number; } reference.state.current = page.number; @@ -101,7 +101,7 @@ function JobPageService ($q) { } return false; - } + }; this.emptyBuffer = () => { const reference = this.getReference(); @@ -137,8 +137,9 @@ function JobPageService ($q) { }; this.trim = left => { - let reference = this.getActiveReference(); - let excess = reference.cache.length - this.page.limit; + const reference = this.getActiveReference(); + const excess = reference.cache.length - this.page.limit; + let ejected; if (left) { @@ -152,9 +153,8 @@ function JobPageService ($q) { return ejected.reduce((total, page) => total + page.lines, 0); }; - this.isPageBookmarked = number => { - return number >= this.page.bookmark.first && number <= this.page.bookmark.last; - }; + this.isPageBookmarked = number => number >= this.page.bookmark.first && + number <= this.page.bookmark.last; this.updateLineCount = (lines, stream) => { let reference; @@ -168,15 +168,10 @@ function JobPageService ($q) { const index = reference.cache.findIndex(item => item.number === reference.state.current); reference.cache[index].lines += lines; - } - - this.isBookmarkPending = () => { - return this.bookmark.pending; }; - this.isBookmarkSet = () => { - return this.bookmark.set; - }; + this.isBookmarkPending = () => this.bookmark.pending; + this.isBookmarkSet = () => this.bookmark.set; this.setBookmark = () => { if (this.isBookmarkSet()) { @@ -190,12 +185,11 @@ function JobPageService ($q) { } this.bookmark.state.first = this.page.state.first; - this.bookmark.state.last = this.page.state.last - 1; + this.bookmark.state.last = this.page.state.last - 1; this.bookmark.state.current = this.page.state.current - 1; this.bookmark.cache = JSON.parse(JSON.stringify(this.page.cache)); this.bookmark.set = true; this.bookmark.pending = false; - console.log('applied', JSON.stringify(this.bookmark.state, 0, 2)); }; this.removeBookmark = () => { @@ -271,19 +265,16 @@ function JobPageService ($q) { }); }; - this.buildRequestConfig = number => { - return { - page: number, - related: this.resource.related, - params: { - order_by: 'start_line' - } - }; - }; + this.buildRequestConfig = number => ({ + page: number, + related: this.resource.related, + params: { + order_by: 'start_line' + } + }); - this.getActiveReference = () => { - return this.isBookmarkSet() ? this.getReference(true) : this.getReference(); - }; + this.getActiveReference = () => (this.isBookmarkSet() ? + this.getReference(true) : this.getReference()); this.getReference = (bookmark) => { if (bookmark) { diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js index 9f883a126c..e8d1cd0dd0 100644 --- a/awx/ui/client/features/output/render.service.js +++ b/awx/ui/client/features/output/render.service.js @@ -5,8 +5,6 @@ const ELEMENT_TBODY = '#atStdoutResultTable'; const EVENT_START_TASK = 'playbook_on_task_start'; const EVENT_START_PLAY = 'playbook_on_play_start'; const EVENT_STATS_PLAY = 'playbook_on_stats'; -const JOB_START = 'playbook_on_start'; -const JOB_END = 'playbook_on_stats'; const EVENT_GROUPS = [ EVENT_START_TASK, @@ -70,7 +68,7 @@ function JobRenderService ($q, $sce, $window) { const current = this.createRecord(ln, lines, event); - const html = lines.reduce((html, line, i) => { + const html = lines.reduce((concat, line, i) => { ln++; const isLastLine = i === lines.length - 1; @@ -81,7 +79,7 @@ function JobRenderService ($q, $sce, $window) { count++; } - return `${html}${row}`; + return `${concat}${row}`; }, ''); return { html, count }; @@ -191,16 +189,16 @@ function JobRenderService ($q, $sce, $window) { ${tdEvent} ${timestamp} `; - } + }; - this.getTimestamp = (created) => { + this.getTimestamp = created => { const date = new Date(created); const hour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours(); const minute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes(); const second = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds(); return `${hour}:${minute}:${second}`; - } + }; this.getParentEvents = (uuid, list) => { list = list || []; @@ -216,9 +214,7 @@ function JobRenderService ($q, $sce, $window) { return list; }; - this.getEvents = () => { - return this.hooks.get(); - }; + this.getEvents = () => this.hooks.get(); this.insert = (events, insert) => { const result = this.transformEventGroup(events); @@ -229,23 +225,19 @@ function JobRenderService ($q, $sce, $window) { .then(() => result.lines); }; - this.remove = elements => { - return this.requestAnimationFrame(() => { - elements.remove(); - }); - }; + this.remove = elements => this.requestAnimationFrame(() => { + elements.remove(); + }); - this.requestAnimationFrame = fn => { - return $q(resolve => { - $window.requestAnimationFrame(() => { - if (fn) { - fn(); - } + this.requestAnimationFrame = fn => $q(resolve => { + $window.requestAnimationFrame(() => { + if (fn) { + fn(); + } - return resolve(); - }); + return resolve(); }); - }; + }); this.compile = html => { this.hooks.compile(html); @@ -271,13 +263,8 @@ function JobRenderService ($q, $sce, $window) { return this.remove(elements); }; - this.prepend = events => { - return this.insert(events, html => this.el.prepend(html)) - }; - - this.append = events => { - return this.insert(events, html => this.el.append(html)) - }; + this.prepend = events => this.insert(events, html => this.el.prepend(html)); + this.append = events => this.insert(events, html => this.el.append(html)); // TODO: stdout from the API should not be trusted. this.sanitize = html => { diff --git a/awx/ui/client/features/output/scroll.service.js b/awx/ui/client/features/output/scroll.service.js index 791d495796..ae186798e4 100644 --- a/awx/ui/client/features/output/scroll.service.js +++ b/awx/ui/client/features/output/scroll.service.js @@ -42,7 +42,6 @@ function JobScrollService ($q, $timeout) { this.register = () => { this.pause(); - const height = this.getScrollHeight(); const current = this.getScrollPosition(); const downward = current > this.position.previous; @@ -71,19 +70,16 @@ function JobScrollService ($q, $timeout) { }; this.isBeyondThreshold = (downward, current) => { - const previous = this.position.previous; - const height = this.getScrollHeight(); + const height = this.getScrollHeight(); - if (downward) { + if (downward) { current += this.getViewableHeight(); if (current >= height || ((height - current) / height) < THRESHOLD) { return true; } - } else { - if (current <= 0 || (current / height) < THRESHOLD) { - return true; - } + } else if (current <= 0 || (current / height) < THRESHOLD) { + return true; } return false; @@ -111,17 +107,9 @@ function JobScrollService ($q, $timeout) { this.setScrollPosition(top + height); }; - this.getScrollHeight = () => { - return this.el[0].scrollHeight; - }; - - this.getViewableHeight = () => { - return this.el[0].offsetHeight; - }; - - this.getScrollPosition = () => { - return this.el[0].scrollTop; - }; + this.getScrollHeight = () => this.el[0].scrollHeight; + this.getViewableHeight = () => this.el[0].offsetHeight; + this.getScrollPosition = () => this.el[0].scrollTop; this.setScrollPosition = position => { this.position.previous = this.position.current; @@ -159,9 +147,7 @@ function JobScrollService ($q, $timeout) { this.state.paused = true; }; - this.isPaused = () => { - return this.state.paused; - }; + this.isPaused = () => this.state.paused; this.lock = () => { this.state.locked = true; @@ -171,9 +157,7 @@ function JobScrollService ($q, $timeout) { this.state.locked = false; }; - this.isLocked = () => { - return this.state.locked; - }; + this.isLocked = () => this.state.locked; } JobScrollService.$inject = ['$q', '$timeout']; diff --git a/awx/ui/client/features/output/search-key.directive.js b/awx/ui/client/features/output/search-key.directive.js index f9a1ad1928..91455e3637 100644 --- a/awx/ui/client/features/output/search-key.directive.js +++ b/awx/ui/client/features/output/search-key.directive.js @@ -13,12 +13,11 @@ function AtSearchKeyController () { vm.examples = scope.examples || []; vm.fields = scope.fields || []; vm.relatedFields = scope.relatedFields || []; - } + }; } AtSearchKeyController.$inject = ['$scope']; - function atSearchKey () { return { templateUrl, diff --git a/awx/ui/client/features/output/stream.service.js b/awx/ui/client/features/output/stream.service.js index 05603d103a..a7e73cbdfa 100644 --- a/awx/ui/client/features/output/stream.service.js +++ b/awx/ui/client/features/output/stream.service.js @@ -102,26 +102,24 @@ function JobStreamService ($q) { .then(() => --this.lag); }; - this.renderFrame = events => { - return this.hooks.render(events) - .then(() => { - if (this.scroll.isLocked()) { - this.scroll.scrollToBottom(); + this.renderFrame = events => this.hooks.render(events) + .then(() => { + if (this.scroll.isLocked()) { + this.scroll.scrollToBottom(); + } + + if (this.isEnding()) { + const lastEvents = this.page.emptyBuffer(); + + if (lastEvents.length) { + return this.renderFrame(lastEvents); } - if (this.isEnding()) { - const lastEvents = this.page.emptyBuffer(); + this.end(true); + } - if (lastEvents.length) { - return this.renderFrame(lastEvents); - } - - this.end(true); - } - - return $q.resolve(); - }); - }; + return $q.resolve(); + }); this.resume = done => { if (done) { diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index 98f2b25007..f3b0d4dfba 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -400,7 +400,7 @@ function extend (related, config) { function goToPage (config) { const params = config.params || {}; - const page = config.page; + const { page } = config; let url; let key; @@ -473,7 +473,7 @@ function goToPage (config) { return { results: data.results, page: pageNumber - } + }; }); } From 81c85913ac933db1f042b3c99e1ff74ca344875b Mon Sep 17 00:00:00 2001 From: gconsidine Date: Fri, 9 Mar 2018 15:30:44 -0500 Subject: [PATCH 56/89] Add encoding of html entities in stdout from the API --- awx/ui/client/features/output/render.service.js | 15 ++++++++------- awx/ui/package.json | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js index e8d1cd0dd0..d7b32ae408 100644 --- a/awx/ui/client/features/output/render.service.js +++ b/awx/ui/client/features/output/render.service.js @@ -1,5 +1,6 @@ import Ansi from 'ansi-to-html'; import hasAnsi from 'has-ansi'; +import Entities from 'html-entities'; const ELEMENT_TBODY = '#atStdoutResultTable'; const EVENT_START_TASK = 'playbook_on_task_start'; @@ -18,6 +19,7 @@ const TIME_EVENTS = [ ]; const ansi = new Ansi(); +const entities = new Entities.AllHtmlEntities(); function JobRenderService ($q, $sce, $window) { this.init = ({ compile, apply, get }) => { @@ -60,7 +62,7 @@ function JobRenderService ($q, $sce, $window) { return { html: '', count: 0 }; } - const { stdout } = event; + const stdout = this.sanitize(event.stdout); const lines = stdout.split('\r\n'); let count = lines.length; @@ -72,6 +74,7 @@ function JobRenderService ($q, $sce, $window) { ln++; const isLastLine = i === lines.length - 1; + let row = this.createRow(current, ln, line); if (current && current.isTruncated && isLastLine) { @@ -218,7 +221,7 @@ function JobRenderService ($q, $sce, $window) { this.insert = (events, insert) => { const result = this.transformEventGroup(events); - const html = this.sanitize(result.html); + const html = this.trustHtml(result.html); return this.requestAnimationFrame(() => insert(html)) .then(() => this.compile(html)) @@ -264,14 +267,12 @@ function JobRenderService ($q, $sce, $window) { }; this.prepend = events => this.insert(events, html => this.el.prepend(html)); + this.append = events => this.insert(events, html => this.el.append(html)); - // TODO: stdout from the API should not be trusted. - this.sanitize = html => { - html = $sce.trustAsHtml(html); + this.trustHtml = html => $sce.getTrustedHtml($sce.trustAsHtml(html)); - return $sce.getTrustedHtml(html); - }; + this.sanitize = html => entities.encode(html); } JobRenderService.$inject = ['$q', '$sce', '$window']; diff --git a/awx/ui/package.json b/awx/ui/package.json index 5f8ff0ad5c..9c381f466f 100644 --- a/awx/ui/package.json +++ b/awx/ui/package.json @@ -115,6 +115,7 @@ "components-font-awesome": "^4.6.1", "d3": "~3.3.13", "has-ansi": "^3.0.0", + "html-entities": "^1.2.1", "javascript-detect-element-resize": "^0.5.3", "jquery": "~2.2.4", "jquery-ui": "^1.12.1", From e3d42d8e1b0d31339d7db09da10d4538cbfddfd5 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Mon, 12 Mar 2018 15:19:01 -0400 Subject: [PATCH 57/89] Fix webpack-dev-server proxy to accommodate auth changes --- awx/ui/build/webpack.watch.js | 47 +++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/awx/ui/build/webpack.watch.js b/awx/ui/build/webpack.watch.js index a68c2eee25..c764ec4b85 100644 --- a/awx/ui/build/webpack.watch.js +++ b/awx/ui/build/webpack.watch.js @@ -55,26 +55,37 @@ const watch = { host: '127.0.0.1', https: true, port: 3000, - https: true, clientLogLevel: 'none', - proxy: { - '/': { - target: TARGET, - secure: false, - ws: false, - bypass: req => req.originalUrl.includes('hot-update.json') - }, - '/websocket': { - target: TARGET, - secure: false, - ws: true - }, - '/network_ui': { - target: TARGET, - secure: false, - ws: true + proxy: [{ + context: (pathname, req) => !(pathname === '/api/login/' && req.method === 'POST'), + target: TARGET, + secure: false, + ws: false, + bypass: req => req.originalUrl.includes('hot-update.json') + }, + { + context: '/api/login/', + target: TARGET, + secure: false, + ws: false, + headers: { + Host: `localhost:${TARGET_PORT}`, + Origin: TARGET, + Referer: `${TARGET}/` } - } + }, + { + context: '/websocket', + target: TARGET, + secure: false, + ws: true + }, + { + context: '/network_ui', + target: TARGET, + secure: false, + ws: true + }] } }; From 033314e4f648312a367dcc331692cbd13cdb37fb Mon Sep 17 00:00:00 2001 From: gconsidine Date: Tue, 13 Mar 2018 15:24:12 -0400 Subject: [PATCH 58/89] Add fixes to results - Handle out of order events by batching lines until all lines are present - In static mode, fetch pages of results until container is full and scroll bar appears (for scroll events related to pagination) --- .../features/output/index.controller.js | 12 +++++- awx/ui/client/features/output/index.js | 4 +- .../client/features/output/scroll.service.js | 2 + .../client/features/output/stream.service.js | 39 ++++++++++++++++++- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index b65cb52a68..f2b6235476 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -114,7 +114,12 @@ function next () { } return shift() - .then(() => append(events)); + .then(() => append(events)) + .then(() => { + if(scroll.isMissing()) { + return next(); + } + }); }); } @@ -193,6 +198,11 @@ function scrollHome () { .then(() => { scroll.resetScrollPosition(); scroll.resume(); + }) + .then(() => { + if(scroll.isMissing()) { + return next(); + } }); }); } diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index ebf69f5243..b56e09a1ff 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -13,8 +13,8 @@ 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 PAGE_LIMIT = 5; +const PAGE_SIZE = 50; const WS_PREFIX = 'ws'; function resolveResource ( diff --git a/awx/ui/client/features/output/scroll.service.js b/awx/ui/client/features/output/scroll.service.js index ae186798e4..a568813ddc 100644 --- a/awx/ui/client/features/output/scroll.service.js +++ b/awx/ui/client/features/output/scroll.service.js @@ -1,4 +1,5 @@ const ELEMENT_CONTAINER = '.at-Stdout-container'; +const ELEMENT_TBODY = '#atStdoutResultTable'; const DELAY = 100; const THRESHOLD = 0.1; @@ -158,6 +159,7 @@ function JobScrollService ($q, $timeout) { }; this.isLocked = () => this.state.locked; + this.isMissing = () => $(ELEMENT_TBODY)[0].clientHeight < this.getViewableHeight(); } JobScrollService.$inject = ['$q', '$timeout']; diff --git a/awx/ui/client/features/output/stream.service.js b/awx/ui/client/features/output/stream.service.js index a7e73cbdfa..65aa44ea43 100644 --- a/awx/ui/client/features/output/stream.service.js +++ b/awx/ui/client/features/output/stream.service.js @@ -27,6 +27,14 @@ function JobStreamService ($q) { listen }; + this.lines = { + used: [], + missing: [], + ready: false, + min: 0, + max: 0 + }; + this.hooks.listen(resource.ws.namespace, this.listen); }; @@ -72,6 +80,31 @@ function JobStreamService ($q) { } }; + this.checkLines = data => { + for (let i = data.start_line; i < data.end_line; i++) { + if (i > this.lines.max) { + this.lines.max = i; + } + + this.lines.used.push(i); + } + + let missing = []; + for (let i = this.lines.min; i < this.lines.max; i++) { + if (this.lines.used.indexOf(i) === -1) { + missing.push(i); + } + } + + if (missing.length === 0) { + this.lines.ready = true; + this.lines.min = this.lines.max + 1; + this.lines.used = []; + } else { + this.lines.ready = false; + } + }; + this.listen = data => { this.lag++; @@ -87,10 +120,11 @@ function JobStreamService ($q) { } } + this.checkLines(data); this.buffer(data); this.count++; - if (this.isPaused() || !this.isBatchFull()) { + if (!this.isReadyToRender()) { return $q.resolve(); } @@ -166,6 +200,9 @@ function JobStreamService ($q) { this.state.ending = true; }; + this.isReadyToRender = () => this.isEnding() || + (!this.isPaused() && this.hasAllLines() && this.isBatchFull()); + this.hasAllLines = () => this.lines.ready; this.isBatchFull = () => this.count % this.framesPerRender === 0; this.isPaused = () => this.state.paused; this.isPausing = () => this.state.pausing; From a23e5e920f100ae0e9c7887b9295b9f6be0540bc Mon Sep 17 00:00:00 2001 From: gconsidine Date: Wed, 14 Mar 2018 15:21:33 -0400 Subject: [PATCH 59/89] Add support for in progress jobs and omit expand * Any event received by the stream service will start rendering (instead of JOB_START events only) * Expand/collapse only shown for static results --- awx/ui/client/features/output/index.controller.js | 5 +++-- awx/ui/client/features/output/render.service.js | 6 +++--- awx/ui/client/features/output/stream.service.js | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index f2b6235476..5167eab0b5 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -72,12 +72,13 @@ function JobsIndexController ( function init (pageMode) { page.init({ - resource + resource, }); render.init({ get: () => resource.model.get(`related.${resource.related}.results`), - compile: html => $compile(html)($scope) + compile: html => $compile(html)($scope), + isStreamActive: stream.isActive }); scroll.init({ diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js index d7b32ae408..e55f3901f1 100644 --- a/awx/ui/client/features/output/render.service.js +++ b/awx/ui/client/features/output/render.service.js @@ -22,11 +22,11 @@ const ansi = new Ansi(); const entities = new Entities.AllHtmlEntities(); function JobRenderService ($q, $sce, $window) { - this.init = ({ compile, apply, get }) => { + this.init = ({ compile, apply, isStreamActive }) => { this.parent = null; this.record = {}; this.el = $(ELEMENT_TBODY); - this.hooks = { get, compile, apply }; + this.hooks = { isStreamActive, compile, apply }; }; this.sortByLineNumber = (a, b) => { @@ -155,7 +155,7 @@ function JobRenderService ($q, $sce, $window) { } if (current) { - if (current.isParent && current.line === ln) { + if (!this.hooks.isStreamActive() && current.isParent && current.line === ln) { id = current.uuid; tdToggle = ``; } diff --git a/awx/ui/client/features/output/stream.service.js b/awx/ui/client/features/output/stream.service.js index 65aa44ea43..d7f8a72555 100644 --- a/awx/ui/client/features/output/stream.service.js +++ b/awx/ui/client/features/output/stream.service.js @@ -110,7 +110,7 @@ function JobStreamService ($q) { this.chain = this.chain .then(() => { - if (data.event === JOB_START) { + if (!this.isActive()) { this.start(); } else if (data.event === JOB_END) { if (this.isPaused()) { From f65d170caba88b8d5ab832571b26825927e8f175 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 12 Mar 2018 17:17:00 -0400 Subject: [PATCH 60/89] initial details panel integration --- .../features/output/details.directive.js | 502 ++++++++++++++++++ .../features/output/details.partial.html | 236 ++++++++ .../features/output/index.controller.js | 7 + awx/ui/client/features/output/index.js | 20 +- awx/ui/client/features/output/index.view.html | 1 + awx/ui/client/features/output/jobs.strings.js | 5 + awx/ui/client/lib/models/Base.js | 5 +- 7 files changed, 765 insertions(+), 11 deletions(-) create mode 100644 awx/ui/client/features/output/details.directive.js create mode 100644 awx/ui/client/features/output/details.partial.html diff --git a/awx/ui/client/features/output/details.directive.js b/awx/ui/client/features/output/details.directive.js new file mode 100644 index 0000000000..2393284913 --- /dev/null +++ b/awx/ui/client/features/output/details.directive.js @@ -0,0 +1,502 @@ +const templateUrl = require('~features/output/details.partial.html'); + +let $http; +let $filter; +let $state; + +let error; +let parse; +let prompt; +let resource; +let strings; +let wait; + +function mapChoices (choices) { + return Object.assign(...choices.map(([k, v]) => ({[k]: v}))); +} + +function getStatusDetails (status) { + const value = status || resource.model.get('status'); + const label = 'Status'; + const choices = mapChoices(resource.model.options('actions.GET.status.choices')); + + const displayValue = choices[value]; + + return { displayValue, label, value }; +} + +function getStartTimeDetails (started) { + const value = started || resource.model.get('started'); + const label = 'Started'; + + let displayValue; + + if (value) { + displayValue = $filter('longDate')(value); + } else { + displayValue = 'Not Started'; + } + + return { displayValue, label, value }; +} + +function getFinishTimeDetails (finished) { + const value = finished || resource.model.get('finished'); + const label = 'Finished'; + + let displayValue; + + if (value) { + displayValue = $filter('longDate')(value); + } else { + displayValue = 'Not Finished'; + } + + return { displayValue, label, value }; +} + +function getJobTypeDetails () { + const value = resource.model.get('job_type'); + const label = 'Job Type'; + const choices = mapChoices(resource.model.options('actions.GET.job_type.choices')); + + const displayValue = choices[value]; + + return { displayValue, label, value }; +} + + +function getVerbosityDetails () { + const value = resource.model.get('verbosity'); + const choices = mapChoices(resource.model.options('actions.GET.verbosity.choices')); + + const displayValue = choices[value]; + const label = 'Verbosity'; + + return { displayValue, label, value }; +} + +function getSourceWorkflowJobDetails () { + const sourceWorkflowJob = resource.model.get('summary_fields.source_workflow_job'); + + if (!sourceWorkflowJob) { + return null; + } + + const link = `/#/workflows/${sourceWorkflowJob.id}`; + + return { link }; +} + +function getJobTemplateDetails () { + const jobTemplate = resource.model.get('summary_fields.job_template'); + + if (!jobTemplate) { + return null; + } + + const label = 'Job Template'; + const link = `/#/templates/job_template/${jobTemplate.id}`; + const value = $filter('sanitize')(jobTemplate.name); + + return { label, link, value }; +} + +function getLaunchedByDetails () { + const createdBy = resource.model.get('summary_fields.created_by'); + const jobTemplate = resource.model.get('summary_fields.job_template'); + + const relatedSchedule = resource.model.get('related.schedule'); + const schedule = resource.model.get('summary_fields.schedule'); + + if (!createdBy && !schedule) { + return null; + } + + const label = 'Launched By'; + + let link; + let tooltip; + let value; + + if (createdBy) { + tooltip = 'Edit the User'; + link = `/#/users/${createdBy.id}`; + value = $filter('sanitize')(createdBy.username); + } else if (relatedSchedule && jobTemplate) { + tooltip = 'Edit the Schedule'; + link = `/#/templates/job_template/${jobTemplate.id}/schedules/${schedule.id}`; + value = $filter('sanitize')(schedule.name); + } else { + tooltip = null; + link = null; + value = $filter('sanitize')(schedule.name); + } + + return { label, link, tooltip, value }; +} + +function getInventoryDetails () { + const inventory = resource.model.get('summary_fields.inventory'); + + if (!inventory) { + return null; + } + + const label = 'Inventory'; + const tooltip = 'Edit the inventory'; + const value = $filter('sanitize')(inventory.name); + + let link; + + if (inventory.kind === 'smart') { + link = `/#/inventories/smart/${inventory.id}`; + } else { + link = `/#/inventories/inventory/${inventory.id}`; + } + + return { label, link, tooltip, value }; +} + +function getProjectDetails () { + const project = resource.model.get('summary_fields.project'); + const projectUpdate = resource.model.get('summary_fields.project_update'); + + if (!project) { + return null; + } + + const label = 'Project'; + const link = `/#/projects/${project.id}`; + const value = $filter('sanitize')(project.name); + + if (projectUpdate) { + const update = { + link: `/#/jobz/project/${projectUpdate.id}`, + tooltip: 'View project checkout results', + status: projectUpdate.status, + }; + + return { label, link, value, update }; + } + + return { label, link, value }; +} + +function getSCMRevisionDetails () { + const label = 'Revision'; + const value = resource.model.get('scm_revision'); + + if (!value) { + return null; + } + + return { label, value }; +} + +function getPlaybookDetails () { + const label = 'Playbook'; + const value = resource.model.get('playbook'); + + if (!value) { + return null; + } + + return { label, value }; +} + +function getJobExplanationDetails () { + const jobExplanation = resource.model.get('job_explanation'); + + if (!jobExplanation) { + return null; + } + + const value = null; + + return { value }; +} + +function getResultTracebackDetails () { + const previousTaskFailed = false; + const resultTraceback = resource.model.get('result_traceback'); + + if (!resultTraceback) { + return null; + } + + if (!previousTaskFailed) { + return null; + } + + const label = 'Results Traceback'; + const value = null; + + return { label, value }; +} + +function getMachineCredentialDetails () { + const machineCredential = resource.model.get('summary_fields.credential'); + + if (!machineCredential) { + return null; + } + + const label = 'Machine Credential'; + const link = `/#/credentials/${machineCredential.id}`; + const tooltip = 'Edit the Credential'; + const value = $filter('sanitize')(machineCredential.name); + + return { label, link, tooltip, value }; +} + +function getForkDetails () { + const label = 'Forks'; + const value = resource.model.get('forks'); + + if (!value) { + return null; + } + + return { label, value }; +} + +function getLimitDetails () { + const label = 'Limit'; + const value = resource.model.get('limit'); + + if (!value) { + return null; + } + + return { label, value }; +} + +function getInstanceGroupDetails () { + + const instanceGroup = resource.model.get('summary_fields.instance_group'); + + if (!instanceGroup) { + return null; + } + + const label = 'Instance Group'; + const value = $filter('sanitize')(instanceGroup.name); + + let isolated = null; + + if (instanceGroup.controller_id) { + isolated = 'Isolated'; + } + + return { label, value, isolated }; +} + +function getJobTagDetails () { + const label = 'Job Tags'; + const value = resource.model.get('job_tags'); + + if (!value) { + return null; + } + + return { label, value }; +} + +function getSkipTagDetails () { + const label = 'Skip Tags'; + const value = resource.model.get('skip_tags'); + + if (!value) { + return null; + } + + return { label, value }; +} + +function getExtraVarsDetails () { + const extraVars = resource.model.get('extra_vars'); + + if (!extraVars) { + return null; + } + + const label = 'Extra Variables'; + const tooltip = 'Read-only view of extra variables added to the job template.'; + const value = parse(extraVars); + + return { label, tooltip, value }; +} + +function getLabelDetails () { + const jobLabels = _.get(resource.model.get('related.labels'), 'results', []); + + if (jobLabels.length < 1) { + return null; + } + + const label = 'Labels'; + const value = jobLabels.map(({ name }) => name).map($filter('sanitize')); + + let more = false; + + return { label, more, value }; +} + +function createErrorHandler (path, action) { + return ({ data, status }) => { + const hdr = strings.get('error.HEADER'); + const msg = strings.get('error.CALL', { path, action, status }); + + error($scope, data, status, null, { hdr, msg }); + }; +} + +const ELEMENT_LABELS = '#job-results-labels'; +const ELEMENT_PROMPT_MODAL = '#prompt-modal'; +const LABELS_SLIDE_DISTANCE = 200; + +function toggleLabels () { + if (!this.labels.more) { + $(ELEMENT_LABELS).slideUp(LABELS_SLIDE_DISTANCE); + this.labels.more = true; + } else { + $(ELEMENT_LABELS).slideDown(LABELS_SLIDE_DISTANCE); + this.labels.more = false; + } +} + +function cancelJob () { + const actionText = strings.get('CANCEL'); + const hdr = strings.get('warnings.CANCEL_HEADER'); + const warning = strings.get('warnings.CANCEL_BODY'); + + const id = resource.model.get('id'); + const name = $filter('sanitize')(resource.model.get('name')); + + const body = `
${warning}
`; + const resourceName = `#${id} ${name}`; + + const method = 'POST'; + const url = `${resource.model.path}/${id}/cancel/`; + + const errorHandler = createErrorHandler('cancel job', method); + + const action = () => { + wait('start'); + $http({ method, url }) + .then(() => $state.go('jobs')) + .catch(errorHandler) + .finally(() => { + $(ELEMENT_PROMPT_MODAL).modal('hide'); + wait('stop'); + }); + }; + + prompt({ hdr, resourceName, body, actionText, action }); +} + +function deleteJob () { + return; +} + +function AtDetailsController ( + _$http_, + _$filter_, + _$state_, + _error_, + _prompt_, + _strings_, + _wait_, + ParseTypeChange, + ParseVariableString, +) { + const vm = this || {}; + + $http = _$http_; + $filter = _$filter_; + $state = _$state_; + + error = _error_; + // resource = _resource_; + parse = ParseVariableString; + prompt = _prompt_; + strings = _strings_; + wait = _wait_; + + // statusChoices = mapChoices(resource.options('status.choices')); + + vm.init = scope => { + vm.job = scope.job || {}; + resource = scope.resource; + + vm.status = getStatusDetails(scope.status); + vm.startTime = getStartTimeDetails(); + vm.finishTime = getFinishTimeDetails(); + vm.jobType = getJobTypeDetails(); + vm.jobTemplate = getJobTemplateDetails(); + vm.sourceWorkflowJob = getSourceWorkflowJobDetails(); + vm.inventory = getInventoryDetails(); + vm.project = getProjectDetails(); + vm.scmRevision = getSCMRevisionDetails(); + vm.playbook = getPlaybookDetails(); + vm.resultTraceback = getResultTracebackDetails(); + vm.launchedBy = getLaunchedByDetails(); + vm.jobExplanation = getJobExplanationDetails(); + vm.verbosity = getVerbosityDetails(); + vm.machineCredential = getMachineCredentialDetails(); + vm.forks = getForkDetails(); + vm.limit = getLimitDetails(); + vm.instanceGroup = getInstanceGroupDetails(); + vm.jobTags = getJobTagDetails(); + vm.skipTags = getSkipTagDetails(); + vm.extraVars = getExtraVarsDetails(); + vm.labels = getLabelDetails(); + + vm.cancelJob = cancelJob; + vm.deleteJob = deleteJob; + vm.toggleLabels = toggleLabels; + + // codemirror + const cm = { parseType: 'yaml', variables: vm.extraVars.value, $apply: scope.$apply }; + ParseTypeChange({ scope: cm, field_id: 'cm-extra-vars', readOnly: true }); + + scope.$watch('status', value => { vm.status = getStatusDetails(value); }); + } +} + +AtDetailsController.$inject = [ + '$http', + '$filter', + '$state', + 'ProcessErrors', + 'Prompt', + 'JobStrings', + 'Wait', + 'ParseTypeChange', + 'ParseVariableString', +]; + +function atDetailsLink (scope, el, attrs, controllers) { + const [atDetailsController] = controllers; + + atDetailsController.init(scope); +} + +function atDetails () { + return { + templateUrl, + restrict: 'E', + require: ['atDetails'], + controllerAs: 'vm', + link: atDetailsLink, + controller: AtDetailsController, + scope: { + job: '=', + status: '=', + resource: '=', + }, + }; +} + +export default atDetails; diff --git a/awx/ui/client/features/output/details.partial.html b/awx/ui/client/features/output/details.partial.html new file mode 100644 index 0000000000..2741d43d0a --- /dev/null +++ b/awx/ui/client/features/output/details.partial.html @@ -0,0 +1,236 @@ + + +
+
DETAILS
+ +
+ + + + + + + + +
+
+ + +
+ +
+ +
+ + {{ vm.status.displayValue | translate }} +
+
+ + + +
+ +
+ {{ vm.startTime.displayValue }} +
+
+ + +
+ +
+ {{ vm.finishTime.displayValue }} +
+
+ + +
+ +
+
+ + +
+ + +
+ + +
+ +
{{ vm.jobType.displayValue }}
+
+ + +
+ + +
+ {{ vm.launchedBy.value }} +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
{{ vm.playbook.value }}
+
+ + +
+ + +
+ + +
+ +
{{ vm.forks.value }}
+
+ + +
+ +
{{ vm.limit.value }}
+
+ + +
+ +
{{ vm.verbosity.displayValue }}
+
+ + +
+ +
+ {{ vm.instanceGroup.value }} + + {{ vm.instanceGroup.isolated }} + +
+
+ + +
+ +
{{ vm.jobTags.value }}
+
+ + +
+ +
{{ vm.skipTags.value }}
+
+ + +
+ + +
+ + +
+ +
+
+
{{ label }}
+
+
+
+
diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 5167eab0b5..055e9c9a95 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -67,6 +67,13 @@ function JobsIndexController ( vm.removeSearchTag = removeSearchTag; vm.searchTags = getSearchTags(getCurrentQueryset()); + // details + vm.details = { + job: resource.model.model.GET, + status: resource.model.model.GET.status, + resource, + }; + render.requestAnimationFrame(() => init()); } diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index b56e09a1ff..367f5c1f89 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -8,6 +8,7 @@ import RenderService from '~features/output/render.service'; import ScrollService from '~features/output/scroll.service'; import SearchKeyDirective from '~features/output/search-key.directive'; import StreamService from '~features/output/stream.service'; +import DetailsDirective from '~features/output/details.directive.js'; const Template = require('~features/output/index.view.html'); @@ -55,21 +56,21 @@ function resolveResource ( } const params = { page_size: PAGE_SIZE, order_by: 'start_line' }; + const config = { pageCache: PAGE_CACHE, pageLimit: PAGE_LIMIT, params }; if (job_event_search) { - const searchParams = qs.encodeQuerysetObject(qs.decodeArr(job_event_search)); + const queryParams = qs.encodeQuerysetObject(qs.decodeArr(job_event_search)); - Object.assign(params, searchParams); + Object.assign(config.params, queryParams); } Wait('start'); - return new Resource('get', id) - .then(model => model.extend(related, { - pageCache: PAGE_CACHE, - pageLimit: PAGE_LIMIT, - params, - })) - .then(model => ({ + return new Resource(['get', 'options'], [id, id]) + .then(model => Promise.all([ + model.extend('labels'), + model.extend(related, config) + ])) + .then(([ model ]) => ({ id, type, model, @@ -197,6 +198,7 @@ angular .service('JobPageService', PageService) .service('JobScrollService', ScrollService) .service('JobStreamService', StreamService) + .directive('atDetails', DetailsDirective) .directive('atSearchKey', SearchKeyDirective) .run(JobsRun); diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index dfb6790d78..778a44d7d8 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -1,6 +1,7 @@
+

diff --git a/awx/ui/client/features/output/jobs.strings.js b/awx/ui/client/features/output/jobs.strings.js index aa1afcdfaf..a77da49f62 100644 --- a/awx/ui/client/features/output/jobs.strings.js +++ b/awx/ui/client/features/output/jobs.strings.js @@ -7,6 +7,11 @@ function JobsStrings (BaseString) { ns.state = { TITLE: t.s('JOBZ') }; + + ns.warnings = { + CANCEL_BODY: t.s('Are you sure you want to cancel this job?'), + CANCEL_HEADER: t.s('Cancel Job'), + }; } JobsStrings.$inject = ['BaseStringService']; diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index f3b0d4dfba..5111f25f6d 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -353,10 +353,11 @@ function has (method, keys) { return value !== undefined && value !== null; } -function extend (related, config) { +function extend (related, config = {}) { + const req = this.parseRequestConfig('GET', config); - if (config.params.page_size) { + if (_.get(config, 'params.page_size')) { this.page.size = config.params.page_size; this.page.current = 1; From b577f5093030c85809adabefca338ff05477fe77 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 19 Mar 2018 12:01:17 -0400 Subject: [PATCH 61/89] event processing for details panel and initial stats bar integration --- awx/ui/client/features/output/_index.less | 82 ++++++++++++ .../features/output/details.directive.js | 120 +++++++++++------- .../features/output/details.partial.html | 20 +-- .../features/output/index.controller.js | 44 ++++++- awx/ui/client/features/output/index.js | 26 +++- awx/ui/client/features/output/index.view.html | 14 +- awx/ui/client/features/output/jobs.strings.js | 5 + .../features/output/status.directive.js | 92 ++++++++++++++ .../features/output/status.partial.html | 37 ++++++ .../client/features/output/stream.service.js | 16 ++- awx/ui/client/lib/models/Job.js | 34 ++++- awx/ui/client/lib/models/ProjectUpdate.js | 39 +++++- 12 files changed, 447 insertions(+), 82 deletions(-) create mode 100644 awx/ui/client/features/output/status.directive.js create mode 100644 awx/ui/client/features/output/status.partial.html diff --git a/awx/ui/client/features/output/_index.less b/awx/ui/client/features/output/_index.less index 989b532ab6..ab4e589213 100644 --- a/awx/ui/client/features/output/_index.less +++ b/awx/ui/client/features/output/_index.less @@ -199,3 +199,85 @@ width: 100%; flex-wrap: wrap; } + + +// Status Bar ----------------------------------------------------------------------------- +.HostStatusBar { + display: flex; + flex: 0 0 auto; + width: 100%; + margin-top: 10px; +} + +.HostStatusBar-ok, +.HostStatusBar-changed, +.HostStatusBar-unreachable, +.HostStatusBar-failed, +.HostStatusBar-skipped, +.HostStatusBar-noData { + height: 15px; + border-top: 5px solid @default-bg; + border-bottom: 5px solid @default-bg; +} + +.HostStatusBar-ok { + background-color: @default-succ; + display: flex; + flex: 0 0 auto; +} + +.HostStatusBar-changed { + background-color: @default-warning; + flex: 0 0 auto; +} + +.HostStatusBar-unreachable { + background-color: @default-unreachable; + flex: 0 0 auto; +} + +.HostStatusBar-failed { + background-color: @default-err; + flex: 0 0 auto; +} + +.HostStatusBar-skipped { + background-color: @default-link; + flex: 0 0 auto; +} + +.HostStatusBar-noData { + background-color: @default-icon-hov; + flex: 1 0 auto; +} + +.HostStatusBar-tooltipLabel { + text-transform: uppercase; + margin-right: 15px; +} + +.HostStatusBar-tooltipBadge { + border-radius: 5px; + border: 1px solid @default-bg; +} + +.HostStatusBar-tooltipBadge--ok { + background-color: @default-succ; +} + +.HostStatusBar-tooltipBadge--unreachable { + background-color: @default-unreachable; +} + +.HostStatusBar-tooltipBadge--skipped { + background-color: @default-link; +} + +.HostStatusBar-tooltipBadge--changed { + background-color: @default-warning; +} + +.HostStatusBar-tooltipBadge--failed { + background-color: @default-err; + +} diff --git a/awx/ui/client/features/output/details.directive.js b/awx/ui/client/features/output/details.directive.js index 2393284913..309c50612d 100644 --- a/awx/ui/client/features/output/details.directive.js +++ b/awx/ui/client/features/output/details.directive.js @@ -2,6 +2,7 @@ const templateUrl = require('~features/output/details.partial.html'); let $http; let $filter; +let $scope; let $state; let error; @@ -12,68 +13,86 @@ let strings; let wait; function mapChoices (choices) { - return Object.assign(...choices.map(([k, v]) => ({[k]: v}))); + if (!choices) return {}; + return Object.assign(...choices.map(([k, v]) => ({ [k]: v }))); } function getStatusDetails (status) { - const value = status || resource.model.get('status'); - const label = 'Status'; + const unmapped = status || resource.model.get('status'); + + if (!unmapped) { + return null; + } + const choices = mapChoices(resource.model.options('actions.GET.status.choices')); - const displayValue = choices[value]; + const label = 'Status'; + const icon = `fa icon-job-${unmapped}`; + const value = choices[unmapped]; - return { displayValue, label, value }; + return { label, icon, value }; } function getStartTimeDetails (started) { - const value = started || resource.model.get('started'); + const unfiltered = started || resource.model.get('started'); + const label = 'Started'; - let displayValue; + let value; - if (value) { - displayValue = $filter('longDate')(value); + if (unfiltered) { + value = $filter('longDate')(unfiltered); } else { - displayValue = 'Not Started'; + value = 'Not Started'; } - return { displayValue, label, value }; + return { label, value }; } function getFinishTimeDetails (finished) { - const value = finished || resource.model.get('finished'); + const unfiltered = finished || resource.model.get('finished'); + const label = 'Finished'; - let displayValue; + let value; - if (value) { - displayValue = $filter('longDate')(value); + if (unfiltered) { + value = $filter('longDate')(unfiltered); } else { - displayValue = 'Not Finished'; + value = 'Not Finished'; } - return { displayValue, label, value }; + return { label, value }; } function getJobTypeDetails () { - const value = resource.model.get('job_type'); - const label = 'Job Type'; + const unmapped = resource.model.get('job_type'); + + if (!unmapped) { + return null; + } + const choices = mapChoices(resource.model.options('actions.GET.job_type.choices')); - const displayValue = choices[value]; + const label = 'Job Type'; + const value = choices[unmapped]; - return { displayValue, label, value }; + return { label, value }; } - function getVerbosityDetails () { - const value = resource.model.get('verbosity'); + const verbosity = resource.model.get('verbosity'); + + if (!verbosity) { + return null; + } + const choices = mapChoices(resource.model.options('actions.GET.verbosity.choices')); - const displayValue = choices[value]; const label = 'Verbosity'; + const value = choices[value]; - return { displayValue, label, value }; + return { label, value }; } function getSourceWorkflowJobDetails () { @@ -273,7 +292,6 @@ function getLimitDetails () { } function getInstanceGroupDetails () { - const instanceGroup = resource.model.get('summary_fields.instance_group'); if (!instanceGroup) { @@ -336,9 +354,9 @@ function getLabelDetails () { } const label = 'Labels'; - const value = jobLabels.map(({ name }) => name).map($filter('sanitize')); + const more = false; - let more = false; + const value = jobLabels.map(({ name }) => name).map($filter('sanitize')); return { label, more, value }; } @@ -396,9 +414,7 @@ function cancelJob () { prompt({ hdr, resourceName, body, actionText, action }); } -function deleteJob () { - return; -} +function deleteJob () {} function AtDetailsController ( _$http_, @@ -418,21 +434,18 @@ function AtDetailsController ( $state = _$state_; error = _error_; - // resource = _resource_; parse = ParseVariableString; prompt = _prompt_; strings = _strings_; wait = _wait_; - // statusChoices = mapChoices(resource.options('status.choices')); + vm.init = _$scope_ => { + $scope = _$scope_; + resource = $scope.resource; - vm.init = scope => { - vm.job = scope.job || {}; - resource = scope.resource; - - vm.status = getStatusDetails(scope.status); - vm.startTime = getStartTimeDetails(); - vm.finishTime = getFinishTimeDetails(); + vm.status = getStatusDetails(); + vm.started = getStartTimeDetails(); + vm.finished = getFinishTimeDetails(); vm.jobType = getJobTypeDetails(); vm.jobTemplate = getJobTemplateDetails(); vm.sourceWorkflowJob = getSourceWorkflowJobDetails(); @@ -457,12 +470,24 @@ function AtDetailsController ( vm.deleteJob = deleteJob; vm.toggleLabels = toggleLabels; - // codemirror - const cm = { parseType: 'yaml', variables: vm.extraVars.value, $apply: scope.$apply }; - ParseTypeChange({ scope: cm, field_id: 'cm-extra-vars', readOnly: true }); + const observe = (key, transform) => { + $scope.$watch(key, value => { this[key] = transform(value); }); + }; - scope.$watch('status', value => { vm.status = getStatusDetails(value); }); - } + observe('status', getStatusDetails); + observe('started', getStartTimeDetails); + observe('finished', getFinishTimeDetails); + + // relaunch component + $scope.job = _.get(resource.model, 'model.GET', {}); + this.job = $scope.job; + + // codemirror + if (this.extraVars) { + const cm = { parseType: 'yaml', variables: this.extraVars.value, $apply: $scope.$apply }; + ParseTypeChange({ scope: cm, field_id: 'cm-extra-vars', readOnly: true }); + } + }; } AtDetailsController.$inject = [ @@ -492,9 +517,10 @@ function atDetails () { link: atDetailsLink, controller: AtDetailsController, scope: { - job: '=', - status: '=', resource: '=', + status: '=', + started: '=', + finished: '=', }, }; } diff --git a/awx/ui/client/features/output/details.partial.html b/awx/ui/client/features/output/details.partial.html index 2741d43d0a..bb80346a7b 100644 --- a/awx/ui/client/features/output/details.partial.html +++ b/awx/ui/client/features/output/details.partial.html @@ -36,25 +36,25 @@
- - {{ vm.status.displayValue | translate }} + + {{ vm.status.value }}
-
- +
+
- {{ vm.startTime.displayValue }} + {{ vm.started.value }}
-
- +
+
- {{ vm.finishTime.displayValue }} + {{ vm.finished.value }}
@@ -81,7 +81,7 @@
-
{{ vm.jobType.displayValue }}
+
{{ vm.jobType.value }}
@@ -164,7 +164,7 @@
-
{{ vm.verbosity.displayValue }}
+
{{ vm.verbosity.value }}
diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 055e9c9a95..a2fc44a8a8 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -10,6 +10,8 @@ let resource; let $state; let qs; +let hack; + function JobsIndexController ( _resource_, _page_, @@ -51,7 +53,7 @@ function JobsIndexController ( vm.expand = expand; vm.isExpanded = true; - // search + // Search $state = _$state_; qs = _qs_; @@ -67,17 +69,47 @@ function JobsIndexController ( vm.removeSearchTag = removeSearchTag; vm.searchTags = getSearchTags(getCurrentQueryset()); - // details + // Host Status Bar + vm.status = { + running: Boolean(resource.model.get('started')) && !resource.model.get('finished'), + stats: resource.stats, + } + + // Details vm.details = { - job: resource.model.model.GET, - status: resource.model.model.GET.status, resource, + started: resource.model.get('started'), + finished: resource.model.get('finished'), + status: resource.model.get('status'), }; render.requestAnimationFrame(() => init()); } +function onStreamStart (data) { + const status = _.get(data, 'summary_fields.job.status'); + + if (!hack) { + hack = true; + vm.details.status = status; + vm.details.started = data.created; + + vm.status.running = true; + } +} + +function onStreamFinish (data) { + const failed = _.get(data, 'summary_fields.job.failed'); + + vm.details.status = failed ? 'failed' : 'successful'; + vm.details.finished = data.created; + + vm.status = { stats: data, running: false }; +}; + function init (pageMode) { + hack = false; + page.init({ resource, }); @@ -98,10 +130,12 @@ function init (pageMode) { page, scroll, resource, + onStreamStart, + onStreamFinish, render: events => shift().then(() => append(events, true)), listen: (namespace, listener) => { $scope.$on(namespace, (scope, data) => listener(data)); - } + }, }); if (pageMode) { diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 367f5c1f89..707b333adb 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -6,13 +6,16 @@ import Controller from '~features/output/index.controller'; import PageService from '~features/output/page.service'; import RenderService from '~features/output/render.service'; import ScrollService from '~features/output/scroll.service'; -import SearchKeyDirective from '~features/output/search-key.directive'; import StreamService from '~features/output/stream.service'; -import DetailsDirective from '~features/output/details.directive.js'; + +import DetailsDirective from '~features/output/details.directive'; +import SearchKeyDirective from '~features/output/search-key.directive'; +import StatusDirective from '~features/output/status.directive'; const Template = require('~features/output/index.view.html'); const MODULE_NAME = 'at.features.output'; + const PAGE_CACHE = true; const PAGE_LIMIT = 5; const PAGE_SIZE = 50; @@ -66,13 +69,21 @@ function resolveResource ( Wait('start'); return new Resource(['get', 'options'], [id, id]) - .then(model => Promise.all([ - model.extend('labels'), - model.extend(related, config) - ])) - .then(([ model ]) => ({ + .then(model => { + const promises = [model.getStats()]; + + if (model.has('related.labels')) { + promises.push(model.extend('labels')); + } + + promises.push(model.extend(related, config)); + + return Promise.all(promises); + }) + .then(([stats, model]) => ({ id, type, + stats, model, related, ws: { @@ -200,6 +211,7 @@ angular .service('JobStreamService', StreamService) .directive('atDetails', DetailsDirective) .directive('atSearchKey', SearchKeyDirective) + .directive('atStatus', StatusDirective) .run(JobsRun); export default MODULE_NAME; diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 778a44d7d8..8034b13182 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -1,14 +1,20 @@
- -

+ + +

- + +
- +
({ [key]: 0 }))); + + HOST_STATUS_KEYS.forEach(key => { + const hostData = _.get(statsEvent, ['event_data', key], {}); + + Object.keys(hostData).forEach(hostName => { + const isAlreadyCounted = (countedHostNames.indexOf(hostName) > -1); + const shouldBeCounted = ((!isAlreadyCounted) && hostData[hostName] > 0); + + if (shouldBeCounted) { + countedHostNames.push(hostName); + counts[key]++; + } + }); + }); + + return counts; +} + +function createStatusBarTooltip (key, count) { + const label = `${key}`; + const badge = `${count}`; + + return `${label}${badge}`; +} + +function atStatusLink (scope, el, attrs, controllers) { + const [atStatusController] = controllers; + + atStatusController.init(scope); +} + +function AtStatusController (strings) { + const vm = this || {}; + + vm.tooltips = { + running: strings.get('status.RUNNING'), + unavailable: strings.get('status.UNAVAILABLE'), + }; + + vm.init = scope => { + const { running, stats } = scope; + + vm.running = running || false; + vm.setStats(stats); + + scope.$watch('running', value => { vm.running = value; }); + scope.$watch('stats', vm.setStats); + }; + + vm.setStats = stats => { + const counts = getHostStatusCounts(stats); + + HOST_STATUS_KEYS.forEach(key => { + const count = counts[key]; + const statusBarElement = $(`.HostStatusBar-${key}`); + + statusBarElement.css('flex', `${count} 0 auto`); + + vm.tooltips[key] = createStatusBarTooltip(key, count); + }); + + vm.statsAreAvailable = Boolean(stats); + }; +} + +function atStatus () { + return { + templateUrl, + restrict: 'E', + require: ['atStatus'], + controllerAs: 'vm', + link: atStatusLink, + controller: [ + 'JobStrings', + AtStatusController + ], + scope: { + running: '=', + stats: '=', + }, + }; +} + +export default atStatus; diff --git a/awx/ui/client/features/output/status.partial.html b/awx/ui/client/features/output/status.partial.html new file mode 100644 index 0000000000..6608a23262 --- /dev/null +++ b/awx/ui/client/features/output/status.partial.html @@ -0,0 +1,37 @@ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/features/output/stream.service.js b/awx/ui/client/features/output/stream.service.js index d7f8a72555..0ce0199ac4 100644 --- a/awx/ui/client/features/output/stream.service.js +++ b/awx/ui/client/features/output/stream.service.js @@ -3,7 +3,7 @@ const JOB_END = 'playbook_on_stats'; const MAX_LAG = 120; function JobStreamService ($q) { - this.init = ({ resource, scroll, page, render, listen }) => { + this.init = ({ resource, scroll, page, onStreamStart, onStreamFinish, render, listen }) => { this.resource = resource; this.scroll = scroll; this.page = page; @@ -23,8 +23,10 @@ function JobStreamService ($q) { }; this.hooks = { + onStreamStart, + onStreamFinish, render, - listen + listen, }; this.lines = { @@ -35,7 +37,7 @@ function JobStreamService ($q) { max: 0 }; - this.hooks.listen(resource.ws.namespace, this.listen); + this.hooks.listen(resource.ws.namespace, this.listener); }; this.getBatchFactors = size => { @@ -105,19 +107,25 @@ function JobStreamService ($q) { } }; - this.listen = data => { + this.listener = data => { this.lag++; this.chain = this.chain .then(() => { + // console.log(data); if (!this.isActive()) { this.start(); + if (!this.isEnding()) { + this.hooks.onStreamStart(data); + } } else if (data.event === JOB_END) { if (this.isPaused()) { this.end(true); } else { this.end(); } + + this.hooks.onStreamFinish(data); } this.checkLines(data); diff --git a/awx/ui/client/lib/models/Job.js b/awx/ui/client/lib/models/Job.js index ef80c5dafb..07b5da3174 100644 --- a/awx/ui/client/lib/models/Job.js +++ b/awx/ui/client/lib/models/Job.js @@ -23,26 +23,54 @@ function postRelaunch (params) { return $http(req); } +function getStats () { + if (!this.has('GET', 'id')) { + return Promise.reject(new Error('No property, id, exists')); + } + + if (!this.has('GET', 'related.job_events')) { + return Promise.reject(new Error('No related property, job_events, exists')); + } + + const req = { + method: 'GET', + url: `${this.path}${this.get('id')}/job_events/`, + params: { event: 'playbook_on_stats' }, + }; + + return $http(req) + .then(({ data }) => { + if (data.results.length > 0) { + return data.results[0]; + } + + return null; + }); +} + + function JobModel (method, resource, config) { BaseModel.call(this, 'jobs'); this.Constructor = JobModel; + this.postRelaunch = postRelaunch.bind(this); this.getRelaunch = getRelaunch.bind(this); + this.getStats = getStats.bind(this); return this.create(method, resource, config); } -function JobModelLoader (_BaseModel_, _$http_) { - BaseModel = _BaseModel_; +function JobModelLoader (_$http_, _BaseModel_) { $http = _$http_; + BaseModel = _BaseModel_; return JobModel; } JobModelLoader.$inject = [ + '$http', 'BaseModel', - '$http' ]; export default JobModelLoader; diff --git a/awx/ui/client/lib/models/ProjectUpdate.js b/awx/ui/client/lib/models/ProjectUpdate.js index 84fae23f50..f600eb7e7c 100644 --- a/awx/ui/client/lib/models/ProjectUpdate.js +++ b/awx/ui/client/lib/models/ProjectUpdate.js @@ -1,19 +1,54 @@ +let $http; let BaseModel; +function getStats () { + if (!this.has('GET', 'id')) { + return Promise.reject(new Error('No property, id, exists')); + } + + if (!this.has('GET', 'related.events')) { + return Promise.reject(new Error('No related property, events, exists')); + } + + + + const req = { + method: 'GET', + url: `${this.path}${this.get('id')}/events/`, + params: { event: 'playbook_on_stats' }, + }; + + return $http(req) + .then(({ data }) => { + console.log(data); + if (data.results.length > 0) { + return data.results[0]; + } + + return null; + }) +} + function ProjectUpdateModel (method, resource, config) { BaseModel.call(this, 'project_updates'); + this.getStats = getStats; + this.Constructor = ProjectUpdateModel; return this.create(method, resource, config); } -function ProjectUpdateModelLoader (_BaseModel_) { +function ProjectUpdateModelLoader (_$http_, _BaseModel_) { + $http = _$http_; BaseModel = _BaseModel_; return ProjectUpdateModel; } -ProjectUpdateModelLoader.$inject = ['BaseModel']; +ProjectUpdateModelLoader.$inject = [ + '$http', + 'BaseModel' +]; export default ProjectUpdateModelLoader; From faa33e0becbf0e91fca31b02a1747207596841c2 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 19 Mar 2018 12:01:48 -0400 Subject: [PATCH 62/89] navigate to new job results view on relaunch --- .../launchjob.factory.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js index 37fd3292b3..2e80cdc369 100644 --- a/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js +++ b/awx/ui/client/src/job-submission/job-submission-factories/launchjob.factory.js @@ -149,8 +149,8 @@ export default if(base !== 'portal' && Empty(data.system_job) || (base === 'home')){ // use $state.go with reload: true option to re-instantiate sockets in - var goTojobResults = function(state) { - $state.go(state, {id: job}, {reload:true}); + var goTojobResults = function(type) { + $state.go('jobz', {id: job, type}, {reload:true}); }; if($state.includes('jobs')) { @@ -159,23 +159,23 @@ export default else { if(_.has(data, 'job')) { - goTojobResults('jobResult'); + goTojobResults('playbook'); } else if(data.type && data.type === 'workflow_job') { job = data.id; - goTojobResults('workflowResults'); + goTojobResults('workflow_job'); } else if(_.has(data, 'ad_hoc_command')) { - goTojobResults('adHocJobStdout'); + goTojobResults('ad_hoc_command'); } else if(_.has(data, 'system_job')) { - goTojobResults('managementJobStdout'); + goTojobResults('system_job'); } else if(_.has(data, 'project_update')) { // If we are on the projects list or any child state of that list // then we want to stay on that page. Otherwise go to the stdout // view. if(!$state.includes('projects')) { - goTojobResults('scmUpdateStdout'); + goTojobResults('project_update'); } } else if(_.has(data, 'inventory_update')) { @@ -183,7 +183,7 @@ export default // page then we want to stay on that page. Otherwise go to the stdout // view. if(!$state.includes('inventories.edit')) { - goTojobResults('inventorySyncStdout'); + goTojobResults('playbook'); } } } From 450eaeca96cfc57f04a63c833c8bf9fa76924118 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Fri, 23 Mar 2018 11:38:20 -0400 Subject: [PATCH 63/89] add event processing for stats and host status components --- awx/ui/client/features/output/_index.less | 7 +- .../features/output/details.directive.js | 34 +++-- .../features/output/details.partial.html | 5 +- .../features/output/index.controller.js | 127 ++++++++++++------ awx/ui/client/features/output/index.js | 8 +- awx/ui/client/features/output/index.view.html | 9 +- .../client/features/output/render.service.js | 1 - ...status.directive.js => stats.directive.js} | 43 ++++-- ...status.partial.html => stats.partial.html} | 0 .../client/features/output/stream.service.js | 44 +++--- .../client/lib/components/panel/_index.less | 12 ++ .../relaunchButton.component.js | 3 +- awx/ui/client/lib/models/Base.js | 7 +- awx/ui/client/lib/models/Job.js | 1 - awx/ui/client/lib/models/ProjectUpdate.js | 9 +- awx/ui/client/lib/theme/_global.less | 11 ++ awx/ui/client/lib/theme/_utility.less | 8 ++ .../host-status-bar.block.less | 10 +- .../src/standard-out/standard-out.block.less | 6 +- 19 files changed, 227 insertions(+), 118 deletions(-) rename awx/ui/client/features/output/{status.directive.js => stats.directive.js} (65%) rename awx/ui/client/features/output/{status.partial.html => stats.partial.html} (100%) diff --git a/awx/ui/client/features/output/_index.less b/awx/ui/client/features/output/_index.less index ab4e589213..e44be3b39f 100644 --- a/awx/ui/client/features/output/_index.less +++ b/awx/ui/client/features/output/_index.less @@ -206,12 +206,11 @@ display: flex; flex: 0 0 auto; width: 100%; - margin-top: 10px; } .HostStatusBar-ok, .HostStatusBar-changed, -.HostStatusBar-unreachable, +.HostStatusBar-dark, .HostStatusBar-failed, .HostStatusBar-skipped, .HostStatusBar-noData { @@ -231,7 +230,7 @@ flex: 0 0 auto; } -.HostStatusBar-unreachable { +.HostStatusBar-dark { background-color: @default-unreachable; flex: 0 0 auto; } @@ -265,7 +264,7 @@ background-color: @default-succ; } -.HostStatusBar-tooltipBadge--unreachable { +.HostStatusBar-tooltipBadge--dark { background-color: @default-unreachable; } diff --git a/awx/ui/client/features/output/details.directive.js b/awx/ui/client/features/output/details.directive.js index 309c50612d..22c66d12cc 100644 --- a/awx/ui/client/features/output/details.directive.js +++ b/awx/ui/client/features/output/details.directive.js @@ -441,7 +441,7 @@ function AtDetailsController ( vm.init = _$scope_ => { $scope = _$scope_; - resource = $scope.resource; + resource = $scope.resource; // eslint-disable-line prefer-destructuring vm.status = getStatusDetails(); vm.started = getStartTimeDetails(); @@ -466,27 +466,35 @@ function AtDetailsController ( vm.extraVars = getExtraVarsDetails(); vm.labels = getLabelDetails(); + // Relaunch Component + vm.job = _.get(resource.model, 'model.GET', {}); + + // XX - Codemirror + if (vm.extraVars) { + const cm = { + parseType: 'yaml', + $apply: $scope.$apply, + variables: vm.extraVars.value, + }; + + ParseTypeChange({ + field_id: 'cm-extra-vars', + readOnly: true, + scope: cm, + }); + } + vm.cancelJob = cancelJob; vm.deleteJob = deleteJob; vm.toggleLabels = toggleLabels; const observe = (key, transform) => { - $scope.$watch(key, value => { this[key] = transform(value); }); + $scope.$watch(key, value => { vm[key] = transform(value); }); }; + observe('finished', getFinishTimeDetails); observe('status', getStatusDetails); observe('started', getStartTimeDetails); - observe('finished', getFinishTimeDetails); - - // relaunch component - $scope.job = _.get(resource.model, 'model.GET', {}); - this.job = $scope.job; - - // codemirror - if (this.extraVars) { - const cm = { parseType: 'yaml', variables: this.extraVars.value, $apply: $scope.$apply }; - ParseTypeChange({ scope: cm, field_id: 'cm-extra-vars', readOnly: true }); - } }; } diff --git a/awx/ui/client/features/output/details.partial.html b/awx/ui/client/features/output/details.partial.html index bb80346a7b..e72b682e6e 100644 --- a/awx/ui/client/features/output/details.partial.html +++ b/awx/ui/client/features/output/details.partial.html @@ -1,11 +1,10 @@ - - +
DETAILS
- + + + + + +
+
-
+ aw-tool-tip="{{ vm.tooltips.dark }}" + data-tip-watch="vm.tooltips.dark">
{ jobObj.getRelaunch({ id: vm.job.id @@ -182,7 +184,7 @@ function atRelaunchCtrl ( project.postUpdate(vm.job.project) .then((postUpdateRes) => { if (!$state.includes('jobs')) { - $state.go('scmUpdateStdout', { id: postUpdateRes.data.id }, { reload: true }); + $state.go('jobz', { id: postUpdateRes.data.id, type: 'project' }, { reload: true }); } }); } else { @@ -218,7 +220,7 @@ function atRelaunchCtrl ( id: vm.job.id }).then((launchRes) => { if (!$state.includes('jobs')) { - $state.go('adHocJobStdout', { id: launchRes.data.id }, { reload: true }); + $state.go('jobz', { id: launchRes.data.id, type: 'command' }, { reload: true }); } }); } diff --git a/awx/ui/client/lib/models/AdHocCommand.js b/awx/ui/client/lib/models/AdHocCommand.js index 9f259a929a..7bea2677ac 100644 --- a/awx/ui/client/lib/models/AdHocCommand.js +++ b/awx/ui/client/lib/models/AdHocCommand.js @@ -19,12 +19,17 @@ function postRelaunch (params) { return $http(req); } +function getStats () { + return Promise.resolve(null); +} + function AdHocCommandModel (method, resource, config) { BaseModel.call(this, 'ad_hoc_commands'); this.Constructor = AdHocCommandModel; this.postRelaunch = postRelaunch.bind(this); this.getRelaunch = getRelaunch.bind(this); + this.getStats = getStats.bind(this); return this.create(method, resource, config); } diff --git a/awx/ui/client/src/shared/socket/socket.service.js b/awx/ui/client/src/shared/socket/socket.service.js index b26dae58d7..0e50b0e671 100644 --- a/awx/ui/client/src/shared/socket/socket.service.js +++ b/awx/ui/client/src/shared/socket/socket.service.js @@ -216,7 +216,7 @@ export default // socket-enabled AND socket-disabled, and whether the $state // requires a subscribe or an unsubscribe var self = this; - socketPromise.promise.then(function(){ + return socketPromise.promise.then(function(){ if(!state.data || !state.data.socket){ _.merge(state.data, {socket: {groups: {}}}); self.unsubscribe(state); diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js index 70bab1727b..7cfc754d86 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js @@ -898,13 +898,13 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge let goToJobResults = function(job_type) { if(job_type === 'job') { - $state.go('jobResult', {id: d.job.id}); + $state.go('jobz', {id: d.job.id, type: 'playbook'}); } else if(job_type === 'inventory_update') { - $state.go('inventorySyncStdout', {id: d.job.id}); + $state.go('jobz', {id: d.job.id, type: 'inventory'}); } else if(job_type === 'project_update') { - $state.go('scmUpdateStdout', {id: d.job.id}); + $state.go('jobz', {id: d.job.id, type: 'project'}); } }; From 8da2c3cad2686f04f41e60465a120f71fbd4be28 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 28 Mar 2018 20:58:11 -0400 Subject: [PATCH 66/89] fix regression with opening credential edit/add --- awx/ui/client/features/credentials/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/features/credentials/index.js b/awx/ui/client/features/credentials/index.js index e3a1ea7734..2fa094ef9f 100644 --- a/awx/ui/client/features/credentials/index.js +++ b/awx/ui/client/features/credentials/index.js @@ -11,7 +11,7 @@ function CredentialsResolve ($q, $stateParams, Me, Credential, CredentialType, O const id = $stateParams.credential_id; const promises = { - me: new Me('get').then((me) => me.extend('get', 'admin_of_organizations')) + me: new Me('get').then((me) => me.extend('admin_of_organizations')) }; if (!id) { From a53f70f0afe45e138a215f8ab3daea5344d7ce15 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 28 Mar 2018 21:58:35 -0400 Subject: [PATCH 67/89] add inventory updates --- .../features/output/details.directive.js | 27 ++++++++++++------- .../features/output/details.partial.html | 12 ++++----- .../features/output/index.controller.js | 2 +- awx/ui/client/features/output/index.js | 6 ++++- .../relaunchButton.component.js | 4 +-- awx/ui/client/lib/models/InventoryUpdate.js | 27 +++++++++++++++++++ awx/ui/client/lib/models/SystemJob.js | 6 +++++ awx/ui/client/lib/models/index.js | 2 ++ 8 files changed, 66 insertions(+), 20 deletions(-) create mode 100644 awx/ui/client/lib/models/InventoryUpdate.js diff --git a/awx/ui/client/features/output/details.directive.js b/awx/ui/client/features/output/details.directive.js index 7f36ee84e6..221fc2266a 100644 --- a/awx/ui/client/features/output/details.directive.js +++ b/awx/ui/client/features/output/details.directive.js @@ -92,7 +92,7 @@ function getVerbosityDetails () { const choices = mapChoices(resource.model.options('actions.GET.verbosity.choices')); const label = 'Verbosity'; - const value = choices[value]; + const value = choices[verbosity]; return { label, value }; } @@ -256,17 +256,26 @@ function getResultTracebackDetails () { return { label, value }; } -function getMachineCredentialDetails () { - const machineCredential = resource.model.get('summary_fields.credential'); +function getCredentialDetails () { + const credential = resource.model.get('summary_fields.credential'); - if (!machineCredential) { + if (!credential) { return null; } - const label = 'Machine Credential'; - const link = `/#/credentials/${machineCredential.id}`; + let label = 'Credential'; + + if (resource.type === 'playbook') { + label = 'Machine Credential'; + } + + if (resource.type === 'inventory') { + label = 'Source Credential'; + } + + const link = `/#/credentials/${credential.id}`; const tooltip = 'Edit the Credential'; - const value = $filter('sanitize')(machineCredential.name); + const value = $filter('sanitize')(credential.name); return { label, link, tooltip, value }; } @@ -427,7 +436,7 @@ function handleSocketEvent (data) { vm.project.update = vm.project.update || {}; vm.project.update.status = data.status; } -}; +} function AtDetailsController ( _$http_, @@ -470,7 +479,7 @@ function AtDetailsController ( vm.launchedBy = getLaunchedByDetails(); vm.jobExplanation = getJobExplanationDetails(); vm.verbosity = getVerbosityDetails(); - vm.machineCredential = getMachineCredentialDetails(); + vm.credential = getCredentialDetails(); vm.forks = getForkDetails(); vm.limit = getLimitDetails(); vm.instanceGroup = getInstanceGroupDetails(); diff --git a/awx/ui/client/features/output/details.partial.html b/awx/ui/client/features/output/details.partial.html index e72b682e6e..4d26151b9e 100644 --- a/awx/ui/client/features/output/details.partial.html +++ b/awx/ui/client/features/output/details.partial.html @@ -136,14 +136,14 @@
{{ vm.playbook.value }}
- -
- + + diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index df2a06034c..e938e45cd6 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -84,7 +84,7 @@ function JobsIndexController ( eventCounter = null; statsEvent = resource.stats; - // Panel Title + // Panel vm.title = resource.model.get('name'); // Status Bar diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 62155aed1e..33c67d6f11 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -27,6 +27,7 @@ function resolveResource ( AdHocCommand, SystemJob, WorkflowJob, + InventoryUpdate, $stateParams, qs, Wait @@ -51,6 +52,9 @@ function resolveResource ( case 'system': Resource = SystemJob; break; + case 'inventory': + Resource = InventoryUpdate; + break; // case 'workflow': // todo: integrate workflow chart components into this view // break; @@ -117,7 +121,6 @@ function resolveWebSocketConnection ($stateParams, SocketService) { }; return SocketService.addStateResolve(state, id); - } function resolveBreadcrumb (strings) { @@ -181,6 +184,7 @@ function JobsRun ($stateRegistry) { 'AdHocCommandModel', 'SystemJobModel', 'WorkflowJobModel', + 'InventoryUpdateModel', '$stateParams', 'QuerySet', 'Wait', diff --git a/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js b/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js index e6ec40733b..a76e62eb52 100644 --- a/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js +++ b/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js @@ -23,8 +23,6 @@ function atRelaunchCtrl ( const jobObj = new Job(); const jobTemplate = new JobTemplate(); - - const checkRelaunchPlaybook = (option) => { jobObj.getRelaunch({ id: vm.job.id @@ -165,7 +163,7 @@ function atRelaunchCtrl ( inventorySource.postUpdate(vm.job.inventory_source) .then((postUpdateRes) => { if (!$state.includes('jobs')) { - $state.go('inventorySyncStdout', { id: postUpdateRes.data.id }, { reload: true }); + $state.go('jobz', { id: postUpdateRes.data.id, type: 'inventory' }, { reload: true }); } }); } else { diff --git a/awx/ui/client/lib/models/InventoryUpdate.js b/awx/ui/client/lib/models/InventoryUpdate.js new file mode 100644 index 0000000000..668a05459d --- /dev/null +++ b/awx/ui/client/lib/models/InventoryUpdate.js @@ -0,0 +1,27 @@ +let BaseModel; + +function getStats () { + return Promise.resolve(null); +} + +function InventoryUpdateModel (method, resource, config) { + BaseModel.call(this, 'inventory_updates'); + + this.getStats = getStats.bind(this); + + this.Constructor = InventoryUpdateModel; + + return this.create(method, resource, config); +} + +function InventoryUpdateModelLoader (_BaseModel_) { + BaseModel = _BaseModel_; + + return InventoryUpdateModel; +} + +InventoryUpdateModelLoader.$inject = [ + 'BaseModel' +]; + +export default InventoryUpdateModelLoader; diff --git a/awx/ui/client/lib/models/SystemJob.js b/awx/ui/client/lib/models/SystemJob.js index cc092ff8f4..1f1f1c5ee3 100644 --- a/awx/ui/client/lib/models/SystemJob.js +++ b/awx/ui/client/lib/models/SystemJob.js @@ -1,8 +1,14 @@ let BaseModel; +function getStats () { + return Promise.resolve(null); +} + function SystemJobModel (method, resource, config) { BaseModel.call(this, 'system_jobs'); + this.getStats = getStats.bind(this); + this.Constructor = SystemJobModel; return this.create(method, resource, config); diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index 3875975d4d..d50c825e22 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -11,6 +11,7 @@ import InstanceGroup from '~models/InstanceGroup'; import Inventory from '~models/Inventory'; import InventoryScript from '~models/InventoryScript'; import InventorySource from '~models/InventorySource'; +import InventoryUpdate from '~models/InventoryUpdate'; import Job from '~models/Job'; import JobEvent from '~models/JobEvent'; import JobTemplate from '~models/JobTemplate'; @@ -46,6 +47,7 @@ angular .service('InventoryModel', Inventory) .service('InventoryScriptModel', InventoryScript) .service('InventorySourceModel', InventorySource) + .service('InventoryUpdateModel', InventoryUpdate) .service('JobEventModel', JobEvent) .service('JobModel', Job) .service('JobTemplateModel', JobTemplate) From 6b302ef1674c8eb62e95de99361139b184ae7bdd Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 28 Mar 2018 22:46:40 -0400 Subject: [PATCH 68/89] job results link-in --- awx/ui/client/features/jobs/jobsList.controller.js | 10 +++++----- .../launchTemplateButton.component.js | 4 ++-- .../relaunchButton/relaunchButton.component.js | 2 +- .../home/dashboard/lists/jobs/jobs-list.directive.js | 11 ++++++++++- .../inventories/adhoc/adhoc.controller.js | 2 +- .../host-summary-popover.controller.js | 2 +- .../source-summary-popover.controller.js | 2 +- .../sources/factories/view-update-status.factory.js | 2 +- .../job-submission-factories/adhoc-run.factory.js | 2 +- .../src/management-jobs/card/card.controller.js | 4 ++-- .../organizations-inventories.controller.js | 2 +- .../controllers/organizations-projects.controller.js | 2 +- .../src/projects/list/projects-list.controller.js | 2 +- .../src/smart-status/smart-status.controller.js | 2 +- 14 files changed, 29 insertions(+), 20 deletions(-) diff --git a/awx/ui/client/features/jobs/jobsList.controller.js b/awx/ui/client/features/jobs/jobsList.controller.js index b78a1b77b6..ea57c4e9f5 100644 --- a/awx/ui/client/features/jobs/jobsList.controller.js +++ b/awx/ui/client/features/jobs/jobsList.controller.js @@ -52,19 +52,19 @@ function ListJobsController ( switch (type) { case 'job': - link = `/#/jobs/${id}`; + link = `/#/jobz/playbook/${id}`; break; case 'ad_hoc_command': - link = `/#/ad_hoc_commands/${id}`; + link = `/#/jobz/command/${id}`; break; case 'system_job': - link = `/#/management_jobs/${id}`; + link = `/#/jobz/system/${id}`; break; case 'project_update': - link = `/#/scm_update/${id}`; + link = `/#/jobz/project/${id}`; break; case 'inventory_update': - link = `/#/inventory_sync/${id}`; + link = `/#/jobz/inventory/${id}`; break; case 'workflow_job': link = `/#/workflows/${id}`; diff --git a/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js b/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js index d4c9403823..40dcf2907c 100644 --- a/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js +++ b/awx/ui/client/lib/components/launchTemplateButton/launchTemplateButton.component.js @@ -41,7 +41,7 @@ function atLaunchTemplateCtrl ( selectedJobTemplate .postLaunch({ id: vm.template.id }) .then(({ data }) => { - $state.go('jobResult', { id: data.job }, { reload: true }); + $state.go('jobz', { id: data.job, type: 'playbook' }, { reload: true }); }); } else { const promptData = { @@ -138,7 +138,7 @@ function atLaunchTemplateCtrl ( id: vm.promptData.template, launchData: jobLaunchData }).then((launchRes) => { - $state.go('jobResult', { id: launchRes.data.job }, { reload: true }); + $state.go('jobz', { id: launchRes.data.job, type: 'playbook' }, { reload: true }); }).catch(createErrorHandler('launch job template', 'POST')); } else if (vm.promptData.templateType === 'workflow_job_template') { workflowTemplate.create().postLaunch({ diff --git a/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js b/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js index a76e62eb52..3d7d6b09ff 100644 --- a/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js +++ b/awx/ui/client/lib/components/relaunchButton/relaunchButton.component.js @@ -238,7 +238,7 @@ function atRelaunchCtrl ( relaunchData: PromptService.bundlePromptDataForRelaunch(vm.promptData) }).then((launchRes) => { if (!$state.includes('jobs')) { - $state.go('jobResult', { id: launchRes.data.job }, { reload: true }); + $state.go('jobz', { id: launchRes.data.job, type: 'playbook' }, { reload: true }); } }).catch(({ data, status }) => { ProcessErrors($scope, data, status, null, { diff --git a/awx/ui/client/src/home/dashboard/lists/jobs/jobs-list.directive.js b/awx/ui/client/src/home/dashboard/lists/jobs/jobs-list.directive.js index 4d8be63ccd..93a2147834 100644 --- a/awx/ui/client/src/home/dashboard/lists/jobs/jobs-list.directive.js +++ b/awx/ui/client/src/home/dashboard/lists/jobs/jobs-list.directive.js @@ -28,8 +28,17 @@ export default function createList(list) { // detailsUrl, status, name, time scope.jobs = _.map(list, function(job){ + + let detailsUrl; + + if (job.type === 'workflow_job') { + detailsUrl = `/#/workflows/${job.id}`; + } else { + detailsUrl = `/#/jobz/playbook/${job.id}`; + } + return { - detailsUrl: job.type && job.type === 'workflow_job' ? job.url.replace(/api\/v\d+\/workflow_jobs/, "#/workflows") : job.url.replace(/api\/v\d+/, "#"), + detailsUrl, status: job.status, name: job.name, id: job.id, diff --git a/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js b/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js index 5aa5938714..e5e020fc6d 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/adhoc/adhoc.controller.js @@ -241,7 +241,7 @@ function adhocController($q, $scope, $stateParams, Rest.post(data) .then(({data}) => { Wait('stop'); - $state.go('adHocJobStdout', {id: data.id}); + $state.go('jobz', {id: data.id, type: 'command'}); }) .catch(({data, status}) => { ProcessErrors($scope, data, status, adhocForm, { diff --git a/awx/ui/client/src/inventories-hosts/inventories/list/host-summary-popover/host-summary-popover.controller.js b/awx/ui/client/src/inventories-hosts/inventories/list/host-summary-popover/host-summary-popover.controller.js index 495c201646..7b2e28e8aa 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/list/host-summary-popover/host-summary-popover.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/list/host-summary-popover/host-summary-popover.controller.js @@ -23,7 +23,7 @@ export default [ '$scope', 'Empty', 'Wait', 'GetBasePath', 'Rest', 'ProcessError }; $scope.viewJob = function(jobId) { - $state.go('jobResult', {id: jobId}); + $state.go('jobz', { id: jobId, type: 'playbook' }); }; } diff --git a/awx/ui/client/src/inventories-hosts/inventories/list/source-summary-popover/source-summary-popover.controller.js b/awx/ui/client/src/inventories-hosts/inventories/list/source-summary-popover/source-summary-popover.controller.js index 661d122c88..725069044e 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/list/source-summary-popover/source-summary-popover.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/list/source-summary-popover/source-summary-popover.controller.js @@ -20,7 +20,7 @@ export default [ '$scope', 'Wait', 'Empty', 'Rest', 'ProcessErrors', '$state', $scope.viewJob = function(url) { // Pull the id out of the URL var id = url.replace(/^\//, '').split('/')[3]; - $state.go('inventorySyncStdout', {id: id}); + $state.go('jobz', { id, type: 'inventory' } ); }; } diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/factories/view-update-status.factory.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/factories/view-update-status.factory.js index e7820f79c8..c889517506 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/factories/view-update-status.factory.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/factories/view-update-status.factory.js @@ -17,7 +17,7 @@ export default // Get the ID from the correct summary field var update_id = (data.summary_fields.current_update) ? data.summary_fields.current_update.id : data.summary_fields.last_update.id; - $state.go('inventorySyncStdout', {id: update_id}); + $state.go('jobz', { id: update_id, type: 'inventory' }); }) .catch(({data, status}) => { ProcessErrors(scope, data, status, null, { hdr: 'Error!', diff --git a/awx/ui/client/src/job-submission/job-submission-factories/adhoc-run.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/adhoc-run.factory.js index 240c41df5b..65a7b1c578 100644 --- a/awx/ui/client/src/job-submission/job-submission-factories/adhoc-run.factory.js +++ b/awx/ui/client/src/job-submission/job-submission-factories/adhoc-run.factory.js @@ -89,7 +89,7 @@ .then(({data}) => { Wait('stop'); if($location.path().replace(/^\//, '').split('/')[0] !== 'jobs') { - $state.go('adHocJobStdout', {id: data.id}); + $state.go('jobz', { id: data.id, type: 'command' }); } }) .catch(({data, status}) => { diff --git a/awx/ui/client/src/management-jobs/card/card.controller.js b/awx/ui/client/src/management-jobs/card/card.controller.js index a66f5d7ec5..12dceb22b9 100644 --- a/awx/ui/client/src/management-jobs/card/card.controller.js +++ b/awx/ui/client/src/management-jobs/card/card.controller.js @@ -132,7 +132,7 @@ export default Wait('stop'); $("#prompt-for-days-facts").dialog("close"); $("#configure-dialog").dialog('close'); - $state.go('managementJobStdout', {id: data.system_job}, {reload:true}); + $state.go('jobz', { id: data.system_job, type: 'system' }, { reload: true }); }) .catch(({data, status}) => { let template_id = scope.job_template_id; @@ -222,7 +222,7 @@ export default Wait('stop'); $("#prompt-for-days").dialog("close"); // $("#configure-dialog").dialog('close'); - $state.go('managementJobStdout', {id: data.system_job}, {reload:true}); + $state.go('jobz', { id: data.system_job, type: 'system' }, { reload: true }); }) .catch(({data, status}) => { let template_id = scope.job_template_id; diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-inventories.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-inventories.controller.js index 0e74eb5132..b26fffb264 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-inventories.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-inventories.controller.js @@ -234,7 +234,7 @@ export default ['$scope', '$rootScope', '$location', $scope.viewJob = function(url) { // Pull the id out of the URL var id = url.replace(/^\//, '').split('/')[3]; - $state.go('inventorySyncStdout', { id: id }); + $state.go('jobz', { id: id, type: 'inventory' }); }; diff --git a/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js index 39c322e183..853534f142 100644 --- a/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js +++ b/awx/ui/client/src/organizations/linkout/controllers/organizations-projects.controller.js @@ -187,7 +187,7 @@ export default ['$scope', '$rootScope', '$log', '$stateParams', 'Rest', 'Alert', // Grab the id from summary_fields var id = (data.summary_fields.current_update) ? data.summary_fields.current_update.id : data.summary_fields.last_update.id; - $state.go('scmUpdateStdout', { id: id }); + $state.go('jobz', { id: id, type: 'project' }); } else { Alert('No Updates Available', 'There is no SCM update information available for this project. An update has not yet been ' + diff --git a/awx/ui/client/src/projects/list/projects-list.controller.js b/awx/ui/client/src/projects/list/projects-list.controller.js index ec21e0009c..1bcd7db2d6 100644 --- a/awx/ui/client/src/projects/list/projects-list.controller.js +++ b/awx/ui/client/src/projects/list/projects-list.controller.js @@ -146,7 +146,7 @@ export default ['$scope', '$rootScope', '$log', 'Rest', 'Alert', // Grab the id from summary_fields var id = (data.summary_fields.current_update) ? data.summary_fields.current_update.id : data.summary_fields.last_update.id; - $state.go('scmUpdateStdout', { id: id }); + $state.go('jobz', { id: id, type: 'project'}, { reload: true }); } else { Alert(i18n._('No Updates Available'), i18n._('There is no SCM update information available for this project. An update has not yet been ' + diff --git a/awx/ui/client/src/smart-status/smart-status.controller.js b/awx/ui/client/src/smart-status/smart-status.controller.js index 8026a4d10c..1966e0b68c 100644 --- a/awx/ui/client/src/smart-status/smart-status.controller.js +++ b/awx/ui/client/src/smart-status/smart-status.controller.js @@ -27,7 +27,7 @@ export default ['$scope', '$filter', if (typeof $scope.templateType !== 'undefined' && $scope.templateType === 'workflow_job_template') { detailsBaseUrl = '/#/workflows/'; } else { - detailsBaseUrl = '/#/jobs/'; + detailsBaseUrl = '/#/jobz/playbook/'; } var sparkData = From 07186e1606335d040f4d87effe7e625bfa2db1e0 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 29 Mar 2018 00:32:46 -0400 Subject: [PATCH 69/89] disable search when running --- .../features/output/index.controller.js | 70 ++++++++++--------- awx/ui/client/features/output/index.view.html | 38 +++++----- 2 files changed, 59 insertions(+), 49 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index e938e45cd6..6e160f02a5 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -50,36 +50,11 @@ function JobsIndexController ( // Development helper(s) vm.clear = devClear; - // Stdout Navigation - vm.scroll = { - showBackToTop: false, - home: scrollHome, - end: scrollEnd, - down: scrollPageDown, - up: scrollPageUp - }; - // Expand/collapse vm.toggle = toggle; vm.expand = expand; vm.isExpanded = true; - // Search - $state = _$state_; - qs = _qs_; - - vm.searchValue = ''; - vm.searchRejected = null; - vm.searchKey = false; - vm.searchKeyExamples = searchKeyExamples; - vm.searchKeyFields = searchKeyFields; - - vm.clearSearch = clearSearch; - vm.search = search; - vm.toggleSearchKey = toggleSearchKey; - vm.removeSearchTag = removeSearchTag; - vm.searchTags = getSearchTags(getCurrentQueryset()); - // Events eventCounter = null; statsEvent = resource.stats; @@ -103,6 +78,33 @@ function JobsIndexController ( finished: resource.model.get('finished'), }; + // Search + $state = _$state_; + qs = _qs_; + + vm.search = { + clearSearch, + searchKeyExamples, + searchKeyFields, + toggleSearchKey, + removeSearchTag, + submitSearch, + value: '', + key: false, + rejected: false, + disabled: !resource.model.get('finished'), + tags: getSearchTags(getCurrentQueryset()), + }; + + // Stdout Navigation + vm.scroll = { + showBackToTop: false, + home: scrollHome, + end: scrollEnd, + down: scrollPageDown, + up: scrollPageUp + }; + render.requestAnimationFrame(() => init(!vm.status.running)); } @@ -134,11 +136,15 @@ function init (pageMode) { vm.status.plays = 0; vm.status.tasks = 0; vm.status.running = true; + + vm.search.disabled = true; }, onStop () { vm.status.stats = statsEvent; vm.status.running = false; + vm.search.disabled = false; + vm.details.status = statsEvent.failed ? 'failed' : 'successful'; vm.details.finished = statsEvent.created; } @@ -398,7 +404,7 @@ const searchKeyExamples = ['id:>1', 'task:set', 'created:>=2000-01-01']; const searchKeyFields = ['changed', 'failed', 'host_name', 'stdout', 'task', 'role', 'playbook', 'play']; function toggleSearchKey () { - vm.searchKey = !vm.searchKey; + vm.search.key = !vm.search.key; } function getCurrentQueryset () { @@ -416,31 +422,31 @@ function getSearchTags (queryset) { } function removeSearchTag (index) { - const searchTerm = vm.searchTags[index]; + const searchTerm = vm.search.tags[index]; const currentQueryset = getCurrentQueryset(); const modifiedQueryset = qs.removeTermsFromQueryset(currentQueryset, searchTerm); - vm.searchTags = getSearchTags(modifiedQueryset); + vm.search.tags = getSearchTags(modifiedQueryset); $state.params.job_event_search = qs.encodeArr(modifiedQueryset); $state.transitionTo($state.current, $state.params, searchReloadOptions); } -function search () { - const searchInputQueryset = qs.getSearchInputQueryset(vm.searchValue); +function submitSearch () { + const searchInputQueryset = qs.getSearchInputQueryset(vm.search.value); const currentQueryset = getCurrentQueryset(); const modifiedQueryset = qs.mergeQueryset(currentQueryset, searchInputQueryset); - vm.searchTags = getSearchTags(modifiedQueryset); + vm.search.tags = getSearchTags(modifiedQueryset); $state.params.job_event_search = qs.encodeArr(modifiedQueryset); $state.transitionTo($state.current, $state.params, searchReloadOptions); } function clearSearch () { - vm.searchTags = []; + vm.search.tags = []; $state.params.job_event_search = ''; $state.transitionTo($state.current, $state.params, searchReloadOptions); diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 4dde59725a..4ba0d1c91b 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -22,31 +22,31 @@ tasks="vm.status.tasks"> - +
+ ng-class="{ 'at-Input--rejected': vm.search.rejected }" + ng-model="vm.search.value" + ng-attr-placeholder="{{ vm.search.disabled ? 'CANNOT SEARCH RUNNING JOB' : 'SEARCH' }}" + ng-disabled="vm.search.disabled"> @@ -54,16 +54,20 @@
-
+
{{ tag }}
-
+
- +
- + +
From fc01af22988fd716b59cb6dd7f9d8f8a48905630 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 29 Mar 2018 00:39:08 -0400 Subject: [PATCH 70/89] hide dev utility --- awx/ui/client/features/output/index.view.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 4ba0d1c91b..78a4ecd395 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -6,7 +6,7 @@ started="vm.details.started" finished="vm.details.finished"> -

+
From 181d7e0e011a120127fb6efd81b352a55a20b815 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 29 Mar 2018 01:24:57 -0400 Subject: [PATCH 71/89] add delete and cancel --- .../features/output/details.directive.js | 36 ++++++++++++++++--- .../features/output/details.partial.html | 19 ++++++---- awx/ui/client/features/output/jobs.strings.js | 3 ++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/awx/ui/client/features/output/details.directive.js b/awx/ui/client/features/output/details.directive.js index 221fc2266a..8a466371f6 100644 --- a/awx/ui/client/features/output/details.directive.js +++ b/awx/ui/client/features/output/details.directive.js @@ -396,7 +396,7 @@ function toggleLabels () { } function cancelJob () { - const actionText = strings.get('CANCEL'); + const actionText = strings.get('warnings.CANCEL_ACTION'); const hdr = strings.get('warnings.CANCEL_HEADER'); const warning = strings.get('warnings.CANCEL_BODY'); @@ -414,7 +414,6 @@ function cancelJob () { const action = () => { wait('start'); $http({ method, url }) - .then(() => $state.go('jobs')) .catch(errorHandler) .finally(() => { $(ELEMENT_PROMPT_MODAL).modal('hide'); @@ -425,7 +424,35 @@ function cancelJob () { prompt({ hdr, resourceName, body, actionText, action }); } -function deleteJob () {} +function deleteJob () { + const actionText = strings.get('DELETE'); + const hdr = strings.get('warnings.DELETE_HEADER'); + const warning = strings.get('warnings.DELETE_BODY'); + + const id = resource.model.get('id'); + const name = $filter('sanitize')(resource.model.get('name')); + + const body = `
${warning}
`; + const resourceName = `#${id} ${name}`; + + const method = 'DELETE'; + const url = `${resource.model.path}/${id}/`; + + const errorHandler = createErrorHandler('delete job', method); + + const action = () => { + wait('start'); + $http({ method, url }) + .then(() => $state.go('jobs')) + .catch(errorHandler) + .finally(() => { + $(ELEMENT_PROMPT_MODAL).modal('hide'); + wait('stop'); + }); + }; + + prompt({ hdr, resourceName, body, actionText, action }); +} function handleSocketEvent (data) { const project = resource.model.get('project'); @@ -488,8 +515,9 @@ function AtDetailsController ( vm.extraVars = getExtraVarsDetails(); vm.labels = getLabelDetails(); - // Relaunch Component + // Relaunch and Delete Components vm.job = _.get(resource.model, 'model.GET', {}); + vm.canDelete = resource.model.get('summary_fields.user_capabilities.delete'); // XX - Codemirror if (vm.extraVars) { diff --git a/awx/ui/client/features/output/details.partial.html b/awx/ui/client/features/output/details.partial.html index 4d26151b9e..edd92f6fe0 100644 --- a/awx/ui/client/features/output/details.partial.html +++ b/awx/ui/client/features/output/details.partial.html @@ -7,21 +7,26 @@ - - - + + - - - -
- + - - - - - -
{ - vm.examples = scope.examples || []; - vm.fields = scope.fields || []; - vm.relatedFields = scope.relatedFields || []; - }; -} - -AtSearchKeyController.$inject = ['$scope']; - -function atSearchKey () { - return { - templateUrl, - restrict: 'E', - require: ['atSearchKey'], - controllerAs: 'vm', - link: atSearchKeyLink, - controller: AtSearchKeyController, - scope: { - examples: '=', - fields: '=', - relatedFields: '=', - }, - }; -} - -export default atSearchKey; diff --git a/awx/ui/client/features/output/search-key.partial.html b/awx/ui/client/features/output/search-key.partial.html deleted file mode 100644 index d2790d285f..0000000000 --- a/awx/ui/client/features/output/search-key.partial.html +++ /dev/null @@ -1,20 +0,0 @@ - -
-
-
-
-
EXAMPLES:
- -
-
-
- FIELDS: - {{ field }}, -
-
- ADDITIONAL INFORMATION: - For additional information on advanced search search syntax please see the Ansible Tower - documentation. -
-
-
diff --git a/awx/ui/client/features/output/search.directive.js b/awx/ui/client/features/output/search.directive.js new file mode 100644 index 0000000000..5945594ab1 --- /dev/null +++ b/awx/ui/client/features/output/search.directive.js @@ -0,0 +1,119 @@ +const templateUrl = require('~features/output/search.partial.html'); + +const searchReloadOptions = { reload: true, inherit: false }; +const searchKeyExamples = ['id:>1', 'task:set', 'created:>=2000-01-01']; +const searchKeyFields = ['changed', 'failed', 'host_name', 'stdout', 'task', 'role', 'playbook', 'play']; + +let $state; +let status; +let qs; + +let vm; + +function toggleSearchKey () { + vm.key = !vm.key; +} + +function getCurrentQueryset () { + const { job_event_search } = $state.params; // eslint-disable-line camelcase + + return qs.decodeArr(job_event_search); +} + +function getSearchTags (queryset) { + return qs.createSearchTagsFromQueryset(queryset) + .filter(tag => !tag.startsWith('event')) + .filter(tag => !tag.startsWith('-event')) + .filter(tag => !tag.startsWith('page_size')) + .filter(tag => !tag.startsWith('order_by')); +} + +function removeSearchTag (index) { + const searchTerm = vm.tags[index]; + + const currentQueryset = getCurrentQueryset(); + const modifiedQueryset = qs.removeTermsFromQueryset(currentQueryset, searchTerm); + + vm.tags = getSearchTags(modifiedQueryset); + + $state.params.job_event_search = qs.encodeArr(modifiedQueryset); + $state.transitionTo($state.current, $state.params, searchReloadOptions); +} + +function submitSearch () { + const searchInputQueryset = qs.getSearchInputQueryset(vm.value); + + const currentQueryset = getCurrentQueryset(); + const modifiedQueryset = qs.mergeQueryset(currentQueryset, searchInputQueryset); + + vm.tags = getSearchTags(modifiedQueryset); + + $state.params.job_event_search = qs.encodeArr(modifiedQueryset); + $state.transitionTo($state.current, $state.params, searchReloadOptions); +} + +function clearSearch () { + vm.tags = []; + + $state.params.job_event_search = ''; + $state.transitionTo($state.current, $state.params, searchReloadOptions); +} + +function atJobSearchLink (scope, el, attrs, controllers) { + const [atJobSearchController] = controllers; + + atJobSearchController.init(scope); +} + +function AtJobSearchController (_$state_, _status_, _qs_) { + $state = _$state_; + status = _status_; + qs = _qs_; + + vm = this || {}; + + vm.value = ''; + vm.key = false; + vm.rejected = false; + vm.disabled = true; + vm.tags = getSearchTags(getCurrentQueryset()); + + vm.clearSearch = clearSearch; + vm.searchKeyExamples = searchKeyExamples; + vm.searchKeyFields = searchKeyFields; + vm.toggleSearchKey = toggleSearchKey; + vm.removeSearchTag = removeSearchTag; + vm.submitSearch = submitSearch; + + vm.init = scope => { + vm.examples = scope.examples || searchKeyExamples; + vm.fields = scope.fields || searchKeyFields; + vm.relatedFields = scope.relatedFields || []; + + scope.$watch(status.isRunning, value => { vm.disabled = value; }); + }; +} + +AtJobSearchController.$inject = [ + '$state', + 'JobStatusService', + 'QuerySet', +]; + +function atJobSearch () { + return { + templateUrl, + restrict: 'E', + require: ['atJobSearch'], + controllerAs: 'vm', + link: atJobSearchLink, + controller: AtJobSearchController, + scope: { + examples: '=', + fields: '=', + relatedFields: '=', + }, + }; +} + +export default atJobSearch; diff --git a/awx/ui/client/features/output/search.partial.html b/awx/ui/client/features/output/search.partial.html new file mode 100644 index 0000000000..af34bac344 --- /dev/null +++ b/awx/ui/client/features/output/search.partial.html @@ -0,0 +1,61 @@ + +
+
+ + + + + + +
+
+ +
+
+
{{ tag }}
+
+ +
+
+ +
+ +
+
+
+
+
EXAMPLES:
+ +
+
+
+ FIELDS: + {{ field }}, +
+
+ ADDITIONAL INFORMATION: + For additional information on advanced search search syntax please see the Ansible Tower + documentation. +
+
+
From 95a37fab0573963ebd9ca4913e181cda153e224b Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 3 Apr 2018 18:06:20 -0400 Subject: [PATCH 77/89] fix lint errors --- .../features/output/index.controller.js | 75 ++++++++++--------- awx/ui/client/features/output/page.service.js | 4 +- .../shared/smart-search/queryset.service.js | 6 +- .../smart-search/smart-search.controller.js | 2 +- 4 files changed, 44 insertions(+), 43 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index c06e744faa..0fee736561 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -38,8 +38,8 @@ function JobsIndexController ( vm.clear = devClear; // Expand/collapse - vm.toggle = toggle; - vm.expand = expand; + // vm.toggle = toggle; + // vm.expand = expand; vm.isExpanded = true; // Panel @@ -160,10 +160,10 @@ function previous () { }); } -function append (events, engine) { +function append (events, eng) { return render.append(events) .then(count => { - page.updateLineCount(count, engine); + page.updateLineCount(count, eng); }); } @@ -277,48 +277,49 @@ function scrollIsAtRest (isAtRest) { vm.scroll.showBackToTop = !isAtRest; } -function expand () { - vm.toggle(parent, true); -} +// function expand () { +// vm.toggle(parent, true); +// } -function showHostDetails (id) { - jobEvent.request('get', id) - .then(() => { - const title = jobEvent.get('host_name'); +// function showHostDetails (id) { +// jobEvent.request('get', id) +// .then(() => { +// const title = jobEvent.get('host_name'); - vm.host = { - menu: true, - stdout: jobEvent.get('stdout') - }; +// vm.host = { +// menu: true, +// stdout: jobEvent.get('stdout') +// }; - $scope.jobs.modal.show(title); - }); -} +// $scope.jobs.modal.show(title); +// }); +// } -function toggle (uuid, menu) { - const lines = $(`.child-of-${uuid}`); - let icon = $(`#${uuid} .at-Stdout-toggle > i`); +// function toggle (uuid, menu) { +// const lines = $(`.child-of-${uuid}`); +// let icon = $(`#${uuid} .at-Stdout-toggle > i`); - if (menu || record[uuid].level === 1) { - vm.isExpanded = !vm.isExpanded; - } +// if (menu || record[uuid].level === 1) { +// vm.isExpanded = !vm.isExpanded; +// } - if (record[uuid].children) { - icon = icon.add($(`#${record[uuid].children.join(', #')}`).find('.at-Stdout-toggle > i')); - } +// if (record[uuid].children) { +// icon = icon.add($(`#${record[uuid].children.join(', #')}`) +// .find('.at-Stdout-toggle > i')); +// } - if (icon.hasClass('fa-angle-down')) { - icon.addClass('fa-angle-right'); - icon.removeClass('fa-angle-down'); +// if (icon.hasClass('fa-angle-down')) { +// icon.addClass('fa-angle-right'); +// icon.removeClass('fa-angle-down'); - lines.addClass('hidden'); - } else { - icon.addClass('fa-angle-down'); - icon.removeClass('fa-angle-right'); +// lines.addClass('hidden'); +// } else { +// icon.addClass('fa-angle-down'); +// icon.removeClass('fa-angle-right'); - lines.removeClass('hidden'); - } -} +// lines.removeClass('hidden'); +// } +// } JobsIndexController.$inject = [ 'resource', diff --git a/awx/ui/client/features/output/page.service.js b/awx/ui/client/features/output/page.service.js index 3f6461345e..5f19fe921a 100644 --- a/awx/ui/client/features/output/page.service.js +++ b/awx/ui/client/features/output/page.service.js @@ -156,10 +156,10 @@ function JobPageService ($q) { this.isPageBookmarked = number => number >= this.page.bookmark.first && number <= this.page.bookmark.last; - this.updateLineCount = (lines, stream) => { + this.updateLineCount = (lines, engine) => { let reference; - if (stream) { + if (engine) { reference = this.getReference(); } else { reference = this.getActiveReference(); diff --git a/awx/ui/client/src/shared/smart-search/queryset.service.js b/awx/ui/client/src/shared/smart-search/queryset.service.js index 780900d32f..7d79ac1ec1 100644 --- a/awx/ui/client/src/shared/smart-search/queryset.service.js +++ b/awx/ui/client/src/shared/smart-search/queryset.service.js @@ -53,8 +53,8 @@ function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearc .map(value => { value = this.replaceDefaultFlags(value); value = this.replaceEncodedTokens(value); - return [key, value] - }) + return [key, value]; + }); }, // encodes ui-router params from {operand__key__comparator: value} pairs to API-consumable URL @@ -83,7 +83,7 @@ function QuerysetService ($q, Rest, ProcessErrors, $rootScope, Wait, DjangoSearc for (let encodedIndex in encodedTerms) { const [encodedKey, encodedValue] = encodedTerms[encodedIndex]; obj[encodedKey] = obj[encodedKey] || []; - obj[encodedKey].push(encodedValue) + obj[encodedKey].push(encodedValue); } return obj; diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js index 414a0763db..7c1e7eaca8 100644 --- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js +++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js @@ -6,7 +6,7 @@ function SmartSearchController ( configService, GetBasePath, i18n, - qs, + qs ) { const searchKey = `${$scope.iterator}_search`; const optionsKey = `${$scope.list.iterator}_options`; From cf4b29c6d557936b597449a7b7b766ae4ade07c6 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 3 Apr 2018 21:32:45 -0400 Subject: [PATCH 78/89] remove has-ansi dependency --- awx/ui/client/features/output/render.service.js | 10 +++++++++- awx/ui/package.json | 1 - 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js index 06a6bc84a2..351d038387 100644 --- a/awx/ui/client/features/output/render.service.js +++ b/awx/ui/client/features/output/render.service.js @@ -1,5 +1,4 @@ import Ansi from 'ansi-to-html'; -import hasAnsi from 'has-ansi'; import Entities from 'html-entities'; const ELEMENT_TBODY = '#atStdoutResultTable'; @@ -21,6 +20,15 @@ const TIME_EVENTS = [ const ansi = new Ansi(); const entities = new Entities.AllHtmlEntities(); +// https://github.com/chalk/ansi-regex +const pattern = [ + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)', + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))' +].join('|'); + +const re = new RegExp(pattern); +const hasAnsi = input => re.test(input); + function JobRenderService ($q, $sce, $window) { this.init = ({ compile, apply, isStreamActive }) => { this.parent = null; diff --git a/awx/ui/package.json b/awx/ui/package.json index 9c381f466f..24a42aa207 100644 --- a/awx/ui/package.json +++ b/awx/ui/package.json @@ -114,7 +114,6 @@ "codemirror": "^5.17.0", "components-font-awesome": "^4.6.1", "d3": "~3.3.13", - "has-ansi": "^3.0.0", "html-entities": "^1.2.1", "javascript-detect-element-resize": "^0.5.3", "jquery": "~2.2.4", From e4ad34fa14d8aeca2f1f1a44165dda4677bce86c Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 3 Apr 2018 22:45:03 -0400 Subject: [PATCH 79/89] update smoke test for new job results view --- awx/ui/test/e2e/tests/smoke.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/test/e2e/tests/smoke.js b/awx/ui/test/e2e/tests/smoke.js index 5ef4a4ebfd..931ef9f6d5 100644 --- a/awx/ui/test/e2e/tests/smoke.js +++ b/awx/ui/test/e2e/tests/smoke.js @@ -296,7 +296,7 @@ module.exports = { client.waitForElementVisible('div.spinny'); client.waitForElementNotVisible('div.spinny'); - client.waitForElementVisible('.JobResults-detailsPanel'); + client.waitForElementVisible('at-job-details'); client.waitForElementNotPresent(running, 60000); client.waitForElementVisible(success, 60000); From 379e2226fa1c3b7d95e5649cfeeafeedb868facc Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 3 Apr 2018 22:46:24 -0400 Subject: [PATCH 80/89] rename search tag test --- .../e2e/tests/test-searchez.js => test-search-tag-add-remove.js | 2 -- 1 file changed, 2 deletions(-) rename awx/ui/test/e2e/tests/test-searchez.js => test-search-tag-add-remove.js (97%) diff --git a/awx/ui/test/e2e/tests/test-searchez.js b/test-search-tag-add-remove.js similarity index 97% rename from awx/ui/test/e2e/tests/test-searchez.js rename to test-search-tag-add-remove.js index 9442ac66c0..db491712e9 100644 --- a/awx/ui/test/e2e/tests/test-searchez.js +++ b/test-search-tag-add-remove.js @@ -2,8 +2,6 @@ import { range } from 'lodash'; import { getAdminMachineCredential } from '../fixtures'; -// AWX_E2E_URL='https://localhost:3000' npm --prefix awx/ui run e2e -- --filter="*jobz*" - const spinny = 'div.spinny'; const searchInput = 'smart-search input'; const searchSubmit = 'smart-search i[class*="search"]'; From 356defff09d61fb1241dafcd4b496141dddc5d84 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 3 Apr 2018 22:53:30 -0400 Subject: [PATCH 81/89] remove unused lib code --- .../lib/components/code/events.directive.js | 104 --------------- .../lib/components/code/events.partial.html | 0 .../components/code/menu-bottom.directive.js | 41 ------ .../components/code/menu-bottom.partial.html | 7 -- .../lib/components/code/menu-top.directive.js | 50 -------- .../lib/components/code/menu-top.partial.html | 12 -- .../lib/components/code/stdout.directive.js | 105 ---------------- .../lib/components/code/stdout.partial.html | 19 --- awx/ui/client/lib/components/index.js | 8 -- awx/ui/test/e2e/tests/test-jobz.js | 118 ------------------ 10 files changed, 464 deletions(-) delete mode 100644 awx/ui/client/lib/components/code/events.directive.js delete mode 100644 awx/ui/client/lib/components/code/events.partial.html delete mode 100644 awx/ui/client/lib/components/code/menu-bottom.directive.js delete mode 100644 awx/ui/client/lib/components/code/menu-bottom.partial.html delete mode 100644 awx/ui/client/lib/components/code/menu-top.directive.js delete mode 100644 awx/ui/client/lib/components/code/menu-top.partial.html delete mode 100644 awx/ui/client/lib/components/code/stdout.directive.js delete mode 100644 awx/ui/client/lib/components/code/stdout.partial.html delete mode 100644 awx/ui/test/e2e/tests/test-jobz.js diff --git a/awx/ui/client/lib/components/code/events.directive.js b/awx/ui/client/lib/components/code/events.directive.js deleted file mode 100644 index 8426bf3ae6..0000000000 --- a/awx/ui/client/lib/components/code/events.directive.js +++ /dev/null @@ -1,104 +0,0 @@ -import Ansi from 'ansi-to-html'; -import hasAnsi from 'has-ansi'; - -const templateUrl = require('~components/code/events.partial.html'); - -let $sce; -let $timeout; -let ansi; - -function atOutputEventLink (scope, element, attrs, controller) { - controller.init(scope, element); -} - -function AtOutputEventController (_$sce_, _$timeout_) { - const vm = this || {}; - - $timeout = _$timeout_; - $sce = _$sce_; - ansi = new Ansi(); - - let scope; - let element; - - vm.init = (_scope_, _element_) => { - scope = _scope_; - element = _element_; - - scope.$watch('state.stdout', curr => { - if (!curr) { - return; - } - - render(scope.state.stdout); - }); - }; - - vm.scroll = position => { - const container = element.find('.at-Stdout-container')[0]; - - if (position === 'bottom') { - container.scrollTop = container.scrollHeight; - } else { - container.scrollTop = 0; - } - }; -} - -AtOutputEventController.$inject = [ - '$sce', - '$timeout', -]; - -function render (stdout) { - const html = $sce.trustAsHtml(parseStdout(stdout)); - - $timeout(() => { - const table = $('#atStdoutTBody'); - - table.html($sce.getTrustedHtml(html)); - }); -} - -function parseStdout (stdout) { - const lines = stdout.split('\r\n'); - - let ln = 0; - - return lines.reduce((html, line) => { - ln++; - - return `${html}${createRow(ln, line)}`; - }, ''); -} - -function createRow (ln, content) { - content = content || ''; - - if (hasAnsi(content)) { - content = ansi.toHtml(content); - } - - return ` - - ${ln} - ${content} - `; -} -function atOutputEvent () { - return { - restrict: 'E', - transclude: true, - replace: true, - require: 'atOutputEvent', - templateUrl, - controller: AtOutputEventController, - controllerAs: 'vm', - link: atOutputEventLink, - scope: { - state: '=', - } - }; -} - -export default atOutputEvent; diff --git a/awx/ui/client/lib/components/code/events.partial.html b/awx/ui/client/lib/components/code/events.partial.html deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/awx/ui/client/lib/components/code/menu-bottom.directive.js b/awx/ui/client/lib/components/code/menu-bottom.directive.js deleted file mode 100644 index 5b15a7bc10..0000000000 --- a/awx/ui/client/lib/components/code/menu-bottom.directive.js +++ /dev/null @@ -1,41 +0,0 @@ -const templateUrl = require('~components/code/menu-bottom.partial.html'); - -function atCodeMenuBottomLink (scope, element, attrs, controller) { - controller.init(scope, element); -} - -function AtCodeMenuBottomController () { - const vm = this || {}; - - let element; - - vm.init = (_scope_, _element_) => { - element = _element_; - }; - - vm.scroll = () => { - const container = element.find('.at-Stdout-container')[0]; - - container.scrollTop = container.scrollHeight; - }; -} - -AtCodeMenuBottomController.$inject = []; - -function atCodeMenuBottom () { - return { - restrict: 'E', - transclude: true, - replace: true, - require: 'atCodeMenuBottom', - templateUrl, - controller: AtCodeMenuBottomController, - controllerAs: 'vm', - link: atCodeMenuBottomLink, - scope: { - state: '=', - } - }; -} - -export default atCodeMenuBottom; diff --git a/awx/ui/client/lib/components/code/menu-bottom.partial.html b/awx/ui/client/lib/components/code/menu-bottom.partial.html deleted file mode 100644 index 1a0717a360..0000000000 --- a/awx/ui/client/lib/components/code/menu-bottom.partial.html +++ /dev/null @@ -1,7 +0,0 @@ -
-
- -
- -
-
diff --git a/awx/ui/client/lib/components/code/menu-top.directive.js b/awx/ui/client/lib/components/code/menu-top.directive.js deleted file mode 100644 index e78ead1bae..0000000000 --- a/awx/ui/client/lib/components/code/menu-top.directive.js +++ /dev/null @@ -1,50 +0,0 @@ -const templateUrl = require('~components/code/menu-top.partial.html'); - -function atCodeMenuTopLink (scope, element, attrs, controller) { - controller.init(scope, element); -} - -function AtCodeMenuTopController () { - const vm = this || {}; - - let element; - let scope; - - vm.init = (_scope_, _element_) => { - scope = _scope_; - element = _element_; - - scope.state.isExpanded = scope.state.isExpanded || false; - }; - - vm.scroll = () => { - const container = element.parent().find('.at-Stdout-container')[0]; - - container.scrollTop = 0; - }; - - vm.expand = () => { - scope.state.isExpanded = !scope.state.isExpanded; - scope.state.expand(); - }; -} - -AtCodeMenuTopController.$inject = []; - -function atCodeMenuTop () { - return { - restrict: 'E', - transclude: true, - replace: true, - require: 'atCodeMenuTop', - templateUrl, - controller: AtCodeMenuTopController, - controllerAs: 'vm', - link: atCodeMenuTopLink, - scope: { - state: '=', - } - }; -} - -export default atCodeMenuTop; diff --git a/awx/ui/client/lib/components/code/menu-top.partial.html b/awx/ui/client/lib/components/code/menu-top.partial.html deleted file mode 100644 index 2e21dc1b8b..0000000000 --- a/awx/ui/client/lib/components/code/menu-top.partial.html +++ /dev/null @@ -1,12 +0,0 @@ -
-
- -
- -
- -
- -
-
diff --git a/awx/ui/client/lib/components/code/stdout.directive.js b/awx/ui/client/lib/components/code/stdout.directive.js deleted file mode 100644 index c73e90b7e8..0000000000 --- a/awx/ui/client/lib/components/code/stdout.directive.js +++ /dev/null @@ -1,105 +0,0 @@ -import Ansi from 'ansi-to-html'; -import hasAnsi from 'has-ansi'; - -const templateUrl = require('~components/code/stdout.partial.html'); - -let $sce; -let $timeout; -let ansi; - -function atCodeStdoutLink (scope, element, attrs, controller) { - controller.init(scope, element); -} - -function AtCodeStdoutController (_$sce_, _$timeout_) { - const vm = this || {}; - - $timeout = _$timeout_; - $sce = _$sce_; - ansi = new Ansi(); - - let scope; - let element; - - vm.init = (_scope_, _element_) => { - scope = _scope_; - element = _element_; - - scope.$watch('state.stdout', curr => { - if (!curr) { - return; - } - - render(scope.state.stdout); - }); - }; - - vm.scroll = position => { - const container = element.find('.at-Stdout-container')[0]; - - if (position === 'bottom') { - container.scrollTop = container.scrollHeight; - } else { - container.scrollTop = 0; - } - }; -} - -AtCodeStdoutController.$inject = [ - '$sce', - '$timeout', -]; - -function render (stdout) { - const html = $sce.trustAsHtml(parseStdout(stdout)); - - $timeout(() => { - const table = $('#atStdoutTBody'); - - table.html($sce.getTrustedHtml(html)); - }); -} - -function parseStdout (stdout) { - const lines = stdout.split('\r\n'); - - let ln = 0; - - return lines.reduce((html, line) => { - ln++; - - return `${html}${createRow(ln, line)}`; - }, ''); -} - -function createRow (ln, content) { - content = content || ''; - - if (hasAnsi(content)) { - content = ansi.toHtml(content); - } - - return ` - - ${ln} - ${content} - `; -} - -function atCodeStdout () { - return { - restrict: 'E', - transclude: true, - replace: true, - require: 'atCodeStdout', - templateUrl, - controller: AtCodeStdoutController, - controllerAs: 'vm', - link: atCodeStdoutLink, - scope: { - state: '=', - } - }; -} - -export default atCodeStdout; diff --git a/awx/ui/client/lib/components/code/stdout.partial.html b/awx/ui/client/lib/components/code/stdout.partial.html deleted file mode 100644 index ca38d736f4..0000000000 --- a/awx/ui/client/lib/components/code/stdout.partial.html +++ /dev/null @@ -1,19 +0,0 @@ -
-
-
- -
- -
-
- -
- -
-
- -
- -
-
-
diff --git a/awx/ui/client/lib/components/index.js b/awx/ui/client/lib/components/index.js index bf45c63a6d..9ac933628c 100644 --- a/awx/ui/client/lib/components/index.js +++ b/awx/ui/client/lib/components/index.js @@ -1,10 +1,6 @@ import atLibServices from '~services'; import actionGroup from '~components/action/action-group.directive'; -import codeMenuBottom from '~components/code/menu-bottom.directive'; -import codeMenuTop from '~components/code/menu-top.directive'; -import codeEvents from '~components/code/events.directive'; -import codeStdout from '~components/code/stdout.directive'; import divider from '~components/utility/divider.directive'; import form from '~components/form/form.directive'; import formAction from '~components/form/action.directive'; @@ -49,10 +45,6 @@ angular atLibServices ]) .directive('atActionGroup', actionGroup) - .directive('atCodeEvents', codeEvents) - .directive('atCodeMenuBottom', codeMenuBottom) - .directive('atCodeMenuTop', codeMenuTop) - .directive('atCodeStdout', codeStdout) .directive('atDivider', divider) .directive('atForm', form) .directive('atFormAction', formAction) diff --git a/awx/ui/test/e2e/tests/test-jobz.js b/awx/ui/test/e2e/tests/test-jobz.js deleted file mode 100644 index 9f76fc7bec..0000000000 --- a/awx/ui/test/e2e/tests/test-jobz.js +++ /dev/null @@ -1,118 +0,0 @@ -import uuid from 'uuid'; - -import { - get, - post, -} from '../api'; -import { - getAdminMachineCredential, - getInventory, - getOrCreate, - getOrganization, - waitForJob, -} from '../fixtures'; - -// AWX_E2E_URL='https://localhost:3000' npm --prefix awx/ui run e2e -- --filter="*jobz*" - -const session = `e2e-${uuid().substr(0, 8)}`; - -const SCM_URL = 'https://github.com/jakemcdermott/ansible-playbooks'; -const PLAYBOOK = 'setfact_50.yml'; -const PARAMS = '?job_event_search=page_size:200;order_by:start_line;not__event__in:playbook_on_start,playbook_on_play_start,playbook_on_task_start,playbook_on_stats;task:set'; - -let data; - -const waitForJobz = endpoint => { - const interval = 2000; - const statuses = ['successful', 'failed', 'error', 'canceled']; - - let attempts = 20; - - return new Promise((resolve, reject) => { - (function pollStatus () { - get(endpoint).then(update => { - const completed = statuses.indexOf(update.data.status) > -1; - - if (completed) { - return resolve(update.data); - } - - if (--attempts <= 0) { - return reject(new Error('Retry limit exceeded.')); - } - - return setTimeout(pollStatus, interval); - }); - }()); - }); -}; - -const getProject = (namespace = session) => getOrganization(namespace) - .then(organization => getOrCreate('/projects/', { - name: `${namespace}-project`, - description: namespace, - organization: organization.id, - scm_url: SCM_URL, - scm_type: 'git' - }) - .then(project => { - if (project.related.current_update) { - return waitForJobz(project.related.current_update) - .then(() => project); - } - return project; - })); - -const getJobTemplate = (namespace = session) => { - const promises = [ - getInventory(namespace), - getAdminMachineCredential(namespace), - getProject(namespace) - ]; - - return Promise.all(promises) - .then(([inventory, credential, project]) => getOrCreate('/job_templates/', { - name: `${namespace}-job-template`, - description: namespace, - inventory: inventory.id, - credential: credential.id, - project: project.id, - playbook: PLAYBOOK, - })); -}; - -const getJob = (namespace = session) => getJobTemplate(namespace) - .then(template => { - if (template.related.last_job) { - return waitForJobz(template.related.last_job); - } - - return post(template.related.launch, {}) - .then(res => waitForJobz(res.data.url)); - }); - -module.exports = { - before: (client, done) => { - getJob() - .then(job => { - data = { job }; - done(); - }) - }, - 'test jobz': client => { - const location = `${client.globals.launch_url}/#/jobz/playbook/${data.job.id}`; - const templates = client.page.templates(); - - client.useCss(); - client.resizeWindow(1200, 800); - client.login(); - client.waitForAngular(); - - // client.url(location); - client.url(`${location}${PARAMS}`); - - client.pause(); - - client.end(); - }, -}; From a7bcb491d761d86638ef29442b60d55d8911cf7a Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 4 Apr 2018 15:52:07 -0400 Subject: [PATCH 82/89] disable search while searching --- awx/ui/client/features/output/search.directive.js | 12 +++++++++++- awx/ui/client/features/output/search.partial.html | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/features/output/search.directive.js b/awx/ui/client/features/output/search.directive.js index 5945594ab1..0a688f92bb 100644 --- a/awx/ui/client/features/output/search.directive.js +++ b/awx/ui/client/features/output/search.directive.js @@ -4,6 +4,9 @@ const searchReloadOptions = { reload: true, inherit: false }; const searchKeyExamples = ['id:>1', 'task:set', 'created:>=2000-01-01']; const searchKeyFields = ['changed', 'failed', 'host_name', 'stdout', 'task', 'role', 'playbook', 'play']; +const PLACEHOLDER_RUNNING = 'CANNOT SEARCH RUNNING JOB'; +const PLACEHOLDER_DEFAULT = 'SEARCH'; + let $state; let status; let qs; @@ -35,6 +38,7 @@ function removeSearchTag (index) { const modifiedQueryset = qs.removeTermsFromQueryset(currentQueryset, searchTerm); vm.tags = getSearchTags(modifiedQueryset); + vm.disabled = true; $state.params.job_event_search = qs.encodeArr(modifiedQueryset); $state.transitionTo($state.current, $state.params, searchReloadOptions); @@ -47,6 +51,7 @@ function submitSearch () { const modifiedQueryset = qs.mergeQueryset(currentQueryset, searchInputQueryset); vm.tags = getSearchTags(modifiedQueryset); + vm.disabled = true; $state.params.job_event_search = qs.encodeArr(modifiedQueryset); $state.transitionTo($state.current, $state.params, searchReloadOptions); @@ -54,6 +59,7 @@ function submitSearch () { function clearSearch () { vm.tags = []; + vm.disabled = true; $state.params.job_event_search = ''; $state.transitionTo($state.current, $state.params, searchReloadOptions); @@ -88,9 +94,13 @@ function AtJobSearchController (_$state_, _status_, _qs_) { vm.init = scope => { vm.examples = scope.examples || searchKeyExamples; vm.fields = scope.fields || searchKeyFields; + vm.placeholder = PLACEHOLDER_DEFAULT; vm.relatedFields = scope.relatedFields || []; - scope.$watch(status.isRunning, value => { vm.disabled = value; }); + scope.$watch(status.isRunning, value => { + vm.disabled = value; + vm.placeholder = value ? PLACEHOLDER_RUNNING : PLACEHOLDER_DEFAULT; + }); }; } diff --git a/awx/ui/client/features/output/search.partial.html b/awx/ui/client/features/output/search.partial.html index af34bac344..d7acedc3d4 100644 --- a/awx/ui/client/features/output/search.partial.html +++ b/awx/ui/client/features/output/search.partial.html @@ -5,7 +5,7 @@ class="form-control at-Input" ng-class="{ 'at-Input--rejected': vm.rejected }" ng-model="vm.value" - ng-attr-placeholder="{{ vm.disabled ? 'CANNOT SEARCH RUNNING JOB' : 'SEARCH' }}" + ng-attr-placeholder="{{ vm.placeholder }}" ng-disabled="vm.disabled"> + + - - - - -
From fe58b74d1eb1a84c54d35d1daa7c3b2ab9f9dc5b Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Mon, 2 Apr 2018 11:21:11 -0700 Subject: [PATCH 84/89] Adds the host event modal to the standard out feature Removes old host modal code --- awx/ui/client/features/output/_index.less | 1 + .../features/output/details.partial.html | 1 + .../output/host-event/_index.less} | 1 + .../host-event-codemirror.partial.html | 0 .../host-event/host-event-modal.partial.html | 12 +- .../host-event/host-event-stderr.partial.html | 0 .../host-event/host-event-stdout.partial.html | 0 .../host-event/host-event.controller.js | 170 ++++++++++++++++++ .../output/host-event/host-event.route.js | 72 ++++++++ .../output/host-event/host-event.service.js | 69 +++++++ .../features/output/host-event/index.js | 26 +++ awx/ui/client/features/output/index.js | 4 +- .../client/features/output/render.service.js | 3 +- awx/ui/client/lib/theme/index.less | 1 - .../host-event/host-event.controller.js | 143 --------------- .../host-event/host-event.route.js | 66 ------- .../client/src/job-results/host-event/main.js | 20 --- awx/ui/client/src/job-results/main.js | 3 +- 18 files changed, 352 insertions(+), 240 deletions(-) rename awx/ui/client/{src/job-results/host-event/host-event.block.less => features/output/host-event/_index.less} (99%) rename awx/ui/client/{src/job-results => features/output}/host-event/host-event-codemirror.partial.html (100%) rename awx/ui/client/{src/job-results => features/output}/host-event/host-event-modal.partial.html (83%) rename awx/ui/client/{src/job-results => features/output}/host-event/host-event-stderr.partial.html (100%) rename awx/ui/client/{src/job-results => features/output}/host-event/host-event-stdout.partial.html (100%) create mode 100644 awx/ui/client/features/output/host-event/host-event.controller.js create mode 100644 awx/ui/client/features/output/host-event/host-event.route.js create mode 100644 awx/ui/client/features/output/host-event/host-event.service.js create mode 100644 awx/ui/client/features/output/host-event/index.js delete mode 100644 awx/ui/client/src/job-results/host-event/host-event.controller.js delete mode 100644 awx/ui/client/src/job-results/host-event/host-event.route.js delete mode 100644 awx/ui/client/src/job-results/host-event/main.js diff --git a/awx/ui/client/features/output/_index.less b/awx/ui/client/features/output/_index.less index e44be3b39f..5fa8bec237 100644 --- a/awx/ui/client/features/output/_index.less +++ b/awx/ui/client/features/output/_index.less @@ -1,3 +1,4 @@ +@import 'host-event/_index'; .at-Stdout { &-menuTop { color: @at-gray-848992; diff --git a/awx/ui/client/features/output/details.partial.html b/awx/ui/client/features/output/details.partial.html index edd92f6fe0..9417d30670 100644 --- a/awx/ui/client/features/output/details.partial.html +++ b/awx/ui/client/features/output/details.partial.html @@ -1,4 +1,5 @@ +
DETAILS
diff --git a/awx/ui/client/src/job-results/host-event/host-event.block.less b/awx/ui/client/features/output/host-event/_index.less similarity index 99% rename from awx/ui/client/src/job-results/host-event/host-event.block.less rename to awx/ui/client/features/output/host-event/_index.less index 6153466934..bec8548cd2 100644 --- a/awx/ui/client/src/job-results/host-event/host-event.block.less +++ b/awx/ui/client/features/output/host-event/_index.less @@ -15,6 +15,7 @@ } .HostEvent .CodeMirror{ overflow-x: hidden; + max-height: none!important; } .HostEvent-close:hover{ diff --git a/awx/ui/client/src/job-results/host-event/host-event-codemirror.partial.html b/awx/ui/client/features/output/host-event/host-event-codemirror.partial.html similarity index 100% rename from awx/ui/client/src/job-results/host-event/host-event-codemirror.partial.html rename to awx/ui/client/features/output/host-event/host-event-codemirror.partial.html diff --git a/awx/ui/client/src/job-results/host-event/host-event-modal.partial.html b/awx/ui/client/features/output/host-event/host-event-modal.partial.html similarity index 83% rename from awx/ui/client/src/job-results/host-event/host-event-modal.partial.html rename to awx/ui/client/features/output/host-event/host-event-modal.partial.html index 7da83dfb43..a79b3cde68 100644 --- a/awx/ui/client/src/job-results/host-event/host-event-modal.partial.html +++ b/awx/ui/client/features/output/host-event/host-event-modal.partial.html @@ -40,19 +40,19 @@
- - - diff --git a/awx/ui/client/src/job-results/host-event/host-event-stderr.partial.html b/awx/ui/client/features/output/host-event/host-event-stderr.partial.html similarity index 100% rename from awx/ui/client/src/job-results/host-event/host-event-stderr.partial.html rename to awx/ui/client/features/output/host-event/host-event-stderr.partial.html diff --git a/awx/ui/client/src/job-results/host-event/host-event-stdout.partial.html b/awx/ui/client/features/output/host-event/host-event-stdout.partial.html similarity index 100% rename from awx/ui/client/src/job-results/host-event/host-event-stdout.partial.html rename to awx/ui/client/features/output/host-event/host-event-stdout.partial.html diff --git a/awx/ui/client/features/output/host-event/host-event.controller.js b/awx/ui/client/features/output/host-event/host-event.controller.js new file mode 100644 index 0000000000..67105ba7a0 --- /dev/null +++ b/awx/ui/client/features/output/host-event/host-event.controller.js @@ -0,0 +1,170 @@ +function HostEventsController ( + $scope, + $state, + HostEventService, + hostEvent +) { + $scope.processEventStatus = HostEventService.processEventStatus; + $scope.processResults = processResults; + $scope.isActiveState = isActiveState; + $scope.getActiveHostIndex = getActiveHostIndex; + $scope.closeHostEvent = closeHostEvent; + + function init () { + hostEvent.event_name = hostEvent.event; + $scope.event = _.cloneDeep(hostEvent); + + // grab standard out & standard error if present from the host + // event's 'res' object, for things like Ansible modules. Small + // wrinkle in this implementation is that the stdout/stderr tabs + // should be shown if the `res` object has stdout/stderr keys, even + // if they're a blank string. The presence of these keys is + // potentially significant to a user. + if (_.has(hostEvent.event_data, 'task_action')) { + $scope.module_name = hostEvent.event_data.task_action; + } else if (!_.has(hostEvent.event_data, 'task_action')) { + $scope.module_name = 'No result found'; + } + + if (_.has(hostEvent.event_data, 'res.result.stdout')) { + if (hostEvent.event_data.res.stdout === '') { + $scope.stdout = ' '; + } else { + $scope.stdout = hostEvent.event_data.res.stdout; + } + } + + if (_.has(hostEvent.event_data, 'res.result.stderr')) { + if (hostEvent.event_data.res.stderr === '') { + $scope.stderr = ' '; + } else { + $scope.stderr = hostEvent.event_data.res.stderr; + } + } + + if (_.has(hostEvent.event_data, 'res')) { + $scope.json = hostEvent.event_data.res; + } + + if ($scope.module_name === 'debug' && + _.has(hostEvent.event_data, 'res.result.stdout')) { + $scope.stdout = hostEvent.event_data.res.result.stdout; + } + if ($scope.module_name === 'yum' && + _.has(hostEvent.event_data, 'res.results') && + _.isArray(hostEvent.event_data.res.results)) { + const event = hostEvent.event_data.res.results; + $scope.stdout = event[0];// eslint-disable-line prefer-destructuring + } + // instantiate Codemirror + if ($state.current.name === 'jobz.host-event.json') { + try { + if (_.has(hostEvent.event_data, 'res')) { + initCodeMirror( + 'HostEvent-codemirror', + JSON.stringify($scope.json, null, 4), + { name: 'javascript', json: true } + ); + resize(); + } else { + $scope.no_json = true; + } + } catch (err) { + // element with id HostEvent-codemirror is not the view + // controlled by this instance of HostEventController + } + } else if ($state.current.name === 'jobz.host-event.stdout') { + try { + resize(); + } catch (err) { + // element with id HostEvent-codemirror is not the view + // controlled by this instance of HostEventController + } + } else if ($state.current.name === 'jobz.host-event.stderr') { + try { + resize(); + } catch (err) { + // element with id HostEvent-codemirror is not the view + // controlled by this instance of HostEventController + } + } + $('#HostEvent').modal('show'); + $('.modal-content').resizable({ + minHeight: 523, + minWidth: 600 + }); + $('.modal-dialog').draggable({ + cancel: '.HostEvent-view--container' + }); + + function resize () { + if ($state.current.name === 'jobz.host-event.json') { + const editor = $('.CodeMirror')[0].CodeMirror; + const height = $('.modal-dialog').height() - $('.HostEvent-header').height() - $('.HostEvent-details').height() - $('.HostEvent-nav').height() - $('.HostEvent-controls').height() - 120; + editor.setSize('100%', height); + } else if ($state.current.name === 'jobz.host-event.stdout' || $state.current.name === 'jobz.host-event.stderr') { + const height = $('.modal-dialog').height() - $('.HostEvent-header').height() - $('.HostEvent-details').height() - $('.HostEvent-nav').height() - $('.HostEvent-controls').height() - 120; + $('.HostEvent-stdout').width('100%'); + $('.HostEvent-stdout').height(height); + $('.HostEvent-stdoutContainer').height(height); + $('.HostEvent-numberColumnPreload').height(height); + } + } + + $('.modal-dialog').on('resize', resize); + + $('#HostEvent').on('hidden.bs.modal', $scope.closeHostEvent); + } + + function processResults (value) { + if (typeof value === 'object') { + return false; + } + return true; + } + + function initCodeMirror (el, data, mode) { + const container = document.getElementById(el); + const options = {}; + options.lineNumbers = true; + options.mode = mode; + options.readOnly = true; + options.scrollbarStyle = null; + const editor = CodeMirror.fromTextArea(// eslint-disable-line no-undef + container, + options + ); + editor.setSize('100%', 200); + editor.getDoc().setValue(data); + } + + function isActiveState (name) { + return $state.current.name === name; + } + + function getActiveHostIndex () { + function hostResultfilter (obj) { + return obj.id === $scope.event.id; + } + const result = $scope.hostResults.filter(hostResultfilter); + return $scope.hostResults.indexOf(result[0]); + } + + function closeHostEvent () { + // Unbind the listener so it doesn't fire when we close the modal via navigation + $('#HostEvent').off('hidden.bs.modal'); + $('#HostEvent').modal('hide'); + $state.go('jobz'); + } + $scope.init = init; + $scope.init(); +} + +HostEventsController.$inject = [ + '$scope', + '$state', + 'HostEventService', + 'hostEvent', +]; + +module.exports = HostEventsController; diff --git a/awx/ui/client/features/output/host-event/host-event.route.js b/awx/ui/client/features/output/host-event/host-event.route.js new file mode 100644 index 0000000000..06f3eeac51 --- /dev/null +++ b/awx/ui/client/features/output/host-event/host-event.route.js @@ -0,0 +1,72 @@ +const HostEventModalTemplate = require('~features/output/host-event/host-event-modal.partial.html'); +const HostEventCodeMirrorTemplate = require('~features/output/host-event/host-event-codemirror.partial.html'); +const HostEventStdoutTemplate = require('~features/output/host-event/host-event-stdout.partial.html'); +const HostEventStderrTemplate = require('~features/output/host-event/host-event-stderr.partial.html'); + +function exit () { + // close the modal + // using an onExit event to handle cases where the user navs away + // using the url bar / back and not modal "X" + $('#HostEvent').modal('hide'); + // hacky way to handle user browsing away via URL bar + $('.modal-backdrop').remove(); + $('body').removeClass('modal-open'); +} + +function HostEventResolve (HostEventService, $stateParams) { + return HostEventService.getRelatedJobEvents($stateParams.id, { + id: $stateParams.eventId + }).then((response) => response.data.results[0]); +} + +HostEventResolve.$inject = [ + 'HostEventService', + '$stateParams', +]; + +const hostEventModal = { + name: 'jobz.host-event', + url: '/host-event/:eventId', + controller: 'HostEventsController', + templateUrl: HostEventModalTemplate, + abstract: false, + ncyBreadcrumb: { + skip: true + }, + resolve: { + hostEvent: HostEventResolve + }, + onExit: exit +}; + +const hostEventJson = { + name: 'jobz.host-event.json', + url: '/json', + controller: 'HostEventsController', + templateUrl: HostEventCodeMirrorTemplate, + ncyBreadcrumb: { + skip: true + }, +}; + +const hostEventStdout = { + name: 'jobz.host-event.stdout', + url: '/stdout', + controller: 'HostEventsController', + templateUrl: HostEventStdoutTemplate, + ncyBreadcrumb: { + skip: true + }, +}; + +const hostEventStderr = { + name: 'jobz.host-event.stderr', + url: '/stderr', + controller: 'HostEventsController', + templateUrl: HostEventStderrTemplate, + ncyBreadcrumb: { + skip: true + }, +}; + +export { hostEventJson, hostEventModal, hostEventStdout, hostEventStderr }; diff --git a/awx/ui/client/features/output/host-event/host-event.service.js b/awx/ui/client/features/output/host-event/host-event.service.js new file mode 100644 index 0000000000..a1e6952725 --- /dev/null +++ b/awx/ui/client/features/output/host-event/host-event.service.js @@ -0,0 +1,69 @@ +function HostEventService ( + Rest, + ProcessErrors, + GetBasePath, + $rootScope +) { + // GET events related to a job run + // e.g. + // ?event=playbook_on_stats + // ?parent=206&event__startswith=runner&page_size=200&order=host_name,counter + this.getRelatedJobEvents = (id, params) => { + let url = GetBasePath('jobs'); + url = `${url}${id}/job_events/?${this.stringifyParams(params)}`; + Rest.setUrl(url); + return Rest.get() + .then(response => response) + .catch(({ data, status }) => { + ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', + msg: `Call to ${url}. GET returned: ${status}` }); + }); + }; + + this.stringifyParams = params => { + function reduceFunction (result, value, key) { + return `${result}${key}=${value}&`; + } + return _.reduce(params, reduceFunction, ''); + }; + + // Generate a helper class for job_event statuses + // the stack for which status to display is + // unreachable > failed > changed > ok + // uses the API's runner events and convenience properties .failed .changed to determine status. + // see: job_event_callback.py for more filters to support + this.processEventStatus = event => { + const obj = {}; + if (event.event === 'runner_on_unreachable') { + obj.class = 'HostEvent-status--unreachable'; + obj.status = 'unreachable'; + } + // equiv to 'runner_on_error' && 'runner on failed' + if (event.failed) { + obj.class = 'HostEvent-status--failed'; + obj.status = 'failed'; + } + // catch the changed case before ok, because both can be true + if (event.changed) { + obj.class = 'HostEvent-status--changed'; + obj.status = 'changed'; + } + if (event.event === 'runner_on_ok' || event.event === 'runner_on_async_ok') { + obj.class = 'HostEvent-status--ok'; + obj.status = 'ok'; + } + if (event.event === 'runner_on_skipped') { + obj.class = 'HostEvent-status--skipped'; + obj.status = 'skipped'; + } + return obj; + }; +} + +HostEventService.$inject = [ + 'Rest', + 'ProcessErrors', + 'GetBasePath', + '$rootScope' +]; +export default HostEventService; diff --git a/awx/ui/client/features/output/host-event/index.js b/awx/ui/client/features/output/host-event/index.js new file mode 100644 index 0000000000..f00b0b36b4 --- /dev/null +++ b/awx/ui/client/features/output/host-event/index.js @@ -0,0 +1,26 @@ +import { + hostEventModal, + hostEventJson, + hostEventStdout, + hostEventStderr +} from './host-event.route'; +import controller from './host-event.controller'; +import service from './host-event.service'; + +const MODULE_NAME = 'hostEvents'; + +function hostEventRun ($stateExtender) { + $stateExtender.addState(hostEventModal); + $stateExtender.addState(hostEventJson); + $stateExtender.addState(hostEventStdout); + $stateExtender.addState(hostEventStderr); +} +hostEventRun.$inject = [ + '$stateExtender' +]; + +angular.module(MODULE_NAME, []) + .controller('HostEventsController', controller) + .service('HostEventService', service) + .run(hostEventRun); +export default MODULE_NAME; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index e1727c3dc5..9542b072ad 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -12,6 +12,7 @@ import StatusService from '~features/output/status.service'; import DetailsDirective from '~features/output/details.directive'; import SearchDirective from '~features/output/search.directive'; import StatsDirective from '~features/output/stats.directive'; +import HostEvent from './host-event/index'; const Template = require('~features/output/index.view.html'); @@ -211,7 +212,8 @@ JobsRun.$inject = ['$stateRegistry']; angular .module(MODULE_NAME, [ atLibModels, - atLibComponents + atLibComponents, + HostEvent ]) .service('JobStrings', Strings) .service('JobPageService', PageService) diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js index 351d038387..12ca797b32 100644 --- a/awx/ui/client/features/output/render.service.js +++ b/awx/ui/client/features/output/render.service.js @@ -169,7 +169,7 @@ function JobRenderService ($q, $sce, $window) { } if (current.isHost) { - tdEvent = `${content}`; + tdEvent = `${content}`; } if (current.time && current.line === ln) { @@ -251,6 +251,7 @@ function JobRenderService ($q, $sce, $window) { }); this.compile = html => { + html = $(this.el); this.hooks.compile(html); return this.requestAnimationFrame(); diff --git a/awx/ui/client/lib/theme/index.less b/awx/ui/client/lib/theme/index.less index 9a9f564840..ec42e88a45 100644 --- a/awx/ui/client/lib/theme/index.less +++ b/awx/ui/client/lib/theme/index.less @@ -81,7 +81,6 @@ @import '../../src/inventories-hosts/inventories/inventories.block.less'; @import '../../src/inventories-hosts/shared/associate-groups/associate-groups.block.less'; @import '../../src/inventories-hosts/shared/associate-hosts/associate-hosts.block.less'; -@import '../../src/job-results/host-event/host-event.block.less'; @import '../../src/job-results/host-status-bar/host-status-bar.block.less'; @import '../../src/job-results/job-results-stdout/job-results-stdout.block.less'; @import '../../src/job-results/job-results.block.less'; diff --git a/awx/ui/client/src/job-results/host-event/host-event.controller.js b/awx/ui/client/src/job-results/host-event/host-event.controller.js deleted file mode 100644 index 330e581189..0000000000 --- a/awx/ui/client/src/job-results/host-event/host-event.controller.js +++ /dev/null @@ -1,143 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - - export default - ['$scope', '$state', 'jobResultsService', 'hostEvent', - function($scope, $state, jobResultsService, hostEvent){ - - $scope.processEventStatus = jobResultsService.processEventStatus; - $scope.processResults = function(value){ - if (typeof value === 'object'){return false;} - else {return true;} - }; - - var initCodeMirror = function(el, data, mode){ - var container = document.getElementById(el); - var editor = CodeMirror.fromTextArea(container, { // jshint ignore:line - lineNumbers: true, - mode: mode, - readOnly: true, - scrollbarStyle: null - }); - editor.setSize("100%", 200); - editor.getDoc().setValue(data); - }; - /*ignore jslint end*/ - $scope.isActiveState = function(name){ - return $state.current.name === name; - }; - - $scope.getActiveHostIndex = function(){ - var result = $scope.hostResults.filter(function( obj ) { - return obj.id === $scope.event.id; - }); - return $scope.hostResults.indexOf(result[0]); - }; - - $scope.closeHostEvent = function() { - // Unbind the listener so it doesn't fire when we close the modal via navigation - $('#HostEvent').off('hidden.bs.modal'); - $('#HostEvent').modal('hide'); - $state.go('jobResult'); - }; - - var init = function(){ - hostEvent.event_name = hostEvent.event; - $scope.event = _.cloneDeep(hostEvent); - - // grab standard out & standard error if present from the host - // event's "res" object, for things like Ansible modules. Small - // wrinkle in this implementation is that the stdout/stderr tabs - // should be shown if the `res` object has stdout/stderr keys, even - // if they're a blank string. The presence of these keys is - // potentially significant to a user. - try{ - $scope.module_name = hostEvent.event_data.task_action || "No result found"; - $scope.stdout = hostEvent.event_data.res.stdout ? hostEvent.event_data.res.stdout : hostEvent.event_data.res.stdout === "" ? " " : undefined; - $scope.stderr = hostEvent.event_data.res.stderr ? hostEvent.event_data.res.stderr : hostEvent.event_data.res.stderr === "" ? " " : undefined; - $scope.json = hostEvent.event_data.res; - } - catch(err){ - // do nothing, no stdout/stderr for this module - } - if($scope.module_name === "debug" && - _.has(hostEvent.event_data, "res.result.stdout")){ - $scope.stdout = hostEvent.event_data.res.result.stdout; - } - if($scope.module_name === "yum" && - _.has(hostEvent.event_data, "res.results") && - _.isArray(hostEvent.event_data.res.results)){ - $scope.stdout = hostEvent.event_data.res.results[0]; - } - // instantiate Codemirror - // try/catch pattern prevents the abstract-state controller from complaining about element being null - if ($state.current.name === 'jobResult.host-event.json'){ - try{ - if(_.has(hostEvent.event_data, "res")){ - initCodeMirror('HostEvent-codemirror', JSON.stringify($scope.json, null, 4), {name: "javascript", json: true}); - resize(); - } - else{ - $scope.no_json = true; - } - - } - catch(err){ - // element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController - } - } - else if ($state.current.name === 'jobResult.host-event.stdout'){ - try{ - resize(); - } - catch(err){ - // element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController - } - } - else if ($state.current.name === 'jobResult.host-event.stderr'){ - try{ - resize(); - } - catch(err){ - // element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController - } - } - $('#HostEvent').modal('show'); - $('.modal-content').resizable({ - minHeight: 523, - minWidth: 600 - }); - $('.modal-dialog').draggable({ - cancel: '.HostEvent-view--container' - }); - - function resize(){ - if ($state.current.name === 'jobResult.host-event.json'){ - let editor = $('.CodeMirror')[0].CodeMirror; - let height = $('.modal-dialog').height() - $('.HostEvent-header').height() - $('.HostEvent-details').height() - $('.HostEvent-nav').height() - $('.HostEvent-controls').height() - 120; - editor.setSize("100%", height); - } - else if($state.current.name === 'jobResult.host-event.stdout' || $state.current.name === 'jobResult.host-event.stderr'){ - let height = $('.modal-dialog').height() - $('.HostEvent-header').height() - $('.HostEvent-details').height() - $('.HostEvent-nav').height() - $('.HostEvent-controls').height() - 120; - $(".HostEvent-stdout").width("100%"); - $(".HostEvent-stdout").height(height); - $(".HostEvent-stdoutContainer").height(height); - $(".HostEvent-numberColumnPreload").height(height); - } - - } - - $('.modal-dialog').on('resize', function(){ - resize(); - }); - - $('#HostEvent').on('hidden.bs.modal', function () { - $scope.closeHostEvent(); - }); - }; - init(); - }]; diff --git a/awx/ui/client/src/job-results/host-event/host-event.route.js b/awx/ui/client/src/job-results/host-event/host-event.route.js deleted file mode 100644 index e0a32ad7e4..0000000000 --- a/awx/ui/client/src/job-results/host-event/host-event.route.js +++ /dev/null @@ -1,66 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import { templateUrl } from '../../shared/template-url/template-url.factory'; - -var hostEventModal = { - name: 'jobResult.host-event', - url: '/host-event/:eventId', - controller: 'HostEventController', - templateUrl: templateUrl('job-results/host-event/host-event-modal'), - 'abstract': false, - ncyBreadcrumb: { - skip: true - }, - resolve: { - hostEvent: ['jobResultsService', '$stateParams', function(jobResultsService, $stateParams) { - return jobResultsService.getRelatedJobEvents($stateParams.id, { - id: $stateParams.eventId - }).then((response) => response.data.results[0]); - }] - }, - onExit: function() { - // close the modal - // using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X" - $('#HostEvent').modal('hide'); - // hacky way to handle user browsing away via URL bar - $('.modal-backdrop').remove(); - $('body').removeClass('modal-open'); - } -}; - -var hostEventJson = { - name: 'jobResult.host-event.json', - url: '/json', - controller: 'HostEventController', - templateUrl: templateUrl('job-results/host-event/host-event-codemirror'), - ncyBreadcrumb: { - skip: true - }, -}; - -var hostEventStdout = { - name: 'jobResult.host-event.stdout', - url: '/stdout', - controller: 'HostEventController', - templateUrl: templateUrl('job-results/host-event/host-event-stdout'), - ncyBreadcrumb: { - skip: true - }, -}; - -var hostEventStderr = { - name: 'jobResult.host-event.stderr', - url: '/stderr', - controller: 'HostEventController', - templateUrl: templateUrl('job-results/host-event/host-event-stderr'), - ncyBreadcrumb: { - skip: true - }, -}; - - -export { hostEventJson, hostEventModal, hostEventStdout, hostEventStderr }; diff --git a/awx/ui/client/src/job-results/host-event/main.js b/awx/ui/client/src/job-results/host-event/main.js deleted file mode 100644 index 76832b45e5..0000000000 --- a/awx/ui/client/src/job-results/host-event/main.js +++ /dev/null @@ -1,20 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - import {hostEventModal, - hostEventJson, hostEventStdout, hostEventStderr} from './host-event.route'; - import controller from './host-event.controller'; - - export default - angular.module('jobResults.hostEvent', []) - .controller('HostEventController', controller) - - .run(['$stateExtender', function($stateExtender){ - $stateExtender.addState(hostEventModal); - $stateExtender.addState(hostEventJson); - $stateExtender.addState(hostEventStdout); - $stateExtender.addState(hostEventStderr); - }]); diff --git a/awx/ui/client/src/job-results/main.js b/awx/ui/client/src/job-results/main.js index f0aedc7c43..f48ac081d8 100644 --- a/awx/ui/client/src/job-results/main.js +++ b/awx/ui/client/src/job-results/main.js @@ -6,7 +6,6 @@ import hostStatusBar from './host-status-bar/main'; import jobResultsStdOut from './job-results-stdout/main'; -import hostEvent from './host-event/main'; import route from './job-results.route.js'; @@ -17,7 +16,7 @@ import eventQueueService from './event-queue.service'; import parseStdoutService from './parse-stdout.service'; export default - angular.module('jobResults', [hostStatusBar.name, jobResultsStdOut.name, hostEvent.name, 'angularMoment']) + angular.module('jobResults', [hostStatusBar.name, jobResultsStdOut.name, 'angularMoment']) .run(['$stateExtender', function($stateExtender) { $stateExtender.addState(route); }]) From b44c7127f71a0a2cde514f8375e52176c16f8ae1 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 4 Apr 2018 20:11:53 -0400 Subject: [PATCH 85/89] reactivate linter for dev server --- awx/ui/build/webpack.watch.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/awx/ui/build/webpack.watch.js b/awx/ui/build/webpack.watch.js index c764ec4b85..5bf1aad89c 100644 --- a/awx/ui/build/webpack.watch.js +++ b/awx/ui/build/webpack.watch.js @@ -20,18 +20,16 @@ const watch = { output: { filename: OUTPUT }, - /* - *module: { - * rules: [ - * { - * test: /\.js$/, - * enforce: 'pre', - * exclude: /node_modules/, - * loader: 'eslint-loader' - * } - * ] - *}, - */ + module: { + rules: [ + { + test: /\.js$/, + enforce: 'pre', + exclude: /node_modules/, + loader: 'eslint-loader' + } + ] + }, plugins: [ new HtmlWebpackHarddiskPlugin(), new HardSourceWebpackPlugin({ From 939666f17288ac729ab2628b5c3b1dde4ef662fa Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 4 Apr 2018 20:13:04 -0400 Subject: [PATCH 86/89] add polyfills for phantomjs --- awx/ui/test/spec/karma.spec.js | 1 + awx/ui/test/spec/polyfills.js | 31 +++++++++++++++++++++++++++++++ awx/ui/test/unit/karma.unit.js | 1 + awx/ui/test/unit/polyfills.js | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 awx/ui/test/spec/polyfills.js create mode 100644 awx/ui/test/unit/polyfills.js diff --git a/awx/ui/test/spec/karma.spec.js b/awx/ui/test/spec/karma.spec.js index abff3172f5..ae2dc8899d 100644 --- a/awx/ui/test/spec/karma.spec.js +++ b/awx/ui/test/spec/karma.spec.js @@ -13,6 +13,7 @@ module.exports = config => { frameworks: ['jasmine'], reporters: ['progress', 'junit'], files:[ + './polyfills.js', path.join(SRC_PATH, '**/*.html'), path.join(SRC_PATH, 'vendor.js'), path.join(NODE_MODULES, 'angular-mocks/angular-mocks.js'), diff --git a/awx/ui/test/spec/polyfills.js b/awx/ui/test/spec/polyfills.js new file mode 100644 index 0000000000..8b7342e5fd --- /dev/null +++ b/awx/ui/test/spec/polyfills.js @@ -0,0 +1,31 @@ +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill + +if (typeof Object.assign != 'function') { + // Must be writable: true, enumerable: false, configurable: true + Object.defineProperty(Object, "assign", { + value: function assign(target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + var to = Object(target); + + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }, + writable: true, + configurable: true + }); +} diff --git a/awx/ui/test/unit/karma.unit.js b/awx/ui/test/unit/karma.unit.js index 5ccd1fe151..d839aff5f2 100644 --- a/awx/ui/test/unit/karma.unit.js +++ b/awx/ui/test/unit/karma.unit.js @@ -14,6 +14,7 @@ module.exports = config => { browsers: ['PhantomJS'], reporters: ['progress', 'junit'], files: [ + './polyfills.js', path.join(SRC_PATH, 'vendor.js'), path.join(SRC_PATH, 'app.js'), path.join(SRC_PATH, '**/*.html'), diff --git a/awx/ui/test/unit/polyfills.js b/awx/ui/test/unit/polyfills.js new file mode 100644 index 0000000000..25b18055bd --- /dev/null +++ b/awx/ui/test/unit/polyfills.js @@ -0,0 +1,33 @@ +/* eslint-disable */ + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill + +if (typeof Object.assign != 'function') { + // Must be writable: true, enumerable: false, configurable: true + Object.defineProperty(Object, "assign", { + value: function assign(target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + var to = Object(target); + + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }, + writable: true, + configurable: true + }); +} From 01d9c8546e31f12d5de5ba7769ea87e8053586ca Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 4 Apr 2018 23:55:53 -0400 Subject: [PATCH 87/89] fix team, credential, and workflow copy regressions --- awx/ui/client/features/credentials/index.js | 2 +- awx/ui/client/features/output/index.js | 4 ++-- awx/ui/client/lib/models/Base.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/ui/client/features/credentials/index.js b/awx/ui/client/features/credentials/index.js index 2fa094ef9f..e3a1ea7734 100644 --- a/awx/ui/client/features/credentials/index.js +++ b/awx/ui/client/features/credentials/index.js @@ -11,7 +11,7 @@ function CredentialsResolve ($q, $stateParams, Me, Credential, CredentialType, O const id = $stateParams.credential_id; const promises = { - me: new Me('get').then((me) => me.extend('admin_of_organizations')) + me: new Me('get').then((me) => me.extend('get', 'admin_of_organizations')) }; if (!id) { diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index 9542b072ad..3aaadc2553 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -80,10 +80,10 @@ function resolveResource ( const promises = [model.getStats()]; if (model.has('related.labels')) { - promises.push(model.extend('labels')); + promises.push(model.extend('get', 'labels')); } - promises.push(model.extend(related, config)); + promises.push(model.extend('get', related, config)); return Promise.all(promises); }) diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index 3aa507ce10..912d9a984c 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -353,8 +353,8 @@ function has (method, keys) { return value !== undefined && value !== null; } -function extend (related, config = {}) { - const req = this.parseRequestConfig('GET', config); +function extend (method, related, config = {}) { + const req = this.parseRequestConfig(method.toUpperCase(), config); if (_.get(config, 'params.page_size')) { this.page.size = config.params.page_size; From cf68df41d57f78f30248a60a496ec6af32f059ca Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 5 Apr 2018 00:51:45 -0400 Subject: [PATCH 88/89] remove unused code --- awx/ui/client/features/output/_index.less | 251 ++++++ awx/ui/client/lib/theme/index.less | 5 +- awx/ui/client/src/app.js | 22 - .../src/job-results/event-queue.service.js | 77 -- .../host-status-bar.block.less | 87 -- .../host-status-bar.directive.js | 47 -- .../host-status-bar.partial.html | 30 - .../src/job-results/host-status-bar/main.js | 11 - .../job-results-stdout.block.less | 271 ------ .../job-results-stdout.directive.js | 415 --------- .../job-results-stdout.partial.html | 65 -- .../job-results/job-results-stdout/main.js | 11 - .../src/job-results/job-results.block.less | 248 ------ .../src/job-results/job-results.controller.js | 784 ------------------ .../src/job-results/job-results.partial.html | 566 ------------- .../src/job-results/job-results.route.js | 187 ----- .../src/job-results/job-results.service.js | 269 ------ awx/ui/client/src/job-results/main.js | 26 - .../src/job-results/parse-stdout.service.js | 293 ------- .../adhoc/standard-out-adhoc.partial.html | 147 ---- .../adhoc/standard-out-adhoc.route.js | 36 - .../standard-out-inventory-sync.partial.html | 152 ---- .../standard-out-inventory-sync.route.js | 38 - awx/ui/client/src/standard-out/log/main.js | 10 - .../log/standard-out-log.controller.js | 202 ----- .../log/standard-out-log.directive.js | 47 -- .../log/standard-out-log.partial.html | 8 - awx/ui/client/src/standard-out/main.js | 22 - .../standard-out-management-jobs.partial.html | 84 -- .../standard-out-management-jobs.route.js | 36 - .../standard-out-scm-update.partial.html | 113 --- .../standard-out-scm-update.route.js | 38 - .../delete-job.factory.js | 145 ---- .../lookup-name.factory.js | 36 - .../standard-out-factories/main.js | 13 - .../standard-out/standard-out.controller.js | 266 ------ .../standard-out.block.less | 2 +- .../job-results.controller-test.js | 701 ---------------- .../job-results/job-results.service-test.js | 50 -- .../job-results/parse-stdout.service-test.js | 212 ----- 40 files changed, 253 insertions(+), 5770 deletions(-) delete mode 100644 awx/ui/client/src/job-results/event-queue.service.js delete mode 100644 awx/ui/client/src/job-results/host-status-bar/host-status-bar.block.less delete mode 100644 awx/ui/client/src/job-results/host-status-bar/host-status-bar.directive.js delete mode 100644 awx/ui/client/src/job-results/host-status-bar/host-status-bar.partial.html delete mode 100644 awx/ui/client/src/job-results/host-status-bar/main.js delete mode 100644 awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.block.less delete mode 100644 awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js delete mode 100644 awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.partial.html delete mode 100644 awx/ui/client/src/job-results/job-results-stdout/main.js delete mode 100644 awx/ui/client/src/job-results/job-results.block.less delete mode 100644 awx/ui/client/src/job-results/job-results.controller.js delete mode 100644 awx/ui/client/src/job-results/job-results.partial.html delete mode 100644 awx/ui/client/src/job-results/job-results.route.js delete mode 100644 awx/ui/client/src/job-results/job-results.service.js delete mode 100644 awx/ui/client/src/job-results/main.js delete mode 100644 awx/ui/client/src/job-results/parse-stdout.service.js delete mode 100644 awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.partial.html delete mode 100644 awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js delete mode 100644 awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html delete mode 100644 awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.route.js delete mode 100644 awx/ui/client/src/standard-out/log/main.js delete mode 100644 awx/ui/client/src/standard-out/log/standard-out-log.controller.js delete mode 100644 awx/ui/client/src/standard-out/log/standard-out-log.directive.js delete mode 100644 awx/ui/client/src/standard-out/log/standard-out-log.partial.html delete mode 100644 awx/ui/client/src/standard-out/main.js delete mode 100644 awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html delete mode 100644 awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js delete mode 100644 awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html delete mode 100644 awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js delete mode 100644 awx/ui/client/src/standard-out/standard-out-factories/delete-job.factory.js delete mode 100644 awx/ui/client/src/standard-out/standard-out-factories/lookup-name.factory.js delete mode 100644 awx/ui/client/src/standard-out/standard-out-factories/main.js delete mode 100644 awx/ui/client/src/standard-out/standard-out.controller.js rename awx/ui/client/src/{standard-out => workflow-results}/standard-out.block.less (99%) delete mode 100644 awx/ui/test/spec/job-results/job-results.controller-test.js delete mode 100644 awx/ui/test/spec/job-results/job-results.service-test.js delete mode 100644 awx/ui/test/spec/job-results/parse-stdout.service-test.js diff --git a/awx/ui/client/features/output/_index.less b/awx/ui/client/features/output/_index.less index 5fa8bec237..71228d20e7 100644 --- a/awx/ui/client/features/output/_index.less +++ b/awx/ui/client/features/output/_index.less @@ -281,3 +281,254 @@ background-color: @default-err; } + +// Job Details --------------------------------------------------------------------------------- + +@breakpoint-md: 1200px; + +.JobResults { + .OnePlusTwo-container(100%, @breakpoint-md); + + &.fullscreen { + .JobResults-rightSide { + max-width: 100%; + } + } +} + +.JobResults-leftSide { + .OnePlusTwo-left--panel(100%, @breakpoint-md); + max-width: 30%; + height: ~"calc(100vh - 177px)"; + + @media screen and (max-width: @breakpoint-md) { + max-width: 100%; + } +} + +.JobResults-rightSide { + .OnePlusTwo-right--panel(100%, @breakpoint-md); + height: ~"calc(100vh - 177px)"; + + @media (max-width: @breakpoint-md - 1px) { + padding-right: 15px; + } +} + +.JobResults-detailsPanel{ + overflow-y: scroll; +} + +.JobResults-stdoutActionButton--active { + display: none; + visibility: hidden; + flex:none; + width:0px; + padding-right: 0px; +} + +.JobResults-panelHeader { + display: flex; + height: 30px; +} + +.JobResults-panelHeaderText { + color: @default-interface-txt; + flex: 1 0 auto; + font-size: 14px; + font-weight: bold; + margin-right: 10px; + text-transform: uppercase; +} + +.JobResults-panelHeaderButtonActions { + display: flex; +} + +.JobResults-resultRow { + width: 100%; + display: flex; + padding-bottom: 10px; + flex-wrap: wrap; +} + +.JobResults-resultRow--variables { + flex-direction: column; + + #cm-variables-container { + width: 100%; + } +} + +.JobResults-resultRowLabel { + text-transform: uppercase; + color: @default-interface-txt; + font-size: 12px; + font-weight: normal!important; + width: 30%; + margin-right: 20px; + + @media screen and (max-width: @breakpoint-md) { + flex: 2.5 0 auto; + } +} + +.JobResults-resultRowLabel--fullWidth { + width: 100%; + margin-right: 0px; +} + +.JobResults-resultRowText { + width: ~"calc(70% - 20px)"; + flex: 1 0 auto; + text-transform: none; + word-wrap: break-word; +} + +.JobResults-resultRowText--fullWidth { + width: 100%; +} + +.JobResults-expandArrow { + color: #D7D7D7; + font-size: 14px; + font-weight: bold; + margin-right: 10px; + text-transform: uppercase; + margin-left: 10px; +} + +.JobResults-resultRowText--instanceGroup { + display: flex; +} + +.JobResults-isolatedBadge { + align-items: center; + background-color: @default-list-header-bg; + border-radius: 5px; + color: @default-stdout-txt; + display: flex; + font-size: 10px; + height: 16px; + margin: 3px 0 0 10px; + padding: 0 10px; + text-transform: uppercase; +} + +.JobResults-statusResultIcon { + padding-left: 0px; + padding-right: 10px; +} + +.JobResults-badgeRow { + display: flex; + align-items: center; + margin-right: 5px; +} + +.JobResults-badgeTitle{ + color: @default-interface-txt; + font-size: 14px; + margin-right: 10px; + font-weight: normal; + text-transform: uppercase; + margin-left: 20px; +} + +@media (max-width: @breakpoint-md) { + .JobResults-detailsPanel { + overflow-y: auto; + } + + .JobResults-rightSide { + height: inherit; + } +} + +.JobResults-timeBadge { + float:right; + font-size: 11px; + font-weight: normal; + padding: 1px 10px; + height: 14px; + margin: 3px 15px; + width: 80px; + background-color: @default-bg; + border-radius: 5px; + color: @default-interface-txt; + margin-right: -5px; +} + +.JobResults-panelRight { + display: flex; + flex-direction: column; +} + +.JobResults-panelRight .SmartSearch-bar { + width: 100%; +} + +.JobResults-panelRightTitle{ + flex-wrap: wrap; +} + +.JobResults-panelRightTitleText{ + word-wrap: break-word; + word-break: break-all; + max-width: 100%; +} + +.JobResults-badgeAndActionRow{ + display:flex; + flex: 1 0 auto; + justify-content: flex-end; + flex-wrap: wrap; + max-width: 100%; +} + +.StandardOut-panelHeader { + flex: initial; +} + +.StandardOut-panelHeader--jobIsRunning { + margin-bottom: 20px; +} + +host-status-bar { + flex: initial; + margin-bottom: 20px; +} + +smart-search { + flex: initial; +} + +job-results-standard-out { + flex: 1; + flex-basis: auto; + height: ~"calc(100% - 800px)"; + display: flex; + border: 1px solid @d7grey; + border-radius: 5px; + margin-top: 20px; +} +@media screen and (max-width: @breakpoint-md) { + job-results-standard-out { + height: auto; + } +} + +.JobResults-extraVarsHelp { + margin-left: 10px; + color: @default-icon; +} + +.JobResults-seeMoreLess { + color: #337AB7; + margin: 4px 0px; + text-transform: uppercase; + padding: 2px 0px; + cursor: pointer; + border-radius: 5px; + font-size: 11px; +} \ No newline at end of file diff --git a/awx/ui/client/lib/theme/index.less b/awx/ui/client/lib/theme/index.less index ec42e88a45..a0f7738272 100644 --- a/awx/ui/client/lib/theme/index.less +++ b/awx/ui/client/lib/theme/index.less @@ -81,9 +81,6 @@ @import '../../src/inventories-hosts/inventories/inventories.block.less'; @import '../../src/inventories-hosts/shared/associate-groups/associate-groups.block.less'; @import '../../src/inventories-hosts/shared/associate-hosts/associate-hosts.block.less'; -@import '../../src/job-results/host-status-bar/host-status-bar.block.less'; -@import '../../src/job-results/job-results-stdout/job-results-stdout.block.less'; -@import '../../src/job-results/job-results.block.less'; @import '../../src/job-submission/job-submission.block.less'; @import '../../src/license/license.block.less'; @import '../../src/login/loginModal/thirdPartySignOn/thirdPartySignOn.block.less'; @@ -116,7 +113,7 @@ @import '../../src/shared/text-label'; @import '../../src/shared/upgrade/upgrade.block.less'; @import '../../src/smart-status/smart-status.block.less'; -@import '../../src/standard-out/standard-out.block.less'; +@import '../../src/workflow-results/standard-out.block.less'; @import '../../src/system-tracking/date-picker/date-picker.block.less'; @import '../../src/system-tracking/fact-data-table/fact-data-table.block.less'; @import '../../src/system-tracking/fact-module-filter.block.less'; diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 7399b6d130..0eec0fa356 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -19,7 +19,6 @@ import credentialTypes from './credential-types/main'; import organizations from './organizations/main'; import managementJobs from './management-jobs/main'; import workflowResults from './workflow-results/main'; -import jobResults from './job-results/main'; import jobSubmission from './job-submission/main'; import notifications from './notifications/main'; import about from './about/main'; @@ -30,7 +29,6 @@ import configuration from './configuration/main'; import home from './home/main'; import login from './login/main'; import activityStream from './activity-stream/main'; -import standardOut from './standard-out/main'; import Templates from './templates/main'; import teams from './teams/main'; import users from './users/main'; @@ -67,7 +65,6 @@ angular 'gettext', 'Timezones', 'lrInfiniteScroll', - about.name, access.name, license.name, @@ -86,10 +83,8 @@ angular login.name, activityStream.name, workflowResults.name, - jobResults.name, jobSubmission.name, notifications.name, - standardOut.name, Templates.name, portalMode.name, teams.name, @@ -242,23 +237,6 @@ angular $rootScope.crumbCache = []; $transitions.onStart({}, function(trans) { - // Remove any lingering intervals - // except on jobResults.* states - var jobResultStates = [ - 'jobResult', - 'jobResult.host-summary', - 'jobResult.host-event.details', - 'jobResult.host-event.json', - 'jobResult.host-events', - 'jobResult.host-event.stdout' - ]; - if ($rootScope.jobResultInterval && !_.includes(jobResultStates, trans.to().name) ) { - window.clearInterval($rootScope.jobResultInterval); - } - if ($rootScope.jobStdOutInterval && !_.includes(jobResultStates, trans.to().name) ) { - window.clearInterval($rootScope.jobStdOutInterval); - } - $rootScope.flashMessage = null; $('#form-modal2 .modal-body').empty(); diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js deleted file mode 100644 index c97861ac6b..0000000000 --- a/awx/ui/client/src/job-results/event-queue.service.js +++ /dev/null @@ -1,77 +0,0 @@ -/************************************************* -* Copyright (c) 2016 Ansible, Inc. -* -* All Rights Reserved -*************************************************/ - -export default ['jobResultsService', 'parseStdoutService', function(jobResultsService, parseStdoutService){ - var val = {}; - - val = { - populateDefers: {}, - queue: {}, - // munge the raw event from the backend into the event_queue's format - munge: function(event) { - // basic data needed in the munged event - var mungedEvent = { - counter: event.counter, - id: event.id, - processed: false, - name: event.event_name, - changes: [] - }; - - // the interface for grabbing standard out is generalized and - // present across many types of events, so go ahead and check for - // updates to it - if (event.stdout) { - mungedEvent.stdout = parseStdoutService.parseStdout(event); - mungedEvent.start_line = event.start_line + 1; - mungedEvent.end_line = event.end_line + 1; - mungedEvent.actual_end_line = parseStdoutService.actualEndLine(event) + 1; - mungedEvent.changes.push('stdout'); - } - - // for different types of events, you need different types of data - if (event.event_name === 'playbook_on_start') { - mungedEvent.startTime = event.modified; - mungedEvent.changes.push('startTime'); - } if (event.event_name === 'playbook_on_stats') { - // get the data for populating the host status bar - mungedEvent.count = jobResultsService - .getCountsFromStatsEvent(event.event_data); - mungedEvent.finishedTime = event.modified; - mungedEvent.changes.push('count'); - mungedEvent.changes.push('countFinished'); - mungedEvent.changes.push('finishedTime'); - } - return mungedEvent; - }, - // reinitializes the event queue value for the job results page - initialize: function() { - val.queue = {}; - val.populateDefers = {}; - }, - // populates the event queue - populate: function(event) { - if (event) { - val.queue[event.counter] = val.munge(event); - - if (!val.queue[event.counter].processed) { - return val.munge(event); - } else { - return {}; - } - } else { - return {}; - } - }, - // the event has been processed in the view and should be marked as - // completed in the queue - markProcessed: function(event) { - val.queue[event.counter].processed = true; - } - }; - - return val; -}]; diff --git a/awx/ui/client/src/job-results/host-status-bar/host-status-bar.block.less b/awx/ui/client/src/job-results/host-status-bar/host-status-bar.block.less deleted file mode 100644 index ff27489751..0000000000 --- a/awx/ui/client/src/job-results/host-status-bar/host-status-bar.block.less +++ /dev/null @@ -1,87 +0,0 @@ -.HostStatusBar { - display: flex; - flex: 0 0 auto; - width: 100%; -} - -.HostStatusBar-ok, -.HostStatusBar-changed, -.HostStatusBar-unreachable, -.HostStatusBar-failures, -.HostStatusBar-skipped, -.HostStatusBar-noData { - height: 15px; - border-top: 5px solid @default-bg; - border-bottom: 5px solid @default-bg; -} - -.HostStatusBar-ok { - background-color: @default-succ; - display: flex; - flex: 0 0 auto; -} - -.HostStatusBar-changed { - background-color: @default-warning; - flex: 0 0 auto; -} - -.HostStatusBar-unreachable { - background-color: @default-unreachable; - flex: 0 0 auto; -} - -.HostStatusBar-dark { - background-color: @default-unreachable; - flex: 0 0 auto; -} - -.HostStatusBar-failures { - background-color: @default-err; - flex: 0 0 auto; -} - -.HostStatusBar-skipped { - background-color: @default-link; - flex: 0 0 auto; -} - -.HostStatusBar-noData { - background-color: @default-icon-hov; - flex: 1 0 auto; -} - -.HostStatusBar-tooltipLabel { - text-transform: uppercase; - margin-right: 15px; -} - -.HostStatusBar-tooltipBadge { - border-radius: 5px; - border: 1px solid @default-bg; -} - -.HostStatusBar-tooltipBadge--ok { - background-color: @default-succ; -} - -.HostStatusBar-tooltipBadge--unreachable { - background-color: @default-unreachable; -} - -.HostStatusBar-tooltipBadge--dark { - background-color: @default-unreachable; -} - -.HostStatusBar-tooltipBadge--skipped { - background-color: @default-link; -} - -.HostStatusBar-tooltipBadge--changed { - background-color: @default-warning; -} - -.HostStatusBar-tooltipBadge--failures { - background-color: @default-err; - -} diff --git a/awx/ui/client/src/job-results/host-status-bar/host-status-bar.directive.js b/awx/ui/client/src/job-results/host-status-bar/host-status-bar.directive.js deleted file mode 100644 index 5a8b5b3206..0000000000 --- a/awx/ui/client/src/job-results/host-status-bar/host-status-bar.directive.js +++ /dev/null @@ -1,47 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -// import hostStatusBarController from './host-status-bar.controller'; -export default [ 'templateUrl', - function(templateUrl) { - return { - scope: true, - templateUrl: templateUrl('job-results/host-status-bar/host-status-bar'), - restrict: 'E', - // controller: standardOutLogController, - link: function(scope) { - // as count is changed by event data coming in, - // update the host status bar - var toDestroy = scope.$watch('count', function(val) { - if (val) { - Object.keys(val).forEach(key => { - // reposition the hosts status bar by setting - // the various flex values to the count of - // those hosts - $(`.HostStatusBar-${key}`) - .css('flex', `${val[key]} 0 auto`); - - // set the tooltip to give how many hosts of - // each type - if (val[key] > 0) { - scope[`${key}CountTip`] = `${key}${val[key]}`; - } - }); - - // if there are any hosts that have finished, don't - // show default grey bar - scope.hasCount = (Object - .keys(val) - .filter(key => (val[key] > 0)).length > 0); - } - }); - - scope.$on('$destroy', function(){ - toDestroy(); - }); - } - }; -}]; diff --git a/awx/ui/client/src/job-results/host-status-bar/host-status-bar.partial.html b/awx/ui/client/src/job-results/host-status-bar/host-status-bar.partial.html deleted file mode 100644 index a8854b6d09..0000000000 --- a/awx/ui/client/src/job-results/host-status-bar/host-status-bar.partial.html +++ /dev/null @@ -1,30 +0,0 @@ -
-
-
-
-
-
-
-
-
diff --git a/awx/ui/client/src/job-results/host-status-bar/main.js b/awx/ui/client/src/job-results/host-status-bar/main.js deleted file mode 100644 index 2b17a2e414..0000000000 --- a/awx/ui/client/src/job-results/host-status-bar/main.js +++ /dev/null @@ -1,11 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import hostStatusBar from './host-status-bar.directive'; - -export default - angular.module('hostStatusBarDirective', []) - .directive('hostStatusBar', hostStatusBar); diff --git a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.block.less b/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.block.less deleted file mode 100644 index d186677cd1..0000000000 --- a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.block.less +++ /dev/null @@ -1,271 +0,0 @@ -@breakpoint-md: 1200px; - -.JobResultsStdOut { - height: auto; - width: 100%; - display: flex; - flex-direction: column; - align-items: stretch; -} - -.JobResultsStdOut-toolbar { - flex: initial; - display: flex; - border-bottom: 0px; - border-radius: 5px; - border-bottom-left-radius: 0px; - border-bottom-right-radius: 0px; - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; -} - -.JobResultsStdOut-toolbarNumberColumn { - background-color: @default-list-header-bg; - color: @b7grey; - flex: initial; - display: flex; - justify-content: space-between; - width: 70px; - padding-bottom: 10px; - padding-left: 8px; - padding-right: 8px; - padding-top: 10px; - border-top-left-radius: 5px; - z-index: 1; - border-right: 1px solid @d7grey; -} - -.JobResultsStdOut-expandAllButton { - height: 18px; - width: 18px; - padding-left: 4px; - padding-top: 1px; - border-radius: 50%; - background-color: @default-bg; - font-size: 12px; - cursor: pointer; - color: #848992; -} - -.JobResultsStdOut-expandAllButton:hover .JobResultsStdOut-expandAllIcon, -.JobResultsStdOut-expandAllIcon:hover { - color: @default-data-txt; -} - -.JobResultsStdOut-toolbarStdoutColumn { - white-space: normal; - flex: 1; - display: flex; - justify-content: flex-end; - padding-right: 10px; - background-color: @default-secondary-bg; - border-top-right-radius: 5px; -} - -.JobResultsStdOut-followButton { - cursor: pointer; - width: 18px; - height: 18px; - width: 18px; - padding: 1px 0 0 4px; - border-radius: 50%; - margin-top: 10px; - font-size: 11px; - background-color: @default-icon; - color: @default-bg; -} - -.JobResultsStdOut-followIcon { - color: @default-bg; -} - -.JobResultsStdOut-followButton:hover { - background-color: @default-data-txt; -} - -.JobResultsStdOut-followButton.is-engaged { - background-color: @default-link; - color: @default-bg; -} - -.JobResultsStdOut-followButton.is-engaged .JobResultsStdOut-followIcon { - color: @default-bg; -} - -.JobResultsStdOut-followButton.is-engaged:hover { - background-color: @default-icon; -} - -.JobResultsStdOut-followButton.is-engaged:hover .JobResultsStdOut-followIcon, -.JobResultsStdOut-followButton.is-engaged .JobResultsStdOut-followIcon:hover { - color: @default-border; -} - -.JobResultsStdOut-stdoutContainer { - flex: 1; - position: relative; - background-color: @default-secondary-bg; - overflow-y: scroll; - overflow-x: hidden; -} - -.JobResultsStdOut-numberColumnPreload { - background-color: @default-list-header-bg; - border-right: 1px solid @d7grey; - position: absolute; - height: 100%; - width: 70px; -} - -.JobResultsStdOut-aLineOfStdOut { - display: flex; - font-family: Monaco, Menlo, Consolas, "Courier New", monospace; -} - -.JobResultsStdOut-lineExpander { - text-align: left; - padding-left: 11px; - margin-right: auto; -} - -.JobResultsStdOut-lineExpanderIcon { - font-size: 19px; - cursor: pointer; -} - -.JobResultsStdOut-lineExpanderIcon:hover { - color: @default-data-txt; -} - -.JobResultsStdOut-lineNumberColumn { - display: flex; - background-color: @default-list-header-bg; - text-align: right; - padding-right: 10px; - padding-top: 2px; - padding-bottom: 2px; - width: 75px; - flex: 1 0 70px; - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - z-index: 1; - border-right: 1px solid @d7grey; - color: @default-icon; -} - -.JobResultsStdOut-stdoutColumn { - padding-left: 20px; - padding-right: 20px; - padding-top: 2px; - padding-bottom: 2px; - color: @default-interface-txt; - display: inline-block; - white-space: pre-wrap; - word-break: break-all; - width:100%; - background-color: @default-secondary-bg; -} - -.JobResultsStdOut-stdoutColumn--tooMany { - font-weight: bold; - text-transform: uppercase; - color: @default-err; -} - -.JobResultsStdOut-stdoutColumn--clickable { - cursor: pointer; -} - -.JobResultsStdOut-aLineOfStdOut:hover, -.JobResultsStdOut-aLineOfStdOut:hover .JobResultsStdOut-lineNumberColumn, -.JobResultsStdOut-aLineOfStdOut:hover .JobResultsStdOut-stdoutColumn { - background-color: @default-bg; -} - -.JobResultsStdOut-aLineOfStdOut:hover .JobResultsStdOut-lineNumberColumn { - border-right: 1px solid @default-bg; -} - -.JobResultsStdOut-footer { - height: 20px; - border-bottom-right-radius: 5px; - border-bottom-left-radius: 5px; - background-color: @default-secondary-bg; - border-top: 0px; - border-radius: 5px; - border-top-left-radius: 0px; - border-top-right-radius: 0px; - overflow: hidden; - margin-top: -1px; -} - -.JobResultsStdOut-footerNumberColumn { - background-color: @default-list-header-bg; - width: 70px; - height: 100%; - border-right: 1px solid @d7grey; -} - -.JobResultsStdOut-followAnchor { - height: 0px; -} - -.JobResultsStdOut-toTop { - color: @default-icon; - cursor: pointer; - font-family: monaco; - font-size: 10px; - margin-right: 20px; - text-align: right; - display: flex; - - span { - margin-left: auto; - } -} - -.JobResultsStdOut-toTop--numberColumn { - background: @default-list-header-bg; - height: 40px; - width: 70px; - border-right: 1px solid #D7D7D7; -} - -.JobResultsStdOut-toTop:hover { - color: @default-data-txt; -} - -.JobResultsStdOut-cappedLine { - color: @b7grey; - font-style: italic; -} - -@media (max-width: @breakpoint-md) { - .JobResultsStdOut-numberColumnPreload { - display: none; - } - - .JobResultsStdOut-topAnchor { - position: static; - width: 100%; - top: -20px; - margin-top: -250px; - margin-bottom: 250px; - } - - .JobResultsStdOut-followAnchor { - height: 0px; - } - - .JobResultsStdOut-stdoutContainer { - overflow-y: auto; - } - - .JobResultsStdOut-lineAnchor { - display: none !important; - } -} diff --git a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js b/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js deleted file mode 100644 index 14a34a607a..0000000000 --- a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js +++ /dev/null @@ -1,415 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -// import hostStatusBarController from './host-status-bar.controller'; -export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll', - function(templateUrl, $timeout, $location, $anchorScroll) { - return { - scope: false, - templateUrl: templateUrl('job-results/job-results-stdout/job-results-stdout'), - restrict: 'E', - link: function(scope, element) { - var toDestroy = [], - resizer, - scrollWatcher; - - scope.$on('$destroy', function(){ - $(window).off("resize", resizer); - $(window).off("scroll", scrollWatcher); - $(".JobResultsStdOut-stdoutContainer").off('scroll', - scrollWatcher); - toDestroy.forEach(closureFunc => closureFunc()); - }); - - scope.stdoutContainerAvailable.resolve("container available"); - // utility function used to find the top visible line and - // parent header in the pane - // - // note that while this function is called when in mobile width - // the line anchor is not displayed in the css so calls - // to lineAnchor do nothing - var findTopLines = function() { - var $container = $('.JobResultsStdOut-stdoutContainer'); - - // get the first visible line's head element - var getHeadElement = function (line) { - var lineHasHeaderClass = !!(line - .hasClass("header_play") || - line.hasClass("header_task")); - var lineClassList; - var lineUUIDClass; - - if (lineHasHeaderClass) { - // find head element when the first visible - // line is a header - - lineClassList = line.attr("class") - .split(" "); - - // get the header class w task uuid... - lineUUIDClass = lineClassList - .filter(n => n - .indexOf("header_task_") > -1)[0]; - - // ...if that doesn't exist get the one - // w play uuid - if (!lineUUIDClass) { - lineUUIDClass = lineClassList - .filter(n => n. - indexOf("header_play_") > -1)[0]; - } - - // get the header line (not their might - // be more than one, so get the one with - // the actual header class) - // - // TODO it might be better in this case to just - // return `line` (less jumping with a cowsay - // case) - return $(".actual_header." + - lineUUIDClass); - } else { - // find head element when the first visible - // line is not a header - - lineClassList = line.attr("class") - .split(" "); - - // get the class w task uuid... - lineUUIDClass = lineClassList - .filter(n => n - .indexOf("task_") > -1)[0]; - - // ...if that doesn't exist get the one - // w play uuid - if (!lineUUIDClass) { - lineUUIDClass = lineClassList - .filter(n => n - .indexOf("play_") > -1)[0]; - } - - // get the header line (not their might - // be more than one, so get the one with - // the actual header class) - return $(".actual_header.header_" + - lineUUIDClass); - } - }; - - var visItem, - parentItem; - - // iterate through each line of standard out - $container.find('.JobResultsStdOut-aLineOfStdOut:visible') - .each( function () { - var $this = $(this); - - // check to see if the line is the first visible - // line in the viewport... - if ($this.position().top >= 0) { - - // ...if it is, return the line number - // for this line - visItem = parseInt($($this - .children()[0]) - .text()); - - // as well as the line number for it's - // closest parent header line - var $head = getHeadElement($this); - parentItem = parseInt($($head - .children()[0]) - .text()); - - // stop iterating over the standard out - // lines once the first one has been - // found - - $this = null; - return false; - } - - $this = null; - }); - - $container = null; - - return { - visLine: visItem, - parentVisLine: parentItem - }; - }; - - // find if window is initially mobile or desktop width - if (window.innerWidth <= 1200) { - scope.isMobile = true; - } else { - scope.isMobile = false; - } - - resizer = function() { - // and update the isMobile var accordingly - if (window.innerWidth <= 1200 && !scope.isMobile) { - scope.isMobile = true; - } else if (window.innerWidth > 1200 & scope.isMobile) { - scope.isMobile = false; - } - }; - // watch changes to the window size - $(window).resize(resizer); - - var lastScrollTop; - - var initScrollTop = function() { - lastScrollTop = 0; - }; - scrollWatcher = function() { - var st = $(this).scrollTop(); - var netScroll = st + $(this).innerHeight(); - var fullHeight; - - if (st < lastScrollTop){ - // user up scrolled, so disengage follow - scope.followEngaged = false; - } - - if (scope.isMobile) { - // for mobile the height is the body of the entire - // page - fullHeight = $("body").height(); - } else { - // for desktop the height is the body of the - // stdoutContainer, minus the "^ TOP" indicator - fullHeight = $(this)[0].scrollHeight - 25; - } - - if(netScroll >= fullHeight) { - // user scrolled all the way to bottom, so engage - // follow - scope.followEngaged = true; - } - - // pane is now overflowed, show top indicator. - if (st > 0) { - scope.stdoutOverflowed = true; - } - - lastScrollTop = st; - - st = null; - netScroll = null; - fullHeight = null; - }; - - // update scroll watchers when isMobile changes based on - // window resize - toDestroy.push(scope.$watch('isMobile', function(val) { - if (val === true) { - // make sure ^ TOP always shown for mobile - scope.stdoutOverflowed = true; - - // unbind scroll watcher on standard out container - $(".JobResultsStdOut-stdoutContainer") - .unbind('scroll'); - - // init scroll watcher on window - initScrollTop(); - $(window).on('scroll', scrollWatcher); - - } else if (val === false) { - // unbind scroll watcher on window - $(window).unbind('scroll'); - - // init scroll watcher on standard out container - initScrollTop(); - $(".JobResultsStdOut-stdoutContainer").on('scroll', - scrollWatcher); - } - })); - - // called to scroll to follow anchor - scope.followScroll = function() { - // a double-check to make sure the follow anchor is at - // the bottom of the standard out container - $(".JobResultsStdOut-followAnchor") - .appendTo(".JobResultsStdOut-stdoutContainer"); - - $location.hash('followAnchor'); - $anchorScroll(); - }; - - // called to scroll to top of standard out (when "^ TOP" is - // clicked) - scope.toTop = function() { - $location.hash('topAnchor'); - $anchorScroll(); - }; - - // called to scroll to the current line anchor - // when expand all/collapse all/filtering is engaged - // - // note that while this function can be called when in mobile - // width the line anchor is not displayed in the css so those - // calls do nothing - scope.lineAnchor = function() { - $location.hash('lineAnchor'); - $anchorScroll(); - }; - - // if following becomes active, go ahead and get to the bottom - // of the standard out pane - toDestroy.push(scope.$watch('followEngaged', function(val) { - // scroll to follow point if followEngaged is true - if (val) { - scope.followScroll(); - } - - // set up tooltip changes for not finsihed job - if (!scope.jobFinished) { - if (val) { - scope.followTooltip = "Currently following standard out as it comes in. Click to unfollow."; - } else { - scope.followTooltip = "Click to follow standard out as it comes in."; - } - } - })); - - // follow button ng-click function - scope.followToggleClicked = function() { - if (scope.jobFinished) { - // when the job is finished engage followScroll - scope.followScroll(); - } else { - // when the job is not finished toggle followEngaged - // which is watched above - scope.followEngaged = !scope.followEngaged; - } - }; - - // expand all/collapse all ng-click function - scope.toggleAllStdout = function(type) { - // find the top visible line in the container currently, - // as well as the header parent of that line - var topLines = findTopLines(); - - if (type === 'expand') { - // for expand prepend the lineAnchor to the visible - // line - $(".line_num_" + topLines.visLine) - .prepend($("#lineAnchor")); - } else { - // for collapse prepent the lineAnchor to the - // visible line's parent header - $(".line_num_" + topLines.parentVisLine) - .prepend($("#lineAnchor")); - } - - var expandClass; - if (type === 'expand') { - // for expand all, you'll need to find all the - // collapsed headers to expand them - expandClass = "fa-caret-right"; - } else { - // for collapse all, you'll need to find all the - // expanded headers to collapse them - expandClass = "fa-caret-down"; - } - - // find all applicable task headers that need to be - // toggled - element.find(".expanderizer--task."+expandClass) - .each((i, val) => { - // and trigger their expansion/collapsing - $timeout(function(){ - // TODO change to a direct call of the - // toggleLine function - angular.element(val).trigger('click'); - // TODO only call lineAnchor for those - // that are above the first visible line - scope.lineAnchor(); - }); - }); - - // find all applicable play headers that need to be - // toggled - element.find(".expanderizer--play."+expandClass) - .each((i, val) => { - // for collapse all, only collapse play - // headers that do not have children task - // headers - if(angular.element("." + - angular.element(val).attr("data-uuid")) - .find(".expanderizer--task") - .length === 0 || - type !== 'collapse') { - - // trigger their expansion/ - // collapsing - $timeout(function(){ - // TODO change to a direct - // call of the - // toggleLine function - angular.element(val) - .trigger('click'); - // TODO only call lineAnchor - // for those that are above - // the first visible line - scope.lineAnchor(); - }); - } - }); - }; - - // expand/collapse triangle ng-click function - scope.toggleLine = function($event, id) { - // if the section is currently expanded - if ($($event.currentTarget).hasClass("fa-caret-down")) { - // hide all the children lines - $(id).hide(); - - // and change the triangle for the header to collapse - $($event.currentTarget) - .removeClass("fa-caret-down"); - $($event.currentTarget) - .addClass("fa-caret-right"); - } else { - // if the section is currently collapsed - - // show all the children lines - $(id).show(); - - // and change the triangle for the header to expanded - $($event.currentTarget) - .removeClass("fa-caret-right"); - $($event.currentTarget) - .addClass("fa-caret-down"); - - // if the section you expanded is a play - if ($($event.currentTarget) - .hasClass("expanderizer--play")) { - // find all children task headers and - // expand them too - $("." + $($event.currentTarget) - .attr("data-uuid")) - .find(".expanderizer--task") - .each((i, val) => { - if ($(val) - .hasClass("fa-caret-right")) { - $timeout(function(){ - // TODO change to a - // direct call of the - // toggleLine function - angular.element(val) - .trigger('click'); - }); - } - }); - } - } - }; - } - }; -}]; diff --git a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.partial.html b/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.partial.html deleted file mode 100644 index 63ae96d90c..0000000000 --- a/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.partial.html +++ /dev/null @@ -1,65 +0,0 @@ -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
-
-
-
-
-
-
-
- -
-
The standard output is too large to display. Please specify additional filters to narrow the standard out.
-
Too much previous output to display. Showing running standard output.
-
Job details are not available for this job. Please download to view standard out.
-
- -
-
-
-
- -
diff --git a/awx/ui/client/src/job-results/job-results-stdout/main.js b/awx/ui/client/src/job-results/job-results-stdout/main.js deleted file mode 100644 index 5fc583b9b1..0000000000 --- a/awx/ui/client/src/job-results/job-results-stdout/main.js +++ /dev/null @@ -1,11 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import jobResultsStdOut from './job-results-stdout.directive'; - -export default - angular.module('jobResultStdOutDirective', []) - .directive('jobResultsStandardOut', jobResultsStdOut); diff --git a/awx/ui/client/src/job-results/job-results.block.less b/awx/ui/client/src/job-results/job-results.block.less deleted file mode 100644 index 356aeb5a6a..0000000000 --- a/awx/ui/client/src/job-results/job-results.block.less +++ /dev/null @@ -1,248 +0,0 @@ -@breakpoint-md: 1200px; - -.JobResults { - .OnePlusTwo-container(100%, @breakpoint-md); - - &.fullscreen { - .JobResults-rightSide { - max-width: 100%; - } - } -} - -.JobResults-leftSide { - .OnePlusTwo-left--panel(100%, @breakpoint-md); - max-width: 30%; - height: ~"calc(100vh - 177px)"; - - @media screen and (max-width: @breakpoint-md) { - max-width: 100%; - } -} - -.JobResults-rightSide { - .OnePlusTwo-right--panel(100%, @breakpoint-md); - height: ~"calc(100vh - 177px)"; - - @media (max-width: @breakpoint-md - 1px) { - padding-right: 15px; - } -} - -.JobResults-detailsPanel{ - overflow-y: scroll; -} - -.JobResults-stdoutActionButton--active { - display: none; - visibility: hidden; - flex:none; - width:0px; - padding-right: 0px; -} - -.JobResults-panelHeader { - display: flex; - height: 30px; -} - -.JobResults-panelHeaderText { - color: @default-interface-txt; - flex: 1 0 auto; - font-size: 14px; - font-weight: bold; - margin-right: 10px; - text-transform: uppercase; -} - -.JobResults-panelHeaderButtonActions { - display: flex; -} - -.JobResults-resultRow { - width: 100%; - display: flex; - padding-bottom: 10px; - flex-wrap: wrap; -} - -.JobResults-resultRow--variables { - flex-direction: column; - - #cm-variables-container { - width: 100%; - } -} - -.JobResults-resultRowLabel { - text-transform: uppercase; - color: @default-interface-txt; - font-size: 12px; - font-weight: normal!important; - width: 30%; - margin-right: 20px; - - @media screen and (max-width: @breakpoint-md) { - flex: 2.5 0 auto; - } -} - -.JobResults-resultRowLabel--fullWidth { - width: 100%; - margin-right: 0px; -} - -.JobResults-resultRowText { - width: ~"calc(70% - 20px)"; - flex: 1 0 auto; - text-transform: none; - word-wrap: break-word; -} - -.JobResults-resultRowText--fullWidth { - width: 100%; -} - -.JobResults-expandArrow { - color: #D7D7D7; - font-size: 14px; - font-weight: bold; - margin-right: 10px; - text-transform: uppercase; - margin-left: 10px; -} - -.JobResults-resultRowText--instanceGroup { - display: flex; -} - -.JobResults-isolatedBadge { - align-items: center; - background-color: @default-list-header-bg; - border-radius: 5px; - color: @default-stdout-txt; - display: flex; - font-size: 10px; - height: 16px; - margin: 3px 0 0 10px; - padding: 0 10px; - text-transform: uppercase; -} - -.JobResults-statusResultIcon { - padding-left: 0px; - padding-right: 10px; -} - -.JobResults-badgeRow { - display: flex; - align-items: center; - margin-right: 5px; -} - -.JobResults-badgeTitle{ - color: @default-interface-txt; - font-size: 14px; - margin-right: 10px; - font-weight: normal; - text-transform: uppercase; - margin-left: 20px; -} - -@media (max-width: @breakpoint-md) { - .JobResults-detailsPanel { - overflow-y: auto; - } - - .JobResults-rightSide { - height: inherit; - } -} - -.JobResults-timeBadge { - float:right; - font-size: 11px; - font-weight: normal; - padding: 1px 10px; - height: 14px; - margin: 3px 15px; - width: 80px; - background-color: @default-bg; - border-radius: 5px; - color: @default-interface-txt; - margin-right: -5px; -} - -.JobResults-panelRight { - display: flex; - flex-direction: column; -} - -.JobResults-panelRight .SmartSearch-bar { - width: 100%; -} - -.JobResults-panelRightTitle{ - flex-wrap: wrap; -} - -.JobResults-panelRightTitleText{ - word-wrap: break-word; - word-break: break-all; - max-width: 100%; -} - -.JobResults-badgeAndActionRow{ - display:flex; - flex: 1 0 auto; - justify-content: flex-end; - flex-wrap: wrap; - max-width: 100%; -} - -.StandardOut-panelHeader { - flex: initial; -} - -.StandardOut-panelHeader--jobIsRunning { - margin-bottom: 20px; -} - -host-status-bar { - flex: initial; - margin-bottom: 20px; -} - -smart-search { - flex: initial; -} - -job-results-standard-out { - flex: 1; - flex-basis: auto; - height: ~"calc(100% - 800px)"; - display: flex; - border: 1px solid @d7grey; - border-radius: 5px; - margin-top: 20px; -} -@media screen and (max-width: @breakpoint-md) { - job-results-standard-out { - height: auto; - } -} - -.JobResults-extraVarsHelp { - margin-left: 10px; - color: @default-icon; -} - -.JobResults-seeMoreLess { - color: #337AB7; - margin: 4px 0px; - text-transform: uppercase; - padding: 2px 0px; - cursor: pointer; - border-radius: 5px; - font-size: 11px; -} diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js deleted file mode 100644 index e7e8f2c716..0000000000 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ /dev/null @@ -1,784 +0,0 @@ -export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count', '$scope', 'ParseTypeChange', - 'ParseVariableString', 'jobResultsService', 'eventQueue', '$compile', '$log', 'Dataset', '$q', - 'QuerySet', '$rootScope', 'moment', '$stateParams', 'i18n', 'fieldChoices', 'fieldLabels', - 'workflowResultsService', 'statusSocket', 'GetBasePath', '$state', 'jobExtraCredentials', -function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTypeChange, - ParseVariableString, jobResultsService, eventQueue, $compile, $log, Dataset, $q, - QuerySet, $rootScope, moment, $stateParams, i18n, fieldChoices, fieldLabels, - workflowResultsService, statusSocket, GetBasePath, $state, jobExtraCredentials) { - - var toDestroy = []; - var cancelRequests = false; - var runTimeElapsedTimer = null; - - // download stdout tooltip text - $scope.standardOutTooltip = i18n._('Download Output'); - - // stdout full screen toggle tooltip text - $scope.toggleStdoutFullscreenTooltip = i18n._("Expand Output"); - - // this allows you to manage the timing of rest-call based events as - // filters are updated. see processPage for more info - var currentContext = 1; - $scope.firstCounterFromSocket = -1; - - $scope.explanationLimit = 150; - - // if the user enters the page mid-run, reset the search to include a param - // to only grab events less than the first counter from the websocket events - toDestroy.push($scope.$watch('firstCounterFromSocket', function(counter) { - if (counter > -1) { - // make it so that the search include a counter less than the - // first counter from the socket - let params = _.cloneDeep($stateParams.job_event_search); - params.counter__lte = "" + counter; - - Dataset = QuerySet.search(jobData.related.job_events, - params); - - Dataset.then(function(actualDataset) { - $scope.job_event_dataset = actualDataset.data; - }); - } - })); - - // used for tag search - $scope.job_event_dataset = Dataset.data; - - // used for tag search - $scope.list = { - basePath: jobData.related.job_events, - name: 'job_events' - }; - - // used for tag search - $scope.job_events = $scope.job_event_dataset.results; - - $scope.jobExtraCredentials = jobExtraCredentials; - - var getLinks = function() { - var getLink = function(key) { - if(key === 'schedule') { - if($scope.job.related.schedule) { - return '/#/templates/job_template/' + $scope.job.job_template + '/schedules' + $scope.job.related.schedule.split(/api\/v\d+\/schedules/)[1]; - } - else { - return null; - } - } - else if(key === 'inventory') { - if($scope.job.summary_fields.inventory && $scope.job.summary_fields.inventory.id) { - if($scope.job.summary_fields.inventory.kind && $scope.job.summary_fields.inventory.kind === 'smart') { - return '/#/inventories/smart/' + $scope.job.summary_fields.inventory.id; - } - else { - return '/#/inventories/inventory/' + $scope.job.summary_fields.inventory.id; - } - } - else { - return null; - } - } - else { - if ($scope.job.related[key]) { - return '/#/' + $scope.job.related[key] - .split(/api\/v\d+\//)[1]; - } else { - return null; - } - } - }; - - $scope.created_by_link = getLink('created_by'); - $scope.scheduled_by_link = getLink('schedule'); - $scope.inventory_link = getLink('inventory'); - $scope.project_link = getLink('project'); - $scope.machine_credential_link = getLink('credential'); - $scope.cloud_credential_link = getLink('cloud_credential'); - $scope.network_credential_link = getLink('network_credential'); - $scope.vault_credential_link = getLink('vault_credential'); - $scope.schedule_link = getLink('schedule'); - }; - - // uses options to set scope variables to their readable string - // value - var getLabels = function() { - var getLabel = function(key) { - if ($scope.jobOptions && $scope.jobOptions[key]) { - return $scope.jobOptions[key].choices - .filter(val => val[0] === $scope.job[key]) - .map(val => val[1])[0]; - } else { - return null; - } - }; - - $scope.type_label = getLabel('job_type'); - $scope.verbosity_label = getLabel('verbosity'); - }; - - var getTotalHostCount = function(count) { - return Object - .keys(count).reduce((acc, i) => acc += count[i], 0); - }; - - // put initially resolved request data on scope - $scope.job = jobData; - $scope.jobOptions = jobDataOptions.actions.GET; - $scope.labels = jobLabels; - $scope.jobFinished = jobFinished; - - // update label in left pane and tooltip in right pane when the job_status - // changes - toDestroy.push($scope.$watch('job_status', function(status) { - if (status) { - $scope.status_label = $scope.jobOptions.status.choices - .filter(val => val[0] === status) - .map(val => val[1])[0]; - $scope.status_tooltip = "Job " + $scope.status_label; - } - })); - - $scope.previousTaskFailed = false; - - toDestroy.push($scope.$watch('job.job_explanation', function(explanation) { - if (explanation && explanation.split(":")[0] === "Previous Task Failed") { - $scope.previousTaskFailed = true; - - var taskObj = JSON.parse(explanation.substring(explanation.split(":")[0].length + 1)); - // return a promise from the options request with the permission type choices (including adhoc) as a param - var fieldChoice = fieldChoices({ - $scope: $scope, - url: GetBasePath('unified_jobs'), - field: 'type' - }); - - // manipulate the choices from the options request to be set on - // scope and be usable by the list form - fieldChoice.then(function (choices) { - choices = - fieldLabels({ - choices: choices - }); - $scope.explanation_fail_type = choices[taskObj.job_type]; - $scope.explanation_fail_name = taskObj.job_name; - $scope.explanation_fail_id = taskObj.job_id; - $scope.task_detail = $scope.explanation_fail_type + " failed for " + $scope.explanation_fail_name + " with ID " + $scope.explanation_fail_id + "."; - }); - } else { - $scope.previousTaskFailed = false; - } - })); - - - // update the job_status value. Use the cached rootScope value if there - // is one. This is a workaround when the rest call for the jobData is - // made before some socket events come in for the job status - if ($rootScope['lastSocketStatus' + jobData.id]) { - $scope.job_status = $rootScope['lastSocketStatus' + jobData.id]; - delete $rootScope['lastSocketStatus' + jobData.id]; - } else { - $scope.job_status = jobData.status; - } - - // turn related api browser routes into front end routes - getLinks(); - - // the links below can't be set in getLinks because the - // links on the UI don't directly match the corresponding URL - // on the API browser - if(jobData.summary_fields && jobData.summary_fields.job_template && - jobData.summary_fields.job_template.id){ - $scope.job_template_link = `/#/templates/job_template/${$scope.job.summary_fields.job_template.id}`; - } - if(jobData.summary_fields && jobData.summary_fields.project_update && - jobData.summary_fields.project_update.status){ - $scope.project_status = jobData.summary_fields.project_update.status; - } - if(jobData.summary_fields && jobData.summary_fields.project_update && - jobData.summary_fields.project_update.id){ - $scope.project_update_link = `/#/scm_update/${jobData.summary_fields.project_update.id}`; - } - if(jobData.summary_fields && jobData.summary_fields.source_workflow_job && - jobData.summary_fields.source_workflow_job.id){ - $scope.workflow_result_link = `/#/workflows/${jobData.summary_fields.source_workflow_job.id}`; - } - if(jobData.result_traceback) { - $scope.job.result_traceback = jobData.result_traceback.trim().split('\n').join('
'); - } - - // use options labels to manipulate display of details - getLabels(); - - // set up a read only code mirror for extra vars - $scope.variables = ParseVariableString($scope.job.extra_vars); - $scope.parseType = 'yaml'; - ParseTypeChange({ scope: $scope, - field_id: 'pre-formatted-variables', - readOnly: true }); - - // Click binding for the expand/collapse button on the standard out log - $scope.stdoutFullScreen = false; - $scope.toggleStdoutFullscreen = function() { - $scope.stdoutFullScreen = !$scope.stdoutFullScreen; - - if ($scope.stdoutFullScreen === true) { - $scope.toggleStdoutFullscreenTooltip = i18n._("Collapse Output"); - } else if ($scope.stdoutFullScreen === false) { - $scope.toggleStdoutFullscreenTooltip = i18n._("Expand Output"); - } - }; - - $scope.deleteJob = function() { - jobResultsService.deleteJob($scope.job); - }; - - $scope.cancelJob = function() { - jobResultsService.cancelJob($scope.job); - }; - - $scope.lessLabels = false; - $scope.toggleLessLabels = function() { - if (!$scope.lessLabels) { - $('#job-results-labels').slideUp(200); - $scope.lessLabels = true; - } - else { - $('#job-results-labels').slideDown(200); - $scope.lessLabels = false; - } - }; - - // get initial count from resolve - $scope.count = count.val; - $scope.hostCount = getTotalHostCount(count.val); - $scope.countFinished = count.countFinished; - - // if the job is still running engage following of the last line in the - // standard out pane - $scope.followEngaged = !$scope.jobFinished; - - // follow button for completed job should specify that the - // button will jump to the bottom of the standard out pane, - // not follow lines as they come in - if ($scope.jobFinished) { - $scope.followTooltip = i18n._("Jump to last line of standard out."); - } else { - $scope.followTooltip = i18n._("Currently following standard out as it comes in. Click to unfollow."); - } - - $scope.events = {}; - - function updateJobElapsedTimer(time) { - $scope.job.elapsed = time; - } - - // For elapsed time while a job is running, compute the differnce in seconds, - // from the time the job started until now. Moment() returns the current - // time as a moment object. - if ($scope.job.started !== null && $scope.job.status === 'running') { - runTimeElapsedTimer = workflowResultsService.createOneSecondTimer($scope.job.started, updateJobElapsedTimer); - } - - // EVENT STUFF BELOW - var linesInPane = []; - - function addToLinesInPane(event) { - var arr = _.range(event.start_line, event.actual_end_line); - linesInPane = linesInPane.concat(arr); - linesInPane = linesInPane.sort(function(a, b) { - return a - b; - }); - } - - function appendToBottom (event){ - // if we get here then the event type was either a - // header line, recap line, or one of the additional - // event types, so we append it to the bottom. - // These are the event types for captured - // stdout not directly related to playbook or runner - // events: - // (0, 'debug', _('Debug'), False), - // (0, 'verbose', _('Verbose'), False), - // (0, 'deprecated', _('Deprecated'), False), - // (0, 'warning', _('Warning'), False), - // (0, 'system_warning', _('System Warning'), False), - // (0, 'error', _('Error'), True), - angular - .element(".JobResultsStdOut-stdoutContainer") - .append($compile(event - .stdout)($scope.events[event - .counter])); - } - - function putInCorrectPlace(event) { - if (linesInPane.length) { - for (var i = linesInPane.length - 1; i >= 0; i--) { - if (event.start_line > linesInPane[i]) { - $(`.line_num_${linesInPane[i]}`) - .after($compile(event - .stdout)($scope.events[event - .counter])); - i = -1; - } - } - } else { - appendToBottom(event); - } - } - - // This is where the async updates to the UI actually happen. - // Flow is event queue munging in the service -> $scope setting in here - var processEvent = function(event, context) { - // only care about filter context checking when the event comes - // as a rest call - if (context && context !== currentContext) { - return; - } - // put the event in the queue - var mungedEvent = eventQueue.populate(event); - - // make changes to ui based on the event returned from the queue - if (mungedEvent.changes) { - mungedEvent.changes.forEach(change => { - // we've got a change we need to make to the UI! - // update the necessary scope and make the change - if (change === 'startTime' && !$scope.job.start) { - $scope.job.start = mungedEvent.startTime; - } - - if (change === 'count' && !$scope.countFinished) { - // for all events that affect the host count, - // update the status bar as well as the host - // count badge - $scope.count = mungedEvent.count; - $scope.hostCount = getTotalHostCount(mungedEvent - .count); - } - - if (change === 'finishedTime' && !$scope.job.finished) { - $scope.job.finished = mungedEvent.finishedTime; - $scope.jobFinished = true; - $scope.followTooltip = i18n._("Jump to last line of standard out."); - if ($scope.followEngaged) { - if (!$scope.followScroll) { - $scope.followScroll = function() { - $log.error("follow scroll undefined, standard out directive not loaded yet?"); - }; - } - $scope.followScroll(); - } - } - - if (change === 'countFinished') { - // the playbook_on_stats event actually lets - // us know that we don't need to iteratively - // look at event to update the host counts - // any more. - $scope.countFinished = true; - } - - if(change === 'stdout'){ - if (!$scope.events[mungedEvent.counter]) { - // line hasn't been put in the pane yet - - // create new child scope - $scope.events[mungedEvent.counter] = $scope.$new(); - $scope.events[mungedEvent.counter] - .event = mungedEvent; - - // let's see if we have a specific place to put it in - // the pane - let $prevElem = $(`.next_is_${mungedEvent.start_line}`); - if ($prevElem && $prevElem.length) { - // if so, put it there - $(`.next_is_${mungedEvent.start_line}`) - .after($compile(mungedEvent - .stdout)($scope.events[mungedEvent - .counter])); - addToLinesInPane(mungedEvent); - } else { - var putIn; - var classList = $("div", - "
"+mungedEvent.stdout+"
") - .attr("class").split(" "); - if (classList - .filter(v => v.indexOf("task_") > -1) - .length) { - putIn = classList - .filter(v => v.indexOf("task_") > -1)[0]; - } else if(classList - .filter(v => v.indexOf("play_") > -1) - .length) { - putIn = classList - .filter(v => v.indexOf("play_") > -1)[0]; - } - - var putAfter; - var isDup = false; - - if ($(".header_" + putIn + ",." + putIn).length === 0) { - putInCorrectPlace(mungedEvent); - addToLinesInPane(mungedEvent); - } else { - $(".header_" + putIn + ",." + putIn) - .each((i, v) => { - if (angular.element(v).scope() - .event.start_line < mungedEvent - .start_line) { - putAfter = v; - } else if (angular.element(v).scope() - .event.start_line === mungedEvent - .start_line) { - isDup = true; - return false; - } else if (angular.element(v).scope() - .event.start_line > mungedEvent - .start_line) { - return false; - } else { - appendToBottom(mungedEvent); - addToLinesInPane(mungedEvent); - } - }); - } - - if (!isDup && putAfter) { - addToLinesInPane(mungedEvent); - $(putAfter).after($compile(mungedEvent - .stdout)($scope.events[mungedEvent - .counter])); - } - - - classList = null; - putIn = null; - } - - // delete ref to the elem because it might leak scope - // if you don't - $prevElem = null; - } - - // move the followAnchor to the bottom of the - // container - $(".JobResultsStdOut-followAnchor") - .appendTo(".JobResultsStdOut-stdoutContainer"); - } - }); - - // the changes have been processed in the ui, mark it in the - // queue - eventQueue.markProcessed(event); - } - }; - - $scope.stdoutContainerAvailable = $q.defer(); - $scope.hasSkeleton = $q.defer(); - - eventQueue.initialize(); - - $scope.playCount = 0; - $scope.taskCount = 0; - - - // used to show a message to just download for old jobs - // remove in 3.2.0 - $scope.isOld = 0; - $scope.showLegacyJobErrorMessage = false; - - toDestroy.push($scope.$watch('isOld', function (val) { - if (val >= 2) { - $scope.showLegacyJobErrorMessage = true; - } - })); - - // get header and recap lines - var skeletonPlayCount = 0; - var skeletonTaskCount = 0; - var getSkeleton = function(url) { - jobResultsService.getEvents(url) - .then(events => { - events.results.forEach(event => { - if (event.start_line === 0 && event.end_line === 0) { - $scope.isOld++; - } - // get the name in the same format as the data - // coming over the websocket - event.event_name = event.event; - delete event.event; - - // increment play and task count - if (event.event_name === "playbook_on_play_start") { - skeletonPlayCount++; - } else if (event.event_name === "playbook_on_task_start") { - skeletonTaskCount++; - } - - processEvent(event); - }); - if (events.next) { - getSkeleton(events.next); - } else { - // after the skeleton requests have completed, - // put the play and task count into the dom - $scope.playCount = skeletonPlayCount; - $scope.taskCount = skeletonTaskCount; - $scope.hasSkeleton.resolve("skeleton resolved"); - } - }); - }; - - $scope.stdoutContainerAvailable.promise.then(() => { - getSkeleton(jobData.related.job_events + "?order_by=start_line&or__event__in=playbook_on_start,playbook_on_play_start,playbook_on_task_start,playbook_on_stats"); - }); - - var getEvents; - - var processPage = function(events, context) { - // currentContext is the context of the filter when this request - // to processPage was made - // - // currentContext is the context of the filter currently - // - // if they are not the same, make sure to stop process events/ - // making rest calls for next pages/etc. (you can see context is - // also passed into getEvents and processEvent and similar checks - // exist in these functions) - // - // also, if the page doesn't contain results (i.e.: the response - // returns an error), don't process the page - if (context !== currentContext || events === undefined || - events.results === undefined) { - return; - } - - events.results.forEach(event => { - // get the name in the same format as the data - // coming over the websocket - event.event_name = event.event; - delete event.event; - - processEvent(event, context); - }); - if (events.next && !cancelRequests) { - getEvents(events.next, context); - } else { - // put those paused events into the pane - $scope.gotPreviouslyRanEvents.resolve(""); - } - }; - - // grab non-header recap lines - getEvents = function(url, context) { - if (context !== currentContext) { - return; - } - - jobResultsService.getEvents(url) - .then(events => { - processPage(events, context); - }); - }; - - // grab non-header recap lines - toDestroy.push($scope.$watch('job_event_dataset', function(val) { - if (val) { - eventQueue.initialize(); - - Object.keys($scope.events) - .forEach(v => { - // dont destroy scope events for skeleton lines - let name = $scope.events[v].event.name; - - if (!(name === "playbook_on_play_start" || - name === "playbook_on_task_start" || - name === "playbook_on_stats")) { - $scope.events[v].$destroy(); - $scope.events[v] = null; - delete $scope.events[v]; - } - }); - - // pause websocket events from coming in to the pane - $scope.gotPreviouslyRanEvents = $q.defer(); - currentContext += 1; - - let context = currentContext; - - $( ".JobResultsStdOut-aLineOfStdOut.not_skeleton" ).remove(); - $scope.hasSkeleton.promise.then(() => { - if (val.count > parseInt(val.maxEvents)) { - $(".header_task").hide(); - $(".header_play").hide(); - $scope.standardOutTooltip = '
' + - i18n._('The output is too large to display. Please download.') + - '
' + - '
' + - '' + - '' + - '' + - '' + - '
' + - '
'; - - if ($scope.job_status === "successful" || - $scope.job_status === "failed" || - $scope.job_status === "error" || - $scope.job_status === "canceled") { - $scope.tooManyEvents = true; - $scope.tooManyPastEvents = false; - } else { - $scope.tooManyPastEvents = true; - $scope.tooManyEvents = false; - $scope.gotPreviouslyRanEvents.resolve(""); - } - } else { - $(".header_task").show(); - $(".header_play").show(); - $scope.tooManyEvents = false; - $scope.tooManyPastEvents = false; - processPage(val, context); - } - }); - } - })); - - var buffer = []; - - var processBuffer = function() { - var follow = function() { - // if follow is engaged, - // scroll down to the followAnchor - if ($scope.followEngaged) { - if (!$scope.followScroll) { - $scope.followScroll = function() { - $log.error("follow scroll undefined, standard out directive not loaded yet?"); - }; - } - $scope.followScroll(); - } - }; - - for (let i = 0; i < 4; i++) { - processEvent(buffer[i]); - buffer.splice(i, 1); - } - - follow(); - }; - - var bufferInterval; - - // Processing of job_events messages from the websocket - toDestroy.push($scope.$on(`ws-job_events-${$scope.job.id}`, function(e, data) { - if (!bufferInterval) { - bufferInterval = setInterval(function(){ - processBuffer(); - }, 500); - } - - // use the lowest counter coming over the socket to retrigger pull data - // to only be for stuff lower than that id - // - // only do this for entering the jobs page mid-run (thus the - // data.counter is 1 conditional - if (data.counter === 1) { - $scope.firstCounterFromSocket = -2; - } - - if ($scope.firstCounterFromSocket !== -2 && - $scope.firstCounterFromSocket === -1 || - data.counter < $scope.firstCounterFromSocket) { - $scope.firstCounterFromSocket = data.counter; - } - - $q.all([$scope.gotPreviouslyRanEvents.promise, - $scope.hasSkeleton.promise]).then(() => { - // put the line in the - // standard out pane (and increment play and task - // count if applicable) - if (data.event_name === "playbook_on_play_start") { - $scope.playCount++; - } else if (data.event_name === "playbook_on_task_start") { - $scope.taskCount++; - } - buffer.push(data); - }); - })); - - // get previously set up socket messages from resolve - if (statusSocket && statusSocket[0] && statusSocket[0].job_status) { - $scope.job_status = statusSocket[0].job_status; - } - if ($scope.job_status === "running" && !$scope.job.elapsed) { - runTimeElapsedTimer = workflowResultsService.createOneSecondTimer(moment(), updateJobElapsedTimer); - } - - // Processing of job-status messages from the websocket - toDestroy.push($scope.$on(`ws-jobs`, function(e, data) { - if (parseInt(data.unified_job_id, 10) === - parseInt($scope.job.id,10)) { - // controller is defined, so set the job_status - $scope.job_status = data.status; - if(_.has(data, 'instance_group_name')){ - $scope.job.instance_group = true; - $scope.job.summary_fields.instance_group = { - "name": data.instance_group_name - }; - } - if (data.status === "running") { - if (!runTimeElapsedTimer) { - runTimeElapsedTimer = workflowResultsService.createOneSecondTimer(moment(), updateJobElapsedTimer); - } - } else if (data.status === "successful" || - data.status === "failed" || - data.status === "error" || - data.status === "canceled") { - workflowResultsService.destroyTimer(runTimeElapsedTimer); - - // When the fob is finished retrieve the job data to - // correct anything that was out of sync from the job run - jobResultsService.getJobData($scope.job.id).then(function(data){ - $scope.job = data; - $scope.jobFinished = true; - }); - } - } else if (parseInt(data.project_id, 10) === - parseInt($scope.job.project,10)) { - // this is a project status update message, so set the - // project status in the left pane - $scope.project_status = data.status; - $scope.project_update_link = `/#/scm_update/${data - .unified_job_id}`; - } else { - // controller was previously defined, but is not yet defined - // for this job. cache the socket status on root scope - $rootScope['lastSocketStatus' + data.unified_job_id] = data.status; - } - })); - - if (statusSocket && statusSocket[1]) { - statusSocket[1](); - } - - $scope.$on('$destroy', function(){ - if (statusSocket && statusSocket[1]) { - statusSocket[1](); - } - $( ".JobResultsStdOut-aLineOfStdOut" ).remove(); - cancelRequests = true; - eventQueue.initialize(); - Object.keys($scope.events) - .forEach(v => { - $scope.events[v].$destroy(); - $scope.events[v] = null; - }); - $scope.events = {}; - workflowResultsService.destroyTimer(runTimeElapsedTimer); - if (bufferInterval) { - clearInterval(bufferInterval); - } - toDestroy.forEach(closureFunc => closureFunc()); - }); -}]; diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html deleted file mode 100644 index 9d8e1a119b..0000000000 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ /dev/null @@ -1,566 +0,0 @@ -
-
-
- - -
-
- - -
-
- DETAILS -
- - -
- - - - - - - - - -
-
- - -
- - -
- -
- - {{ status_label | translate }} -
-
- - -
- -
- {{ job.job_explanation }} -
-
- {{task_detail | limitTo:explanationLimit}} - - ... - Show More - - Show Less -
-
- - -
- -
- {{ job.started | longDate }} -
-
- - -
- -
- {{ (job.finished | - longDate) || "Not Finished" }} -
-
- - -
- -
-
-
- - - - - - -
- -
- {{ type_label }} -
-
- - - - - - - - - - - - - - -
- - - -
- - -
- -
- {{ job.playbook }} -
-
- - -
- - -
- - -
- -
- - - {{ extraCredential.name }} - - {{$last ? '' : ', '}} - -
-
- - - - - - - - - - - -
- -
- {{ job.forks }} -
-
- - -
- -
- {{ job.limit }} -
-
- - -
- -
- {{ verbosity_label }} -
-
- - -
- -
- {{ job.summary_fields.instance_group.name }} - - Isolated - -
-
- - -
- -
- {{ job.job_tags }} -
-
- - -
- -
- {{ job.skip_tags }} -
-
- - -
- - -
- - -
- -
-
-
-
- {{ label }} -
-
-
-
-
- -
- -
-
- - -
-
- - -
-
- - - {{ job.name }} -
-
- -
- -
- Plays -
- - {{ playCount || 0}} - - - -
- Tasks -
- - {{ taskCount || 0}} - - - -
- Hosts -
- - {{ hostCount || 0}} - - - - - - - -
- Elapsed -
- - {{ job.elapsed * 1000 | duration: "hh:mm:ss" }} - -
- - -
- - - - - - - - - -
-
-
- - - - -
- -
-
-
diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js deleted file mode 100644 index 60c06de7cd..0000000000 --- a/awx/ui/client/src/job-results/job-results.route.js +++ /dev/null @@ -1,187 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import {templateUrl} from '../shared/template-url/template-url.factory'; - -const defaultParams = { - page_size: "200", - order_by: 'start_line', - not__event__in: 'playbook_on_start,playbook_on_play_start,playbook_on_task_start,playbook_on_stats' -}; - -export default { - name: 'jobResult', - url: '/jobs/{id: int}', - searchPrefix: 'job_event', - ncyBreadcrumb: { - parent: 'jobs', - label: '{{ job.id }} - {{ job.name }}' - }, - data: { - socket: { - "groups":{ - "jobs": ["status_changed", "summary"], - "job_events": [] - } - } - }, - params: { - job_event_search: { - value: defaultParams, - dynamic: true, - squash: '' - } - }, - resolve: { - statusSocket: ['$rootScope', '$stateParams', function($rootScope, $stateParams) { - var preScope = {}; - var eventOn = $rootScope.$on(`ws-jobs`, function(e, data) { - if (parseInt(data.unified_job_id, 10) === - parseInt($stateParams.id,10)) { - preScope.job_status = data.status; - } - }); - return [preScope, eventOn]; - }], - // the GET for the particular job - jobData: ['jobResultsService', '$stateParams', function(jobResultsService, $stateParams) { - return jobResultsService.getJobData($stateParams.id); - }], - Dataset: ['QuerySet', '$stateParams', 'jobData', - function(qs, $stateParams, jobData) { - let path = jobData.related.job_events; - return qs.search(path, $stateParams[`job_event_search`]); - } - ], - // used to signify if job is completed or still running - jobFinished: ['jobData', function(jobData) { - if (jobData.finished) { - return true; - } else { - return false; - } - }], - // after the GET for the job, this helps us keep the status bar from - // flashing as rest data comes in. If the job is finished and - // there's a playbook_on_stats event, go ahead and resolve the count - // so you don't get that flashing! - count: ['jobData', 'jobResultsService', 'Rest', '$q', '$stateParams', '$state', function(jobData, jobResultsService, Rest, $q, $stateParams, $state) { - var defer = $q.defer(); - if (jobData.finished) { - // if the job is finished, grab the playbook_on_stats - // role to get the final count - Rest.setUrl(jobData.related.job_events + - "?event=playbook_on_stats"); - Rest.get() - .then(({data}) => { - if(!data.results[0]){ - defer.resolve({val: { - ok: 0, - skipped: 0, - unreachable: 0, - failures: 0, - changed: 0 - }, countFinished: false}); - } - else { - defer.resolve({ - val: jobResultsService - .getCountsFromStatsEvent(data - .results[0].event_data), - countFinished: true}); - } - }) - .catch(() => { - defer.resolve({val: { - ok: 0, - skipped: 0, - unreachable: 0, - failures: 0, - changed: 0 - }, countFinished: false}); - }); - } else { - // make sure to not include any extra - // search params for a running job (because we can't filter - // incoming job events) - if (!_.isEqual($stateParams.job_event_search, defaultParams)) { - let params = _.cloneDeep($stateParams); - params.job_event_search = defaultParams; - $state.go('.', params, { reload: true }); - } - - // job isn't finished so just send an empty count and read - // from events - defer.resolve({val: { - ok: 0, - skipped: 0, - unreachable: 0, - failures: 0, - changed: 0 - }, countFinished: false}); - } - return defer.promise; - }], - // GET for the particular jobs labels to be displayed in the - // left-hand pane - jobLabels: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) { - var getNext = function(data, arr, resolve) { - Rest.setUrl(data.next); - Rest.get() - .then(({data}) => { - if (data.next) { - getNext(data, arr.concat(data.results), resolve); - } else { - resolve.resolve(arr.concat(data.results) - .map(val => val.name)); - } - }); - }; - - var seeMoreResolve = $q.defer(); - - Rest.setUrl(GetBasePath('jobs') + $stateParams.id + '/labels/'); - Rest.get() - .then(({data}) => { - if (data.next) { - getNext(data, data.results, seeMoreResolve); - } else { - seeMoreResolve.resolve(data.results - .map(val => val.name)); - } - }); - - return seeMoreResolve.promise; - }], - // OPTIONS request for the job. Used to make things like the - // verbosity data in the left-hand pane prettier than just an - // integer - jobDataOptions: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) { - Rest.setUrl(GetBasePath('jobs') + $stateParams.id); - var val = $q.defer(); - Rest.options() - .then(function(data) { - val.resolve(data.data); - }, function(data) { - val.reject(data); - }); - return val.promise; - }], - jobExtraCredentials: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) { - Rest.setUrl(GetBasePath('jobs') + $stateParams.id + '/extra_credentials'); - var val = $q.defer(); - Rest.get() - .then(function(res) { - val.resolve(res.data.results); - }, function(res) { - val.reject(res); - }); - return val.promise; - }] - }, - templateUrl: templateUrl('job-results/job-results'), - controller: 'jobResultsController' -}; diff --git a/awx/ui/client/src/job-results/job-results.service.js b/awx/ui/client/src/job-results/job-results.service.js deleted file mode 100644 index 6b77575fff..0000000000 --- a/awx/ui/client/src/job-results/job-results.service.js +++ /dev/null @@ -1,269 +0,0 @@ -/************************************************* -* Copyright (c) 2016 Ansible, Inc. -* -* All Rights Reserved -*************************************************/ - - -export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', 'GetBasePath', 'Alert', '$rootScope', 'i18n', -function ($q, Prompt, $filter, Wait, Rest, $state, ProcessErrors, GetBasePath, Alert, $rootScope, i18n) { - var val = { - // the playbook_on_stats event returns the count data in a weird format. - // format to what we need! - getCountsFromStatsEvent: function(event_data) { - var hosts = {}, - hostsArr; - - // iterate over the event_data and populate an object with hosts - // and their status data - Object.keys(event_data).forEach(key => { - // failed passes boolean not integer - if (key === "changed" || - key === "dark" || - key === "failures" || - key === "ok" || - key === "skipped") { - // array of hosts from each type ("changed", "dark", etc.) - hostsArr = Object.keys(event_data[key]); - hostsArr.forEach(host => { - if (!hosts[host]) { - // host has not been added to hosts object - // add now - hosts[host] = {}; - } - - if (!hosts[host][key]) { - // host doesn't have key - hosts[host][key] = 0; - } - hosts[host][key] += event_data[key][host]; - }); - } - }); - - var total_hosts_by_state = { - ok: 0, - skipped: 0, - unreachable: 0, - failures: 0, - changed: 0 - }; - - // each host belongs in at most *one* of these states depending on - // the state of its tasks - _.each(hosts, function(host) { - if (host.dark > 0){ - total_hosts_by_state.unreachable++; - } else if (host.failures > 0){ - total_hosts_by_state.failures++; - } else if (host.changed > 0){ - total_hosts_by_state.changed++; - } else if (host.ok > 0){ - total_hosts_by_state.ok++; - } else if (host.skipped > 0){ - total_hosts_by_state.skipped++; - } - }); - - return total_hosts_by_state; - }, - // rest call to grab previously complete job_events - getEvents: function(url) { - var val = $q.defer(); - - Rest.setUrl(url); - Rest.get() - .then(({data}) => { - val.resolve({results: data.results, - next: data.next}); - }) - .catch(({obj, status}) => { - ProcessErrors(null, obj, status, null, { - hdr: 'Error!', - msg: `Could not get job events. - Returned status: ${status}` - }); - val.reject(obj); - }); - - return val.promise; - }, - deleteJob: function(job) { - Prompt({ - hdr: i18n._("Delete Job"), - resourceName: `#${job.id} ` + $filter('sanitize')(job.name), - body: `
- ${i18n._("Are you sure you want to delete this job?")} -
`, - action: function() { - Wait('start'); - Rest.setUrl(job.url); - Rest.destroy() - .then(() => { - Wait('stop'); - $('#prompt-modal').modal('hide'); - $state.go('jobs'); - }) - .catch(({obj, status}) => { - Wait('stop'); - $('#prompt-modal').modal('hide'); - ProcessErrors(null, obj, status, null, { - hdr: 'Error!', - msg: `Could not delete job. - Returned status: ${status}` - }); - }); - }, - actionText: i18n._('DELETE') - }); - }, - cancelJob: function(job) { - var doCancel = function() { - Rest.setUrl(job.url + 'cancel'); - Rest.post({}) - .then(() => { - Wait('stop'); - $('#prompt-modal').modal('hide'); - }) - .catch(({obj, status}) => { - Wait('stop'); - $('#prompt-modal').modal('hide'); - ProcessErrors(null, obj, status, null, { - hdr: 'Error!', - msg: `Could not cancel job. - Returned status: ${status}` - }); - }); - }; - - Prompt({ - hdr: i18n._('Cancel Job'), - resourceName: `#${job.id} ` + $filter('sanitize')(job.name), - body: `
- ${i18n._("Are you sure you want to cancel this job?")} -
`, - action: function() { - Wait('start'); - Rest.setUrl(job.url + 'cancel'); - Rest.get() - .then(({data}) => { - if (data.can_cancel === true) { - doCancel(); - } else { - $('#prompt-modal').modal('hide'); - ProcessErrors(null, data, null, null, { - hdr: 'Error!', - msg: `Job has completed, - unabled to be canceled.` - }); - } - }); - }, - actionText: i18n._('PROCEED') - }); - }, - getJobData: function(id){ - var val = $q.defer(); - - Rest.setUrl(GetBasePath('jobs') + id ); - Rest.get() - .then(function(data) { - val.resolve(data.data); - }, function(data) { - val.reject(data); - - if (data.status === 404) { - Alert('Job Not Found', 'Cannot find job.', 'alert-info'); - } else if (data.status === 403) { - Alert('Insufficient Permissions', 'You do not have permission to view this job.', 'alert-info'); - } - - $state.go('jobs'); - }); - - return val.promise; - }, - // Generate a helper class for job_event statuses - // the stack for which status to display is - // unreachable > failed > changed > ok - // uses the API's runner events and convenience properties .failed .changed to determine status. - // see: job_event_callback.py for more filters to support - processEventStatus: function(event){ - if (event.event === 'runner_on_unreachable'){ - return { - class: 'HostEvent-status--unreachable', - status: 'unreachable' - }; - } - // equiv to 'runner_on_error' && 'runner on failed' - if (event.failed){ - return { - class: 'HostEvent-status--failed', - status: 'failed' - }; - } - // catch the changed case before ok, because both can be true - if (event.changed){ - return { - class: 'HostEvent-status--changed', - status: 'changed' - }; - } - if (event.event === 'runner_on_ok' || event.event === 'runner_on_async_ok'){ - return { - class: 'HostEvent-status--ok', - status: 'ok' - }; - } - if (event.event === 'runner_on_skipped'){ - return { - class: 'HostEvent-status--skipped', - status: 'skipped' - }; - } - }, - // GET events related to a job run - // e.g. - // ?event=playbook_on_stats - // ?parent=206&event__startswith=runner&page_size=200&order=host_name,counter - getRelatedJobEvents: function(id, params){ - var url = GetBasePath('jobs'); - url = url + id + '/job_events/?' + this.stringifyParams(params); - Rest.setUrl(url); - return Rest.get() - .then((response) => { - return response; - }) - .catch(({data, status}) => { - ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + '. GET returned: ' + status }); - }); - }, - stringifyParams: function(params){ - return _.reduce(params, (result, value, key) => { - return result + key + '=' + value + '&'; - }, ''); - }, - // the the API passes through Ansible's event_data response - // we need to massage away the verbose & redundant stdout/stderr properties - processJson: function(data){ - // configure fields to ignore - var ignored = [ - 'type', - 'event_data', - 'related', - 'summary_fields', - 'url', - 'ansible_facts', - ]; - // remove ignored properties - var result = _.chain(data).cloneDeep().forEach(function(value, key, collection){ - if (ignored.indexOf(key) > -1){ - delete collection[key]; - } - }).value(); - return result; - } - }; - return val; -}]; diff --git a/awx/ui/client/src/job-results/main.js b/awx/ui/client/src/job-results/main.js deleted file mode 100644 index f48ac081d8..0000000000 --- a/awx/ui/client/src/job-results/main.js +++ /dev/null @@ -1,26 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import hostStatusBar from './host-status-bar/main'; -import jobResultsStdOut from './job-results-stdout/main'; - -import route from './job-results.route.js'; - -import jobResultsController from './job-results.controller'; - -import jobResultsService from './job-results.service'; -import eventQueueService from './event-queue.service'; -import parseStdoutService from './parse-stdout.service'; - -export default - angular.module('jobResults', [hostStatusBar.name, jobResultsStdOut.name, 'angularMoment']) - .run(['$stateExtender', function($stateExtender) { - $stateExtender.addState(route); - }]) - .controller('jobResultsController', jobResultsController) - .service('jobResultsService', jobResultsService) - .service('eventQueue', eventQueueService) - .service('parseStdoutService', parseStdoutService); diff --git a/awx/ui/client/src/job-results/parse-stdout.service.js b/awx/ui/client/src/job-results/parse-stdout.service.js deleted file mode 100644 index 66c969b9c4..0000000000 --- a/awx/ui/client/src/job-results/parse-stdout.service.js +++ /dev/null @@ -1,293 +0,0 @@ -/************************************************* -* Copyright (c) 2016 Ansible, Inc. -* -* All Rights Reserved -*************************************************/ - -export default ['$log', 'moment', 'i18n', function($log, moment, i18n){ - var val = { - // parses stdout string from api and formats various codes to the - // correct dom structure - prettify: function(line, unstyled){ - line = line - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - - // TODO: remove once Chris's fixes to the [K lines comes in - if (line.indexOf("[K") > -1) { - $log.error(line); - } - - if(!unstyled){ - // add span tags with color styling - line = line.replace(/u001b/g, ''); - - // ansi classes - /* jshint ignore:start */ - line = line.replace(/(|)\[1;im/g, ''); - line = line.replace(/(|)\[0;30m/g, ''); - line = line.replace(/(|)\[1;30m/g, ''); - line = line.replace(/(|)\[[0,1];31m/g, ''); - line = line.replace(/(|)\[0;32m(=|)/g, ''); - line = line.replace(/(|)\[0;32m1/g, ''); - line = line.replace(/(|)\[0;33m/g, ''); - line = line.replace(/(|)\[0;34m/g, ''); - line = line.replace(/(|)\[[0,1];35m/g, ''); - line = line.replace(/(|)\[0;36m/g, ''); - line = line.replace(/()\s/g, '$1'); - - //end span - line = line.replace(/(|)\[0m/g, ''); - /* jshint ignore:end */ - } else { - // For the host event modal in the standard out tab, - // the styling isn't necessary - line = line.replace(/u001b/g, ''); - - // ansi classes - /* jshint ignore:start */ - line = line.replace(/(|)\[[0,1];3[0-9]m(1|=|)/g, ''); - line = line.replace(/()\s/g, '$1'); - - //end span - line = line.replace(/(|)\[0m/g, ''); - /* jshint ignore:end */ - } - - return line; - }, - // adds anchor tags and tooltips to host status lines - getAnchorTags: function(event){ - if(event.event_name.indexOf("runner_") === -1){ - return `"`; - } - else{ - return ` JobResultsStdOut-stdoutColumn--clickable" ui-sref="jobResult.host-event.json({eventId: ${event.id}, taskUuid: '${event.event_data.task_uuid}' })" aw-tool-tip="${i18n._("Event ID")}: ${event.id}
${i18n._("Status")}: ${event.event_display}
${i18n._("Click for details")}" data-placement="top"`; - } - - }, - // this adds classes based on event data to the - // .JobResultsStdOut-aLineOfStdOut element - getLineClasses: function(event, line, lineNum) { - var string = ""; - - if (lineNum === event.end_line) { - // used to tell you where to put stuff in the pane - string += ` next_is_${event.end_line + 1}`; - } - - if (event.event_name === "playbook_on_play_start") { - // play header classes - string += " header_play"; - string += " header_play_" + event.event_data.play_uuid; - - // give the actual header class to the line with the - // actual header info (think cowsay) - if (line.indexOf("PLAY") > -1) { - string += " actual_header"; - } - } else if (event.event_name === "playbook_on_task_start") { - // task header classes - string += " header_task"; - string += " header_task_" + event.event_data.task_uuid; - - // give the actual header class to the line with the - // actual header info (think cowsay) - if (line.indexOf("TASK") > -1 || - line.indexOf("RUNNING HANDLER") > -1) { - string += " actual_header"; - } - - // task headers also get classed by their parent play - // if applicable - if (event.event_data.play_uuid) { - string += " play_" + event.event_data.play_uuid; - } - } else if (event.event_name !== "playbook_on_stats"){ - string += " not_skeleton"; - // host status or debug line - - // these get classed by their parent play if applicable - if (event.event_data.play_uuid) { - string += " play_" + event.event_data.play_uuid; - } - // as well as their parent task if applicable - if (event.event_data.task_uuid) { - string += " task_" + event.event_data.task_uuid; - } - } - - // TODO: adding this line_num_XX class is hacky because the - // line number is availabe in children of this dom element - string += " line_num_" + lineNum; - - return string; - }, - getStartTimeBadge: function(event, line){ - // This will return a div with the badge class - // for the start time to show at the right hand - // side of each stdout header line. - // returns an empty string if not a header line - var emptySpan = "", time; - if ((event.event_name === "playbook_on_play_start" || - event.event_name === "playbook_on_task_start") && - line !== "") { - time = moment(event.created).format('HH:mm:ss'); - return `
${time}
`; - } - else if(event.event_name === "playbook_on_stats" && line.indexOf("PLAY") > -1){ - time = moment(event.created).format('HH:mm:ss'); - return `
${time}
`; - } - else { - return emptySpan; - } - - }, - // used to add expand/collapse icon next to line numbers of headers - getCollapseIcon: function(event, line) { - var clickClass, - expanderizerSpecifier; - - var emptySpan = ` -`; - - if ((event.event_name === "playbook_on_play_start" || - event.event_name === "playbook_on_task_start") && - line !== "") { - if (event.event_name === "playbook_on_play_start" && - line.indexOf("PLAY") > -1) { - // play header specific attrs - expanderizerSpecifier = "play"; - clickClass = "play_" + - event.event_data.play_uuid; - } else if (line.indexOf("TASK") > -1 || - line.indexOf("RUNNING HANDLER") > -1) { - // task header specific attrs - expanderizerSpecifier = "task"; - clickClass = "task_" + - event.event_data.task_uuid; - } else { - // header lines that don't have PLAY, TASK, - // or RUNNING HANDLER in them don't get - // expand icon. - // This provides cowsay support. - return emptySpan; - } - - - var expandDom = ` - - - -`; - return expandDom; - } else { - // non-header lines don't get an expander - return emptySpan; - } - }, - distributeColors: function(lines) { - var colorCode; - return lines.map(line => { - - if (colorCode) { - line = colorCode + line; - } - - if (line.indexOf("[0m") === -1) { - if (line.indexOf("[1;31m") > -1) { - colorCode = "[1;31m"; - } else if (line.indexOf("[1;30m") > -1) { - colorCode = "[1;30m"; - } else if (line.indexOf("[0;31m") > -1) { - colorCode = "[0;31m"; - } else if (line.indexOf("[0;32m=") > -1) { - colorCode = "[0;32m="; - } else if (line.indexOf("[0;32m1") > -1) { - colorCode = "[0;32m1"; - } else if (line.indexOf("[0;32m") > -1) { - colorCode = "[0;32m"; - } else if (line.indexOf("[0;33m") > -1) { - colorCode = "[0;33m"; - } else if (line.indexOf("[0;34m") > -1) { - colorCode = "[0;34m"; - } else if (line.indexOf("[0;35m") > -1) { - colorCode = "[0;35m"; - } else if (line.indexOf("[1;35m") > -1) { - colorCode = "[1;35m"; - } else if (line.indexOf("[0;36m") > -1) { - colorCode = "[0;36m"; - } - } else { - colorCode = null; - } - - return line; - }); - }, - getLineArr: function(event) { - let lineNums = _.range(event.start_line + 1, - event.end_line + 1); - - // hack around no-carriage return issues - if (!lineNums.length) { - lineNums = [event.start_line + 1]; - } - - let lines = event.stdout - .replace("\t", " ") - .split("\r\n"); - - if (lineNums.length > lines.length) { - lineNums = lineNums.slice(0, lines.length); - } - - lines = this.distributeColors(lines); - - // hack around no-carriage return issues - if (lineNums.length === lines.length) { - return _.zip(lineNums, lines); - } - - return _.zip(lineNums, lines).slice(0, -1); - }, - actualEndLine: function(event) { - return event.start_line + this.getLineArr(event).length; - }, - // public function that provides the parsed stdout line, given a - // job_event - parseStdout: function(event){ - // this utilizes the start/end lines and stdout blob - // to create an array in the format: - // [ - // [lineNum, lineText], - // [lineNum, lineText], - // ] - var lineArr = this.getLineArr(event); - - // this takes each `[lineNum: lineText]` element and calls the - // relevant helper functions in this service to build the - // parsed line of standard out - lineArr = lineArr - .map(lineArr => { - return ` -
-
${this.getCollapseIcon(event, lineArr[1])}${lineArr[0]}
-
-
-
-
-
-
-
- RESULTS -
-
-
- -
- - -
-
-
- -
-
Name
-
{{ job.module_name }}
-
- -
-
STATUS
-
- - {{ job.status }} -
-
- -
-
STARTED
-
- {{ job.started | longDate }} -
-
- -
-
FINISHED
-
- {{ job.finished | longDate }} -
-
- -
-
ELAPSED
-
- {{ job.elapsed }} seconds -
-
- -
-
Module Args
-
{{ job.module_args }}
-
- -
-
Inventory
- -
- -
-
Credential
- -
- -
-
Launched By
- -
- - -
-
Forks
-
{{ forks }}
-
- -
-
Limit
-
{{ limit }}
-
- - -
-
Verbosity
-
{{ verbosity }}
-
- -
-
- {{ 'Extra Variables' | translate }} - - -
-
- -
-
- -
-
-
-
-
-
-
STANDARD OUT
-
- - - - -
-
- -
-
-
-
-
diff --git a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js b/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js deleted file mode 100644 index 899a98e9f7..0000000000 --- a/awx/ui/client/src/standard-out/adhoc/standard-out-adhoc.route.js +++ /dev/null @@ -1,36 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import { templateUrl } from '../../shared/template-url/template-url.factory'; - -export default { - name: 'adHocJobStdout', - route: '/ad_hoc_commands/:id', - templateUrl: templateUrl('standard-out/adhoc/standard-out-adhoc'), - controller: 'JobStdoutController', - ncyBreadcrumb: { - parent: "jobs", - label: "{{ job.module_name }}" - }, - data: { - jobType: 'ad_hoc_commands', - socket: { - "groups": { - "jobs": ["status_changed", "summary"], - "ad_hoc_command_events": [] - } - } - }, - resolve: { - jobData: ['Rest', 'GetBasePath', '$stateParams', function(Rest, GetBasePath, $stateParams) { - Rest.setUrl(GetBasePath('base') + 'ad_hoc_commands/' + $stateParams.id + '/'); - return Rest.get() - .then(({data}) => { - return data; - }); - }] - } -}; diff --git a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html deleted file mode 100644 index 48f2d65b7e..0000000000 --- a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html +++ /dev/null @@ -1,152 +0,0 @@ -
-
-
-
-
-
-
- RESULTS -
-
- - - -
-
-
- - - -
-
STATUS
-
- - {{ job.status }} -
-
- - -
-
EXPLANATION
-
- {{task_detail | limitTo:explanationLimit}} - - ... - Show More - - Show Less -
-
- -
-
LICENSE ERROR
-
- {{ job.license_error }} -
-
- -
-
STARTED
-
- {{ job.started | longDate }} -
-
- -
-
FINISHED
-
- {{ job.finished | longDate }} -
-
- -
-
ELAPSED
-
- {{ job.elapsed }} seconds -
-
- -
-
LAUNCH TYPE
-
- {{ job.launch_type }} -
-
- -
-
CREDENTIAL
- -
- -
-
SOURCE
-
- {{ source }} -
-
- -
-
REGIONS
-
- {{ source_regions }} -
-
- -
-
OVERWRITE
-
- {{ job.overwrite }} -
-
- -
-
OVERWRITE VARS
-
- {{ job.overwrite_vars }} -
-
- -
-
-
-
-
-
-
STANDARD OUT
-
- - - - -
-
- -
-
-
-
-
diff --git a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.route.js b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.route.js deleted file mode 100644 index bdd1a9a2b1..0000000000 --- a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.route.js +++ /dev/null @@ -1,38 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import {templateUrl} from '../../shared/template-url/template-url.factory'; - -// TODO: figure out what this route should be - should it be inventory_sync? - -export default { - name: 'inventorySyncStdout', - route: '/inventory_sync/:id', - templateUrl: templateUrl('standard-out/inventory-sync/standard-out-inventory-sync'), - controller: 'JobStdoutController', - ncyBreadcrumb: { - parent: "jobs", - label: "{{ inventory_source_name }}" - }, - data: { - socket: { - "groups":{ - "jobs": ["status_changed", "summary"], - "inventory_update_events": [], - } - }, - jobType: 'inventory_updates' - }, - resolve: { - jobData: ['Rest', 'GetBasePath', '$stateParams', function(Rest, GetBasePath, $stateParams) { - Rest.setUrl(GetBasePath('base') + 'inventory_updates/' + $stateParams.id + '/'); - return Rest.get() - .then(({data}) => { - return data; - }); - }] - } -}; diff --git a/awx/ui/client/src/standard-out/log/main.js b/awx/ui/client/src/standard-out/log/main.js deleted file mode 100644 index bb97a737be..0000000000 --- a/awx/ui/client/src/standard-out/log/main.js +++ /dev/null @@ -1,10 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import standardOutLog from './standard-out-log.directive'; -export default - angular.module('standardOutLogDirective', []) - .directive('standardOutLog', standardOutLog); diff --git a/awx/ui/client/src/standard-out/log/standard-out-log.controller.js b/awx/ui/client/src/standard-out/log/standard-out-log.controller.js deleted file mode 100644 index 728f8faedf..0000000000 --- a/awx/ui/client/src/standard-out/log/standard-out-log.controller.js +++ /dev/null @@ -1,202 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -export default ['$log', '$rootScope', '$scope', '$state', '$stateParams', 'ProcessErrors', 'Rest', 'Wait', - function ($log, $rootScope, $scope, $state, $stateParams, ProcessErrors, Rest, Wait) { - - var api_complete = false, - current_range, - loaded_sections = [], - event_queue = 0, - auto_scroll_down=true, // programmatic scroll to bottom - live_event_processing = true, - page_size = 500, - job_id = $stateParams.id; - - $scope.should_apply_live_events = true; - - // Open up a socket for events depending on the type of job - function openSockets() { - if ($state.current.name === 'jobResult') { - $log.debug("socket watching on job_events-" + job_id); - $scope.$on(`ws-job_events-${job_id}`, function() { - $log.debug("socket fired on job_events-" + job_id); - if (api_complete) { - event_queue++; - } - }); - } - if ($state.current.name === 'adHocJobStdout') { - $log.debug("socket watching on ad_hoc_command_events-" + job_id); - $scope.$on(`ws-ad_hoc_command_events-${job_id}`, function() { - $log.debug("socket fired on ad_hoc_command_events-" + job_id); - if (api_complete) { - event_queue++; - } - }); - } - } - - openSockets(); - - // This is a trigger for loading up the standard out - if ($scope.removeLoadStdout) { - $scope.removeLoadStdout(); - } - $scope.removeLoadStdout = $scope.$on('LoadStdout', function() { - if (loaded_sections.length === 0) { - loadStdout(); - } - else if (live_event_processing) { - getNextSection(); - } - }); - - // This interval checks to see whether or not we've gotten a new - // event via sockets. If so, go out and update the standard out - // log. - $rootScope.jobStdOutInterval = setInterval( function() { - if (event_queue > 0) { - // events happened since the last check - $log.debug('checking for stdout...'); - if (loaded_sections.length === 0) { ////this if statement for refresh - $log.debug('calling LoadStdout'); - loadStdout(); - } - else if (live_event_processing) { - $log.debug('calling getNextSection'); - getNextSection(); - } - event_queue = 0; - } - }, 2000); - - // stdoutEndpoint gets passed through in the directive declaration. - // This watcher fires off loadStdout() when the endpoint becomes - // available. - $scope.$watch('stdoutEndpoint', function(newVal, oldVal) { - if(newVal && newVal !== oldVal) { - // Fire off the server call - loadStdout(); - } - }); - - // stdoutText optionall gets passed through in the directive declaration. - $scope.$watch('stdoutText', function(newVal, oldVal) { - if(newVal && newVal !== oldVal) { - $('#pre-container-content').html(newVal); - } - }); - - function loadStdout() { - if (!$scope.stdoutEndpoint) { - return; - } - - Rest.setUrl($scope.stdoutEndpoint + '?format=json&start_line=0&end_line=' + page_size); - Rest.get() - .then(({data}) => { - Wait('stop'); - if (data.content) { - api_complete = true; - $('#pre-container-content').html(data.content); - current_range = data.range; - if (data.content !== "Waiting for results...") { - loaded_sections.push({ - start: (data.range.start < 0) ? 0 : data.range.start, - end: data.range.end - }); - } - - $('#pre-container').scrollTop($('#pre-container').prop("scrollHeight")); - } - else { - api_complete = true; - } - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve stdout for job: ' + job_id + '. GET returned: ' + status }); - }); - } - - function getNextSection() { - if (!$scope.stdoutEndpoint) { - return; - } - - // get the next range of data from the API - var start = loaded_sections[loaded_sections.length - 1].end, url; - url = $scope.stdoutEndpoint + '?format=json&start_line=' + start + '&end_line=' + (start + page_size); - $('#stdoutMoreRowsBottom').fadeIn(); - Rest.setUrl(url); - Rest.get() - .then(({data}) => { - if ($('#pre-container-content').html() === "Waiting for results...") { - $('#pre-container-content').html(data.content); - } else { - $('#pre-container-content').append(data.content); - } - loaded_sections.push({ - start: (data.range.start < 0) ? 0 : data.range.start, - end: data.range.end - }); - if ($scope.should_apply_live_events) { - // if user has not disabled live event view by scrolling upward, then scroll down to the new content - current_range = data.range; - auto_scroll_down = true; // prevent auto load from happening - $('#pre-container').scrollTop($('#pre-container').prop("scrollHeight")); - } - $('#stdoutMoreRowsBottom').fadeOut(400); - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve stdout for job: ' + job_id + '. GET returned: ' + status }); - }); - } - - // lrInfiniteScroll handler - // grabs the next stdout section - $scope.stdOutGetNextSection = function(){ - if (current_range.absolute_end > current_range.end){ - var url = $scope.stdoutEndpoint + '?format=json&start_line=' + current_range.end + - '&end_line=' + (current_range.end + page_size); - Rest.setUrl(url); - Rest.get() - .then(({data}) => { - $('#pre-container-content').append(data.content); - current_range = data.range; - }) - .catch(({data, status}) => { - ProcessErrors($scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve stdout for job: ' + job_id + '. GET returned: ' + status }); - }); - } - }; - - // We watch for job status changes here. If the job completes we want to clear out the - // stdout interval and kill the live_event_processing flag. - $scope.$on(`ws-jobs`, function(e, data) { - if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10)) { - if (data.status === 'failed' || data.status === 'canceled' || - data.status === 'error' || data.status === 'successful') { - if ($rootScope.jobStdOutInterval) { - window.clearInterval($rootScope.jobStdOutInterval); - } - if (live_event_processing) { - if (loaded_sections.length === 0) { - loadStdout(); - } - else { - getNextSection(); - } - } - live_event_processing = false; - } - } - }); - -}]; diff --git a/awx/ui/client/src/standard-out/log/standard-out-log.directive.js b/awx/ui/client/src/standard-out/log/standard-out-log.directive.js deleted file mode 100644 index d7d0656441..0000000000 --- a/awx/ui/client/src/standard-out/log/standard-out-log.directive.js +++ /dev/null @@ -1,47 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import standardOutLogController from './standard-out-log.controller'; -export default [ 'templateUrl', - function(templateUrl) { - return { - scope: { - stdoutEndpoint: '=', - stdoutText: '=', - jobId: '=' - }, - templateUrl: templateUrl('standard-out/log/standard-out-log'), - restrict: 'E', - controller: standardOutLogController, - link: function(scope) { - // All of our DOM related stuff will go in here - - var lastScrollTop, - direction; - - function detectDirection() { - var st = $('#pre-container').scrollTop(); - if (st > lastScrollTop) { - direction = "down"; - } else { - direction = "up"; - } - lastScrollTop = st; - return direction; - } - - $('#pre-container').bind('scroll', function() { - if (detectDirection() === "up") { - scope.should_apply_live_events = false; - } - - if ($(this).scrollTop() + $(this).height() === $(this).prop("scrollHeight")) { - scope.should_apply_live_events = true; - } - }); - } - }; -}]; diff --git a/awx/ui/client/src/standard-out/log/standard-out-log.partial.html b/awx/ui/client/src/standard-out/log/standard-out-log.partial.html deleted file mode 100644 index 45d9f70cbb..0000000000 --- a/awx/ui/client/src/standard-out/log/standard-out-log.partial.html +++ /dev/null @@ -1,8 +0,0 @@ -
-
-
-
-
- -
-
diff --git a/awx/ui/client/src/standard-out/main.js b/awx/ui/client/src/standard-out/main.js deleted file mode 100644 index 1e0f451014..0000000000 --- a/awx/ui/client/src/standard-out/main.js +++ /dev/null @@ -1,22 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import stdoutAdhocRoute from './adhoc/standard-out-adhoc.route'; -import stdoutManagementJobsRoute from './management-jobs/standard-out-management-jobs.route'; -import stdoutInventorySyncRoute from './inventory-sync/standard-out-inventory-sync.route'; -import stdoutScmUpdateRoute from './scm-update/standard-out-scm-update.route'; -import {JobStdoutController} from './standard-out.controller'; -import StandardOutHelper from './standard-out-factories/main'; -import standardOutLogDirective from './log/main'; - -export default angular.module('standardOut', [StandardOutHelper.name, standardOutLogDirective.name]) - .controller('JobStdoutController', JobStdoutController) - .run(['$stateExtender', function($stateExtender) { - $stateExtender.addState(stdoutAdhocRoute); - $stateExtender.addState(stdoutManagementJobsRoute); - $stateExtender.addState(stdoutInventorySyncRoute); - $stateExtender.addState(stdoutScmUpdateRoute); - }]); diff --git a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html deleted file mode 100644 index 8c9740a5eb..0000000000 --- a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.partial.html +++ /dev/null @@ -1,84 +0,0 @@ -
-
-
-
-
-
-
- RESULTS -
-
- - -
-
-
- -
-
NAME
-
{{ job.name }}
-
- -
-
STATUS
-
- - {{ job.status }} -
-
- -
-
STARTED
-
- {{ job.started | longDate }} -
-
- -
-
FINISHED
-
- {{ job.finished | longDate }} -
-
- -
-
ELAPSED
-
- {{ job.elapsed }} seconds -
-
- -
-
LAUNCH TYPE
-
- {{ job.launch_type }} -
-
- -
-
EXTRA VARIABLES
-
- -
- -
- -
-
-
-
-
-
-
STANDARD OUT
-
- -
-
- -
-
-
-
-
diff --git a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js b/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js deleted file mode 100644 index e3a59e884d..0000000000 --- a/awx/ui/client/src/standard-out/management-jobs/standard-out-management-jobs.route.js +++ /dev/null @@ -1,36 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import { templateUrl } from '../../shared/template-url/template-url.factory'; - -export default { - name: 'managementJobStdout', - route: '/management_jobs/:id', - templateUrl: templateUrl('standard-out/management-jobs/standard-out-management-jobs'), - controller: 'JobStdoutController', - ncyBreadcrumb: { - parent: "jobs", - label: "{{ job.name }}" - }, - data: { - jobType: 'system_jobs', - socket: { - "groups": { - "jobs": ["status_changed", "summary"], - "system_job_events": [], - } - } - }, - resolve: { - jobData: ['Rest', 'GetBasePath', '$stateParams', function(Rest, GetBasePath, $stateParams) { - Rest.setUrl(GetBasePath('base') + 'system_jobs/' + $stateParams.id + '/'); - return Rest.get() - .then(({data}) => { - return data; - }); - }] - } -}; diff --git a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html deleted file mode 100644 index f9828d4c02..0000000000 --- a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.partial.html +++ /dev/null @@ -1,113 +0,0 @@ -
-
-
-
-
-
-
- RESULTS -
-
- - - -
-
-
- - - -
-
STATUS
-
- - {{ job.status }} -
-
- -
-
STARTED
-
- {{ job.started | longDate }} -
-
- -
-
FINISHED
-
- {{ job.finished | longDate }} -
-
- -
-
ELAPSED
-
- {{ job.elapsed }} seconds -
-
- -
-
LAUNCH TYPE
-
- {{ job.launch_type }} -
-
- -
-
PROJECT
- -
- -
-
CREDENTIAL
- -
- -
-
-
-
-
-
-
STANDARD OUT
-
- - - - -
-
- -
-
-
-
-
diff --git a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js b/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js deleted file mode 100644 index 818509ccc7..0000000000 --- a/awx/ui/client/src/standard-out/scm-update/standard-out-scm-update.route.js +++ /dev/null @@ -1,38 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import { templateUrl } from '../../shared/template-url/template-url.factory'; - -// TODO: figure out what this route should be - should it be scm_update? - -export default { - name: 'scmUpdateStdout', - route: '/scm_update/:id', - templateUrl: templateUrl('standard-out/scm-update/standard-out-scm-update'), - controller: 'JobStdoutController', - ncyBreadcrumb: { - parent: "jobs", - label: "{{ project_name }}" - }, - data: { - jobType: 'project_updates', - socket: { - "groups": { - "jobs": ["status_changed", "summary"], - "project_update_events": [], - } - }, - }, - resolve: { - jobData: ['Rest', 'GetBasePath', '$stateParams', function(Rest, GetBasePath, $stateParams) { - Rest.setUrl(GetBasePath('base') + 'project_updates/' + $stateParams.id + '/'); - return Rest.get() - .then(({data}) => { - return data; - }); - }] - } -}; diff --git a/awx/ui/client/src/standard-out/standard-out-factories/delete-job.factory.js b/awx/ui/client/src/standard-out/standard-out-factories/delete-job.factory.js deleted file mode 100644 index 6e28362f3a..0000000000 --- a/awx/ui/client/src/standard-out/standard-out-factories/delete-job.factory.js +++ /dev/null @@ -1,145 +0,0 @@ -export default -function DeleteJob($state, Find, Rest, Wait, ProcessErrors, Prompt, Alert, - $filter, i18n) { - return function(params) { - var scope = params.scope, - id = params.id, - job = params.job, - callback = params.callback, - action, jobs, url, action_label, hdr; - - if (!job) { - if (scope.completed_jobs) { - jobs = scope.completed_jobs; - } - else if (scope.running_jobs) { - jobs = scope.running_jobs; - } - else if (scope.queued_jobs) { - jobs = scope.queued_jobs; - } - else if (scope.all_jobs) { - jobs = scope.all_jobs; - } - else if (scope.jobs) { - jobs = scope.jobs; - } - job = Find({list: jobs, key: 'id', val: id }); - } - - if (job.status === 'pending' || job.status === 'running' || job.status === 'waiting') { - url = job.related.cancel; - action_label = 'cancel'; - hdr = i18n._('Cancel'); - } else { - url = job.url; - action_label = 'delete'; - hdr = i18n._('Delete'); - } - - action = function () { - Wait('start'); - Rest.setUrl(url); - if (action_label === 'cancel') { - Rest.post() - .then(() => { - $('#prompt-modal').modal('hide'); - if (callback) { - scope.$emit(callback, action_label); - } - else { - $state.reload(); - Wait('stop'); - } - }) - .catch(({obj, status}) => { - Wait('stop'); - $('#prompt-modal').modal('hide'); - if (status === 403) { - Alert('Error', obj.detail); - } - // Ignore the error. The job most likely already finished. - // ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + - // ' failed. POST returned status: ' + status }); - }); - } else { - Rest.destroy() - .then(() => { - $('#prompt-modal').modal('hide'); - if (callback) { - scope.$emit(callback, action_label); - } - else { - let reloadListStateParams = null; - - if(scope.jobs.length === 1 && $state.params.job_search && !_.isEmpty($state.params.job_search.page) && $state.params.job_search.page !== '1') { - reloadListStateParams = _.cloneDeep($state.params); - reloadListStateParams.job_search.page = (parseInt(reloadListStateParams.job_search.page)-1).toString(); - } - - $state.go('.', reloadListStateParams, {reload: true}); - Wait('stop'); - } - }) - .catch(({obj, status}) => { - Wait('stop'); - $('#prompt-modal').modal('hide'); - if (status === 403) { - Alert('Error', obj.detail); - } - // Ignore the error. The job most likely already finished. - //ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + - // ' failed. DELETE returned status: ' + status }); - }); - } - }; - - if (scope.removeCancelNotAllowed) { - scope.removeCancelNotAllowed(); - } - scope.removeCancelNotAllowed = scope.$on('CancelNotAllowed', function() { - Wait('stop'); - Alert('Job Completed', 'The request to cancel the job could not be submitted. The job already completed.', 'alert-info'); - }); - - if (scope.removeCancelJob) { - scope.removeCancelJob(); - } - scope.removeCancelJob = scope.$on('CancelJob', function() { - var cancelBody = "
" + i18n._("Are you sure you want to submit the request to cancel this job?") + "
"; - var deleteBody = "
" + i18n._("Are you sure you want to delete this job?") + "
"; - Prompt({ - hdr: hdr, - resourceName: `#${job.id} ` + $filter('sanitize')(job.name), - body: (action_label === 'cancel' || job.status === 'new') ? cancelBody : deleteBody, - action: action, - actionText: (action_label === 'cancel' || job.status === 'new') ? i18n._("OK") : i18n._("DELETE") - }); - }); - - if (action_label === 'cancel') { - Rest.setUrl(url); - Rest.get() - .then(({data}) => { - if (data.can_cancel) { - scope.$emit('CancelJob'); - } - else { - scope.$emit('CancelNotAllowed'); - } - }) - .catch(({data, status}) => { - ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Call to ' + url + - ' failed. GET returned: ' + status }); - }); - } - else { - scope.$emit('CancelJob'); - } - }; -} - -DeleteJob.$inject = -[ '$state', 'Find', 'Rest', 'Wait', - 'ProcessErrors', 'Prompt', 'Alert', '$filter', 'i18n' -]; diff --git a/awx/ui/client/src/standard-out/standard-out-factories/lookup-name.factory.js b/awx/ui/client/src/standard-out/standard-out-factories/lookup-name.factory.js deleted file mode 100644 index 43b39da58c..0000000000 --- a/awx/ui/client/src/standard-out/standard-out-factories/lookup-name.factory.js +++ /dev/null @@ -1,36 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - - export default - ['Rest', 'ProcessErrors', 'Empty', function(Rest, ProcessErrors, Empty) { - return function(params) { - var url = params.url, - scope_var = params.scope_var, - scope = params.scope, - callback = params.callback; - Rest.setUrl(url); - Rest.get() - .then(({data}) => { - if (scope_var === 'inventory_source') { - scope.inventory = data.inventory; - } - if (!Empty(data.name)) { - scope[scope_var + '_name'] = data.name; - } - - if (callback) { - scope.$emit(callback, data); - } - }) - .catch(({data, status}) => { - if (status === 403 && params.ignore_403) { - return; - } - ProcessErrors(scope, data, status, null, { hdr: 'Error!', - msg: 'Failed to retrieve ' + url + '. GET returned: ' + status }); - }); - }; - }]; diff --git a/awx/ui/client/src/standard-out/standard-out-factories/main.js b/awx/ui/client/src/standard-out/standard-out-factories/main.js deleted file mode 100644 index 935c8dca37..0000000000 --- a/awx/ui/client/src/standard-out/standard-out-factories/main.js +++ /dev/null @@ -1,13 +0,0 @@ -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -import lookUpName from './lookup-name.factory'; -import DeleteJob from './delete-job.factory'; - -export default - angular.module('StandardOutHelper', []) - .factory('LookUpName', lookUpName) - .factory('DeleteJob', DeleteJob); diff --git a/awx/ui/client/src/standard-out/standard-out.controller.js b/awx/ui/client/src/standard-out/standard-out.controller.js deleted file mode 100644 index 1a707aa706..0000000000 --- a/awx/ui/client/src/standard-out/standard-out.controller.js +++ /dev/null @@ -1,266 +0,0 @@ -/************************************************* - * Copyright (c) 2015 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -/** - * @ngdoc function - * @name controllers.function:JobStdout - * @description This controller's for the standard out page that can be displayed when a job runs -*/ - -export function JobStdoutController ($rootScope, $scope, $state, $stateParams, - GetBasePath, Rest, ProcessErrors, Empty, GetChoices, LookUpName, - ParseTypeChange, ParseVariableString, DeleteJob, Wait, i18n, - fieldChoices, fieldLabels, Project, Alert, InventorySource, - jobData) { - - var job_id = $stateParams.id, - jobType = $state.current.data.jobType; - - // This scope variable controls whether or not the left panel is shown and the right panel - // is expanded to take up the full screen - $scope.stdoutFullScreen = false; - $scope.toggleStdoutFullscreenTooltip = i18n._("Expand Output"); - - $scope.explanationLimit = 150; - - // Listen for job status updates that may come across via sockets. We need to check the payload - // to see whethere the updated job is the one that we're currently looking at. - $scope.$on(`ws-jobs`, function(e, data) { - if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10) && $scope.job) { - $scope.job.status = data.status; - } - - if (data.status === 'failed' || data.status === 'canceled' || data.status === 'error' || data.status === 'successful') { - // Go out and refresh the job details - - Rest.setUrl(GetBasePath('base') + jobType + '/' + job_id + '/'); - Rest.get() - .then(({data}) => { - updateJobObj(data); - }); - } - }); - - $scope.previousTaskFailed = false; - - $scope.$watch('job.job_explanation', function(explanation) { - if (explanation && explanation.split(":")[0] === "Previous Task Failed") { - $scope.previousTaskFailed = true; - - var taskObj = JSON.parse(explanation.substring(explanation.split(":")[0].length + 1)); - // return a promise from the options request with the permission type choices (including adhoc) as a param - var fieldChoice = fieldChoices({ - $scope: $scope, - url: GetBasePath('unified_jobs'), - field: 'type' - }); - - // manipulate the choices from the options request to be set on - // scope and be usable by the list form - fieldChoice.then(function (choices) { - choices = - fieldLabels({ - choices: choices - }); - $scope.explanation_fail_type = choices[taskObj.job_type]; - $scope.explanation_fail_name = taskObj.job_name; - $scope.explanation_fail_id = taskObj.job_id; - $scope.task_detail = $scope.explanation_fail_type + " failed for " + $scope.explanation_fail_name + " with ID " + $scope.explanation_fail_id + "."; - }); - } else { - $scope.previousTaskFailed = false; - } - }); - - // Set the parse type so that CodeMirror knows how to display extra params YAML/JSON - $scope.parseType = 'yaml'; - - function updateJobObj(updatedJobData) { - - // Go out and get the job details based on the job type. jobType gets defined - // in the data block of the route declaration for each of the different types - // of stdout jobs. - - $scope.job = updatedJobData; - $scope.job_template_name = updatedJobData.name; - $scope.created_by = updatedJobData.summary_fields.created_by; - $scope.project_name = (updatedJobData.summary_fields.project) ? updatedJobData.summary_fields.project.name : ''; - $scope.inventory_name = (updatedJobData.summary_fields.inventory) ? updatedJobData.summary_fields.inventory.name : ''; - $scope.job_template_url = '/#/templates/' + updatedJobData.unified_job_template; - if($scope.inventory_name && updatedJobData.inventory && updatedJobData.summary_fields.inventory && updatedJobData.summary_fields.inventory.kind) { - if(updatedJobData.summary_fields.inventory.kind === '') { - $scope.inventory_url = '/#/inventories/inventory' + updatedJobData.inventory; - } - else if(updatedJobData.summary_fields.inventory.kind === 'smart') { - $scope.inventory_url = '/#/inventories/smart_inventory' + updatedJobData.inventory; - } - } - else { - $scope.inventory_url = ''; - } - $scope.project_url = ($scope.project_name && updatedJobData.project) ? '/#/projects/' + updatedJobData.project : ''; - $scope.credential_name = (updatedJobData.summary_fields.credential) ? updatedJobData.summary_fields.credential.name : ''; - $scope.credential_url = (updatedJobData.credential) ? '/#/credentials/' + updatedJobData.credential : ''; - $scope.cloud_credential_url = (updatedJobData.cloud_credential) ? '/#/credentials/' + updatedJobData.cloud_credential : ''; - if(updatedJobData.summary_fields && updatedJobData.summary_fields.source_workflow_job && - updatedJobData.summary_fields.source_workflow_job.id){ - $scope.workflow_result_link = `/#/workflows/${updatedJobData.summary_fields.source_workflow_job.id}`; - } - $scope.playbook = updatedJobData.playbook; - $scope.credential = updatedJobData.credential; - $scope.cloud_credential = updatedJobData.cloud_credential; - $scope.forks = updatedJobData.forks; - $scope.limit = updatedJobData.limit; - $scope.verbosity = updatedJobData.verbosity; - $scope.job_tags = updatedJobData.job_tags; - $scope.job.module_name = updatedJobData.module_name; - if (updatedJobData.extra_vars) { - $scope.variables = ParseVariableString(updatedJobData.extra_vars); - } - - $scope.$on('getInventorySource', function(e, d) { - $scope.inv_manage_group_link = '/#/inventories/inventory/' + d.inventory + '/inventory_sources/edit/' + d.id; - }); - - // If we have a source then we have to go get the source choices from the server - if (!Empty(updatedJobData.source)) { - if ($scope.removeChoicesReady) { - $scope.removeChoicesReady(); - } - $scope.removeChoicesReady = $scope.$on('ChoicesReady', function() { - $scope.source_choices.every(function(e) { - if (e.value === updatedJobData.source) { - $scope.source = e.label; - return false; - } - return true; - }); - }); - // GetChoices can be found in the helper: Utilities.js - // It attaches the source choices to $scope.source_choices. - // Then, when the callback is fired, $scope.source is bound - // to the corresponding label. - GetChoices({ - scope: $scope, - url: GetBasePath('inventory_sources'), - field: 'source', - variable: 'source_choices', - choice_name: 'choices', - callback: 'ChoicesReady' - }); - } - - // LookUpName can be found in the lookup-name.factory - // It attaches the name that it gets (based on the url) - // to the $scope variable defined by the attribute scope_var. - if (!Empty(updatedJobData.credential)) { - LookUpName({ - scope: $scope, - scope_var: 'credential', - url: GetBasePath('credentials') + updatedJobData.credential + '/', - ignore_403: true - }); - } - - if (!Empty(updatedJobData.inventory)) { - LookUpName({ - scope: $scope, - scope_var: 'inventory', - url: GetBasePath('inventory') + updatedJobData.inventory + '/' - }); - } - - if (!Empty(updatedJobData.project)) { - LookUpName({ - scope: $scope, - scope_var: 'project', - url: GetBasePath('projects') + updatedJobData.project + '/' - }); - } - - if (!Empty(updatedJobData.cloud_credential)) { - LookUpName({ - scope: $scope, - scope_var: 'cloud_credential', - url: GetBasePath('credentials') + updatedJobData.cloud_credential + '/', - ignore_403: true - }); - } - - if (!Empty(updatedJobData.inventory_source)) { - LookUpName({ - scope: $scope, - scope_var: 'inventory_source', - url: GetBasePath('inventory_sources') + updatedJobData.inventory_source + '/', - callback: 'getInventorySource' - }); - } - - if (updatedJobData.extra_vars) { - ParseTypeChange({ - scope: $scope, - field_id: 'pre-formatted-variables', - readOnly: true - }); - } - - // If the job isn't running we want to clear out the interval that goes out and checks for stdout updates. - // This interval is defined in the standard out log directive controller. - if (updatedJobData.status === 'successful' || updatedJobData.status === 'failed' || updatedJobData.status === 'error' || updatedJobData.status === 'canceled') { - if ($rootScope.jobStdOutInterval) { - window.clearInterval($rootScope.jobStdOutInterval); - } - } - - } - - if ($scope.removeDeleteFinished) { - $scope.removeDeleteFinished(); - } - $scope.removeDeleteFinished = $scope.$on('DeleteFinished', function(e, action) { - Wait('stop'); - if (action !== 'cancel') { - Wait('stop'); - $state.go('jobs'); - } - }); - - // TODO: this is currently not used but is necessary for cases where sockets - // are not available and a manual refresh trigger is needed. - $scope.refresh = function(){ - $scope.$emit('LoadStdout'); - }; - - // Click binding for the expand/collapse button on the standard out log - $scope.toggleStdoutFullscreen = function() { - $scope.stdoutFullScreen = !$scope.stdoutFullScreen; - - if ($scope.stdoutFullScreen === true) { - $scope.toggleStdoutFullscreenTooltip = i18n._("Collapse Output"); - } else if ($scope.stdoutFullScreen === false) { - $scope.toggleStdoutFullscreenTooltip = i18n._("Expand Output"); - } - }; - - $scope.deleteJob = function() { - DeleteJob({ - scope: $scope, - id: $scope.job.id, - job: $scope.job, - callback: 'DeleteFinished' - }); - }; - - updateJobObj(jobData); - -} - -JobStdoutController.$inject = [ '$rootScope', '$scope', '$state', - '$stateParams', 'GetBasePath', 'Rest', 'ProcessErrors', - 'Empty', 'GetChoices', 'LookUpName', 'ParseTypeChange', - 'ParseVariableString', 'DeleteJob', 'Wait', 'i18n', - 'fieldChoices', 'fieldLabels', 'ProjectModel', 'Alert', 'InventorySourceModel', - 'jobData']; diff --git a/awx/ui/client/src/standard-out/standard-out.block.less b/awx/ui/client/src/workflow-results/standard-out.block.less similarity index 99% rename from awx/ui/client/src/standard-out/standard-out.block.less rename to awx/ui/client/src/workflow-results/standard-out.block.less index 5fd5742e82..980f401c75 100644 --- a/awx/ui/client/src/standard-out/standard-out.block.less +++ b/awx/ui/client/src/workflow-results/standard-out.block.less @@ -169,4 +169,4 @@ standard-out-log { cursor: pointer; border-radius: 5px; font-size: 11px; -} +} \ No newline at end of file diff --git a/awx/ui/test/spec/job-results/job-results.controller-test.js b/awx/ui/test/spec/job-results/job-results.controller-test.js deleted file mode 100644 index c05f2329f7..0000000000 --- a/awx/ui/test/spec/job-results/job-results.controller-test.js +++ /dev/null @@ -1,701 +0,0 @@ -'use strict'; -import moment from 'moment'; - -describe('Controller: jobResultsController', () => { - // Setup - let jobResultsController; - - let jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTypeChange, ParseVariableString, jobResultsService, eventQueue, $compile, eventResolve, populateResolve, $rScope, q, $log, Dataset, Rest, $state, QuerySet, i18n,fieldChoices, fieldLabels, $interval, workflowResultsService, statusSocket, jobExtraCredentials; - - statusSocket = function() { - var fn = function() {}; - return fn; - }; - jobData = { - related: {}, - summary_fields: { - inventory: { - id: null, - kind: '' - } - } - }; - jobDataOptions = { - actions: { - get: {} - } - }; - jobLabels = {}; - jobFinished = true; - count = { - val: {}, - countFinished: false - }; - eventResolve = { - results: [] - }; - populateResolve = {}; - - Dataset = { - data: {foo: "bar"} - }; - - let provideVals = () => { - angular.mock.module('jobResults', ($provide) => { - ParseTypeChange = jasmine.createSpy('ParseTypeChange'); - ParseVariableString = jasmine.createSpy('ParseVariableString'); - jobResultsService = jasmine.createSpyObj('jobResultsService', [ - 'deleteJob', - 'cancelJob', - 'relaunchJob', - 'getEvents', - 'getJobData', - ]); - eventQueue = jasmine.createSpyObj('eventQueue', [ - 'populate', - 'markProcessed', - 'initialize' - ]); - - Rest = jasmine.createSpyObj('Rest', [ - 'setUrl', - 'get' - ]); - - $state = jasmine.createSpyObj('$state', [ - 'reload' - ]); - - QuerySet = jasmine.createSpyObj('QuerySet', [ - 'encodeQueryset' - ]); - - i18n = { - _: function(txt) { - return txt; - } - }; - - $provide.service('workflowResultsService', () => { - return jasmine.createSpyObj('workflowResultsService', ['createOneSecondTimer', 'destroyTimer']); - }); - - $provide.value('statusSocket', statusSocket); - - $provide.value('jobData', jobData); - $provide.value('jobDataOptions', jobDataOptions); - $provide.value('jobLabels', jobLabels); - $provide.value('jobFinished', jobFinished); - $provide.value('count', count); - $provide.value('ParseTypeChange', ParseTypeChange); - $provide.value('ParseVariableString', ParseVariableString); - $provide.value('jobResultsService', jobResultsService); - $provide.value('eventQueue', eventQueue); - $provide.value('Dataset', Dataset); - $provide.value('Rest', Rest); - $provide.value('$state', $state); - $provide.value('QuerySet', QuerySet); - $provide.value('i18n', i18n); - $provide.value('fieldChoices', fieldChoices); - $provide.value('fieldLabels', fieldLabels); - $provide.value('jobExtraCredentials', jobExtraCredentials); - }); - }; - - let injectVals = () => { - angular.mock.inject((_jobData_, _jobDataOptions_, _jobLabels_, _jobFinished_, _count_, _ParseTypeChange_, _ParseVariableString_, _jobResultsService_, _eventQueue_, _$compile_, $rootScope, $controller, $q, $httpBackend, _$log_, _Dataset_, _Rest_, _$state_, _QuerySet_, _$interval_, _workflowResultsService_, _statusSocket_) => { - // when you call $scope.$apply() (which you need to do to - // to get inside of .then blocks to test), something is - // causing a request for all static files. - // - // this is a hack to just pass those requests through - // - // from googling this is probably due to angular-router - // weirdness - $httpBackend.when("GET", (url) => (url - .indexOf("/static/") !== -1)) - .respond(''); - - $httpBackend - .whenGET('/api') - .respond(200, ''); - - $scope = $rootScope.$new(); - $rScope = $rootScope; - q = $q; - jobData = _jobData_; - jobDataOptions = _jobDataOptions_; - jobLabels = _jobLabels_; - jobFinished = _jobFinished_; - count = _count_; - ParseTypeChange = _ParseTypeChange_; - ParseVariableString = _ParseVariableString_; - ParseVariableString.and.returnValue(jobData.extra_vars); - jobResultsService = _jobResultsService_; - eventQueue = _eventQueue_; - $log = _$log_; - Dataset = _Dataset_; - Rest = _Rest_; - $state = _$state_; - QuerySet = _QuerySet_; - $interval = _$interval_; - workflowResultsService = _workflowResultsService_; - statusSocket = _statusSocket_; - - jobResultsService.getEvents.and - .returnValue(eventResolve); - eventQueue.populate.and - .returnValue(populateResolve); - - jobResultsService.getJobData = function() { - var deferred = $q.defer(); - deferred.resolve({}); - return deferred.promise; - }; - - $compile = _$compile_; - - jobResultsController = $controller('jobResultsController', { - $scope: $scope, - jobData: jobData, - jobDataOptions: jobDataOptions, - jobLabels: jobLabels, - jobFinished: jobFinished, - count: count, - ParseTypeChange: ParseTypeChange, - jobResultsService: jobResultsService, - eventQueue: eventQueue, - $compile: $compile, - $log: $log, - $q: q, - Dataset: Dataset, - Rest: Rest, - $state: $state, - QuerySet: QuerySet, - statusSocket: statusSocket - }); - }); - }; - - beforeEach(angular.mock.module('shared')); - - let bootstrapTest = () => { - provideVals(); - injectVals(); - }; - - describe('bootstrap resolve values on scope', () => { - beforeEach(() => { - bootstrapTest(); - }); - - it('should set values to scope based on resolve', () => { - expect($scope.job).toBe(jobData); - expect($scope.jobOptions).toBe(jobDataOptions.actions.GET); - expect($scope.labels).toBe(jobLabels); - }); - }); - - describe('getLinks()', () => { - beforeEach(() => { - jobData.related = { - "created_by": "api/v2/users/12", - "inventory": "api/v2/inventories/12", - "project": "api/v2/projects/12", - "credential": "api/v2/credentials/12", - "cloud_credential": "api/v2/credentials/13", - "network_credential": "api/v2/credentials/14", - }; - - jobData.summary_fields.inventory = { - id: 12, - kind: '' - }; - - bootstrapTest(); - }); - - it('should transform related links and set to scope var', () => { - expect($scope.created_by_link).toBe('/#/users/12'); - expect($scope.inventory_link).toBe('/#/inventories/inventory/12'); - expect($scope.project_link).toBe('/#/projects/12'); - expect($scope.machine_credential_link).toBe('/#/credentials/12'); - expect($scope.cloud_credential_link).toBe('/#/credentials/13'); - expect($scope.network_credential_link).toBe('/#/credentials/14'); - }); - }); - - describe('getLabels()', () => { - beforeEach(() => { - jobDataOptions.actions.GET = { - status: { - choices: [ - ["new", - "New"] - ] - }, - job_type: { - choices: [ - ["job", - "Playbook Run"] - ] - }, - verbosity: { - choices: [ - [0, - "0 (Normal)"] - ] - } - }; - jobData.status = "new"; - jobData.job_type = "job"; - jobData.verbosity = 0; - - bootstrapTest(); - }); - - it('should set scope variables based on options', () => { - $scope.job_status = jobData.status; - - $scope.$apply(); - expect($scope.status_label).toBe("New"); - expect($scope.type_label).toBe("Playbook Run"); - expect($scope.verbosity_label).toBe("0 (Normal)"); - }); - }); - - describe('elapsed timer', () => { - describe('job running', () => { - beforeEach(() => { - jobData.started = moment(); - jobData.status = 'running'; - - bootstrapTest(); - }); - - it('should start timer', () => { - expect(workflowResultsService.createOneSecondTimer).toHaveBeenCalled(); - }); - }); - - describe('job waiting', () => { - beforeEach(() => { - jobData.started = null; - jobData.status = 'waiting'; - - bootstrapTest(); - }); - - it('should not start timer', () => { - expect(workflowResultsService.createOneSecondTimer).not.toHaveBeenCalled(); - }); - }); - - describe('job transitions to running', () => { - beforeEach(() => { - jobData.started = null; - jobData.status = 'waiting'; - jobData.id = 13; - - bootstrapTest(); - - $rScope.$broadcast('ws-jobs', { unified_job_id: jobData.id, status: 'running' }); - }); - - it('should start timer', () => { - expect(workflowResultsService.createOneSecondTimer).toHaveBeenCalled(); - }); - - describe('job transitions from running to finished', () => { - it('should cleanup timer', () => { - $rScope.$broadcast('ws-jobs', { unified_job_id: jobData.id, status: 'successful' }); - expect(workflowResultsService.destroyTimer).toHaveBeenCalled(); - }); - }); - }); - }); - - describe('extra vars stuff', () => { - let extraVars = "foo"; - - beforeEach(() => { - jobData.extra_vars = extraVars; - - bootstrapTest(); - }); - - it('should have extra vars on scope', () => { - expect($scope.job.extra_vars).toBe(extraVars); - }); - - it('should call ParseVariableString and set to scope', () => { - expect(ParseVariableString) - .toHaveBeenCalledWith(extraVars); - expect($scope.variables).toBe(extraVars); - }); - - it('should set the parse type to yaml', () => { - expect($scope.parseType).toBe('yaml'); - }); - - it('should call ParseTypeChange with proper params', () => { - let params = { - scope: $scope, - field_id: 'pre-formatted-variables', - readOnly: true - }; - - expect(ParseTypeChange) - .toHaveBeenCalledWith(params); - }); - }); - - describe('$scope.toggleStdoutFullscreen', () => { - beforeEach(() => { - bootstrapTest(); - }); - - it('should toggle $scope.stdoutFullScreen', () => { - // essentially set to false - expect($scope.stdoutFullScreen).toBe(false); - - // toggle once to true - $scope.toggleStdoutFullscreen(); - expect($scope.stdoutFullScreen).toBe(true); - - // toggle again to false - $scope.toggleStdoutFullscreen(); - expect($scope.stdoutFullScreen).toBe(false); - }); - }); - - describe('$scope.deleteJob', () => { - beforeEach(() => { - bootstrapTest(); - }); - - it('should delete the job', () => { - let job = $scope.job; - $scope.deleteJob(); - expect(jobResultsService.deleteJob).toHaveBeenCalledWith(job); - }); - }); - - describe('$scope.cancelJob', () => { - beforeEach(() => { - bootstrapTest(); - }); - - it('should cancel the job', () => { - let job = $scope.job; - $scope.cancelJob(); - expect(jobResultsService.cancelJob).toHaveBeenCalledWith(job); - }); - }); - - describe('count stuff', () => { - beforeEach(() => { - count = { - val: { - ok: 1, - skipped: 2, - unreachable: 3, - failures: 4, - changed: 5 - }, - countFinished: true - }; - - bootstrapTest(); - }); - - it('should set count values to scope', () => { - expect($scope.count).toBe(count.val); - expect($scope.countFinished).toBe(true); - }); - - it('should find the hostCount based on the count', () => { - expect($scope.hostCount).toBe(15); - }); - }); - - describe('follow stuff - incomplete', () => { - beforeEach(() => { - jobFinished = false; - - bootstrapTest(); - }); - - it('should set followEngaged based on jobFinished incomplete', () => { - expect($scope.followEngaged).toBe(true); - }); - - it('should set followTooltip based on jobFinished incomplete', () => { - expect($scope.followTooltip).toBe("Currently following standard out as it comes in. Click to unfollow."); - }); - }); - - describe('follow stuff - finished', () => { - beforeEach(() => { - jobFinished = true; - - bootstrapTest(); - }); - - it('should set followEngaged based on jobFinished', () => { - expect($scope.followEngaged).toBe(false); - }); - - it('should set followTooltip based on jobFinished', () => { - expect($scope.followTooltip).toBe("Jump to last line of standard out."); - }); - }); - - describe('event stuff', () => { - beforeEach(() => { - jobData.id = 1; - jobData.related.job_events = "url"; - - bootstrapTest(); - }); - - xit('should make a rest call to get already completed events', () => { - expect(jobResultsService.getEvents).toHaveBeenCalledWith("url"); - }); - - xit('should call processEvent when receiving message', () => { - let eventPayload = {"foo": "bar"}; - $rScope.$broadcast('ws-job_events-1', eventPayload); - expect(eventQueue.populate).toHaveBeenCalledWith(eventPayload); - }); - - it('should set the job status on scope when receiving message', () => { - let eventPayload = { - unified_job_id: 1, - status: 'finished' - }; - $rScope.$broadcast('ws-jobs', eventPayload); - expect($scope.job_status).toBe(eventPayload.status); - }); - }); - - describe('getEvents and populate stuff', () => { - describe('getEvents', () => { - let event1 = { - event: 'foo' - }; - - let event2 = { - event_name: 'bar' - }; - - let event1Processed = { - event_name: 'foo' - }; - - beforeEach(() => { - eventResolve = { - results: [ - event1, - event2 - ] - }; - - bootstrapTest(); - - $scope.$apply(); - }); - - xit('should change the event name to event_name', () => { - expect(eventQueue.populate) - .toHaveBeenCalledWith(event1Processed); - }); - - xit('should pass through the event with event_name', () => { - expect(eventQueue.populate) - .toHaveBeenCalledWith(event2); - }); - - xit('should have called populate twice', () => { - expect(eventQueue.populate.calls.count()).toEqual(2); - }); - - // TODO: can't figure out how to a test of events.next... - // if you set events.next to true it causes the tests to - // stop running - }); - - describe('populate - start time', () => { - beforeEach(() => { - jobData.start = ""; - - populateResolve = { - startTime: 'foo', - changes: ['startTime'] - }; - - bootstrapTest(); - - $scope.$apply(); - }); - - xit('sets start time when passed as a change', () => { - expect($scope.job.start).toBe('foo'); - }); - }); - - describe('populate - start time already set', () => { - beforeEach(() => { - jobData.start = "bar"; - - populateResolve = { - startTime: 'foo', - changes: ['startTime'] - }; - - bootstrapTest(); - - $scope.$apply(); - }); - - xit('does not set start time because already set', () => { - expect($scope.job.start).toBe('bar'); - }); - }); - - describe('populate - count already received', () => { - let receiveCount = { - ok: 2, - skipped: 2, - unreachable: 2, - failures: 2, - changed: 2 - }; - - let alreadyCount = { - ok: 3, - skipped: 3, - unreachable: 3, - failures: 3, - changed: 3 - }; - - beforeEach(() => { - count.countFinished = true; - count.val = alreadyCount; - - populateResolve = { - count: receiveCount, - changes: ['count'] - }; - - bootstrapTest(); - - $scope.$apply(); - }); - - xit('count does not change', () => { - expect($scope.count).toBe(alreadyCount); - expect($scope.hostCount).toBe(15); - }); - }); - - describe('populate - playCount, taskCount and countFinished', () => { - beforeEach(() => { - - populateResolve = { - playCount: 12, - taskCount: 13, - changes: ['playCount', 'taskCount', 'countFinished'] - }; - - bootstrapTest(); - - $scope.$apply(); - }); - - xit('sets playCount', () => { - expect($scope.playCount).toBe(12); - }); - - xit('sets taskCount', () => { - expect($scope.taskCount).toBe(13); - }); - - xit('sets countFinished', () => { - expect($scope.countFinished).toBe(true); - }); - }); - - describe('populate - finishedTime', () => { - beforeEach(() => { - jobData.finished = ""; - - populateResolve = { - finishedTime: "finished_time", - changes: ['finishedTime'] - }; - - bootstrapTest(); - - $scope.$apply(); - }); - - xit('sets finished time and changes follow tooltip', () => { - expect($scope.job.finished).toBe('finished_time'); - expect($scope.jobFinished).toBe(true); - expect($scope.followTooltip) - .toBe("Jump to last line of standard out."); - }); - }); - - describe('populate - finishedTime when already finished', () => { - beforeEach(() => { - jobData.finished = "already_set"; - - populateResolve = { - finishedTime: "finished_time", - changes: ['finishedTime'] - }; - - bootstrapTest(); - - $scope.$apply(); - }); - - xit('does not set finished time because already set', () => { - expect($scope.job.finished).toBe('already_set'); - expect($scope.jobFinished).toBe(true); - expect($scope.followTooltip) - .toBe("Jump to last line of standard out."); - }); - }); - - describe('populate - stdout', () => { - beforeEach(() => { - - populateResolve = { - counter: 12, - stdout: "line", - changes: ['stdout'] - }; - - bootstrapTest(); - - spyOn($log, 'error'); - - $scope.followEngaged = true; - - $scope.$apply(); - }); - - xit('creates new child scope for the event', () => { - expect($scope.events[12].event).toBe(populateResolve); - - // in unit test, followScroll should not be defined as - // directive has not been instantiated - expect($log.error).toHaveBeenCalledWith("follow scroll undefined, standard out directive not loaded yet?"); - }); - }); - }); -}); diff --git a/awx/ui/test/spec/job-results/job-results.service-test.js b/awx/ui/test/spec/job-results/job-results.service-test.js deleted file mode 100644 index 1219f2e450..0000000000 --- a/awx/ui/test/spec/job-results/job-results.service-test.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict'; - -describe('jobResultsService', () => { - let jobResultsService; - - beforeEach(angular.mock.module('awApp')); - - beforeEach(angular.mock.inject(( _jobResultsService_) => { - jobResultsService = _jobResultsService_; - })); - - describe('getCountsFromStatsEvent()', () => { - it('properly counts hosts based on task state', () => { - let event_data = { - "skipped": { - "skipped-host": 5 // this host skipped all 5 tasks - }, - "ok": { - "ok-host": 5, // this host was ok on all 5 tasks - "changed-host": 4 // this host had 4 ok tasks, had 1 changed task - }, - "changed": { - "changed-host": 1 - }, - "failures": { - "failed-host": 1 // this host had a failed task - }, - "dark": { - "unreachable-host": 1 // this host was unreachable - }, - "processed": { - "ok-host": 1, - "changed-host": 1, - "skipped-host": 1, - "failed-host": 1, - "unreachable-host": 1 - }, - "playbook_uuid": "c23d8872-c92a-4e96-9f78-abe6fef38f33", - "playbook": "some_playbook.yml", - }; - expect(jobResultsService.getCountsFromStatsEvent(event_data)).toEqual({ - 'ok': 1, - 'skipped': 1, - 'unreachable': 1, - 'failures': 1, - 'changed': 1 - }); - }); - }); -}); diff --git a/awx/ui/test/spec/job-results/parse-stdout.service-test.js b/awx/ui/test/spec/job-results/parse-stdout.service-test.js deleted file mode 100644 index 86441c1ffa..0000000000 --- a/awx/ui/test/spec/job-results/parse-stdout.service-test.js +++ /dev/null @@ -1,212 +0,0 @@ -'use strict'; - -describe('parseStdoutService', () => { - let parseStdoutService, - log; - - beforeEach(angular.mock.module('awApp')); - - beforeEach(angular.mock.module('jobResults',($provide) => { - log = jasmine.createSpyObj('$log', [ - 'error' - ]); - - $provide.value('$log', log); - })); - - beforeEach(angular.mock.inject((_$log_, _parseStdoutService_) => { - parseStdoutService = _parseStdoutService_; - })); - - describe('prettify()', () => { - it('returns lines of stdout with styling classes', () => { - let line = "[0;32mok: [host-00]", - styledLine = 'ok: [host-00]'; - expect(parseStdoutService.prettify(line)).toBe(styledLine); - }); - - it('can return lines of stdout without styling classes', () => { - let line = "[0;32mok: [host-00][0m", - unstyled = "unstyled", - unstyledLine = 'ok: [host-00]'; - expect(parseStdoutService.prettify(line, unstyled)).toBe(unstyledLine); - }); - - it('can return empty strings', () => { - expect(parseStdoutService.prettify("")).toBe(""); - }); - }); - - describe('getLineClasses()', () => { - it('creates a string that is used as a class', () => { - let headerEvent = { - event_name: 'playbook_on_task_start', - event_data: { - play_uuid:"0f667a23-d9ab-4128-a735-80566bcdbca0", - task_uuid: "80dd087c-268b-45e8-9aab-1083bcfd9364" - } - }; - let lineNum = 3; - let line = "TASK [setup] *******************************************************************"; - let styledLine = " header_task header_task_80dd087c-268b-45e8-9aab-1083bcfd9364 actual_header play_0f667a23-d9ab-4128-a735-80566bcdbca0 line_num_3"; - expect(parseStdoutService.getLineClasses(headerEvent, line, lineNum)).toBe(styledLine); - }); - }); - - describe('getStartTime()', () => { - // TODO: the problem is that the date here calls moment, and thus - // the date will be timezone'd in the string (this could be - // different based on where you are) - xit('creates returns a badge with the start time of the event', () => { - let headerEvent = { - event_name: 'playbook_on_play_start', - created: "2016-11-22T21:15:54.736Z" - }; - - let line = "PLAY [add hosts to inventory] **************************************************"; - let badgeDiv = '
13:15:54
'; - expect(parseStdoutService.getStartTimeBadge(headerEvent, line)).toBe(badgeDiv); - }); - }); - - describe('getCollapseIcon()', () => { - let emptySpan = ` -`; - - it('returns empty expander for non-header event', () => { - let nonHeaderEvent = { - event_name: 'not_header', - start_line: 0, - end_line: 1, - stdout:"line1" - }; - expect(parseStdoutService.getCollapseIcon(nonHeaderEvent)) - .toBe(emptySpan); - }); - - it('returns collapse/decollapse icons for header events', () => { - let headerEvent = { - event_name: 'playbook_on_task_start', - start_line: 0, - end_line: 1, - stdout:"line1", - event_data: { - task_uuid: '1da9012d-18e6-4562-85cd-83cf10a97f86' - } - }; - let line = "TASK [setup] *******************************************************************"; - let expandSpan = ` - - - -`; - - expect(parseStdoutService.getCollapseIcon(headerEvent, line)) - .toBe(expandSpan); - }); - }); - - describe('getLineArr()', () => { - it('returns stdout in array format', () => { - let mockEvent = { - start_line: 12, - end_line: 14, - stdout: "line1\r\nline2\r\n" - }; - let expectedReturn = [[13, "line1"],[14, "line2"]]; - - let returnedEvent = parseStdoutService.getLineArr(mockEvent); - - expect(returnedEvent).toEqual(expectedReturn); - }); - - it('deals correctly with capped lines', () => { - let mockEvent = { - start_line: 7, - end_line: 11, - stdout: "a\r\nb\r\nc..." - }; - let expectedReturn = [[8, "a"],[9, "b"], [10,"c..."]]; - - let returnedEvent = parseStdoutService.getLineArr(mockEvent); - - expect(returnedEvent).toEqual(expectedReturn); - }); - }); - - describe('parseStdout()', () => { - let mockEvent = {"foo": "bar"}; - - it('calls functions', function() { - spyOn(parseStdoutService, 'getLineArr').and - .returnValue([[13, 'line1'], [14, 'line2']]); - spyOn(parseStdoutService, 'getLineClasses').and - .returnValue(""); - spyOn(parseStdoutService, 'getCollapseIcon').and - .returnValue(""); - spyOn(parseStdoutService, 'getAnchorTags').and - .returnValue(""); - spyOn(parseStdoutService, 'prettify').and - .returnValue("prettified_line"); - spyOn(parseStdoutService, 'getStartTimeBadge').and - .returnValue(""); - - parseStdoutService.parseStdout(mockEvent); - - expect(parseStdoutService.getLineArr) - .toHaveBeenCalledWith(mockEvent); - expect(parseStdoutService.getLineClasses) - .toHaveBeenCalledWith(mockEvent, 'line1', 13); - expect(parseStdoutService.getCollapseIcon) - .toHaveBeenCalledWith(mockEvent, 'line1'); - expect(parseStdoutService.getAnchorTags) - .toHaveBeenCalledWith(mockEvent); - expect(parseStdoutService.prettify) - .toHaveBeenCalledWith('line1'); - expect(parseStdoutService.getStartTimeBadge) - .toHaveBeenCalledWith(mockEvent, 'line1'); - - // get line arr should be called once for the event - expect(parseStdoutService.getLineArr.calls.count()) - .toBe(1); - - // other functions should be called twice (once for each - // line) - expect(parseStdoutService.getLineClasses.calls.count()) - .toBe(2); - expect(parseStdoutService.getCollapseIcon.calls.count()) - .toBe(2); - expect(parseStdoutService.getAnchorTags.calls.count()) - .toBe(2); - expect(parseStdoutService.prettify.calls.count()) - .toBe(2); - }); - - it('returns dom-ified lines', function() { - spyOn(parseStdoutService, 'getLineArr').and - .returnValue([[13, 'line1']]); - spyOn(parseStdoutService, 'getLineClasses').and - .returnValue("line_classes"); - spyOn(parseStdoutService, 'getCollapseIcon').and - .returnValue("collapse_icon_dom"); - spyOn(parseStdoutService, 'getAnchorTags').and - .returnValue(`" anchor_tag_dom`); - spyOn(parseStdoutService, 'prettify').and - .returnValue("prettified_line"); - spyOn(parseStdoutService, 'getStartTimeBadge').and - .returnValue(""); - - var returnedString = parseStdoutService.parseStdout(mockEvent); - - var expectedString = ` -
-
collapse_icon_dom13
-
prettified_line
-
`; - expect(returnedString).toBe(expectedString); - }); - }); -}); From 1362b444f2c39ba13ac643f8209dc86e0f6e939c Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 5 Apr 2018 01:35:42 -0400 Subject: [PATCH 89/89] move search tag test --- .../ui/test/e2e/tests/test-search-tag-add-remove.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename test-search-tag-add-remove.js => awx/ui/test/e2e/tests/test-search-tag-add-remove.js (99%) diff --git a/test-search-tag-add-remove.js b/awx/ui/test/e2e/tests/test-search-tag-add-remove.js similarity index 99% rename from test-search-tag-add-remove.js rename to awx/ui/test/e2e/tests/test-search-tag-add-remove.js index db491712e9..294f7aa582 100644 --- a/test-search-tag-add-remove.js +++ b/awx/ui/test/e2e/tests/test-search-tag-add-remove.js @@ -38,6 +38,7 @@ const checkTags = (client, tags) => { module.exports = { before: (client, done) => { const resources = range(25).map(n => getAdminMachineCredential(`test-search-${n}`)); + Promise.all(resources).then(done); }, 'add and remove search tags': client => { @@ -120,4 +121,4 @@ module.exports = { client.end(); }, -}; \ No newline at end of file +};