From 8435290cc4afc291b299efb0f6b415e98d8c373e Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 20 Sep 2016 14:59:39 -0400 Subject: [PATCH 01/57] working commit of job results structure --- awx/ui/client/src/app.js | 2 + .../client/src/job-detail/job-detail.route.js | 96 +++++++++++++++---- awx/ui/client/src/job-detail/main.js | 8 +- .../src/job-results/job-results.route.js | 41 ++++++++ awx/ui/client/src/job-results/main.js | 13 +++ 5 files changed, 136 insertions(+), 24 deletions(-) create mode 100644 awx/ui/client/src/job-results/job-results.route.js create mode 100644 awx/ui/client/src/job-results/main.js diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 3a9f5eee2d..4bcfac154e 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -53,6 +53,7 @@ import organizations from './organizations/main'; import managementJobs from './management-jobs/main'; import jobDetail from './job-detail/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'; @@ -123,6 +124,7 @@ var tower = angular.module('Tower', [ footer.name, jobDetail.name, workflowResults.name, + jobResults.name, jobSubmission.name, notifications.name, standardOut.name, diff --git a/awx/ui/client/src/job-detail/job-detail.route.js b/awx/ui/client/src/job-detail/job-detail.route.js index abd45101a1..dd8a7ff5ba 100644 --- a/awx/ui/client/src/job-detail/job-detail.route.js +++ b/awx/ui/client/src/job-detail/job-detail.route.js @@ -1,26 +1,82 @@ +//<<<<<<< bc59236851902d7c768aa26abdb7dc9c9dc27a5a /************************************************* * Copyright (c) 2016 Ansible, Inc. * * All Rights Reserved *************************************************/ -import { templateUrl } from '../shared/template-url/template-url.factory'; - -export default { - name: 'jobDetail', - url: '/jobs/{id: int}', - ncyBreadcrumb: { - parent: 'jobs', - label: "{{ job.id }} - {{ job.name }}" - }, - data: { - socket: { - "groups": { - "jobs": ["status_changed", "summary"], - "job_events": [] - } - } - }, - templateUrl: templateUrl('job-detail/job-detail'), - controller: 'JobDetailController' -}; +// <<<<<<< a3d9eea2c9ddb4e16deec9ec38dea16bf37c559d +// import { templateUrl } from '../shared/template-url/template-url.factory'; +// +// export default { +// name: 'jobDetail', +// url: '/jobs/{id: int}', +// ncyBreadcrumb: { +// parent: 'jobs', +// label: "{{ job.id }} - {{ job.name }}" +// }, +// data: { +// socket: { +// "groups": { +// "jobs": ["status_changed", "summary"], +// "job_events": [] +// } +// } +// }, +// templateUrl: templateUrl('job-detail/job-detail'), +// controller: 'JobDetailController' +// }; +// ======= +// import {templateUrl} from '../shared/template-url/template-url.factory'; +// +// export default { +// name: 'jobDetail', +// url: '/jobs/:id', +// ncyBreadcrumb: { +// parent: 'jobs', +// label: "{{ job.id }} - {{ job.name }}" +// }, +// socket: { +// "groups":{ +// "jobs": ["status_changed", "summary"], +// "job_events": [] +// } +// }, +// templateUrl: templateUrl('job-detail/job-detail'), +// controller: 'JobDetailController' +// }; +//======= +// /************************************************* +// * Copyright (c) 2016 Ansible, Inc. +// * +// * All Rights Reserved +// *************************************************/ +// +// import {templateUrl} from '../shared/template-url/template-url.factory'; +// +// export default { +// name: 'jobDetail', +// url: '/jobs/:id', +// ncyBreadcrumb: { +// parent: 'jobs', +// label: "{{ job.id }} - {{ job.name }}" +// }, +// resolve: { +// jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { +// if (!$rootScope.event_socket) { +// $rootScope.event_socket = Socket({ +// scope: $rootScope, +// endpoint: "job_events" +// }); +// $rootScope.event_socket.init(); +// // returns should really be providing $rootScope.event_socket +// // otherwise, we have to inject the entire $rootScope into the controller +// return true; +// } else { +// return true; +// } +// }] +// }, +// templateUrl: templateUrl('job-detail/job-detail'), +// controller: 'JobDetailController' +// }; diff --git a/awx/ui/client/src/job-detail/main.js b/awx/ui/client/src/job-detail/main.js index 891bfe373b..1307554b6e 100644 --- a/awx/ui/client/src/job-detail/main.js +++ b/awx/ui/client/src/job-detail/main.js @@ -4,7 +4,7 @@ * All Rights Reserved *************************************************/ -import route from './job-detail.route'; +// import route from './job-detail.route'; import controller from './job-detail.controller'; import service from './job-detail.service'; import hostEvents from './host-events/main'; @@ -19,6 +19,6 @@ export default ]) .controller('JobDetailController', controller) .service('JobDetailService', service) - .run(['$stateExtender', function($stateExtender) { - $stateExtender.addState(route); - }]); + // .run(['$stateExtender', function($stateExtender) { + // $stateExtender.addState(route); + // }]); diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js new file mode 100644 index 0000000000..aac4d48f1d --- /dev/null +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -0,0 +1,41 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import {templateUrl} from '../shared/template-url/template-url.factory'; + +export default { + name: 'jobDetail', + url: '/jobs/:id', + ncyBreadcrumb: { + parent: 'jobs', + label: '{{ job.id }} - {{ job.name }}' + }, + resolve: { + jobData: ['Rest', 'GetBasePath', '$stateParams', '$q', '$state', 'Alert', function(Rest, GetBasePath, $stateParams, $q, $state, Alert) { + Rest.setUrl(GetBasePath('jobs') + $stateParams.id); + var val = $q.defer(); + 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; + }] + }, + templateUrl: templateUrl('job-results/job-results'), + controller: ['jobData', '$scope', function(jobData, $scope) { + $scope.job = jobData; + }] +}; diff --git a/awx/ui/client/src/job-results/main.js b/awx/ui/client/src/job-results/main.js new file mode 100644 index 0000000000..71e2e13c7c --- /dev/null +++ b/awx/ui/client/src/job-results/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import route from './job-results.route.js'; + +export default + angular.module('jobResults', []) + .run(['$stateExtender', function($stateExtender) { + $stateExtender.addState(route); + }]); From c7dfcbba5f6d3f1ba71410c117798c46ad3f018b Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 21 Sep 2016 15:21:54 -0400 Subject: [PATCH 02/57] updated left hand pane of job results to mostly match mockup --- .../src/job-detail/job-detail.block.less | 47 +- .../src/job-results/job-results.partial.html | 446 ++++++++++++++++++ .../src/job-results/job-results.route.js | 94 +++- 3 files changed, 559 insertions(+), 28 deletions(-) create mode 100644 awx/ui/client/src/job-results/job-results.partial.html diff --git a/awx/ui/client/src/job-detail/job-detail.block.less b/awx/ui/client/src/job-detail/job-detail.block.less index a22cf4a8c2..c23b8d321d 100644 --- a/awx/ui/client/src/job-detail/job-detail.block.less +++ b/awx/ui/client/src/job-detail/job-detail.block.less @@ -2,7 +2,7 @@ @import '../shared/branding/colors.less'; @import '../shared/branding/colors.default.less'; -@import '../shared/layouts/one-plus-one.less'; +@import '../shared/layouts/one-plus-two.less'; @breakpoint-md: 1200px; @breakpoint-sm: 623px; @@ -21,7 +21,7 @@ } } .JobDetail{ - .OnePlusOne-container(100%, @breakpoint-md); + .OnePlusTwo-container(100%, @breakpoint-md); &.fullscreen { .JobDetail-rightSide { @@ -31,11 +31,11 @@ } .JobDetail-leftSide{ - .OnePlusOne-panel--left(100%, @breakpoint-md); + .OnePlusTwo-left--panel(100%, @breakpoint-md); } .JobDetail-rightSide{ - .OnePlusOne-panel--right(100%, @breakpoint-md); + .OnePlusTwo-right--panel(100%, @breakpoint-md); @media (max-width: @breakpoint-md - 1px) { padding-right: 15px; } @@ -88,51 +88,44 @@ } .JobDetail-resultRow{ - width: 50%; + width: 100%; display: flex; - @media screen and(max-width: @breakpoint-sm){ - width: 100%; - } + padding-bottom: 10px; + flex-wrap: wrap; +} + +.JobDetail-resultRow--variables { + flex-direction: column; } .JobDetail-resultRowLabel{ text-transform: uppercase; -} - -.JobDetail-resultRow label{ color: @default-interface-txt; font-size: 14px; font-weight: normal!important; - flex: 1 0 auto; + width: 30%; + margin-right: 20px; @media screen and(max-width: @breakpoint-md){ flex: 2.5 0 auto; } } -.JobDetail-resultRow--variables{ +.JobDetail-resultRowLabel--fullWidth { width: 100%; - display: flex; - flex-direction: column; - padding-left:15px; -} - -.JobDetail-extraVars{ - text-transform: none; -} - -.JobDetail-extraVarsLabel{ - margin-left:-15px; - padding-bottom: 15px; + margin-right: 0px; } .JobDetail-resultRowText{ - width: 40%; + width: ~"calc(70% - 20px)"; flex: 1 0 auto; - padding:0px 29px; text-transform: none; word-wrap: break-word; } +.JobDetail-resultRowText--fullWidth { + width: 100%; +} + .JobDetail-searchHeaderRow{ display: flex; flex-wrap: wrap; diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html new file mode 100644 index 0000000000..24f4c60dc4 --- /dev/null +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -0,0 +1,446 @@ +
+
+
+ + +
+
+ + +
+
+ RESULTS +
+
+ + + +
+
+ + +
+ + +
+ +
+ {{ job.started | longDate }} +
+
+ + +
+ +
+ {{ (job.finished | + longDate) || "Not Finished" }} +
+
+ + + + + +
+ +
+ {{ type_label }} +
+
+ + + + + + + + + + + +
+ +
+ {{ job.playbook }} +
+
+ + +
+ + +
+ + + + + + + + +
+ +
+ {{ job.forks }} +
+
+ + +
+ +
+ {{ job.limit }} +
+
+ + +
+ +
+ {{ verbosity_label }} +
+
+ + +
+ +
+ {{ job.job_tags }} +
+
+ + +
+ +
+ {{ job.skip_tags }} +
+
+ + +
+ + +
+ + +
+ +
+
+
+
+ {{ label }} +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+
+
+
+
+
diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index aac4d48f1d..d478146b4a 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -32,10 +32,102 @@ export default { $state.go('jobs'); }); return val.promise; + }], + jobLabels: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) { + var getNext = function(data, arr, resolve) { + Rest.setUrl(data.next); + Rest.get() + .success(function (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() + .success(function(data) { + if (data.next) { + getNext(data, data.results, seeMoreResolve); + } else { + seeMoreResolve.resolve(data.results + .map(val => val.name)); + } + }); + + return seeMoreResolve.promise; + }], + 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; }] }, templateUrl: templateUrl('job-results/job-results'), - controller: ['jobData', '$scope', function(jobData, $scope) { + controller: ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeChange', 'ParseVariableString', function(jobData, jobDataOptions, jobLabels, $scope, ParseTypeChange, ParseVariableString) { + + var getTowerLinks = function() { + var getTowerLink = function(key) { + if ($scope.job.related[key]) { + return '/#/' + $scope.job.related[key] + .split('api/v1/')[1]; + } else { + return null; + } + }; + + $scope.job_template_link = getTowerLink('job_template'); + $scope.created_by_link = getTowerLink('created_by'); + $scope.inventory_link = getTowerLink('inventory'); + $scope.project_link = getTowerLink('project'); + $scope.machine_credential_link = getTowerLink('credential'); + $scope.cloud_credential_link = getTowerLink('cloud_credential'); + $scope.network_credential_link = getTowerLink('network_credential'); + }; + + var getTowerLabels = function() { + var getTowerLabel = 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.status_label = getTowerLabel('status'); + $scope.type_label = getTowerLabel('job_type'); + $scope.verbosity_label = getTowerLabel('verbosity'); + }; + + // put initially resolved request data on scope $scope.job = jobData; + $scope.jobOptions = jobDataOptions.actions.GET; + $scope.labels = jobLabels; + + // turn related api browser routes into tower routes + getTowerLinks(); + + // use options labels to manipulate display of details + getTowerLabels(); + + // 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 }); }] }; From 43e4786c7f1f88f4a85fcee19c323a9c0ee32ec9 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 21 Sep 2016 13:10:57 -0700 Subject: [PATCH 03/57] First pass at the right side of the new job details --- .../src/job-results/job-results.block.less | 19 ++++++++++++++ .../src/job-results/job-results.partial.html | 25 +++++++++++++++++++ .../src/job-results/job-results.route.js | 9 +++++++ 3 files changed, 53 insertions(+) create mode 100644 awx/ui/client/src/job-results/job-results.block.less diff --git a/awx/ui/client/src/job-results/job-results.block.less b/awx/ui/client/src/job-results/job-results.block.less new file mode 100644 index 0000000000..c140d2e113 --- /dev/null +++ b/awx/ui/client/src/job-results/job-results.block.less @@ -0,0 +1,19 @@ + +@import '../shared/branding/colors.less'; +@import '../shared/branding/colors.default.less'; + + +.JobResults-badgeTitle{ + color: #707070; + font-size: 14px; + margin-right: 10px; + font-weight: normal; + text-transform: uppercase; + margin-left: 20px; +} + +.JobResults-badgeRow{ + display:flex; + align-items: center; + margin-right: 5px; +} diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index 24f4c60dc4..32a429b2c8 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -438,9 +438,34 @@ +
+
+
+ + {{job.name}} +
+
+
Plays
{{jobData.playCount || 0}} +
Tasks
{{jobData.taskCount || 0}} +
Hosts
{{jobData.hostCount || 0}} +
Elapsed
{{ job_status.elapsed || "00:00:01"}} +
+
+ + + + +
+
+
+ diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index d478146b4a..8728b8af3d 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -116,6 +116,10 @@ export default { $scope.job = jobData; $scope.jobOptions = jobDataOptions.actions.GET; $scope.labels = jobLabels; + $scope.stdoutFullScreen = false; + + $scope.job_status = {"status": ""}; + $scope.job_status.status = (jobData.status === 'waiting' || jobData.status === 'new') ? 'pending' : jobData.status; // turn related api browser routes into tower routes getTowerLinks(); @@ -129,5 +133,10 @@ export default { ParseTypeChange({ scope: $scope, field_id: 'pre-formatted-variables', readOnly: true }); + + // Click binding for the expand/collapse button on the standard out log + $scope.toggleStdoutFullscreen = function() { + $scope.stdoutFullScreen = !$scope.stdoutFullScreen; + }; }] }; From 29c8ef72e0cd2cbf95913ada01548ea4599deef1 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 28 Sep 2016 14:30:36 -0400 Subject: [PATCH 04/57] integration refactor --- .../src/job-results/job-results.block.less | 107 ++++++- .../src/job-results/job-results.controller.js | 74 +++++ .../src/job-results/job-results.partial.html | 266 +++++++++++------- .../src/job-results/job-results.route.js | 67 +---- .../src/job-results/job-results.service.js | 103 +++++++ awx/ui/client/src/job-results/main.js | 4 +- 6 files changed, 439 insertions(+), 182 deletions(-) create mode 100644 awx/ui/client/src/job-results/job-results.controller.js create mode 100644 awx/ui/client/src/job-results/job-results.service.js diff --git a/awx/ui/client/src/job-results/job-results.block.less b/awx/ui/client/src/job-results/job-results.block.less index c140d2e113..fa1e139501 100644 --- a/awx/ui/client/src/job-results/job-results.block.less +++ b/awx/ui/client/src/job-results/job-results.block.less @@ -1,19 +1,110 @@ - @import '../shared/branding/colors.less'; @import '../shared/branding/colors.default.less'; +@import '../shared/layouts/one-plus-two.less'; +@breakpoint-md: 1200px; +@breakpoint-sm: 623px; + +.JobResults { + .OnePlusTwo-container(100%, @breakpoint-md); + + &.fullscreen { + .JobResults-rightSide { + max-width: 100%; + } + } +} + +.JobResults-leftSide { + .OnePlusTwo-left--panel(100%, @breakpoint-md); +} + +.JobResults-rightSide { + .OnePlusTwo-right--panel(100%, @breakpoint-md); + + @media (max-width: @breakpoint-md - 1px) { + padding-right: 15px; + } +} + +.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-resultRow { + width: 100%; + display: flex; + padding-bottom: 10px; + flex-wrap: wrap; +} + +.JobResults-resultRow--variables { + flex-direction: column; +} + +.JobResults-resultRowLabel { + text-transform: uppercase; + color: @default-interface-txt; + font-size: 14px; + 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-statusResultIcon { + padding-left: 0px; + padding-right: 10px; +} + +.JobResults-badgeRow { + display: flex; + align-items: center; + margin-right: 5px; +} .JobResults-badgeTitle{ - color: #707070; + color: @default-interface-txt; font-size: 14px; margin-right: 10px; font-weight: normal; text-transform: uppercase; margin-left: 20px; } - -.JobResults-badgeRow{ - display:flex; - align-items: center; - margin-right: 5px; -} diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js new file mode 100644 index 0000000000..843d5936a6 --- /dev/null +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -0,0 +1,74 @@ +export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', function(jobData, jobDataOptions, jobLabels, $scope, ParseTypeChange, ParseVariableString, jobResultsService) { + var getTowerLinks = function() { + var getTowerLink = function(key) { + if ($scope.job.related[key]) { + return '/#/' + $scope.job.related[key] + .split('api/v1/')[1]; + } else { + return null; + } + }; + + $scope.job_template_link = getTowerLink('job_template'); + $scope.created_by_link = getTowerLink('created_by'); + $scope.inventory_link = getTowerLink('inventory'); + $scope.project_link = getTowerLink('project'); + $scope.machine_credential_link = getTowerLink('credential'); + $scope.cloud_credential_link = getTowerLink('cloud_credential'); + $scope.network_credential_link = getTowerLink('network_credential'); + }; + + var getTowerLabels = function() { + var getTowerLabel = 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.status_label = getTowerLabel('status'); + $scope.type_label = getTowerLabel('job_type'); + $scope.verbosity_label = getTowerLabel('verbosity'); + }; + + // put initially resolved request data on scope + $scope.job = jobData; + $scope.jobOptions = jobDataOptions.actions.GET; + $scope.labels = jobLabels; + + // turn related api browser routes into tower routes + getTowerLinks(); + + // use options labels to manipulate display of details + getTowerLabels(); + + // 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; + }; + + $scope.deleteJob = function() { + jobResultsService.deleteJob($scope.job); + }; + + $scope.cancelJob = function() { + jobResultsService.cancelJob($scope.job); + }; + + // Set the job status + // TODO: pull from websockets + $scope.job_status = {"status": ""}; + $scope.job_status.status = (jobData.status === 'waiting' || + jobData.status === 'new') ? 'pending' : jobData.status; +}]; diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index 32a429b2c8..038998ef14 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -1,27 +1,28 @@
- diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index 8728b8af3d..eb6d72b1a7 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -6,6 +6,8 @@ import {templateUrl} from '../shared/template-url/template-url.factory'; +import jobResultsController from './job-results.controller'; + export default { name: 'jobDetail', url: '/jobs/:id', @@ -75,68 +77,5 @@ export default { }] }, templateUrl: templateUrl('job-results/job-results'), - controller: ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeChange', 'ParseVariableString', function(jobData, jobDataOptions, jobLabels, $scope, ParseTypeChange, ParseVariableString) { - - var getTowerLinks = function() { - var getTowerLink = function(key) { - if ($scope.job.related[key]) { - return '/#/' + $scope.job.related[key] - .split('api/v1/')[1]; - } else { - return null; - } - }; - - $scope.job_template_link = getTowerLink('job_template'); - $scope.created_by_link = getTowerLink('created_by'); - $scope.inventory_link = getTowerLink('inventory'); - $scope.project_link = getTowerLink('project'); - $scope.machine_credential_link = getTowerLink('credential'); - $scope.cloud_credential_link = getTowerLink('cloud_credential'); - $scope.network_credential_link = getTowerLink('network_credential'); - }; - - var getTowerLabels = function() { - var getTowerLabel = 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.status_label = getTowerLabel('status'); - $scope.type_label = getTowerLabel('job_type'); - $scope.verbosity_label = getTowerLabel('verbosity'); - }; - - // put initially resolved request data on scope - $scope.job = jobData; - $scope.jobOptions = jobDataOptions.actions.GET; - $scope.labels = jobLabels; - $scope.stdoutFullScreen = false; - - $scope.job_status = {"status": ""}; - $scope.job_status.status = (jobData.status === 'waiting' || jobData.status === 'new') ? 'pending' : jobData.status; - - // turn related api browser routes into tower routes - getTowerLinks(); - - // use options labels to manipulate display of details - getTowerLabels(); - - // 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.toggleStdoutFullscreen = function() { - $scope.stdoutFullScreen = !$scope.stdoutFullScreen; - }; - }] + 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 new file mode 100644 index 0000000000..fd4a10f588 --- /dev/null +++ b/awx/ui/client/src/job-results/job-results.service.js @@ -0,0 +1,103 @@ +/************************************************* +* Copyright (c) 2016 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ + + +export default ['Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', function (Prompt, $filter, Wait, Rest, $state, ProcessErrors) { + return { + deleteJob: function(job) { + Prompt({ + hdr: 'Delete Job', + body: `
+ Are you sure you want to delete the job below? +
+
+ #${job.id} ${$filter('sanitize')(job.name)} +
`, + action: function() { + Wait('start'); + Rest.setUrl(job.url); + Rest.destroy() + .success(function() { + Wait('stop'); + $('#prompt-modal').modal('hide'); + $state.go('jobs'); + }) + .error(function(obj, status) { + Wait('stop'); + $('#prompt-modal').modal('hide'); + ProcessErrors(null, obj, status, null, { + hdr: 'Error!', + msg: `Could not delete job. + Returned status: ${status}` + }); + }); + }, + actionText: 'DELETE' + }); + }, + cancelJob: function(job) { + var doCancel = function() { + Rest.setUrl(job.url + 'cancel'); + Rest.post({}) + .success(function() { + Wait('stop'); + $('#prompt-modal').modal('hide'); + }) + .error(function(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: 'Cancel Job', + body: `
+ Are you sure you want to cancel the job below? +
+
+ #${job.id} ${$filter('sanitize')(job.name)} +
`, + action: function() { + Wait('start'); + Rest.setUrl(job.url + 'cancel'); + Rest.get() + .success(function(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.` + }); + } + }); + Rest.destroy() + .success(function() { + Wait('stop'); + $('#prompt-modal').modal('hide'); + }) + .error(function(obj, status) { + Wait('stop'); + $('#prompt-modal').modal('hide'); + ProcessErrors(null, obj, status, null, { + hdr: 'Error!', + msg: `Could not cancel job. + Returned status: ${status}` + }); + }); + }, + actionText: 'CANCEL' + }); + } + }; +}]; diff --git a/awx/ui/client/src/job-results/main.js b/awx/ui/client/src/job-results/main.js index 71e2e13c7c..6a67cd000f 100644 --- a/awx/ui/client/src/job-results/main.js +++ b/awx/ui/client/src/job-results/main.js @@ -5,9 +5,11 @@ *************************************************/ import route from './job-results.route.js'; +import jobResultsService from './job-results.service'; export default angular.module('jobResults', []) .run(['$stateExtender', function($stateExtender) { $stateExtender.addState(route); - }]); + }]) + .service('jobResultsService', jobResultsService); From fcb9d8b6b58acfe1f52dc456a54a579a20a73a72 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Thu, 29 Sep 2016 11:15:13 -0400 Subject: [PATCH 05/57] Adding job results bar files --- .../client/src/job-results/duration.filter.js | 120 ++++++++++++++++++ .../host-status-bar.block.less | 39 ++++++ .../host-status-bar.directive.js | 22 ++++ .../host-status-bar.partial.html | 7 + .../src/job-results/host-status-bar/main.js | 11 ++ .../src/job-results/job-results.controller.js | 28 +++- .../src/job-results/job-results.partial.html | 6 +- .../src/job-results/job-results.route.js | 14 ++ .../src/job-results/job-results.service.js | 64 +++++++++- awx/ui/client/src/job-results/main.js | 7 +- 10 files changed, 305 insertions(+), 13 deletions(-) create mode 100644 awx/ui/client/src/job-results/duration.filter.js create mode 100644 awx/ui/client/src/job-results/host-status-bar/host-status-bar.block.less create mode 100644 awx/ui/client/src/job-results/host-status-bar/host-status-bar.directive.js create mode 100644 awx/ui/client/src/job-results/host-status-bar/host-status-bar.partial.html create mode 100644 awx/ui/client/src/job-results/host-status-bar/main.js diff --git a/awx/ui/client/src/job-results/duration.filter.js b/awx/ui/client/src/job-results/duration.filter.js new file mode 100644 index 0000000000..ba23374784 --- /dev/null +++ b/awx/ui/client/src/job-results/duration.filter.js @@ -0,0 +1,120 @@ +// TODO: come back and get this library +export default function() { + var DURATION_FORMATS_SPLIT = /((?:[^ydhms']+)|(?:'(?:[^']|'')*')|(?:y+|d+|h+|m+|s+))(.*)/; + var DURATION_FORMATS = { + 'y': { // years + // "longer" years are not supported + value: 365 * 24 * 60 * 60 * 1000 + }, + 'yy': { + value: 'y', + pad: 2 + }, + 'd': { // days + value: 24 * 60 * 60 * 1000 + }, + 'dd': { + value: 'd', + pad: 2 + }, + 'h': { // hours + value: 60 * 60 * 1000 + }, + 'hh': { // padded hours + value: 'h', + pad: 2 + }, + 'm': { // minutes + value: 60 * 1000 + }, + 'mm': { // padded minutes + value: 'm', + pad: 2 + }, + 's': { // seconds + value: 1000 + }, + 'ss': { // padded seconds + value: 's', + pad: 2 + }, + 'sss': { // milliseconds + value: 1 + }, + 'ssss': { // padded milliseconds + value: 'sss', + pad: 4 + } + }; + + function _parseFormat(string) { + // @inspiration AngularJS date filter + var parts = []; + var format = string; + while(format) { + var match = DURATION_FORMATS_SPLIT.exec(format); + if (match) { + parts = parts.concat(match.slice(1)); + + format = parts.pop(); + } else { + parts.push(format); + format = null; + } + } + return parts; + } + function _formatDuration(timestamp, format) { + var text = ''; + var values = { }; + format.filter(function(format) { // filter only value parts of format + return DURATION_FORMATS.hasOwnProperty(format); + }).map(function(format) { // get formats with values only + var config = DURATION_FORMATS[format]; + if(config.hasOwnProperty('pad')) { + return config.value; + } else { + return format; + } + }).filter(function(format, index, arr) { // remove duplicates + return (arr.indexOf(format) === index); + }).map(function(format) { // get format configurations with values + return angular.extend({ + name: format, + }, DURATION_FORMATS[format]); + }).sort(function(a, b) { // sort formats descending by value + return b.value - a.value; + }).forEach(function(format) { // create values for format parts + var value = values[format.name] = Math.floor(timestamp / format.value); + timestamp = timestamp - (value * format.value); + }); + format.forEach(function(part) { + var format = DURATION_FORMATS[part]; + if(format) { + var value = values[format.value]; + text += (format.hasOwnProperty('pad') ? _padNumber(value, Math.max(format.pad, value.toString().length)) : values[part]); + } else { + text += part.replace(/(^'|'$)/g, '').replace(/''/g, '\''); + } + }); + return text; + } + function _padNumber(number, len) { + return ((new Array(len + 1)).join('0') + number).slice(-len); + } + return function(value, format) { + if (typeof value !== "number") { + return value; + } + + var timestamp = parseInt(value.valueOf(), 10); + if(isNaN(timestamp)) { + return value; + } else { + return _formatDuration( + timestamp, + _parseFormat(format) + ); + } + }; + }; 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 new file mode 100644 index 0000000000..9e85b992a6 --- /dev/null +++ b/awx/ui/client/src/job-results/host-status-bar/host-status-bar.block.less @@ -0,0 +1,39 @@ +@import '../../shared/branding/colors.default.less'; + +.JobResults-hostStatusBar{ + display: flex; + flex: 1 0 auto; + width: 100%; + margin-top: 10px; +} + +.JobResults-hostStatusBar--ok{ + background-color: @default-succ; + display: flex; + flex: 1 0 auto; + height: 5px; +} + +.JobResults-hostStatusBar--changed{ + background-color: @default-warning; + flex: 1 0 auto; + height: 5px; +} + +.JobResults-hostStatusBar--unreachable{ + background-color: @default-unreachable; + flex: 1 0 auto; + height: 5px; +} + +.JobResults-hostStatusBar--failures{ + background-color: @default-err; + flex: 1 0 auto; + height: 5px; +} + +.JobResults-hostStatusBar--skipped{ + background-color: @default-link; + flex: 1 0 auto; + height: 5px; +} 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 new file mode 100644 index 0000000000..e1268336a4 --- /dev/null +++ b/awx/ui/client/src/job-results/host-status-bar/host-status-bar.directive.js @@ -0,0 +1,22 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +// import hostStatusBarController from './host-status-bar.controller'; +export default [ 'templateUrl', + function(templateUrl) { + return { + scope: { + jobData: '=' + }, + templateUrl: templateUrl('job-results/host-status-bar/host-status-bar'), + restrict: 'E', + // controller: standardOutLogController, + link: function(scope) { + // All of our DOM related stuff will go in here + + } + } +}]; 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 new file mode 100644 index 0000000000..bc27f817fb --- /dev/null +++ b/awx/ui/client/src/job-results/host-status-bar/host-status-bar.partial.html @@ -0,0 +1,7 @@ +
+
+
+
+
+
+
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 new file mode 100644 index 0000000000..2b17a2e414 --- /dev/null +++ b/awx/ui/client/src/job-results/host-status-bar/main.js @@ -0,0 +1,11 @@ +/************************************************* + * 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.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index 843d5936a6..65b7d86080 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -1,4 +1,4 @@ -export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', function(jobData, jobDataOptions, jobLabels, $scope, ParseTypeChange, ParseVariableString, jobResultsService) { +export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', '$rootScope', function(jobData, jobDataOptions, jobLabels, $scope, ParseTypeChange, ParseVariableString, jobResultsService, $rootScope) { var getTowerLinks = function() { var getTowerLink = function(key) { if ($scope.job.related[key]) { @@ -57,7 +57,7 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh $scope.toggleStdoutFullscreen = function() { $scope.stdoutFullScreen = !$scope.stdoutFullScreen; }; - + $scope.deleteJob = function() { jobResultsService.deleteJob($scope.job); }; @@ -66,9 +66,23 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh jobResultsService.cancelJob($scope.job); }; - // Set the job status - // TODO: pull from websockets - $scope.job_status = {"status": ""}; - $scope.job_status.status = (jobData.status === 'waiting' || - jobData.status === 'new') ? 'pending' : jobData.status; + $rootScope.event_socket.on("job_events-" + $scope.job.id, function(data) { + console.log(data); + if(data.event_name === "playbook_on_stats"){ + // get the data for populating the host status bar + jobResultsService.getHostStatusBarCounts(data.event_data); + } + + }); + + $rootScope.$on('JobStatusChange-jobDetails', function(e, data) { + console.log(data); + if (parseInt(data.unified_job_id, 10) === parseInt($scope.job.id,10)) { + $scope.job.status = data.status; + + // $scope.job.elapsed = data.elapsed; + } + }); + + }]; diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index 038998ef14..3300afa695 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -447,7 +447,7 @@
+ fa icon-job-{{ job.status }}"> {{ job.name }}
@@ -483,7 +483,7 @@ Elapsed
- {{ job_status.elapsed || "00:00:01"}} + {{ job.elapsed * 1000 | duration: "hh:mm:ss" }}
@@ -513,6 +513,8 @@
+ + diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index eb6d72b1a7..5268acd0fa 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -74,6 +74,20 @@ export default { val.reject(data); }); return val.promise; + }], + jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { + if (!$rootScope.event_socket) { + $rootScope.event_socket = Socket({ + scope: $rootScope, + endpoint: "job_events" + }); + $rootScope.event_socket.init(); + // returns should really be providing $rootScope.event_socket + // otherwise, we have to inject the entire $rootScope into the controller + return true; + } else { + return true; + } }] }, templateUrl: templateUrl('job-results/job-results'), diff --git a/awx/ui/client/src/job-results/job-results.service.js b/awx/ui/client/src/job-results/job-results.service.js index fd4a10f588..57016653c4 100644 --- a/awx/ui/client/src/job-results/job-results.service.js +++ b/awx/ui/client/src/job-results/job-results.service.js @@ -5,8 +5,67 @@ *************************************************/ -export default ['Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', function (Prompt, $filter, Wait, Rest, $state, ProcessErrors) { - return { +export default ['Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', '$rootScope', function (Prompt, $filter, Wait, Rest, $state, ProcessErrors, $rootScope) { + var val = { + getHostStatusBarCounts: function(event_data) { + var hosts = {}; + + // 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 === "failed") { + // array of hosts from failed type + var hostsArr = Object.keys(event_data[key]); + hostsArr.forEach(host => { + if (!hosts[host]) { + // host has not been added to hosts object + // add now + hosts[host] = {}; + } + + hosts[host][key] = event_data[key][host]; + }); + } else { + // array of hosts from each type ("changed", "dark", etc.) + var 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]; + }); + } + }); + + // use the hosts data populate above to get the count + var count = { + ok : _.filter(hosts, function(o){ + return !o.failures && !o.changed && o.ok > 0; + }), + skipped : _.filter(hosts, function(o){ + return o.skipped > 0; + }), + unreachable : _.filter(hosts, function(o){ + return o.dark > 0; + }), + failures : _.filter(hosts, function(o){ + return o.failed === true; + }), + changed : _.filter(hosts, function(o){ + return o.changed > 0; + }) + }; + + return count; + }, deleteJob: function(job) { Prompt({ hdr: 'Delete Job', @@ -100,4 +159,5 @@ export default ['Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', }); } }; + return val; }]; diff --git a/awx/ui/client/src/job-results/main.js b/awx/ui/client/src/job-results/main.js index 6a67cd000f..104ca76086 100644 --- a/awx/ui/client/src/job-results/main.js +++ b/awx/ui/client/src/job-results/main.js @@ -6,10 +6,13 @@ import route from './job-results.route.js'; import jobResultsService from './job-results.service'; +import hostStatusBarDirective from './host-status-bar/main'; +import durationFilter from './duration.filter'; export default - angular.module('jobResults', []) + angular.module('jobResults', [hostStatusBarDirective.name]) .run(['$stateExtender', function($stateExtender) { $stateExtender.addState(route); }]) - .service('jobResultsService', jobResultsService); + .service('jobResultsService', jobResultsService) + .filter('duration', durationFilter); From 68041ee50d0837e46e69ad43e684d02fdb4100b8 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 29 Sep 2016 16:46:03 -0400 Subject: [PATCH 06/57] fixed the host status bar graph --- .../host-status-bar.block.less | 62 +++++++++++++++---- .../host-status-bar.directive.js | 27 ++++++-- .../host-status-bar.partial.html | 31 ++++++++-- .../src/job-results/job-results.controller.js | 10 +-- .../src/job-results/job-results.service.js | 8 ++- 5 files changed, 108 insertions(+), 30 deletions(-) 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 index 9e85b992a6..36895a6b5b 100644 --- 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 @@ -1,39 +1,75 @@ @import '../../shared/branding/colors.default.less'; -.JobResults-hostStatusBar{ +.HostStatusBar { display: flex; - flex: 1 0 auto; + flex: 0 0 auto; width: 100%; margin-top: 10px; } -.JobResults-hostStatusBar--ok{ +.HostStatusBar-ok { background-color: @default-succ; display: flex; - flex: 1 0 auto; + flex: 0 0 auto; height: 5px; } -.JobResults-hostStatusBar--changed{ +.HostStatusBar-changed { background-color: @default-warning; - flex: 1 0 auto; + flex: 0 0 auto; height: 5px; } -.JobResults-hostStatusBar--unreachable{ +.HostStatusBar-unreachable { background-color: @default-unreachable; - flex: 1 0 auto; + flex: 0 0 auto; height: 5px; } -.JobResults-hostStatusBar--failures{ +.HostStatusBar-failures { background-color: @default-err; + flex: 0 0 auto; + height: 5px; +} + +.HostStatusBar-skipped { + background-color: @default-link; + flex: 0 0 auto; + height: 5px; +} + +.HostStatusBar-noData { + background-color: @default-icon-hov; flex: 1 0 auto; height: 5px; } -.JobResults-hostStatusBar--skipped{ - background-color: @default-link; - flex: 1 0 auto; - height: 5px; +.HostStatusBar-tooltipLabel { + text-transform: uppercase; + margin-right: 15px; +} + +.HostStatusBar-tooltipBadge { + border-radius: 5px; +} + +.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--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 index e1268336a4..3a889b0ba8 100644 --- 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 @@ -8,15 +8,32 @@ export default [ 'templateUrl', function(templateUrl) { return { - scope: { - jobData: '=' - }, + scope: true, templateUrl: templateUrl('job-results/host-status-bar/host-status-bar'), restrict: 'E', // controller: standardOutLogController, link: function(scope) { - // All of our DOM related stuff will go in here + // as count is changed by event data coming in, + // update the host status bar + scope.$watch('count', function(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.hostsFinished = (Object + .keys(val).filter(key => (val[key] > 0)).length > 0); + }); } - } + }; }]; 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 index bc27f817fb..24a9170bcd 100644 --- 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 @@ -1,7 +1,26 @@ -
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index 65b7d86080..69a9ac8056 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -58,6 +58,9 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh $scope.stdoutFullScreen = !$scope.stdoutFullScreen; }; + // Initially set the count data to have no hosts as finsihed + $scope.count = {ok: 0, skipped: 0, unreachable: 0, failures: 0, changed: 0}; + $scope.deleteJob = function() { jobResultsService.deleteJob($scope.job); }; @@ -67,20 +70,17 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh }; $rootScope.event_socket.on("job_events-" + $scope.job.id, function(data) { - console.log(data); if(data.event_name === "playbook_on_stats"){ // get the data for populating the host status bar - jobResultsService.getHostStatusBarCounts(data.event_data); + $scope.count = jobResultsService + .getHostStatusBarCounts(data.event_data); } }); $rootScope.$on('JobStatusChange-jobDetails', function(e, data) { - console.log(data); if (parseInt(data.unified_job_id, 10) === parseInt($scope.job.id,10)) { $scope.job.status = data.status; - - // $scope.job.elapsed = data.elapsed; } }); diff --git a/awx/ui/client/src/job-results/job-results.service.js b/awx/ui/client/src/job-results/job-results.service.js index 57016653c4..5e311ca579 100644 --- a/awx/ui/client/src/job-results/job-results.service.js +++ b/awx/ui/client/src/job-results/job-results.service.js @@ -5,7 +5,7 @@ *************************************************/ -export default ['Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', '$rootScope', function (Prompt, $filter, Wait, Rest, $state, ProcessErrors, $rootScope) { +export default ['Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', function (Prompt, $filter, Wait, Rest, $state, ProcessErrors) { var val = { getHostStatusBarCounts: function(event_data) { var hosts = {}; @@ -64,6 +64,12 @@ export default ['Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', }) }; + // turn the count into an actual count, rather than a list of host + // names + Object.keys(count).forEach(key => { + count[key] = count[key].length; + }); + return count; }, deleteJob: function(job) { From c4eb6515b1f04dde06453d0f06256dbc6fcfbd8c Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 29 Sep 2016 21:34:36 -0400 Subject: [PATCH 07/57] made status bar utilize completed jobs and started rudimentary event queue --- .../src/job-results/event-queue.service.js | 32 +++++++++++++++ .../src/job-results/job-results.controller.js | 41 +++++++++++++++---- .../src/job-results/job-results.route.js | 3 ++ .../src/job-results/job-results.service.js | 28 +++++++++++-- awx/ui/client/src/job-results/main.js | 9 +++- 5 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 awx/ui/client/src/job-results/event-queue.service.js diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js new file mode 100644 index 0000000000..7d1e29c736 --- /dev/null +++ b/awx/ui/client/src/job-results/event-queue.service.js @@ -0,0 +1,32 @@ +/************************************************* +* Copyright (c) 2016 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ + + +export default [function(){ + var val = { + queue: {}, + // munge the raw event from the backend into the event_queue's format + mungeEvent: function(event) { + event.processed = false; + return event; + }, + // reinitializes the event queue value for the job results page + initialize: function() { + val.queue = {}; + }, + // populates the event queue + populate: function(event) { + var mungedEvent = val.mungeEvent(event); + val.queue[event.id] = mungedEvent; + }, + // the event has been processed in the view and should be marked as + // completed in the queue + markProcessed: function(event) { + val.queue[event.id].processed = true; + } + }; + return val; +}]; diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index 69a9ac8056..9c81bda573 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -1,4 +1,22 @@ -export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', '$rootScope', function(jobData, jobDataOptions, jobLabels, $scope, ParseTypeChange, ParseVariableString, jobResultsService, $rootScope) { +export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', '$rootScope', 'eventQueue', function(jobData, jobDataOptions, jobLabels, $scope, ParseTypeChange, ParseVariableString, jobResultsService, $rootScope, eventQueue) { + // just putting the event queue on scope so it can be inspected in the + // console + $scope.event_queue = eventQueue.queue; + + var processEvent = function(event) { + // put the event in the queue + eventQueue.populate(event); + + if(event.event_name === "playbook_on_stats"){ + // get the data for populating the host status bar + $scope.count = jobResultsService + .getHostStatusBarCounts(event.event_data); + + // mark the event as processed in the queue; + eventQueue.markProcessed(event); + } + } + var getTowerLinks = function() { var getTowerLink = function(key) { if ($scope.job.related[key]) { @@ -69,20 +87,27 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh jobResultsService.cancelJob($scope.job); }; + // grab completed event data and process each event + jobResultsService.getEvents($scope.job) + .then(events => { + events.forEach(event => { + // get the name in the same format as the data + // coming over the websocket + event.event_name = event.event; + processEvent(event); + }); + }) + + // process incoming job events $rootScope.event_socket.on("job_events-" + $scope.job.id, function(data) { - if(data.event_name === "playbook_on_stats"){ - // get the data for populating the host status bar - $scope.count = jobResultsService - .getHostStatusBarCounts(data.event_data); - } + processEvent(data); }); + // process incoming job status changes $rootScope.$on('JobStatusChange-jobDetails', function(e, data) { if (parseInt(data.unified_job_id, 10) === parseInt($scope.job.id,10)) { $scope.job.status = data.status; } }); - - }]; diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index 5268acd0fa..023fa237d5 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -88,6 +88,9 @@ export default { } else { return true; } + }], + eventQueueInit: ['eventQueue', function(eventQueue) { + eventQueue.initialize(); }] }, templateUrl: templateUrl('job-results/job-results'), diff --git a/awx/ui/client/src/job-results/job-results.service.js b/awx/ui/client/src/job-results/job-results.service.js index 5e311ca579..76ee2bca94 100644 --- a/awx/ui/client/src/job-results/job-results.service.js +++ b/awx/ui/client/src/job-results/job-results.service.js @@ -5,10 +5,11 @@ *************************************************/ -export default ['Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', function (Prompt, $filter, Wait, Rest, $state, ProcessErrors) { +export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', function ($q, Prompt, $filter, Wait, Rest, $state, ProcessErrors) { var val = { getHostStatusBarCounts: function(event_data) { - var hosts = {}; + var hosts = {}, + hostsArr; // iterate over the event_data and populate an object with hosts // and their status data @@ -16,7 +17,7 @@ export default ['Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', // failed passes boolean not integer if (key === "failed") { // array of hosts from failed type - var hostsArr = Object.keys(event_data[key]); + hostsArr = Object.keys(event_data[key]); hostsArr.forEach(host => { if (!hosts[host]) { // host has not been added to hosts object @@ -28,7 +29,7 @@ export default ['Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', }); } else { // array of hosts from each type ("changed", "dark", etc.) - var hostsArr = Object.keys(event_data[key]); + hostsArr = Object.keys(event_data[key]); hostsArr.forEach(host => { if (!hosts[host]) { // host has not been added to hosts object @@ -72,6 +73,25 @@ export default ['Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', return count; }, + getEvents: function(job) { + var val = $q.defer(); + + Rest.setUrl(job.related.job_events); + Rest.get() + .success(function(data) { + val.resolve(data.results); + }) + .error(function(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: 'Delete Job', diff --git a/awx/ui/client/src/job-results/main.js b/awx/ui/client/src/job-results/main.js index 104ca76086..71f08b09be 100644 --- a/awx/ui/client/src/job-results/main.js +++ b/awx/ui/client/src/job-results/main.js @@ -4,15 +4,20 @@ * All Rights Reserved *************************************************/ +import hostStatusBar from './host-status-bar/main'; + import route from './job-results.route.js'; + import jobResultsService from './job-results.service'; -import hostStatusBarDirective from './host-status-bar/main'; +import eventQueueService from './event-queue.service'; + import durationFilter from './duration.filter'; export default - angular.module('jobResults', [hostStatusBarDirective.name]) + angular.module('jobResults', [hostStatusBar.name]) .run(['$stateExtender', function($stateExtender) { $stateExtender.addState(route); }]) .service('jobResultsService', jobResultsService) + .service('eventQueue', eventQueueService) .filter('duration', durationFilter); From 9f5bec7767e72ca3cb333a65edbb665e93536b14 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 30 Sep 2016 08:51:26 -0400 Subject: [PATCH 08/57] figuring out the event_queue flow --- .../src/job-results/event-queue.service.js | 129 ++++++++++++++++-- .../src/job-results/job-results.controller.js | 45 +++--- .../src/job-results/job-results.service.js | 66 --------- 3 files changed, 143 insertions(+), 97 deletions(-) diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js index 7d1e29c736..6c72b4432c 100644 --- a/awx/ui/client/src/job-results/event-queue.service.js +++ b/awx/ui/client/src/job-results/event-queue.service.js @@ -4,23 +4,135 @@ * All Rights Reserved *************************************************/ - export default [function(){ - var val = { + var val = {}; + + // the playbook_on_stats event returns the count data in a weird format. + // format to what we need! + var 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 === "failed") { + // array of hosts from failed type + hostsArr = Object.keys(event_data[key]); + hostsArr.forEach(host => { + if (!hosts[host]) { + // host has not been added to hosts object + // add now + hosts[host] = {}; + } + + hosts[host][key] = event_data[key][host]; + }); + } else { + // 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]; + }); + } + }); + + // use the hosts data populate above to get the count + var count = { + ok : _.filter(hosts, function(o){ + return !o.failures && !o.changed && o.ok > 0; + }), + skipped : _.filter(hosts, function(o){ + return o.skipped > 0; + }), + unreachable : _.filter(hosts, function(o){ + return o.dark > 0; + }), + failures : _.filter(hosts, function(o){ + return o.failed === true; + }), + changed : _.filter(hosts, function(o){ + return o.changed > 0; + }) + }; + + // turn the count into an actual count, rather than a list of host + // names + Object.keys(count).forEach(key => { + count[key] = count[key].length; + }); + + return count; + }; + + // munge the raw event from the backend into the event_queue's format + var mungeEvent = function(event) { + var mungedEvent = { + id: event.id, + processed: false + }; + if (event.event_name === 'playbook_on_start') { + event.count = val.queue.count; + mungedEvent.changes = ['count']; + } else if (event.event_name === 'runner_on_ok' || + event.event_name === 'runner_on_async_ok') { + val.queue.count.ok++; + event.count = val.queue.count; + mungedEvent.changes = ['count']; + } else if (event.event_name === 'runner_on_skipped') { + val.queue.count.skipped++; + event.count = val.queue.count; + mungedEvent.changes = ['count']; + } else if (event.event_name === 'runner_on_unreachable') { + val.queue.count.unreachable++; + event.count = val.queue.count; + mungedEvent.changes = ['count']; + } else if (event.event_name === 'runner_on_error' || + event.event_name === 'runner_on_async_failed') { + val.queue.count.failed++; + event.count = val.queue.count; + mungedEvent.changes = ['count']; + } else if (event.event_name === 'playbook_on_stats') { + // get the data for populating the host status bar + val.queue.count = getCountsFromStatsEvent(event.event_data); + event.count = val.queue.count; + mungedEvent.changes = ['count']; + } + return mungedEvent; + }; + + val = { queue: {}, - // munge the raw event from the backend into the event_queue's format - mungeEvent: function(event) { - event.processed = false; - return event; - }, // reinitializes the event queue value for the job results page initialize: function() { val.queue = {}; + + // initialize the host status counts + val.queue.count = { + ok: 0, + skipped: 0, + unreachable: 0, + failures: 0, + changed: 0 + }; }, // populates the event queue populate: function(event) { - var mungedEvent = val.mungeEvent(event); + var mungedEvent = mungeEvent(event); val.queue[event.id] = mungedEvent; + + return mungedEvent; }, // the event has been processed in the view and should be marked as // completed in the queue @@ -28,5 +140,6 @@ export default [function(){ val.queue[event.id].processed = true; } }; + return val; }]; diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index 9c81bda573..63ffe6d2b1 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -1,22 +1,4 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', '$rootScope', 'eventQueue', function(jobData, jobDataOptions, jobLabels, $scope, ParseTypeChange, ParseVariableString, jobResultsService, $rootScope, eventQueue) { - // just putting the event queue on scope so it can be inspected in the - // console - $scope.event_queue = eventQueue.queue; - - var processEvent = function(event) { - // put the event in the queue - eventQueue.populate(event); - - if(event.event_name === "playbook_on_stats"){ - // get the data for populating the host status bar - $scope.count = jobResultsService - .getHostStatusBarCounts(event.event_data); - - // mark the event as processed in the queue; - eventQueue.markProcessed(event); - } - } - var getTowerLinks = function() { var getTowerLink = function(key) { if ($scope.job.related[key]) { @@ -76,9 +58,6 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh $scope.stdoutFullScreen = !$scope.stdoutFullScreen; }; - // Initially set the count data to have no hosts as finsihed - $scope.count = {ok: 0, skipped: 0, unreachable: 0, failures: 0, changed: 0}; - $scope.deleteJob = function() { jobResultsService.deleteJob($scope.job); }; @@ -87,6 +66,27 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh jobResultsService.cancelJob($scope.job); }; + // EVENT STUFF BELOW + + // just putting the event queue on scope so it can be inspected in the + // console + $scope.event_queue = eventQueue.queue; + + var processEvent = function(event) { + // put the event in the queue + var mungedEvent = eventQueue.populate(event); + + // make changes to ui based on the event returned from the queue + mungedEvent.changes.forEach(change => { + if (change === 'count') { + $scope.count = mungedEvent.count; + } + }); + + // the changes have been processed in the ui, mark it in the queue + eventQueue.markProcessed(event); + }; + // grab completed event data and process each event jobResultsService.getEvents($scope.job) .then(events => { @@ -96,12 +96,11 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh event.event_name = event.event; processEvent(event); }); - }) + }); // process incoming job events $rootScope.event_socket.on("job_events-" + $scope.job.id, function(data) { processEvent(data); - }); // process incoming job status changes diff --git a/awx/ui/client/src/job-results/job-results.service.js b/awx/ui/client/src/job-results/job-results.service.js index 76ee2bca94..c271f7dd3c 100644 --- a/awx/ui/client/src/job-results/job-results.service.js +++ b/awx/ui/client/src/job-results/job-results.service.js @@ -7,72 +7,6 @@ export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', function ($q, Prompt, $filter, Wait, Rest, $state, ProcessErrors) { var val = { - getHostStatusBarCounts: 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 === "failed") { - // array of hosts from failed type - hostsArr = Object.keys(event_data[key]); - hostsArr.forEach(host => { - if (!hosts[host]) { - // host has not been added to hosts object - // add now - hosts[host] = {}; - } - - hosts[host][key] = event_data[key][host]; - }); - } else { - // 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]; - }); - } - }); - - // use the hosts data populate above to get the count - var count = { - ok : _.filter(hosts, function(o){ - return !o.failures && !o.changed && o.ok > 0; - }), - skipped : _.filter(hosts, function(o){ - return o.skipped > 0; - }), - unreachable : _.filter(hosts, function(o){ - return o.dark > 0; - }), - failures : _.filter(hosts, function(o){ - return o.failed === true; - }), - changed : _.filter(hosts, function(o){ - return o.changed > 0; - }) - }; - - // turn the count into an actual count, rather than a list of host - // names - Object.keys(count).forEach(key => { - count[key] = count[key].length; - }); - - return count; - }, getEvents: function(job) { var val = $q.defer(); From e31bfa2f1c247d15923b9a142e4353fda7a3461d Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 30 Sep 2016 09:45:32 -0400 Subject: [PATCH 09/57] make the event_queue actually work --- .../src/job-results/event-queue.service.js | 56 ++++++++++++------- .../host-status-bar.directive.js | 34 ++++++----- .../src/job-results/job-results.controller.js | 12 ++-- 3 files changed, 61 insertions(+), 41 deletions(-) diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js index 6c72b4432c..6a363d3e6c 100644 --- a/awx/ui/client/src/job-results/event-queue.service.js +++ b/awx/ui/client/src/job-results/event-queue.service.js @@ -76,37 +76,60 @@ export default [function(){ return count; }; + // Get the count of the last event + var getPreviousCount = function(id) { + // get the ids of all the queue + var ids = Object.keys(val.queue).map(id => parseInt(id)); + + // iterate backwards to find the last count + while(ids.indexOf(id - 1) > -1) { + id = id - 1; + if (val.queue[id].count) { + // need to create a new copy of count when returning + // so that it is accurate for the particular event + return _.clone(val.queue[id].count); + } + } + + // no count initialized + return { + ok: 0, + skipped: 0, + unreachable: 0, + failures: 0, + changed: 0 + }; + }; + // munge the raw event from the backend into the event_queue's format var mungeEvent = function(event) { var mungedEvent = { id: event.id, - processed: false + processed: false, + name: event.event_name, + count: getPreviousCount(event.id) }; + if (event.event_name === 'playbook_on_start') { - event.count = val.queue.count; + // sets count initially so this is a change mungedEvent.changes = ['count']; } else if (event.event_name === 'runner_on_ok' || event.event_name === 'runner_on_async_ok') { - val.queue.count.ok++; - event.count = val.queue.count; + mungedEvent.count.ok++; mungedEvent.changes = ['count']; } else if (event.event_name === 'runner_on_skipped') { - val.queue.count.skipped++; - event.count = val.queue.count; + mungedEvent.count.skipped++; mungedEvent.changes = ['count']; } else if (event.event_name === 'runner_on_unreachable') { - val.queue.count.unreachable++; - event.count = val.queue.count; + mungedEvent.count.unreachable++; mungedEvent.changes = ['count']; } else if (event.event_name === 'runner_on_error' || event.event_name === 'runner_on_async_failed') { - val.queue.count.failed++; - event.count = val.queue.count; + mungedEvent.count.failed++; mungedEvent.changes = ['count']; } else if (event.event_name === 'playbook_on_stats') { // get the data for populating the host status bar - val.queue.count = getCountsFromStatsEvent(event.event_data); - event.count = val.queue.count; + mungedEvent.count = getCountsFromStatsEvent(event.event_data); mungedEvent.changes = ['count']; } return mungedEvent; @@ -117,15 +140,6 @@ export default [function(){ // reinitializes the event queue value for the job results page initialize: function() { val.queue = {}; - - // initialize the host status counts - val.queue.count = { - ok: 0, - skipped: 0, - unreachable: 0, - failures: 0, - changed: 0 - }; }, // populates the event queue populate: function(event) { 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 index 3a889b0ba8..f7fbd2e8f6 100644 --- 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 @@ -16,23 +16,27 @@ export default [ 'templateUrl', // as count is changed by event data coming in, // update the host status bar scope.$watch('count', function(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`); + 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]}`; - } - }); + // 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.hostsFinished = (Object - .keys(val).filter(key => (val[key] > 0)).length > 0); + // if there are any hosts that have finished, don't + // show default grey bar + scope.hostsFinished = (Object + .keys(val) + .filter(key => (val[key] > 0)).length > 0); + } }); } }; diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index 63ffe6d2b1..2a51f6691b 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -77,11 +77,13 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh var mungedEvent = eventQueue.populate(event); // make changes to ui based on the event returned from the queue - mungedEvent.changes.forEach(change => { - if (change === 'count') { - $scope.count = mungedEvent.count; - } - }); + if (mungedEvent.changes) { + mungedEvent.changes.forEach(change => { + if (change === 'count') { + $scope.count = mungedEvent.count; + } + }); + } // the changes have been processed in the ui, mark it in the queue eventQueue.markProcessed(event); From 11592047d6512aca3e38e363265f461271cb83f1 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 30 Sep 2016 14:47:08 -0400 Subject: [PATCH 10/57] updates to getting host counts --- .../src/job-results/event-queue.service.js | 109 ++++-------------- .../host-status-bar.block.less | 17 ++- .../src/job-results/job-results.controller.js | 42 +++++-- .../src/job-results/job-results.partial.html | 6 +- .../src/job-results/job-results.route.js | 37 ++++++ .../src/job-results/job-results.service.js | 74 +++++++++++- 6 files changed, 179 insertions(+), 106 deletions(-) diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js index 6a363d3e6c..cd3d037557 100644 --- a/awx/ui/client/src/job-results/event-queue.service.js +++ b/awx/ui/client/src/job-results/event-queue.service.js @@ -4,90 +4,21 @@ * All Rights Reserved *************************************************/ -export default [function(){ +export default ['jobResultsService', function(jobResultsService){ var val = {}; - // the playbook_on_stats event returns the count data in a weird format. - // format to what we need! - var 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 === "failed") { - // array of hosts from failed type - hostsArr = Object.keys(event_data[key]); - hostsArr.forEach(host => { - if (!hosts[host]) { - // host has not been added to hosts object - // add now - hosts[host] = {}; - } - - hosts[host][key] = event_data[key][host]; - }); - } else { - // 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]; - }); - } - }); - - // use the hosts data populate above to get the count - var count = { - ok : _.filter(hosts, function(o){ - return !o.failures && !o.changed && o.ok > 0; - }), - skipped : _.filter(hosts, function(o){ - return o.skipped > 0; - }), - unreachable : _.filter(hosts, function(o){ - return o.dark > 0; - }), - failures : _.filter(hosts, function(o){ - return o.failed === true; - }), - changed : _.filter(hosts, function(o){ - return o.changed > 0; - }) - }; - - // turn the count into an actual count, rather than a list of host - // names - Object.keys(count).forEach(key => { - count[key] = count[key].length; - }); - - return count; - }; - // Get the count of the last event - var getPreviousCount = function(id) { + var getPreviousCount = function(counter) { // get the ids of all the queue - var ids = Object.keys(val.queue).map(id => parseInt(id)); + var counters = Object.keys(val.queue).map(counter => parseInt(counter)); // iterate backwards to find the last count - while(ids.indexOf(id - 1) > -1) { - id = id - 1; - if (val.queue[id].count) { + while(counters.indexOf(counter - 1) > -1) { + counter = counter - 1; + if (val.queue[counter].count) { // need to create a new copy of count when returning // so that it is accurate for the particular event - return _.clone(val.queue[id].count); + return _.clone(val.queue[counter].count); } } @@ -104,33 +35,39 @@ export default [function(){ // munge the raw event from the backend into the event_queue's format var mungeEvent = function(event) { var mungedEvent = { + counter: event.counter, id: event.id, processed: false, - name: event.event_name, - count: getPreviousCount(event.id) + name: event.event_name }; if (event.event_name === 'playbook_on_start') { // sets count initially so this is a change + mungedEvent.count = getPreviousCount(mungedEvent.counter); mungedEvent.changes = ['count']; } else if (event.event_name === 'runner_on_ok' || event.event_name === 'runner_on_async_ok') { + mungedEvent.count = getPreviousCount(mungedEvent.counter); mungedEvent.count.ok++; mungedEvent.changes = ['count']; } else if (event.event_name === 'runner_on_skipped') { + mungedEvent.count = getPreviousCount(mungedEvent.counter); mungedEvent.count.skipped++; mungedEvent.changes = ['count']; } else if (event.event_name === 'runner_on_unreachable') { + mungedEvent.count = getPreviousCount(mungedEvent.counter); mungedEvent.count.unreachable++; mungedEvent.changes = ['count']; } else if (event.event_name === 'runner_on_error' || event.event_name === 'runner_on_async_failed') { + mungedEvent.count = getPreviousCount(mungedEvent.counter); mungedEvent.count.failed++; mungedEvent.changes = ['count']; } else if (event.event_name === 'playbook_on_stats') { // get the data for populating the host status bar - mungedEvent.count = getCountsFromStatsEvent(event.event_data); - mungedEvent.changes = ['count']; + mungedEvent.count = jobResultsService + .getCountsFromStatsEvent(event.event_data); + mungedEvent.changes = ['count', 'countFinished']; } return mungedEvent; }; @@ -143,15 +80,19 @@ export default [function(){ }, // populates the event queue populate: function(event) { - var mungedEvent = mungeEvent(event); - val.queue[event.id] = mungedEvent; + // don't populate the event if it's already been added either + // by rest or by websocket event + if (!val.queue[event.counter]) { + var mungedEvent = mungeEvent(event); + val.queue[mungedEvent.counter] = mungedEvent; - return mungedEvent; + return mungedEvent; + } }, // the event has been processed in the view and should be marked as // completed in the queue markProcessed: function(event) { - val.queue[event.id].processed = true; + val.queue[event.counter].processed = true; } }; 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 index 36895a6b5b..893700ce8a 100644 --- 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 @@ -7,41 +7,46 @@ margin-top: 10px; } +.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; - height: 5px; } .HostStatusBar-changed { background-color: @default-warning; flex: 0 0 auto; - height: 5px; } .HostStatusBar-unreachable { background-color: @default-unreachable; flex: 0 0 auto; - height: 5px; } .HostStatusBar-failures { background-color: @default-err; flex: 0 0 auto; - height: 5px; } .HostStatusBar-skipped { background-color: @default-link; flex: 0 0 auto; - height: 5px; } .HostStatusBar-noData { background-color: @default-icon-hov; flex: 1 0 auto; - height: 5px; } .HostStatusBar-tooltipLabel { diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index 2a51f6691b..af4f250062 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -1,4 +1,4 @@ -export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', '$rootScope', 'eventQueue', function(jobData, jobDataOptions, jobLabels, $scope, ParseTypeChange, ParseVariableString, jobResultsService, $rootScope, eventQueue) { +export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', '$rootScope', 'eventQueue', function(jobData, jobDataOptions, jobLabels, count, $scope, ParseTypeChange, ParseVariableString, jobResultsService, $rootScope, eventQueue) { var getTowerLinks = function() { var getTowerLink = function(key) { if ($scope.job.related[key]) { @@ -34,6 +34,11 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh $scope.verbosity_label = getTowerLabel('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; @@ -66,6 +71,11 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh jobResultsService.cancelJob($scope.job); }; + // get initial count from resolve + $scope.count = count.val; + $scope.hostCount = getTotalHostCount(count.val); + $scope.countFinished = count.countFinished; + // EVENT STUFF BELOW // just putting the event queue on scope so it can be inspected in the @@ -79,8 +89,14 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh // make changes to ui based on the event returned from the queue if (mungedEvent.changes) { mungedEvent.changes.forEach(change => { - if (change === 'count') { + if (change === 'count' && !$scope.countFinished) { $scope.count = mungedEvent.count; + $scope.hostCount = getTotalHostCount(mungedEvent + .count); + } + + if (change === 'countFnished') { + $scope.countFinished = true; } }); } @@ -90,15 +106,21 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', '$scope', 'ParseTypeCh }; // grab completed event data and process each event - jobResultsService.getEvents($scope.job) - .then(events => { - events.forEach(event => { - // get the name in the same format as the data - // coming over the websocket - event.event_name = event.event; - processEvent(event); + var getEvents = function(url) { + jobResultsService.getEvents(url) + .then(events => { + events.results.forEach(event => { + // get the name in the same format as the data + // coming over the websocket + event.event_name = event.event; + processEvent(event); + }); + if (events.next) { + getEvents(events.next); + } }); - }); + }; + getEvents($scope.job.related.job_events); // process incoming job events $rootScope.event_socket.on("job_events-" + $scope.job.id, function(data) { diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index 3300afa695..2b98b73592 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -459,7 +459,7 @@ Plays
- {{jobData.playCount || 0}} + {{ playCount || 0}} @@ -467,7 +467,7 @@ Tasks - {{jobData.taskCount || 0}} + {{ taskCount || 0}} @@ -475,7 +475,7 @@ Hosts - {{jobData.hostCount || 0}} + {{ hostCount || 0}} diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index 023fa237d5..1807898387 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -35,6 +35,43 @@ export default { }); return val.promise; }], + count: ['jobData', 'jobResultsService', 'Rest', '$q', function(jobData, jobResultsService, Rest, $q) { + 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() + .success(function(data) { + defer.resolve({ + val: jobResultsService + .getCountsFromStatsEvent(data + .results[0].event_data), + countFinished: true}); + }) + .error(function() { + defer.resolve({val: { + ok: 0, + skipped: 0, + unreachable: 0, + failures: 0, + changed: 0 + }, countFinished: false}); + }); + } else { + // 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; + }], jobLabels: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) { var getNext = function(data, arr, resolve) { Rest.setUrl(data.next); diff --git a/awx/ui/client/src/job-results/job-results.service.js b/awx/ui/client/src/job-results/job-results.service.js index c271f7dd3c..81f4a99fb0 100644 --- a/awx/ui/client/src/job-results/job-results.service.js +++ b/awx/ui/client/src/job-results/job-results.service.js @@ -7,13 +7,81 @@ export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', function ($q, Prompt, $filter, Wait, Rest, $state, ProcessErrors) { var val = { - getEvents: function(job) { + // 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 === "failed") { + // array of hosts from failed type + hostsArr = Object.keys(event_data[key]); + hostsArr.forEach(host => { + if (!hosts[host]) { + // host has not been added to hosts object + // add now + hosts[host] = {}; + } + + hosts[host][key] = event_data[key][host]; + }); + } else { + // 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]; + }); + } + }); + + // use the hosts data populate above to get the count + var count = { + ok : _.filter(hosts, function(o){ + return !o.failures && !o.changed && o.ok > 0; + }), + skipped : _.filter(hosts, function(o){ + return o.skipped > 0; + }), + unreachable : _.filter(hosts, function(o){ + return o.dark > 0; + }), + failures : _.filter(hosts, function(o){ + return o.failed === true; + }), + changed : _.filter(hosts, function(o){ + return o.changed > 0; + }) + }; + + // turn the count into an actual count, rather than a list of host + // names + Object.keys(count).forEach(key => { + count[key] = count[key].length; + }); + + return count; + }, + getEvents: function(url) { var val = $q.defer(); - Rest.setUrl(job.related.job_events); + Rest.setUrl(url); Rest.get() .success(function(data) { - val.resolve(data.results); + val.resolve({results: data.results, next: data.next}); }) .error(function(obj, status) { ProcessErrors(null, obj, status, null, { From bb82bfe95a9a3d47e8e3d92884d5c3875302b90c Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 3 Oct 2016 15:47:37 -0400 Subject: [PATCH 11/57] underlying infrastructure for dealing with commits is working now --- .../src/job-results/event-queue.service.js | 218 ++++++++++++++---- .../src/job-results/job-results.controller.js | 66 ++++-- .../src/job-results/job-results.route.js | 17 ++ 3 files changed, 236 insertions(+), 65 deletions(-) diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js index cd3d037557..825dad429c 100644 --- a/awx/ui/client/src/job-results/event-queue.service.js +++ b/awx/ui/client/src/job-results/event-queue.service.js @@ -4,36 +4,55 @@ * All Rights Reserved *************************************************/ -export default ['jobResultsService', function(jobResultsService){ +export default ['jobResultsService', '$q', function(jobResultsService, $q){ var val = {}; // Get the count of the last event var getPreviousCount = function(counter) { - // get the ids of all the queue - var counters = Object.keys(val.queue).map(counter => parseInt(counter)); + var previousCount = $q.defer(); - // iterate backwards to find the last count - while(counters.indexOf(counter - 1) > -1) { - counter = counter - 1; - if (val.queue[counter].count) { - // need to create a new copy of count when returning - // so that it is accurate for the particular event - return _.clone(val.queue[counter].count); + // iteratively find the last count + var findCount = function(counter) { + if (counter === 0) { + // if counter is 0, no count has been initialized + // initialize one! + previousCount.resolve({ + ok: 0, + skipped: 0, + unreachable: 0, + failures: 0, + changed: 0 + }); + } else if (val.queue[counter] && val.queue[counter].count) { + // this event has a count, resolve! + previousCount.resolve(_.clone(val.queue[counter].count)); + } else { + // this event doesn't have a count, decrement to the + // previous event and check it + findCount(counter - 1); } + }; + + if (val.queue[counter - 1]) { + // if the previous event has been resolved, start the iterative + // get previous count process + findCount(counter - 1); + } else if (val.populateDefers[counter - 1]){ + // if the previous event has not been resolved, wait for it to + // be and then start the iterative get previous count process + val.populateDefers[counter - 1].promise.then(function() { + findCount(counter - 1); + }); } - // no count initialized - return { - ok: 0, - skipped: 0, - unreachable: 0, - failures: 0, - changed: 0 - }; + return previousCount.promise; }; // munge the raw event from the backend into the event_queue's format var mungeEvent = function(event) { + var mungedEventDefer = $q.defer(); + + // basic data needed in the munged event var mungedEvent = { counter: event.counter, id: event.id, @@ -41,58 +60,175 @@ export default ['jobResultsService', function(jobResultsService){ name: event.event_name }; + // for different types of events, you need different types of data if (event.event_name === 'playbook_on_start') { - // sets count initially so this is a change - mungedEvent.count = getPreviousCount(mungedEvent.counter); + mungedEvent.count = { + ok: 0, + skipped: 0, + unreachable: 0, + failures: 0, + changed: 0 + }; mungedEvent.changes = ['count']; + mungedEventDefer.resolve(mungedEvent); } else if (event.event_name === 'runner_on_ok' || event.event_name === 'runner_on_async_ok') { - mungedEvent.count = getPreviousCount(mungedEvent.counter); - mungedEvent.count.ok++; - mungedEvent.changes = ['count']; + getPreviousCount(mungedEvent.counter) + .then(count => { + mungedEvent.count = count; + mungedEvent.count.ok++; + mungedEvent.changes = ['count']; + mungedEventDefer.resolve(mungedEvent); + }); } else if (event.event_name === 'runner_on_skipped') { - mungedEvent.count = getPreviousCount(mungedEvent.counter); - mungedEvent.count.skipped++; - mungedEvent.changes = ['count']; + getPreviousCount(mungedEvent.counter) + .then(count => { + mungedEvent.count = count; + mungedEvent.count.skipped++; + mungedEvent.changes = ['count']; + mungedEventDefer.resolve(mungedEvent); + }); } else if (event.event_name === 'runner_on_unreachable') { - mungedEvent.count = getPreviousCount(mungedEvent.counter); - mungedEvent.count.unreachable++; - mungedEvent.changes = ['count']; + getPreviousCount(mungedEvent.counter) + .then(count => { + mungedEvent.count = count; + mungedEvent.count.unrecahble++; + mungedEvent.changes = ['count']; + mungedEventDefer.resolve(mungedEvent); + }); } else if (event.event_name === 'runner_on_error' || event.event_name === 'runner_on_async_failed') { - mungedEvent.count = getPreviousCount(mungedEvent.counter); - mungedEvent.count.failed++; - mungedEvent.changes = ['count']; + getPreviousCount(mungedEvent.counter) + .then(count => { + mungedEvent.count = count; + mungedEvent.count.failed++; + mungedEvent.changes = ['count']; + mungedEventDefer.resolve(mungedEvent); + }); } else if (event.event_name === 'playbook_on_stats') { // get the data for populating the host status bar mungedEvent.count = jobResultsService .getCountsFromStatsEvent(event.event_data); mungedEvent.changes = ['count', 'countFinished']; + mungedEventDefer.resolve(mungedEvent); + } else { + mungedEventDefer.resolve(mungedEvent); } - return mungedEvent; + + return mungedEventDefer.promise; }; val = { + populateDefers: {}, queue: {}, // reinitializes the event queue value for the job results page + // + // TODO: implement some sort of local storage scheme + // to make viewing job details that the user has + // previous clicked on super quick, this would be where you grab + // from local storage initialize: function() { val.queue = {}; + val.populateDefers = {}; }, // populates the event queue populate: function(event) { - // don't populate the event if it's already been added either - // by rest or by websocket event - if (!val.queue[event.counter]) { - var mungedEvent = mungeEvent(event); - val.queue[mungedEvent.counter] = mungedEvent; - - return mungedEvent; + // if a defer hasn't been set up for the event, + // set one up now + if (!val.populateDefers[event.counter]) { + val.populateDefers[event.counter] = $q.defer(); } + + if (!val.queue[event.counter]) { + var resolvePopulation = function(event) { + // to resolve, put the event on the queue and + // then resolve the deferred value + // + // TODO: implement some sort of local storage scheme + // to make viewing job details that the user has + // previous clicked on super quick, this would be + // where you put in local storage + val.queue[event.counter] = event; + val.populateDefers[event.counter].resolve(event); + } + + if (event.counter === 1) { + // for the first event, go ahead and munge and + // resolve + mungeEvent(event).then(event => { + resolvePopulation(event); + }); + } else { + // for all other events, you have to do some things + // to keep the event processing in the UI synchronous + + if (!val.populateDefers[event.counter - 1]) { + // first, if the previous event doesn't have + // a defer set up (this happens when websocket + // events are coming in and you need to make + // rest calls to catch up), go ahead and set a + // defer for the previous event + val.populateDefers[event.counter - 1] = $q.defer(); + } + + // you can start the munging process... + mungeEvent(event).then(event => { + // ...but wait until the previous event has + // been resolved before resolving this one and + // doing stuff in the ui (that's why we + // needed that previous conditional). this + // could be done in a more asynchronous nature + // if we need a performance boost. See the + // todo note in the markProcessed function + // for an idea + val.populateDefers[event.counter - 1].promise + .then(() => { + resolvePopulation(event); + }); + }); + } + } else { + // don't repopulate the event if it's already been added + // and munged either by rest or by websocket event + val.populateDefers[event.counter] + .resolve(val.queue[event.counter]); + } + + return val.populateDefers[event.counter].promise; }, // 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; + var process = function(event) { + // the event has now done it's work in the UI, record + // that! + val.queue[event.counter].processed = true; + + // TODO: we can actually record what has been done in the + // UI and at what event too! (something like "resolved + // the count on event 63)". + // + // if we do something like this, we actually wouldn't + // have to wait until the previous events had completed + // before resolving and returning to the controller + // in populate()... + // in other words, we could send events out of order to + // the controller, but let the controller know that it's + // an older event that what is in the view so we don't + // need to do anything + }; + + if (!val.queue[event.counter]) { + // sometimes, the process is called in the controller and + // the event queue hasn't caught up and actually added + // the event to the queue yet. Wait until that happens + val.populateDefers[event.counter].promise + .finally(function() { + process(event); + }); + } else { + process(event); + } } }; diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index af4f250062..4d1e9fbb4b 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -76,36 +76,55 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa $scope.hostCount = getTotalHostCount(count.val); $scope.countFinished = count.countFinished; + // Process incoming job status changes + $rootScope.$on('JobStatusChange-jobDetails', function(e, data) { + if (parseInt(data.unified_job_id, 10) === parseInt($scope.job.id,10)) { + $scope.job.status = data.status; + } + }); + // EVENT STUFF BELOW // just putting the event queue on scope so it can be inspected in the // console $scope.event_queue = eventQueue.queue; + $scope.defersArr = eventQueue.populateDefers; + // 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) { // put the event in the queue - var mungedEvent = eventQueue.populate(event); + eventQueue.populate(event).then(mungedEvent => { + // 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 === '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); + } - // make changes to ui based on the event returned from the queue - if (mungedEvent.changes) { - mungedEvent.changes.forEach(change => { - if (change === 'count' && !$scope.countFinished) { - $scope.count = mungedEvent.count; - $scope.hostCount = getTotalHostCount(mungedEvent - .count); - } + 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 === 'countFnished') { - $scope.countFinished = true; - } - }); - } - - // the changes have been processed in the ui, mark it in the queue - eventQueue.markProcessed(event); + // the changes have been processed in the ui, mark it in the queue + eventQueue.markProcessed(event); + }); }; - // grab completed event data and process each event + // PULL! grab completed event data and process each event var getEvents = function(url) { jobResultsService.getEvents(url) .then(events => { @@ -122,15 +141,14 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa }; getEvents($scope.job.related.job_events); - // process incoming job events + // PUSH! process incoming job events $rootScope.event_socket.on("job_events-" + $scope.job.id, function(data) { processEvent(data); }); - // process incoming job status changes - $rootScope.$on('JobStatusChange-jobDetails', function(e, data) { - if (parseInt(data.unified_job_id, 10) === parseInt($scope.job.id,10)) { - $scope.job.status = data.status; - } + // STOP! stop listening to job events + $scope.$on('$destroy', function() { + $rootScope.event_socket.removeAllListeners("job_events-" + + $scope.job.id); }); }]; diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index 1807898387..8151546468 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -16,6 +16,7 @@ export default { label: '{{ job.id }} - {{ job.name }}' }, resolve: { + // the GET for the particular job jobData: ['Rest', 'GetBasePath', '$stateParams', '$q', '$state', 'Alert', function(Rest, GetBasePath, $stateParams, $q, $state, Alert) { Rest.setUrl(GetBasePath('jobs') + $stateParams.id); var val = $q.defer(); @@ -35,6 +36,10 @@ export default { }); return val.promise; }], + // 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', function(jobData, jobResultsService, Rest, $q) { var defer = $q.defer(); if (jobData.finished) { @@ -72,6 +77,8 @@ export default { } 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); @@ -101,6 +108,9 @@ export default { 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(); @@ -112,6 +122,11 @@ export default { }); return val.promise; }], + // This gives us access to the job events socket so we can start + // listening for updates we need to make for the ui as data comes in + // + // TODO: we could probably make this better by not initing + // job_events for completed jobs jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { if (!$rootScope.event_socket) { $rootScope.event_socket = Socket({ @@ -126,6 +141,8 @@ export default { return true; } }], + // This clears out the event queue, otherwise it'd be full of events + // for previous job results the user had navigated to eventQueueInit: ['eventQueue', function(eventQueue) { eventQueue.initialize(); }] From 7b8b83a6ab54006b127e4cd1e3b3e600ddce3d0f Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 4 Oct 2016 10:19:41 -0400 Subject: [PATCH 12/57] added play and task count support --- .../src/job-results/event-queue.service.js | 59 +++++++++++++------ .../src/job-results/job-results.controller.js | 8 +++ 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js index 825dad429c..e7074844fa 100644 --- a/awx/ui/client/src/job-results/event-queue.service.js +++ b/awx/ui/client/src/job-results/event-queue.service.js @@ -8,7 +8,17 @@ export default ['jobResultsService', '$q', function(jobResultsService, $q){ var val = {}; // Get the count of the last event - var getPreviousCount = function(counter) { + var getPreviousCount = function(counter, type) { + var countAttr; + + if (type === 'play') { + countAttr = 'playCount'; + } else if (type === 'task') { + countAttr = 'taskCount'; + } else { + countAttr = 'count'; + } + var previousCount = $q.defer(); // iteratively find the last count @@ -16,16 +26,22 @@ export default ['jobResultsService', '$q', function(jobResultsService, $q){ if (counter === 0) { // if counter is 0, no count has been initialized // initialize one! - previousCount.resolve({ - ok: 0, - skipped: 0, - unreachable: 0, - failures: 0, - changed: 0 - }); - } else if (val.queue[counter] && val.queue[counter].count) { + + if (countAttr === 'count') { + previousCount.resolve({ + ok: 0, + skipped: 0, + unreachable: 0, + failures: 0, + changed: 0 + }); + } else { + previousCount.resolve(0); + } + + } else if (val.queue[counter] && val.queue[counter][countAttr] !== undefined) { // this event has a count, resolve! - previousCount.resolve(_.clone(val.queue[counter].count)); + previousCount.resolve(_.clone(val.queue[counter][countAttr])); } else { // this event doesn't have a count, decrement to the // previous event and check it @@ -70,7 +86,18 @@ export default ['jobResultsService', '$q', function(jobResultsService, $q){ changed: 0 }; mungedEvent.changes = ['count']; - mungedEventDefer.resolve(mungedEvent); + } else if (event.event_name === 'playbook_on_play_start') { + getPreviousCount(mungedEvent.counter, "play") + .then(count => { + mungedEvent.playCount = count + 1; + mungedEvent.changes = ['playCount']; + }); + } else if (event.event_name === 'playbook_on_task_start') { + getPreviousCount(mungedEvent.counter, "task") + .then(count => { + mungedEvent.taskCount = count + 1; + mungedEvent.changes = ['taskCount']; + }); } else if (event.event_name === 'runner_on_ok' || event.event_name === 'runner_on_async_ok') { getPreviousCount(mungedEvent.counter) @@ -78,7 +105,6 @@ export default ['jobResultsService', '$q', function(jobResultsService, $q){ mungedEvent.count = count; mungedEvent.count.ok++; mungedEvent.changes = ['count']; - mungedEventDefer.resolve(mungedEvent); }); } else if (event.event_name === 'runner_on_skipped') { getPreviousCount(mungedEvent.counter) @@ -86,7 +112,6 @@ export default ['jobResultsService', '$q', function(jobResultsService, $q){ mungedEvent.count = count; mungedEvent.count.skipped++; mungedEvent.changes = ['count']; - mungedEventDefer.resolve(mungedEvent); }); } else if (event.event_name === 'runner_on_unreachable') { getPreviousCount(mungedEvent.counter) @@ -94,7 +119,6 @@ export default ['jobResultsService', '$q', function(jobResultsService, $q){ mungedEvent.count = count; mungedEvent.count.unrecahble++; mungedEvent.changes = ['count']; - mungedEventDefer.resolve(mungedEvent); }); } else if (event.event_name === 'runner_on_error' || event.event_name === 'runner_on_async_failed') { @@ -103,18 +127,17 @@ export default ['jobResultsService', '$q', function(jobResultsService, $q){ mungedEvent.count = count; mungedEvent.count.failed++; mungedEvent.changes = ['count']; - mungedEventDefer.resolve(mungedEvent); }); } else if (event.event_name === 'playbook_on_stats') { // get the data for populating the host status bar mungedEvent.count = jobResultsService .getCountsFromStatsEvent(event.event_data); mungedEvent.changes = ['count', 'countFinished']; - mungedEventDefer.resolve(mungedEvent); } else { - mungedEventDefer.resolve(mungedEvent); } + mungedEventDefer.resolve(mungedEvent); + return mungedEventDefer.promise; }; @@ -150,7 +173,7 @@ export default ['jobResultsService', '$q', function(jobResultsService, $q){ // where you put in local storage val.queue[event.counter] = event; val.populateDefers[event.counter].resolve(event); - } + }; if (event.counter === 1) { // for the first event, go ahead and munge and diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index 4d1e9fbb4b..6d10369443 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -109,6 +109,14 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa .count); } + if (change === 'playCount') { + $scope.playCount = mungedEvent.playCount; + } + + if (change === 'taskCount') { + $scope.taskCount = mungedEvent.taskCount; + } + if (change === 'countFinished') { // the playbook_on_stats event actually lets // us know that we don't need to iteratively From 40a0c3ff2300c4add24b917614c598ee14710158 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 7 Oct 2016 13:11:41 -0400 Subject: [PATCH 13/57] fixed typo in event queue --- awx/ui/client/src/job-results/event-queue.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js index e7074844fa..e34905a966 100644 --- a/awx/ui/client/src/job-results/event-queue.service.js +++ b/awx/ui/client/src/job-results/event-queue.service.js @@ -117,7 +117,7 @@ export default ['jobResultsService', '$q', function(jobResultsService, $q){ getPreviousCount(mungedEvent.counter) .then(count => { mungedEvent.count = count; - mungedEvent.count.unrecahble++; + mungedEvent.count.unreachable++; mungedEvent.changes = ['count']; }); } else if (event.event_name === 'runner_on_error' || From 648bd3d1a0063e75ed3f67f5ed1ce07150dbab89 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 7 Oct 2016 13:37:07 -0400 Subject: [PATCH 14/57] added TODO for implementing request retry logic --- awx/ui/client/src/job-results/job-results.controller.js | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index 6d10369443..1d845a697b 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -133,6 +133,7 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa }; // PULL! grab completed event data and process each event + // TODO: implement retry logic in case one of these requests fails var getEvents = function(url) { jobResultsService.getEvents(url) .then(events => { From 3ade961e92a649bbc3875fe19dd907e41e2d3799 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Mon, 10 Oct 2016 11:22:56 -0700 Subject: [PATCH 15/57] Rebase of devel (w/ channels) + socket rework for new job details Replacing old socket.io receiver w/ new websocket receivers --- .../client/src/job-detail/job-detail.route.js | 9 ++++++ .../src/job-results/job-results.controller.js | 31 +++++++++++-------- .../src/job-results/job-results.route.js | 25 ++++----------- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/awx/ui/client/src/job-detail/job-detail.route.js b/awx/ui/client/src/job-detail/job-detail.route.js index dd8a7ff5ba..9f288dac34 100644 --- a/awx/ui/client/src/job-detail/job-detail.route.js +++ b/awx/ui/client/src/job-detail/job-detail.route.js @@ -1,3 +1,4 @@ +<<<<<<< 4cf6a946a1aa14b7d64a8e1e8dabecfd3d056f27 //<<<<<<< bc59236851902d7c768aa26abdb7dc9c9dc27a5a /************************************************* * Copyright (c) 2016 Ansible, Inc. @@ -46,6 +47,8 @@ // controller: 'JobDetailController' // }; //======= +======= +>>>>>>> Rebase of devel (w/ channels) + socket rework for new job details // /************************************************* // * Copyright (c) 2016 Ansible, Inc. // * @@ -61,6 +64,12 @@ // parent: 'jobs', // label: "{{ job.id }} - {{ job.name }}" // }, +// socket: { +// "groups":{ +// "jobs": ["status_changed", "summary"], +// "job_events": [] +// } +// }, // resolve: { // jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { // if (!$rootScope.event_socket) { diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index 1d845a697b..20fd0686b4 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -76,13 +76,6 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa $scope.hostCount = getTotalHostCount(count.val); $scope.countFinished = count.countFinished; - // Process incoming job status changes - $rootScope.$on('JobStatusChange-jobDetails', function(e, data) { - if (parseInt(data.unified_job_id, 10) === parseInt($scope.job.id,10)) { - $scope.job.status = data.status; - } - }); - // EVENT STUFF BELOW // just putting the event queue on scope so it can be inspected in the @@ -150,14 +143,26 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa }; getEvents($scope.job.related.job_events); - // PUSH! process incoming job events - $rootScope.event_socket.on("job_events-" + $scope.job.id, function(data) { + // Processing of job_events messages from the websocket + $scope.$on(`ws-job_events-${$scope.job.id}`, function(e, data) { processEvent(data); }); - // STOP! stop listening to job events - $scope.$on('$destroy', function() { - $rootScope.event_socket.removeAllListeners("job_events-" + - $scope.job.id); + // Processing of job-status messages from the websocket + $scope.$on(`ws-jobs`, function(e, data) { + if (parseInt(data.unified_job_id, 10) === parseInt($scope.job.id,10)) { + $scope.job.status = data.status; + } }); + + // The code below was used in the old job detail controller, + // and is for processing the 'Job Summary' event that is delivered + // for a completed job. Not sure if we have an equivalent function + // at this point. TODO: write function to handle Job Summary + // scope.$on('ws-jobs-summary', function() { + // // the job host summary should now be available from the API + // $log.debug('Trigging reload of job_host_summaries'); + // scope.$emit('InitialLoadComplete'); + // }); + }]; diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index 8151546468..902f9f3203 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -15,6 +15,12 @@ export default { parent: 'jobs', label: '{{ job.id }} - {{ job.name }}' }, + socket: { + "groups":{ + "jobs": ["status_changed", "summary"], + "job_events": [] + } + }, resolve: { // the GET for the particular job jobData: ['Rest', 'GetBasePath', '$stateParams', '$q', '$state', 'Alert', function(Rest, GetBasePath, $stateParams, $q, $state, Alert) { @@ -122,25 +128,6 @@ export default { }); return val.promise; }], - // This gives us access to the job events socket so we can start - // listening for updates we need to make for the ui as data comes in - // - // TODO: we could probably make this better by not initing - // job_events for completed jobs - jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { - if (!$rootScope.event_socket) { - $rootScope.event_socket = Socket({ - scope: $rootScope, - endpoint: "job_events" - }); - $rootScope.event_socket.init(); - // returns should really be providing $rootScope.event_socket - // otherwise, we have to inject the entire $rootScope into the controller - return true; - } else { - return true; - } - }], // This clears out the event queue, otherwise it'd be full of events // for previous job results the user had navigated to eventQueueInit: ['eventQueue', function(eventQueue) { From bba253cdfcad6e00d86dd4726189e358b6f31419 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 18 Oct 2016 13:39:07 -0400 Subject: [PATCH 16/57] added relaunch capability and end time of running job --- .../src/job-results/event-queue.service.js | 7 ++++-- .../src/job-results/job-results.controller.js | 25 ++++++++++--------- .../src/job-results/job-results.service.js | 6 ++++- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js index e34905a966..6092f39ad1 100644 --- a/awx/ui/client/src/job-results/event-queue.service.js +++ b/awx/ui/client/src/job-results/event-queue.service.js @@ -85,7 +85,8 @@ export default ['jobResultsService', '$q', function(jobResultsService, $q){ failures: 0, changed: 0 }; - mungedEvent.changes = ['count']; + mungedEvent.startTime = event.modified; + mungedEvent.changes = ['count', 'startTime']; } else if (event.event_name === 'playbook_on_play_start') { getPreviousCount(mungedEvent.counter, "play") .then(count => { @@ -129,10 +130,12 @@ export default ['jobResultsService', '$q', function(jobResultsService, $q){ mungedEvent.changes = ['count']; }); } else if (event.event_name === 'playbook_on_stats') { + console.log(event.modified); // get the data for populating the host status bar mungedEvent.count = jobResultsService .getCountsFromStatsEvent(event.event_data); - mungedEvent.changes = ['count', 'countFinished']; + mungedEvent.finishedTime = event.modified; + mungedEvent.changes = ['count', 'countFinished', 'finishedTime']; } else { } diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index 20fd0686b4..3dfb513e7a 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -71,6 +71,10 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa jobResultsService.cancelJob($scope.job); }; + $scope.relaunchJob = function() { + jobResultsService.relaunchJob($scope); + }; + // get initial count from resolve $scope.count = count.val; $scope.hostCount = getTotalHostCount(count.val); @@ -93,6 +97,10 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa 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 @@ -110,6 +118,10 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa $scope.taskCount = mungedEvent.taskCount; } + if (change === 'finishedTime' && !$scope.job.finished) { + $scope.job.finished = mungedEvent.finishedTime; + } + if (change === 'countFinished') { // the playbook_on_stats event actually lets // us know that we don't need to iteratively @@ -143,7 +155,7 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa }; getEvents($scope.job.related.job_events); - // Processing of job_events messages from the websocket + // Processing of job_events messages from the websocket $scope.$on(`ws-job_events-${$scope.job.id}`, function(e, data) { processEvent(data); }); @@ -154,15 +166,4 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa $scope.job.status = data.status; } }); - - // The code below was used in the old job detail controller, - // and is for processing the 'Job Summary' event that is delivered - // for a completed job. Not sure if we have an equivalent function - // at this point. TODO: write function to handle Job Summary - // scope.$on('ws-jobs-summary', function() { - // // the job host summary should now be available from the API - // $log.debug('Trigging reload of job_host_summaries'); - // scope.$emit('InitialLoadComplete'); - // }); - }]; diff --git a/awx/ui/client/src/job-results/job-results.service.js b/awx/ui/client/src/job-results/job-results.service.js index 81f4a99fb0..0109eeee55 100644 --- a/awx/ui/client/src/job-results/job-results.service.js +++ b/awx/ui/client/src/job-results/job-results.service.js @@ -5,7 +5,7 @@ *************************************************/ -export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', function ($q, Prompt, $filter, Wait, Rest, $state, ProcessErrors) { +export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErrors', 'InitiatePlaybookRun', function ($q, Prompt, $filter, Wait, Rest, $state, ProcessErrors, InitiatePlaybookRun) { var val = { // the playbook_on_stats event returns the count data in a weird format. // format to what we need! @@ -185,6 +185,10 @@ export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErr }, actionText: 'CANCEL' }); + }, + relaunchJob: function(scope) { + InitiatePlaybookRun({ scope: scope, id: scope.job.id, + relaunch: true }); } }; return val; From e6c342d9ad3a53ca94575b2ab4d04737ceafb6ca Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Tue, 18 Oct 2016 16:04:03 -0700 Subject: [PATCH 17/57] Adding initial files for job results standard out panel with some very rough dummy text in there for now --- .../job-results-stdout.block.less | 24 ++++++++++++ .../job-results-stdout.directive.js | 19 +++++++++ .../job-results-stdout.partial.html | 39 +++++++++++++++++++ .../job-results/job-results-stdout/main.js | 11 ++++++ .../src/job-results/job-results.partial.html | 2 + awx/ui/client/src/job-results/main.js | 3 +- 6 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.block.less create mode 100644 awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js create mode 100644 awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.partial.html create mode 100644 awx/ui/client/src/job-results/job-results-stdout/main.js 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 new file mode 100644 index 0000000000..70923cf45b --- /dev/null +++ b/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.block.less @@ -0,0 +1,24 @@ +@import '../../shared/branding/colors.default.less'; + +.JobResultsStdOut{ + height: 100%; + margin-top: 15px; +} + +.JobResultsStdOut-aLineOfStdOut{ + display: flex; +} + +.JobResultsStdOut-lineNumberColumn{ + flex: 1 0 auto; + background-color: @d7grey; + text-align: right; + padding-right: 10px; + color: @default-icon; +} + +.JobResultsStdOut-stdoutColumn{ + background-color: @default-secondary-bg; + flex: 6 0 auto; + +} 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 new file mode 100644 index 0000000000..e6bdefe91d --- /dev/null +++ b/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.directive.js @@ -0,0 +1,19 @@ +/************************************************* + * 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/job-results-stdout/job-results-stdout'), + restrict: 'E', + // controller: jobResultsStdOutController, + link: function(scope) { + + } + }; +}]; 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 new file mode 100644 index 0000000000..82f1198966 --- /dev/null +++ b/awx/ui/client/src/job-results/job-results-stdout/job-results-stdout.partial.html @@ -0,0 +1,39 @@ +
+ +
+
+ 1 +
+
+ this is some lines of stdout +
+
+ +
+
+ 2 +
+
+ this is some lines of stdout +
+
+ +
+
+ 3 +
+
+ this is some lines of stdout +
+
+ +
+
+ 4 +
+
+ this is some lines of stdout +
+
+ +
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 new file mode 100644 index 0000000000..5fc583b9b1 --- /dev/null +++ b/awx/ui/client/src/job-results/job-results-stdout/main.js @@ -0,0 +1,11 @@ +/************************************************* + * 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.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index 2b98b73592..3aba846bd0 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -514,8 +514,10 @@ + + diff --git a/awx/ui/client/src/job-results/main.js b/awx/ui/client/src/job-results/main.js index 71f08b09be..defad66828 100644 --- a/awx/ui/client/src/job-results/main.js +++ b/awx/ui/client/src/job-results/main.js @@ -5,6 +5,7 @@ *************************************************/ import hostStatusBar from './host-status-bar/main'; +import jobResultsStdOut from './job-results-stdout/main'; import route from './job-results.route.js'; @@ -14,7 +15,7 @@ import eventQueueService from './event-queue.service'; import durationFilter from './duration.filter'; export default - angular.module('jobResults', [hostStatusBar.name]) + angular.module('jobResults', [hostStatusBar.name, jobResultsStdOut.name]) .run(['$stateExtender', function($stateExtender) { $stateExtender.addState(route); }]) From e11eb1d4d857b7053c9bbdee114833bc83048f19 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 26 Oct 2016 12:06:43 -0700 Subject: [PATCH 18/57] adding some dummy lines --- .../job-results-stdout.block.less | 5 +++-- .../job-results-stdout.partial.html | 15 +++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) 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 index 70923cf45b..7475e4a828 100644 --- 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 @@ -10,15 +10,16 @@ } .JobResultsStdOut-lineNumberColumn{ - flex: 1 0 auto; + // flex: 1 0 auto; background-color: @d7grey; text-align: right; padding-right: 10px; color: @default-icon; + width: 100px; } .JobResultsStdOut-stdoutColumn{ background-color: @default-secondary-bg; - flex: 6 0 auto; + // flex: 6 0 auto; } 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 index 82f1198966..6ec3b4ed93 100644 --- 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 @@ -5,7 +5,7 @@ 1
- this is some lines of stdout + Vault password:
@@ -14,7 +14,7 @@ 2
- this is some lines of stdout + PLAY [Create Tower Host] *******************************************************
@@ -23,7 +23,8 @@ 3
- this is some lines of stdout + TASK [create_ec2_instances : Launch Instance] ********************************** + changed: [local]
@@ -32,7 +33,13 @@ 4
- this is some lines of stdout + TASK [create_ec2_instances : Instance Ids] ************************************* + [DEPRECATION WARNING]: Using bare variables is deprecated. Update your + playbooks so that the environment value uses the full variable syntax + ('{{ec2.instances}}'). + This feature will be removed in a future release. + Deprecation warnings can be disabled by setting deprecation_warnings=False in + ansible.cfg.
From 5dad4ef8d54797cf1b5a8399b5fdc48832a67749 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 26 Oct 2016 19:07:49 -0700 Subject: [PATCH 19/57] Adjusting some colors and first pass at real stdout by using an ng-repeat and stdoutArr --- .../src/job-results/event-queue.service.js | 23 ++++++--- .../job-results-stdout.block.less | 24 ++++++--- .../job-results-stdout.partial.html | 51 +++++-------------- .../src/job-results/job-results.controller.js | 5 ++ .../src/job-results/job-results.partial.html | 1 - .../src/job-results/job-results.route.js | 4 +- awx/ui/client/src/job-results/main.js | 2 + .../src/job-results/parse-stdout.service.js | 28 ++++++++++ 8 files changed, 81 insertions(+), 57 deletions(-) create mode 100644 awx/ui/client/src/job-results/parse-stdout.service.js diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js index 6092f39ad1..9d23295ac5 100644 --- a/awx/ui/client/src/job-results/event-queue.service.js +++ b/awx/ui/client/src/job-results/event-queue.service.js @@ -4,7 +4,7 @@ * All Rights Reserved *************************************************/ -export default ['jobResultsService', '$q', function(jobResultsService, $q){ +export default ['jobResultsService', 'parseStdoutService', '$q', function(jobResultsService, parseStdoutService, $q){ var val = {}; // Get the count of the last event @@ -91,13 +91,15 @@ export default ['jobResultsService', '$q', function(jobResultsService, $q){ getPreviousCount(mungedEvent.counter, "play") .then(count => { mungedEvent.playCount = count + 1; - mungedEvent.changes = ['playCount']; + mungedEvent.stdout = parseStdoutService.parseStdout(event); + mungedEvent.changes = ['playCount', 'stdout']; }); } else if (event.event_name === 'playbook_on_task_start') { getPreviousCount(mungedEvent.counter, "task") .then(count => { mungedEvent.taskCount = count + 1; - mungedEvent.changes = ['taskCount']; + mungedEvent.stdout = parseStdoutService.parseStdout(event); + mungedEvent.changes = ['taskCount', 'stdout']; }); } else if (event.event_name === 'runner_on_ok' || event.event_name === 'runner_on_async_ok') { @@ -105,21 +107,24 @@ export default ['jobResultsService', '$q', function(jobResultsService, $q){ .then(count => { mungedEvent.count = count; mungedEvent.count.ok++; - mungedEvent.changes = ['count']; + mungedEvent.stdout = parseStdoutService.parseStdout(event); + mungedEvent.changes = ['count', 'stdout']; }); } else if (event.event_name === 'runner_on_skipped') { getPreviousCount(mungedEvent.counter) .then(count => { mungedEvent.count = count; mungedEvent.count.skipped++; - mungedEvent.changes = ['count']; + mungedEvent.stdout = parseStdoutService.parseStdout(event); + mungedEvent.changes = ['count', 'stdout']; }); } else if (event.event_name === 'runner_on_unreachable') { getPreviousCount(mungedEvent.counter) .then(count => { mungedEvent.count = count; mungedEvent.count.unreachable++; - mungedEvent.changes = ['count']; + mungedEvent.stdout = parseStdoutService.parseStdout(event); + mungedEvent.changes = ['count', 'stdout']; }); } else if (event.event_name === 'runner_on_error' || event.event_name === 'runner_on_async_failed') { @@ -127,15 +132,17 @@ export default ['jobResultsService', '$q', function(jobResultsService, $q){ .then(count => { mungedEvent.count = count; mungedEvent.count.failed++; - mungedEvent.changes = ['count']; + mungedEvent.stdout = event.stdout; + mungedEvent.changes = ['count', 'stdout']; }); } else if (event.event_name === 'playbook_on_stats') { console.log(event.modified); // get the data for populating the host status bar mungedEvent.count = jobResultsService .getCountsFromStatsEvent(event.event_data); + mungedEvent.stdout = event.stdout; mungedEvent.finishedTime = event.modified; - mungedEvent.changes = ['count', 'countFinished', 'finishedTime']; + mungedEvent.changes = ['count', 'countFinished', 'finishedTime', 'stdout']; } else { } 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 index 7475e4a828..cb730f24c1 100644 --- 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 @@ -3,6 +3,7 @@ .JobResultsStdOut{ height: 100%; margin-top: 15px; + background-color: @default-no-items-bord; } .JobResultsStdOut-aLineOfStdOut{ @@ -10,16 +11,23 @@ } .JobResultsStdOut-lineNumberColumn{ - // flex: 1 0 auto; - background-color: @d7grey; + background-color: @default-list-header-bg; text-align: right; - padding-right: 10px; - color: @default-icon; - width: 100px; + padding: 10px 10px 10px; + color: @b7grey; + width: 75px; +} + +.JobResultsStdOut-lineNumberColumn--first{ + text-align: left; + padding: 0px; } .JobResultsStdOut-stdoutColumn{ - background-color: @default-secondary-bg; - // flex: 6 0 auto; - + padding: 10px 20px 10px 20px; + color: @default-interface-txt; +} + +.JobResultsStdOut-stdoutColumn--first{ + padding-top:0px; } 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 index 6ec3b4ed93..b0d187a42e 100644 --- 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 @@ -1,46 +1,21 @@
-
-
- 1 +
+
-
- Vault password: +
- -
-
- 2 -
-
- PLAY [Create Tower Host] ******************************************************* +
+
+
+ {{line.end_line}} +
+
+ {{line.stdout}} +
- -
-
- 3 -
-
- TASK [create_ec2_instances : Launch Instance] ********************************** - changed: [local] -
-
- -
-
- 4 -
-
- TASK [create_ec2_instances : Instance Ids] ************************************* - [DEPRECATION WARNING]: Using bare variables is deprecated. Update your - playbooks so that the environment value uses the full variable syntax - ('{{ec2.instances}}'). - This feature will be removed in a future release. - Deprecation warnings can be disabled by setting deprecation_warnings=False in - ansible.cfg. -
-
-
diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index 3dfb513e7a..f4921a283a 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -79,6 +79,7 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa $scope.count = count.val; $scope.hostCount = getTotalHostCount(count.val); $scope.countFinished = count.countFinished; + $scope.stdoutArr = []; // EVENT STUFF BELOW @@ -129,6 +130,10 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa // any more. $scope.countFinished = true; } + + if(change === 'stdout'){ + $scope.stdoutArr.push(mungedEvent.stdout); + } }); } diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index 3aba846bd0..ef8cb32a7a 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -515,7 +515,6 @@
-
diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index 902f9f3203..6f52e5c67b 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -51,8 +51,8 @@ export default { 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.setUrl(jobData.related.job_events);// + + // "?event=playbook_on_stats"); Rest.get() .success(function(data) { defer.resolve({ diff --git a/awx/ui/client/src/job-results/main.js b/awx/ui/client/src/job-results/main.js index defad66828..d91fb564b8 100644 --- a/awx/ui/client/src/job-results/main.js +++ b/awx/ui/client/src/job-results/main.js @@ -11,6 +11,7 @@ import route from './job-results.route.js'; import jobResultsService from './job-results.service'; import eventQueueService from './event-queue.service'; +import parseStdoutService from './parse-stdout.service'; import durationFilter from './duration.filter'; @@ -21,4 +22,5 @@ export default }]) .service('jobResultsService', jobResultsService) .service('eventQueue', eventQueueService) + .service('parseStdoutService', parseStdoutService) .filter('duration', durationFilter); diff --git a/awx/ui/client/src/job-results/parse-stdout.service.js b/awx/ui/client/src/job-results/parse-stdout.service.js new file mode 100644 index 0000000000..7580c17219 --- /dev/null +++ b/awx/ui/client/src/job-results/parse-stdout.service.js @@ -0,0 +1,28 @@ +/************************************************* +* Copyright (c) 2016 Ansible, Inc. +* +* All Rights Reserved +*************************************************/ + +export default [function(){ + var val = { + prettify: function(line){ + // this function right now just removes the 'rn' strings + // that i'm currently seeing on this branch on the beginning + // and end of each event string. In the future it could be + // used to add styling classes to the actual lines of stdout + return line.replace(/rn/g, ''); + }, + parseStdout: function(event){ + // this object will be used by the ng-repeat in the + // job-results-stdout.partial.html. probably need to add the + // elapsed time in here too + return { + start_line: event.start_line, + end_line: event.end_line, + stdout: this.prettify(event.stdout) + }; + } + }; + return val; +}]; From 5a7c33c17a04fb2ae0b252f941c9cba1f773f0fa Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Mon, 31 Oct 2016 11:54:09 -0700 Subject: [PATCH 20/57] error handling for job-result route for when there are no job_events for a job --- .../client/src/job-detail/job-detail.route.js | 170 +++++++++--------- .../src/job-results/job-results.route.js | 21 ++- 2 files changed, 101 insertions(+), 90 deletions(-) diff --git a/awx/ui/client/src/job-detail/job-detail.route.js b/awx/ui/client/src/job-detail/job-detail.route.js index 9f288dac34..94088c126b 100644 --- a/awx/ui/client/src/job-detail/job-detail.route.js +++ b/awx/ui/client/src/job-detail/job-detail.route.js @@ -1,91 +1,91 @@ -<<<<<<< 4cf6a946a1aa14b7d64a8e1e8dabecfd3d056f27 -//<<<<<<< bc59236851902d7c768aa26abdb7dc9c9dc27a5a -/************************************************* - * Copyright (c) 2016 Ansible, Inc. - * - * All Rights Reserved - *************************************************/ - -// <<<<<<< a3d9eea2c9ddb4e16deec9ec38dea16bf37c559d -// import { templateUrl } from '../shared/template-url/template-url.factory'; -// -// export default { -// name: 'jobDetail', -// url: '/jobs/{id: int}', -// ncyBreadcrumb: { -// parent: 'jobs', -// label: "{{ job.id }} - {{ job.name }}" -// }, -// data: { -// socket: { -// "groups": { -// "jobs": ["status_changed", "summary"], -// "job_events": [] -// } -// } -// }, -// templateUrl: templateUrl('job-detail/job-detail'), -// controller: 'JobDetailController' -// }; -// ======= -// import {templateUrl} from '../shared/template-url/template-url.factory'; -// -// export default { -// name: 'jobDetail', -// url: '/jobs/:id', -// ncyBreadcrumb: { -// parent: 'jobs', -// label: "{{ job.id }} - {{ job.name }}" -// }, -// socket: { -// "groups":{ -// "jobs": ["status_changed", "summary"], -// "job_events": [] -// } -// }, -// templateUrl: templateUrl('job-detail/job-detail'), -// controller: 'JobDetailController' -// }; -//======= -======= ->>>>>>> Rebase of devel (w/ channels) + socket rework for new job details +// <<<<<<< 4cf6a946a1aa14b7d64a8e1e8dabecfd3d056f27 +// //<<<<<<< bc59236851902d7c768aa26abdb7dc9c9dc27a5a // /************************************************* // * Copyright (c) 2016 Ansible, Inc. // * // * All Rights Reserved // *************************************************/ // -// import {templateUrl} from '../shared/template-url/template-url.factory'; -// -// export default { -// name: 'jobDetail', -// url: '/jobs/:id', -// ncyBreadcrumb: { -// parent: 'jobs', -// label: "{{ job.id }} - {{ job.name }}" -// }, -// socket: { -// "groups":{ -// "jobs": ["status_changed", "summary"], -// "job_events": [] -// } -// }, -// resolve: { -// jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { -// if (!$rootScope.event_socket) { -// $rootScope.event_socket = Socket({ -// scope: $rootScope, -// endpoint: "job_events" -// }); -// $rootScope.event_socket.init(); -// // returns should really be providing $rootScope.event_socket -// // otherwise, we have to inject the entire $rootScope into the controller -// return true; -// } else { -// return true; -// } -// }] -// }, -// templateUrl: templateUrl('job-detail/job-detail'), -// controller: 'JobDetailController' -// }; +// // <<<<<<< a3d9eea2c9ddb4e16deec9ec38dea16bf37c559d +// // import { templateUrl } from '../shared/template-url/template-url.factory'; +// // +// // export default { +// // name: 'jobDetail', +// // url: '/jobs/{id: int}', +// // ncyBreadcrumb: { +// // parent: 'jobs', +// // label: "{{ job.id }} - {{ job.name }}" +// // }, +// // data: { +// // socket: { +// // "groups": { +// // "jobs": ["status_changed", "summary"], +// // "job_events": [] +// // } +// // } +// // }, +// // templateUrl: templateUrl('job-detail/job-detail'), +// // controller: 'JobDetailController' +// // }; +// // ======= +// // import {templateUrl} from '../shared/template-url/template-url.factory'; +// // +// // export default { +// // name: 'jobDetail', +// // url: '/jobs/:id', +// // ncyBreadcrumb: { +// // parent: 'jobs', +// // label: "{{ job.id }} - {{ job.name }}" +// // }, +// // socket: { +// // "groups":{ +// // "jobs": ["status_changed", "summary"], +// // "job_events": [] +// // } +// // }, +// // templateUrl: templateUrl('job-detail/job-detail'), +// // controller: 'JobDetailController' +// // }; +// //======= +// ======= +// >>>>>>> Rebase of devel (w/ channels) + socket rework for new job details +// // /************************************************* +// // * Copyright (c) 2016 Ansible, Inc. +// // * +// // * All Rights Reserved +// // *************************************************/ +// // +// // import {templateUrl} from '../shared/template-url/template-url.factory'; +// // +// // export default { +// // name: 'jobDetail', +// // url: '/jobs/:id', +// // ncyBreadcrumb: { +// // parent: 'jobs', +// // label: "{{ job.id }} - {{ job.name }}" +// // }, +// // socket: { +// // "groups":{ +// // "jobs": ["status_changed", "summary"], +// // "job_events": [] +// // } +// // }, +// // resolve: { +// // jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { +// // if (!$rootScope.event_socket) { +// // $rootScope.event_socket = Socket({ +// // scope: $rootScope, +// // endpoint: "job_events" +// // }); +// // $rootScope.event_socket.init(); +// // // returns should really be providing $rootScope.event_socket +// // // otherwise, we have to inject the entire $rootScope into the controller +// // return true; +// // } else { +// // return true; +// // } +// // }] +// // }, +// // templateUrl: templateUrl('job-detail/job-detail'), +// // controller: 'JobDetailController' +// // }; diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index 6f52e5c67b..174f0068db 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -55,11 +55,22 @@ export default { // "?event=playbook_on_stats"); Rest.get() .success(function(data) { - defer.resolve({ - val: jobResultsService - .getCountsFromStatsEvent(data - .results[0].event_data), - countFinished: true}); + 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}); + } }) .error(function() { defer.resolve({val: { From 777a15f763977093f243bd97a9be60d1665ab808 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Mon, 31 Oct 2016 12:36:28 -0700 Subject: [PATCH 21/57] fixing small typo in jobs.py and adding sockets to the new job results page --- awx/ui/client/src/job-results/event-queue.service.js | 4 ++-- awx/ui/client/src/job-results/job-results.route.js | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js index 9d23295ac5..06be4d6ca6 100644 --- a/awx/ui/client/src/job-results/event-queue.service.js +++ b/awx/ui/client/src/job-results/event-queue.service.js @@ -132,7 +132,7 @@ export default ['jobResultsService', 'parseStdoutService', '$q', function(jobRes .then(count => { mungedEvent.count = count; mungedEvent.count.failed++; - mungedEvent.stdout = event.stdout; + mungedEvent.stdout = parseStdoutService.parseStdout(event); mungedEvent.changes = ['count', 'stdout']; }); } else if (event.event_name === 'playbook_on_stats') { @@ -140,7 +140,7 @@ export default ['jobResultsService', 'parseStdoutService', '$q', function(jobRes // get the data for populating the host status bar mungedEvent.count = jobResultsService .getCountsFromStatsEvent(event.event_data); - mungedEvent.stdout = event.stdout; + mungedEvent.stdout = parseStdoutService.parseStdout(event); mungedEvent.finishedTime = event.modified; mungedEvent.changes = ['count', 'countFinished', 'finishedTime', 'stdout']; } else { diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index 174f0068db..54e47e9655 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -15,10 +15,12 @@ export default { parent: 'jobs', label: '{{ job.id }} - {{ job.name }}' }, - socket: { - "groups":{ - "jobs": ["status_changed", "summary"], - "job_events": [] + data: { + socket: { + "groups":{ + "jobs": ["status_changed", "summary"], + "job_events": [] + } } }, resolve: { From 261c91d8360895f554fbc422c17a5189c141ebe5 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Mon, 31 Oct 2016 14:46:43 -0700 Subject: [PATCH 22/57] early styling of stdout --- .../job-results-stdout/job-results-stdout.partial.html | 4 ++-- awx/ui/client/src/job-results/parse-stdout.service.js | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) 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 index b0d187a42e..13515095f8 100644 --- 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 @@ -13,8 +13,8 @@
{{line.end_line}}
-
- {{line.stdout}} +
+
diff --git a/awx/ui/client/src/job-results/parse-stdout.service.js b/awx/ui/client/src/job-results/parse-stdout.service.js index 7580c17219..482aa33812 100644 --- a/awx/ui/client/src/job-results/parse-stdout.service.js +++ b/awx/ui/client/src/job-results/parse-stdout.service.js @@ -11,7 +11,13 @@ export default [function(){ // that i'm currently seeing on this branch on the beginning // and end of each event string. In the future it could be // used to add styling classes to the actual lines of stdout - return line.replace(/rn/g, ''); + line = line.replace(/rn/g, '\r\n'); + line = line.replace(/u001b/g, ''); + line = line.replace(/\[0;32m/g, ''); + line = line.replace(/\[0;32m=/g, ''); + line = line.replace(/\[0;32m1/g, ''); + line = line.replace(/\[0m/g, ''); + return line; }, parseStdout: function(event){ // this object will be used by the ng-repeat in the From 3e2940203fd4f27c62638dbf58d2ac77ae542813 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Mon, 31 Oct 2016 17:11:14 -0700 Subject: [PATCH 23/57] more stdout styling --- .../job-results-stdout/job-results-stdout.block.less | 5 +++++ awx/ui/client/src/job-results/parse-stdout.service.js | 11 ++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) 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 index cb730f24c1..0f15dcb384 100644 --- 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 @@ -16,6 +16,7 @@ padding: 10px 10px 10px; color: @b7grey; width: 75px; + white-space: pre-line; } .JobResultsStdOut-lineNumberColumn--first{ @@ -26,6 +27,10 @@ .JobResultsStdOut-stdoutColumn{ padding: 10px 20px 10px 20px; color: @default-interface-txt; + display: inline-block; + white-space: pre-wrap; + word-wrap: normal; + width:100%; } .JobResultsStdOut-stdoutColumn--first{ diff --git a/awx/ui/client/src/job-results/parse-stdout.service.js b/awx/ui/client/src/job-results/parse-stdout.service.js index 482aa33812..b73301bb70 100644 --- a/awx/ui/client/src/job-results/parse-stdout.service.js +++ b/awx/ui/client/src/job-results/parse-stdout.service.js @@ -11,11 +11,20 @@ export default [function(){ // that i'm currently seeing on this branch on the beginning // and end of each event string. In the future it could be // used to add styling classes to the actual lines of stdout - line = line.replace(/rn/g, '\r\n'); + line = line.replace(/rn/g, ''); line = line.replace(/u001b/g, ''); + + // ok line = line.replace(/\[0;32m/g, ''); + + //unreachable + line = line.replace(/\[1;31m/g, ''); + line = line.replace(/\[0;31m/g, ''); + line = line.replace(/\[0;32m=/g, ''); line = line.replace(/\[0;32m1/g, ''); + + //end span line = line.replace(/\[0m/g, ''); return line; }, From 51bfc0525aab964cd38b0d681e1daa5ac942aa2a Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Mon, 31 Oct 2016 17:20:36 -0700 Subject: [PATCH 24/57] adding new line character and 'ok' host colors --- awx/ui/client/src/job-results/parse-stdout.service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/job-results/parse-stdout.service.js b/awx/ui/client/src/job-results/parse-stdout.service.js index b73301bb70..643fc7e0c1 100644 --- a/awx/ui/client/src/job-results/parse-stdout.service.js +++ b/awx/ui/client/src/job-results/parse-stdout.service.js @@ -11,11 +11,11 @@ export default [function(){ // that i'm currently seeing on this branch on the beginning // and end of each event string. In the future it could be // used to add styling classes to the actual lines of stdout - line = line.replace(/rn/g, ''); + line = line.replace(/rn/g, '\n'); line = line.replace(/u001b/g, ''); // ok - line = line.replace(/\[0;32m/g, ''); + line = line.replace(/\[0;32m/g, ''); //unreachable line = line.replace(/\[1;31m/g, ''); From 3c261b89049afdba0b18ec36f54d257d11b21062 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 2 Nov 2016 16:09:13 -0400 Subject: [PATCH 25/57] working on stdout view line population --- .../src/job-results/event-queue.service.js | 39 ++++++++------- .../job-results-stdout.block.less | 23 +++++++-- .../job-results-stdout.partial.html | 12 +---- .../src/job-results/job-results.controller.js | 2 +- .../src/job-results/parse-stdout.service.js | 48 ++++++++++++++++--- 5 files changed, 85 insertions(+), 39 deletions(-) diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js index 06be4d6ca6..929c5dc9a8 100644 --- a/awx/ui/client/src/job-results/event-queue.service.js +++ b/awx/ui/client/src/job-results/event-queue.service.js @@ -73,9 +73,18 @@ export default ['jobResultsService', 'parseStdoutService', '$q', function(jobRes counter: event.counter, id: event.id, processed: false, - name: event.event_name + 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.changes.push('stdout'); + } + // for different types of events, you need different types of data if (event.event_name === 'playbook_on_start') { mungedEvent.count = { @@ -86,20 +95,19 @@ export default ['jobResultsService', 'parseStdoutService', '$q', function(jobRes changed: 0 }; mungedEvent.startTime = event.modified; - mungedEvent.changes = ['count', 'startTime']; + mungedEvent.changes.push('count'); + mungedEvent.changes.push('startTime'); } else if (event.event_name === 'playbook_on_play_start') { getPreviousCount(mungedEvent.counter, "play") .then(count => { mungedEvent.playCount = count + 1; - mungedEvent.stdout = parseStdoutService.parseStdout(event); - mungedEvent.changes = ['playCount', 'stdout']; + mungedEvent.changes.push('playCount'); }); } else if (event.event_name === 'playbook_on_task_start') { getPreviousCount(mungedEvent.counter, "task") .then(count => { mungedEvent.taskCount = count + 1; - mungedEvent.stdout = parseStdoutService.parseStdout(event); - mungedEvent.changes = ['taskCount', 'stdout']; + mungedEvent.changes.push('taskCount'); }); } else if (event.event_name === 'runner_on_ok' || event.event_name === 'runner_on_async_ok') { @@ -107,24 +115,21 @@ export default ['jobResultsService', 'parseStdoutService', '$q', function(jobRes .then(count => { mungedEvent.count = count; mungedEvent.count.ok++; - mungedEvent.stdout = parseStdoutService.parseStdout(event); - mungedEvent.changes = ['count', 'stdout']; + mungedEvent.changes.push('count'); }); } else if (event.event_name === 'runner_on_skipped') { getPreviousCount(mungedEvent.counter) .then(count => { mungedEvent.count = count; mungedEvent.count.skipped++; - mungedEvent.stdout = parseStdoutService.parseStdout(event); - mungedEvent.changes = ['count', 'stdout']; + mungedEvent.changes.push('count'); }); } else if (event.event_name === 'runner_on_unreachable') { getPreviousCount(mungedEvent.counter) .then(count => { mungedEvent.count = count; mungedEvent.count.unreachable++; - mungedEvent.stdout = parseStdoutService.parseStdout(event); - mungedEvent.changes = ['count', 'stdout']; + mungedEvent.changes.push('count'); }); } else if (event.event_name === 'runner_on_error' || event.event_name === 'runner_on_async_failed') { @@ -132,18 +137,16 @@ export default ['jobResultsService', 'parseStdoutService', '$q', function(jobRes .then(count => { mungedEvent.count = count; mungedEvent.count.failed++; - mungedEvent.stdout = parseStdoutService.parseStdout(event); - mungedEvent.changes = ['count', 'stdout']; + mungedEvent.changes.push('count'); }); } else if (event.event_name === 'playbook_on_stats') { - console.log(event.modified); // get the data for populating the host status bar mungedEvent.count = jobResultsService .getCountsFromStatsEvent(event.event_data); - mungedEvent.stdout = parseStdoutService.parseStdout(event); mungedEvent.finishedTime = event.modified; - mungedEvent.changes = ['count', 'countFinished', 'finishedTime', 'stdout']; - } else { + mungedEvent.changes.push('count'); + mungedEvent.changes.push('countFinished'); + mungedEvent.changes.push('finishedTime'); } mungedEventDefer.resolve(mungedEvent); 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 index 0f15dcb384..a93703c2d8 100644 --- 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 @@ -4,28 +4,45 @@ height: 100%; margin-top: 15px; background-color: @default-no-items-bord; + border-radius: 5px; + margin-bottom: 10px; } -.JobResultsStdOut-aLineOfStdOut{ +.JobResultsStdOut-aLineOfStdOut, +.JobResultsStdOut-expandLine { display: flex; } .JobResultsStdOut-lineNumberColumn{ + display: flex; background-color: @default-list-header-bg; text-align: right; - padding: 10px 10px 10px; + padding-right: 10px; + padding-top: 2px; + padding-bottom: 2px; color: @b7grey; width: 75px; white-space: pre-line; } +.JobResultsStdOut-lineExpander { + text-align: left; + padding-left: 10px; + margin-right: auto; +} + .JobResultsStdOut-lineNumberColumn--first{ text-align: left; padding: 0px; + padding-left: 11px; + padding-top: 10px; + white-space: normal; } .JobResultsStdOut-stdoutColumn{ - padding: 10px 20px 10px 20px; + padding-left: 20px; + padding-top: 2px; + padding-bottom: 2px; color: @default-interface-txt; display: inline-block; white-space: pre-wrap; 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 index 13515095f8..283a80a502 100644 --- 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 @@ -1,5 +1,5 @@
-
+
@@ -8,14 +8,4 @@ JobResultsStdOut-stdoutColumn--first">
-
-
-
- {{line.end_line}} -
-
- -
-
-
diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index f4921a283a..a920cdcdd5 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -132,7 +132,7 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa } if(change === 'stdout'){ - $scope.stdoutArr.push(mungedEvent.stdout); + $(".JobResultsStdOut").append(mungedEvent.stdout); } }); } diff --git a/awx/ui/client/src/job-results/parse-stdout.service.js b/awx/ui/client/src/job-results/parse-stdout.service.js index 643fc7e0c1..585032d7f9 100644 --- a/awx/ui/client/src/job-results/parse-stdout.service.js +++ b/awx/ui/client/src/job-results/parse-stdout.service.js @@ -7,11 +7,12 @@ export default [function(){ var val = { prettify: function(line){ + // this function right now just removes the 'rn' strings // that i'm currently seeing on this branch on the beginning // and end of each event string. In the future it could be // used to add styling classes to the actual lines of stdout - line = line.replace(/rn/g, '\n'); + // line = line.replace(/rn/g, '\n'); line = line.replace(/u001b/g, ''); // ok @@ -23,20 +24,55 @@ export default [function(){ line = line.replace(/\[0;32m=/g, ''); line = line.replace(/\[0;32m1/g, ''); + line = line.replace(/\[0;33m/g, ''); + line = line.replace(/\[0;36m/g, ''); //end span line = line.replace(/\[0m/g, ''); return line; }, + getCollapseClasses: function(event) { + var string = ""; + if (event.event_name === "playbook_on_play_start") { + return string; + } else if (event.event_name === "playbook_on_task_start") { + if (event.event_data.play_uuid) { + string += " play_" + event.event_data.play_uuid; + } + return string; + } else { + if (event.event_data.play_uuid) { + string += " play_" + event.event_data.play_uuid; + } + if (event.event_data.task_uuid) { + string += " task_" + event.event_data.task_uuid; + } + return string; + } + }, + getCollapseIcon: function(event, line) { + if ((event.event_name === "playbook_on_play_start" || event.event_name === "playbook_on_task_start") && line !== "") { + return ``; + } else { + return ``; + } + }, parseStdout: function(event){ + var stdoutStrings = _ + .zip(_.range(event.start_line + 1, + event.end_line + 1), + event.stdout.split("\r\n").slice(0, -1)) + .map(lineArr => { + return ` +
+
${this.getCollapseIcon(event, lineArr[1])}${lineArr[0]}
+
${this.prettify(lineArr[1])}
+
`; + }).join(""); // this object will be used by the ng-repeat in the // job-results-stdout.partial.html. probably need to add the // elapsed time in here too - return { - start_line: event.start_line, - end_line: event.end_line, - stdout: this.prettify(event.stdout) - }; + return stdoutStrings; } }; return val; From 16b4daf4eb6ab40f999941a5591d6ad425290a83 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 2 Nov 2016 16:33:31 -0400 Subject: [PATCH 26/57] disable selecting of line numbers unfortunately this will still copy to clipboard in chrome but not firefox see https://bugs.chromium.org/p/chromium/issues/detail\?id\=147490 for open chromium bug --- .../job-results-stdout/job-results-stdout.block.less | 4 ++++ 1 file changed, 4 insertions(+) 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 index a93703c2d8..a669d2b5a0 100644 --- 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 @@ -23,6 +23,10 @@ color: @b7grey; width: 75px; white-space: pre-line; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; } .JobResultsStdOut-lineExpander { From e1edcc5e5a891bc91a7d3f259ea8821842fc29bd Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Sun, 6 Nov 2016 13:38:55 -0500 Subject: [PATCH 27/57] trying to help make the main layout of the page respond better to scrolling within scrolling --- awx/ui/client/legacy-styles/main-layout.less | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/legacy-styles/main-layout.less b/awx/ui/client/legacy-styles/main-layout.less index dc6722cc6e..99c865d737 100644 --- a/awx/ui/client/legacy-styles/main-layout.less +++ b/awx/ui/client/legacy-styles/main-layout.less @@ -21,6 +21,7 @@ body { padding-bottom: 50px; position: relative; background-color: @default-secondary-bg; + padding-top: 96px; } .container-fluid { @@ -61,7 +62,7 @@ body { } #content-container { - padding-bottom: 40px; + padding-bottom: 0px; } .group-breadcrumbs { From 66e90075d48c09bac2f2c6f9b92ebf3b51d71d89 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Sun, 6 Nov 2016 13:39:46 -0500 Subject: [PATCH 28/57] changing tooltip delay to make them feel more responsive --- awx/ui/client/src/config.js | 1 - awx/ui/client/src/shared/directives.js | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/config.js b/awx/ui/client/src/config.js index b252faec17..bac59a282c 100644 --- a/awx/ui/client/src/config.js +++ b/awx/ui/client/src/config.js @@ -26,7 +26,6 @@ return { // custom_logo: true // load /var/lib/awx/public/static/assets/custom_console_logo.png as the login modal header. if false, will load the standard tower console logo // custom_login_info: "example notice" // have a notice displayed in the login modal for users. note that, as a security measure, custom html is not supported and will be escaped. - tooltip_delay: { show: 500, hide: 100 }, // Default number of milliseconds to delay displaying/hiding tooltips password_length: 8, // Minimum user password length. Set to 0 to not set a limit password_hasLowercase: true, // require a lowercase letter in the password diff --git a/awx/ui/client/src/shared/directives.js b/awx/ui/client/src/shared/directives.js index cd14cdb17a..433bbbcb78 100644 --- a/awx/ui/client/src/shared/directives.js +++ b/awx/ui/client/src/shared/directives.js @@ -492,7 +492,10 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'JobsHelper']) .directive('awToolTip', [function() { return { link: function(scope, element, attrs) { - var delay = (attrs.delay !== undefined && attrs.delay !== null) ? attrs.delay : ($AnsibleConfig) ? $AnsibleConfig.tooltip_delay : { show: 500, hide: 100 }, + // if (attrs.class.indexOf("JobResultsStdOut") > -1) { + // debugger; + // } + var delay = { show: 200, hide: 0 }, placement, stateChangeWatcher; if (attrs.awTipPlacement) { From 783f784e691272f09f6f63f6e0b4550181d53390 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Sun, 6 Nov 2016 13:41:17 -0500 Subject: [PATCH 29/57] update to standard out pane - add lines to pane - support show and hide toggling of lines NOTE: layout is hardcoded, will need to move to dealing with various browser heights better --- .../src/job-results/event-queue.service.js | 6 + .../job-results-stdout.block.less | 149 +++++++++++++++--- .../job-results-stdout.directive.js | 70 +++++++- .../job-results-stdout.partial.html | 32 +++- .../src/job-results/job-results.block.less | 4 + .../src/job-results/job-results.controller.js | 7 +- .../src/job-results/job-results.route.js | 4 +- .../src/job-results/job-results.service.js | 3 +- .../src/job-results/parse-stdout.service.js | 60 ++++--- 9 files changed, 273 insertions(+), 62 deletions(-) diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js index 929c5dc9a8..b8796c361e 100644 --- a/awx/ui/client/src/job-results/event-queue.service.js +++ b/awx/ui/client/src/job-results/event-queue.service.js @@ -175,6 +175,12 @@ export default ['jobResultsService', 'parseStdoutService', '$q', function(jobRes val.populateDefers[event.counter] = $q.defer(); } + if (val.queue[event.counter] && + val.queue[event.counter].processed) { + val.populateDefers.reject("duplicate event: " + + event); + } + if (!val.queue[event.counter]) { var resolvePopulation = function(event) { // to resolve, put the event on the queue and 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 index a669d2b5a0..be005bb7b2 100644 --- 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 @@ -1,19 +1,128 @@ @import '../../shared/branding/colors.default.less'; -.JobResultsStdOut{ +.JobResultsStdOut { height: 100%; - margin-top: 15px; - background-color: @default-no-items-bord; - border-radius: 5px; - margin-bottom: 10px; } -.JobResultsStdOut-aLineOfStdOut, -.JobResultsStdOut-expandLine { +.JobResultsStdOut-toolbar { display: flex; + height: 38px; + margin-top: 15px; + border: 1px solid @default-list-header-bg; + 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-lineNumberColumn{ +.JobResultsStdOut-toolbarNumberColumn { + background-color: @default-list-header-bg; + color: @b7grey; + flex: initial; + display: flex; + justify-content: space-between; + width: 70px; + padding-bottom: 0px; + padding-left: 8px; + padding-right: 8px; + padding-top: 10px; + border-top-left-radius: 5px; +} + +.JobResultsStdOut-expandAllButton { + height: 18px; + width: 18px; + padding-left: 4px; + padding-top: 1px; + border-radius: 50%; + background-color: @default-bg; + font-size: 12px; + cursor: pointer; +} + +.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-no-items-bord; +} + +.JobResultsStdOut-followButton { + cursor: pointer; + width: 18px; + height: 18px; + width: 18px; + padding-left: 3.8px; + border-radius: 50%; + margin-top: 10px; + font-size: 12px; + background-color: @default-icon; + color: @default-border; +} + +.JobResultsStdOut-followIcon { + color: @default-border; +} + +.JobResultsStdOut-followButton:hover { + background-color: @default-icon-hov; +} + +.JobResultsStdOut-followButton:hover .JobResultsStdOut-followIcon, +.JobResultsStdOut-followIcon:hover { + color: @default-interface-txt; +} + +.JobResultsStdOut-stdoutContainer { + height: ~"calc(100% - 108px)"; + background-color: @default-no-items-bord; + border: 1px solid @default-list-header-bg; + border-top: 0px; + border-radius: 5px; + border-top-left-radius: 0px; + border-top-right-radius: 0px; + margin-bottom: 10px; + overflow: scroll; +} + +.JobResultsStdOut-numberColumnPreload { + background-color: #EBEBEB; + width: 70px; + position: fixed; +} + +.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; @@ -22,28 +131,16 @@ padding-bottom: 2px; color: @b7grey; width: 75px; + flex: initial; white-space: pre-line; user-select: none; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; + z-index: 1; } -.JobResultsStdOut-lineExpander { - text-align: left; - padding-left: 10px; - margin-right: auto; -} - -.JobResultsStdOut-lineNumberColumn--first{ - text-align: left; - padding: 0px; - padding-left: 11px; - padding-top: 10px; - white-space: normal; -} - -.JobResultsStdOut-stdoutColumn{ +.JobResultsStdOut-stdoutColumn { padding-left: 20px; padding-top: 2px; padding-bottom: 2px; @@ -54,6 +151,8 @@ width:100%; } -.JobResultsStdOut-stdoutColumn--first{ - padding-top:0px; + +// TODO: needs to be set based on height of browser window +.JobResultsStdOut-numberColumnPreload { + height: 720px; } 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 index e6bdefe91d..dd7366fc11 100644 --- 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 @@ -5,15 +5,75 @@ *************************************************/ // import hostStatusBarController from './host-status-bar.controller'; -export default [ 'templateUrl', - function(templateUrl) { +export default [ 'templateUrl', '$timeout', + function(templateUrl, $timeout) { return { - scope: true, + scope: false, templateUrl: templateUrl('job-results/job-results-stdout/job-results-stdout'), restrict: 'E', - // controller: jobResultsStdOutController, - link: function(scope) { + link: function(scope, element, attrs) { + scope.toggleAllStdout = function(type) { + var expandClass; + if (type === 'expand') { + expandClass = "fa-caret-right"; + } else { + expandClass = "fa-caret-down"; + } + element.find(".expanderizer--task."+expandClass) + .each((i, val) => { + $timeout(function(){ + angular.element(val).trigger('click'); + }); + }); + + element.find(".expanderizer--play."+expandClass) + .each((i, val) => { + if(angular.element("." + + angular.element(val).attr("data-uuid")) + .find(".expanderizer--task") + .length === 0 || + type !== 'collapse') { + + $timeout(function(){ + angular.element(val) + .trigger('click'); + }); + } + }); + }; + + scope.toggleLine = function($event, id) { + if ($($event.currentTarget).hasClass("fa-caret-down")) { + $(id).hide(); + $($event.currentTarget) + .removeClass("fa-caret-down"); + $($event.currentTarget) + .addClass("fa-caret-right"); + } else { + $(id).show(); + $($event.currentTarget) + .removeClass("fa-caret-right"); + $($event.currentTarget) + .addClass("fa-caret-down"); + + if ($($event.currentTarget) + .hasClass("expanderizer--play")) { + $("." + $($event.currentTarget) + .attr("data-uuid")) + .find(".expanderizer--task") + .each((i, val) => { + if ($(val) + .hasClass("fa-caret-right")) { + $timeout(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 index 283a80a502..152429c7d4 100644 --- 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 @@ -1,11 +1,31 @@
-
-
- +
+
+
+ + +
+
+ + +
-
+
+
+ + +
+
+
+
diff --git a/awx/ui/client/src/job-results/job-results.block.less b/awx/ui/client/src/job-results/job-results.block.less index fa1e139501..060c08bdce 100644 --- a/awx/ui/client/src/job-results/job-results.block.less +++ b/awx/ui/client/src/job-results/job-results.block.less @@ -17,10 +17,14 @@ .JobResults-leftSide { .OnePlusTwo-left--panel(100%, @breakpoint-md); + // TODO: needs to be set based on height of browser window + height: 870px !important; } .JobResults-rightSide { .OnePlusTwo-right--panel(100%, @breakpoint-md); + // TODO: needs to be set based on height of browser window + height: 870px !important; @media (max-width: @breakpoint-md - 1px) { padding-right: 15px; diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index a920cdcdd5..73d7c1fc53 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -1,4 +1,4 @@ -export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', '$rootScope', 'eventQueue', function(jobData, jobDataOptions, jobLabels, count, $scope, ParseTypeChange, ParseVariableString, jobResultsService, $rootScope, eventQueue) { +export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'ParseTypeChange', 'ParseVariableString', 'jobResultsService', '$rootScope', 'eventQueue', '$compile', function(jobData, jobDataOptions, jobLabels, count, $scope, ParseTypeChange, ParseVariableString, jobResultsService, $rootScope, eventQueue, $compile) { var getTowerLinks = function() { var getTowerLink = function(key) { if ($scope.job.related[key]) { @@ -132,7 +132,10 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa } if(change === 'stdout'){ - $(".JobResultsStdOut").append(mungedEvent.stdout); + angular + .element(".JobResultsStdOut-stdoutContainer") + .append($compile(mungedEvent + .stdout)($scope)); } }); } diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index 54e47e9655..c2bf5f3e01 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -53,8 +53,8 @@ export default { 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.setUrl(jobData.related.job_events + + "?event=playbook_on_stats"); Rest.get() .success(function(data) { if(!data.results[0]){ diff --git a/awx/ui/client/src/job-results/job-results.service.js b/awx/ui/client/src/job-results/job-results.service.js index 0109eeee55..3ca6019587 100644 --- a/awx/ui/client/src/job-results/job-results.service.js +++ b/awx/ui/client/src/job-results/job-results.service.js @@ -81,7 +81,8 @@ export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErr Rest.setUrl(url); Rest.get() .success(function(data) { - val.resolve({results: data.results, next: data.next}); + val.resolve({results: data.results, + next: data.next}); }) .error(function(obj, status) { ProcessErrors(null, obj, status, null, { diff --git a/awx/ui/client/src/job-results/parse-stdout.service.js b/awx/ui/client/src/job-results/parse-stdout.service.js index 585032d7f9..d569b3420f 100644 --- a/awx/ui/client/src/job-results/parse-stdout.service.js +++ b/awx/ui/client/src/job-results/parse-stdout.service.js @@ -7,21 +7,13 @@ export default [function(){ var val = { prettify: function(line){ - - // this function right now just removes the 'rn' strings - // that i'm currently seeing on this branch on the beginning - // and end of each event string. In the future it could be - // used to add styling classes to the actual lines of stdout - // line = line.replace(/rn/g, '\n'); + // TODO: figure out from Jared what this is line = line.replace(/u001b/g, ''); - // ok + // ansi classes line = line.replace(/\[0;32m/g, ''); - - //unreachable line = line.replace(/\[1;31m/g, ''); line = line.replace(/\[0;31m/g, ''); - line = line.replace(/\[0;32m=/g, ''); line = line.replace(/\[0;32m1/g, ''); line = line.replace(/\[0;33m/g, ''); @@ -34,12 +26,12 @@ export default [function(){ getCollapseClasses: function(event) { var string = ""; if (event.event_name === "playbook_on_play_start") { - return string; + string += " header_play"; } else if (event.event_name === "playbook_on_task_start") { + string += " header_task"; if (event.event_data.play_uuid) { string += " play_" + event.event_data.play_uuid; } - return string; } else { if (event.event_data.play_uuid) { string += " play_" + event.event_data.play_uuid; @@ -47,18 +39,48 @@ export default [function(){ if (event.event_data.task_uuid) { string += " task_" + event.event_data.task_uuid; } - return string; } + + return string; }, getCollapseIcon: function(event, line) { - if ((event.event_name === "playbook_on_play_start" || event.event_name === "playbook_on_task_start") && line !== "") { - return ``; + 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) { + expanderizerSpecifier = "play"; + clickClass = "play_" + + event.event_data.play_uuid; + } else if (line.indexOf("TASK") > -1 || + line.indexOf("RUNNING HANDLER") > -1) { + expanderizerSpecifier = "task"; + clickClass = "task_" + + event.event_data.task_uuid; + } else { + return emptySpan; + } + + return ` + + + +`; } else { - return ``; + return emptySpan; } }, parseStdout: function(event){ - var stdoutStrings = _ + return _ .zip(_.range(event.start_line + 1, event.end_line + 1), event.stdout.split("\r\n").slice(0, -1)) @@ -69,10 +91,6 @@ export default [function(){
${this.prettify(lineArr[1])}
`; }).join(""); - // this object will be used by the ng-repeat in the - // job-results-stdout.partial.html. probably need to add the - // elapsed time in here too - return stdoutStrings; } }; return val; From 7795ad0d2ba1e4435fde5a26644ffca6ef51b0ab Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 8 Nov 2016 11:24:15 -0500 Subject: [PATCH 30/57] styling fixes to job results --- .../job-results-stdout/job-results-stdout.block.less | 3 +-- awx/ui/client/src/job-results/parse-stdout.service.js | 10 +++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) 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 index be005bb7b2..8ea2918ddb 100644 --- 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 @@ -132,7 +132,6 @@ color: @b7grey; width: 75px; flex: initial; - white-space: pre-line; user-select: none; -moz-user-select: none; -webkit-user-select: none; @@ -147,7 +146,7 @@ color: @default-interface-txt; display: inline-block; white-space: pre-wrap; - word-wrap: normal; + word-break: break-word; width:100%; } diff --git a/awx/ui/client/src/job-results/parse-stdout.service.js b/awx/ui/client/src/job-results/parse-stdout.service.js index d569b3420f..011dd1f9ae 100644 --- a/awx/ui/client/src/job-results/parse-stdout.service.js +++ b/awx/ui/client/src/job-results/parse-stdout.service.js @@ -8,16 +8,24 @@ export default [function(){ var val = { prettify: function(line){ // TODO: figure out from Jared what this is + + if (line.indexOf("[K") > -1) { + console.log(line); + } + line = line.replace(/u001b/g, ''); // ansi classes - line = line.replace(/\[0;32m/g, ''); line = line.replace(/\[1;31m/g, ''); line = line.replace(/\[0;31m/g, ''); + line = line.replace(/\[0;32m/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;35m/g, ''); line = line.replace(/\[0;36m/g, ''); + line = line.replace(/()\s/g, '$1'); //end span line = line.replace(/\[0m/g, ''); From 0590a1416dd3423fec6014ed18b44e82272b061b Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 9 Nov 2016 16:14:45 -0500 Subject: [PATCH 31/57] Fitting panels to the size of the viewport --- .../job-results-stdout/job-results-stdout.block.less | 6 ------ awx/ui/client/src/job-results/job-results.block.less | 10 ++++++---- awx/ui/client/src/job-results/job-results.partial.html | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) 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 index 8ea2918ddb..58bd9d2664 100644 --- 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 @@ -149,9 +149,3 @@ word-break: break-word; width:100%; } - - -// TODO: needs to be set based on height of browser window -.JobResultsStdOut-numberColumnPreload { - height: 720px; -} diff --git a/awx/ui/client/src/job-results/job-results.block.less b/awx/ui/client/src/job-results/job-results.block.less index 060c08bdce..137f3b425a 100644 --- a/awx/ui/client/src/job-results/job-results.block.less +++ b/awx/ui/client/src/job-results/job-results.block.less @@ -17,20 +17,22 @@ .JobResults-leftSide { .OnePlusTwo-left--panel(100%, @breakpoint-md); - // TODO: needs to be set based on height of browser window - height: 870px !important; + height: 80vh; } .JobResults-rightSide { .OnePlusTwo-right--panel(100%, @breakpoint-md); - // TODO: needs to be set based on height of browser window - height: 870px !important; + height: 80vh; @media (max-width: @breakpoint-md - 1px) { padding-right: 15px; } } +.JobResults-detailsPanel{ + overflow-y: scroll; +} + .JobResults-stdoutActionButton--active { display: none; visibility: hidden; diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index ef8cb32a7a..f8b14c7d6f 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -8,7 +8,7 @@
-
From 1c907a1f07f7e3032c75149f9706fa6f56376bee Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 10 Nov 2016 17:03:57 -0500 Subject: [PATCH 32/57] layout tweaks --- .../job-results-stdout.block.less | 36 ++++++++++++++----- .../job-results-stdout.partial.html | 3 ++ .../src/job-results/job-results.block.less | 4 +-- 3 files changed, 32 insertions(+), 11 deletions(-) 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 index 58bd9d2664..2b6bc1d9df 100644 --- 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 @@ -1,7 +1,7 @@ @import '../../shared/branding/colors.default.less'; .JobResultsStdOut { - height: 100%; + height: ~"calc(100% - 70px)"; } .JobResultsStdOut-toolbar { @@ -31,6 +31,7 @@ padding-right: 8px; padding-top: 10px; border-top-left-radius: 5px; + z-index: 1; } .JobResultsStdOut-expandAllButton { @@ -85,21 +86,20 @@ } .JobResultsStdOut-stdoutContainer { - height: ~"calc(100% - 108px)"; + height: ~"calc(100% - 48px)"; background-color: @default-no-items-bord; - border: 1px solid @default-list-header-bg; - border-top: 0px; - border-radius: 5px; - border-top-left-radius: 0px; - border-top-right-radius: 0px; - margin-bottom: 10px; - overflow: scroll; + overflow-y: scroll; + overflow-x: hidden; } .JobResultsStdOut-numberColumnPreload { background-color: #EBEBEB; width: 70px; position: fixed; + top: 148px; + bottom: 20px; + margin-top: 65px; + margin-bottom: 65px; } .JobResultsStdOut-aLineOfStdOut { @@ -149,3 +149,21 @@ word-break: break-word; width:100%; } + +.JobResultsStdOut-footer { + height: 10px; + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; + background-color: @default-no-items-bord; + border: 1px solid @default-list-header-bg; + border-top: 0px; + border-radius: 5px; + border-top-left-radius: 0px; + border-top-right-radius: 0px; +} + +.JobResultsStdOut-footerNumberColumn { + background-color: @default-list-header-bg; + width: 70px; + height: 100%; +} 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 index 152429c7d4..c0f13597c4 100644 --- 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 @@ -28,4 +28,7 @@
+
diff --git a/awx/ui/client/src/job-results/job-results.block.less b/awx/ui/client/src/job-results/job-results.block.less index 137f3b425a..f6ac115a70 100644 --- a/awx/ui/client/src/job-results/job-results.block.less +++ b/awx/ui/client/src/job-results/job-results.block.less @@ -17,12 +17,12 @@ .JobResults-leftSide { .OnePlusTwo-left--panel(100%, @breakpoint-md); - height: 80vh; + height: ~"calc(100vh - 177px)"; } .JobResults-rightSide { .OnePlusTwo-right--panel(100%, @breakpoint-md); - height: 80vh; + height: ~"calc(100vh - 177px)"; @media (max-width: @breakpoint-md - 1px) { padding-right: 15px; From c94d86206dd83bc85b15608ba90508ea46af3c51 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 11 Nov 2016 14:27:35 -0500 Subject: [PATCH 33/57] all things scroll following and anchoring --- .../job-results-stdout.block.less | 25 ++++ .../job-results-stdout.directive.js | 126 +++++++++++++++++- .../job-results-stdout.partial.html | 20 ++- .../src/job-results/job-results.controller.js | 12 +- .../src/job-results/job-results.route.js | 8 ++ .../src/job-results/parse-stdout.service.js | 14 +- 6 files changed, 196 insertions(+), 9 deletions(-) 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 index 2b6bc1d9df..ff0329a453 100644 --- 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 @@ -85,6 +85,24 @@ color: @default-interface-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 { height: ~"calc(100% - 48px)"; background-color: @default-no-items-bord; @@ -167,3 +185,10 @@ width: 70px; height: 100%; } + +.foo { + margin-right: auto; + margin-top: 8px; + margin-left: 20px; + font-weight: bold; +} 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 index dd7366fc11..be0c08fc93 100644 --- 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 @@ -5,14 +5,132 @@ *************************************************/ // import hostStatusBarController from './host-status-bar.controller'; -export default [ 'templateUrl', '$timeout', - function(templateUrl, $timeout) { +export default [ 'templateUrl', '$timeout', '$location', '$anchorScroll', '$log', + function(templateUrl, $timeout, $location, $anchorScroll, $log) { return { scope: false, templateUrl: templateUrl('job-results/job-results-stdout/job-results-stdout'), restrict: 'E', - link: function(scope, element, attrs) { + link: function(scope, element) { + + var findTopLines = function() { + scope.visibleItems = ""; + var $container = $('.JobResultsStdOut-stdoutContainer'); + var visItem, + parentItem; + $container.find('.JobResultsStdOut-aLineOfStdOut').each( function () { + var $this = $(this); + if ( $this.position().top + $this.height() > $container.position().top && + $this.position().top < ($container.height() + $container.position().top) ) { + visItem = parseInt($($this.children()[0]).text()); + + var $head, + classList, + header; + if ($this.hasClass("header_play") || $this.hasClass("header_task")) { + classList = $this.attr("class") + .split(" "); + header = classList + .filter(n => n.indexOf("header_task_") > -1)[0]; + if (!header) { + header = classList + .filter(n => n.indexOf("header_play_") > -1)[0]; + } + $head = $(".actual_header." + header); + } else { + classList = $this.attr("class") + .split(" "); + header = classList + .filter(n => n.indexOf("task_") > -1)[0]; + if (!header) { + header = classList + .filter(n => n.indexOf("play_") > -1)[0]; + } + + $head = $(".actual_header.header_" + header); + } + parentItem = parseInt($($head.children()[0]).text()); + return false; + } + }); + scope.visLine = visItem; + scope.parentVisLine = parentItem; + scope.visibleItems = "First visible line: " + visItem + ", Parent line: " + parentItem; + }; + + var lastScrollTop = 0; + $(".JobResultsStdOut-stdoutContainer").on('scroll', function() { + var st = $(this).scrollTop(); + if (st < lastScrollTop){ + // user up scrolled, so disengage follow + scope.followEngaged = false; + } + + if($(this).scrollTop() + $(this).innerHeight() >= + $(this)[0].scrollHeight) { + // user scrolled all the way to bottom, so engage + // follow + scope.followEngaged = true; + } + + lastScrollTop = st; + }); + + scope.followScroll = function() { + $(".JobResultsStdOut-followAnchor") + .appendTo(".JobResultsStdOut-stdoutContainer"); + + $location.hash('followAnchor'); + $anchorScroll(); + }; + + scope.topLineAnchor = function() { + $location.hash('topLineAnchor'); + $anchorScroll(); + } + + // 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 = "Jump to last line"; + } + + // if following becomes active, go ahead and get to the bottom + // of the standard out pane + scope.$watch('followEngaged', function(val) { + if (val) { + scope.followScroll(); + } + + if (!scope.jobFinished) { + if (val) { + scope.followTooltip = "Follow standard out"; + } else { + scope.followTooltip = "Unfollow standard out"; + } + } + }); + + scope.followToggleClicked = function() { + if (scope.jobFinished) { + scope.followScroll(); + } else { + scope.followEngaged = !scope.followEngaged; + } + }; + scope.toggleAllStdout = function(type) { + findTopLines(); + + if (type === 'expand') { + $(".line_num_" + scope.visLine) + .prepend($("#topLineAnchor")); + } else { + $(".line_num_" + scope.parentVisLine) + .prepend($("#topLineAnchor")); + } + var expandClass; if (type === 'expand') { expandClass = "fa-caret-right"; @@ -24,6 +142,7 @@ export default [ 'templateUrl', '$timeout', .each((i, val) => { $timeout(function(){ angular.element(val).trigger('click'); + scope.topLineAnchor(); }); }); @@ -38,6 +157,7 @@ export default [ 'templateUrl', '$timeout', $timeout(function(){ angular.element(val) .trigger('click'); + scope.topLineAnchor(); }); } }); 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 index c0f13597c4..08a3f4ac17 100644 --- 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 @@ -17,16 +17,32 @@
+
+ ng-class="{'is-engaged': followEngaged && !jobFinished}" + aw-tool-tip="Follow standard out" + data-placement="left" + ng-click="followToggleClicked()">
+
+
+
+
-
-
+
+
+ ^ TOP +
-
+
-
+
Date: Tue, 15 Nov 2016 10:40:29 -0500 Subject: [PATCH 41/57] es6ify jobDetails controller tes --- .../job-results/job-results.controller-test.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/awx/ui/tests/spec/job-results/job-results.controller-test.js b/awx/ui/tests/spec/job-results/job-results.controller-test.js index d18f7c9b32..3c51064d29 100644 --- a/awx/ui/tests/spec/job-results/job-results.controller-test.js +++ b/awx/ui/tests/spec/job-results/job-results.controller-test.js @@ -23,7 +23,7 @@ describe('Controller: jobResultsController', () => { eventDefer = {}; populateResolve = {}; - var provideVals = function() { + let provideVals = () => { angular.mock.module('jobResults', ($provide) => { ParseTypeChange = jasmine.createSpy('ParseTypeChange'); ParseVariableString = jasmine.createSpy('ParseVariableString'); @@ -52,7 +52,7 @@ describe('Controller: jobResultsController', () => { }); }; - var injectVals = function() { + let injectVals = () => { angular.mock.inject((_jobData_, _jobDataOptions_, _jobLabels_, _jobFinished_, _count_, _ParseTypeChange_, _ParseVariableString_, _jobResultsService_, _eventQueue_, _$compile_, $rootScope, $controller, $q) => { $scope = $rootScope.$new(); $rScope = $rootScope; @@ -93,7 +93,7 @@ describe('Controller: jobResultsController', () => { beforeEach(angular.mock.module('Tower')); - var bootstrapTest = function() { + let bootstrapTest = () => { provideVals(); injectVals(); }; @@ -173,7 +173,7 @@ describe('Controller: jobResultsController', () => { }); describe('extra vars stuff', () => { - var extraVars = "foo"; + let extraVars = "foo"; beforeEach(() => { jobData.extra_vars = extraVars; @@ -196,7 +196,7 @@ describe('Controller: jobResultsController', () => { }); it('should call ParseTypeChange with proper params', () => { - var params = { + let params = { scope: $scope, field_id: 'pre-formatted-variables', readOnly: true @@ -321,7 +321,7 @@ describe('Controller: jobResultsController', () => { }); }); - describe('event stuff', function() { + describe('event stuff', () => { beforeEach(() => { jobData.id = 1; jobData.related.job_events = "url"; @@ -334,13 +334,13 @@ describe('Controller: jobResultsController', () => { }); it('should call processEvent when receiving message', () => { - var eventPayload = {"foo": "bar"}; + 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', () => { - var eventPayload = { + let eventPayload = { unified_job_id: 1, status: 'finished' }; From f3526ca86ca9d5178d5252e52c2f1beaeadbd910 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 15 Nov 2016 15:28:18 -0500 Subject: [PATCH 42/57] add angular duration format filter lib --- .../client/src/job-results/duration.filter.js | 120 ---- awx/ui/client/src/job-results/main.js | 5 +- awx/ui/client/src/shared/main.js | 2 + awx/ui/npm-shrinkwrap.json | 586 ++++++++++++++++-- awx/ui/package.json | 1 + awx/ui/webpack.config.js | 1 + 6 files changed, 527 insertions(+), 188 deletions(-) delete mode 100644 awx/ui/client/src/job-results/duration.filter.js diff --git a/awx/ui/client/src/job-results/duration.filter.js b/awx/ui/client/src/job-results/duration.filter.js deleted file mode 100644 index ba23374784..0000000000 --- a/awx/ui/client/src/job-results/duration.filter.js +++ /dev/null @@ -1,120 +0,0 @@ -// TODO: come back and get this library -export default function() { - var DURATION_FORMATS_SPLIT = /((?:[^ydhms']+)|(?:'(?:[^']|'')*')|(?:y+|d+|h+|m+|s+))(.*)/; - var DURATION_FORMATS = { - 'y': { // years - // "longer" years are not supported - value: 365 * 24 * 60 * 60 * 1000 - }, - 'yy': { - value: 'y', - pad: 2 - }, - 'd': { // days - value: 24 * 60 * 60 * 1000 - }, - 'dd': { - value: 'd', - pad: 2 - }, - 'h': { // hours - value: 60 * 60 * 1000 - }, - 'hh': { // padded hours - value: 'h', - pad: 2 - }, - 'm': { // minutes - value: 60 * 1000 - }, - 'mm': { // padded minutes - value: 'm', - pad: 2 - }, - 's': { // seconds - value: 1000 - }, - 'ss': { // padded seconds - value: 's', - pad: 2 - }, - 'sss': { // milliseconds - value: 1 - }, - 'ssss': { // padded milliseconds - value: 'sss', - pad: 4 - } - }; - - function _parseFormat(string) { - // @inspiration AngularJS date filter - var parts = []; - var format = string; - while(format) { - var match = DURATION_FORMATS_SPLIT.exec(format); - if (match) { - parts = parts.concat(match.slice(1)); - - format = parts.pop(); - } else { - parts.push(format); - format = null; - } - } - return parts; - } - function _formatDuration(timestamp, format) { - var text = ''; - var values = { }; - format.filter(function(format) { // filter only value parts of format - return DURATION_FORMATS.hasOwnProperty(format); - }).map(function(format) { // get formats with values only - var config = DURATION_FORMATS[format]; - if(config.hasOwnProperty('pad')) { - return config.value; - } else { - return format; - } - }).filter(function(format, index, arr) { // remove duplicates - return (arr.indexOf(format) === index); - }).map(function(format) { // get format configurations with values - return angular.extend({ - name: format, - }, DURATION_FORMATS[format]); - }).sort(function(a, b) { // sort formats descending by value - return b.value - a.value; - }).forEach(function(format) { // create values for format parts - var value = values[format.name] = Math.floor(timestamp / format.value); - timestamp = timestamp - (value * format.value); - }); - format.forEach(function(part) { - var format = DURATION_FORMATS[part]; - if(format) { - var value = values[format.value]; - text += (format.hasOwnProperty('pad') ? _padNumber(value, Math.max(format.pad, value.toString().length)) : values[part]); - } else { - text += part.replace(/(^'|'$)/g, '').replace(/''/g, '\''); - } - }); - return text; - } - function _padNumber(number, len) { - return ((new Array(len + 1)).join('0') + number).slice(-len); - } - return function(value, format) { - if (typeof value !== "number") { - return value; - } - - var timestamp = parseInt(value.valueOf(), 10); - if(isNaN(timestamp)) { - return value; - } else { - return _formatDuration( - timestamp, - _parseFormat(format) - ); - } - }; - }; diff --git a/awx/ui/client/src/job-results/main.js b/awx/ui/client/src/job-results/main.js index c367b26918..3747da4158 100644 --- a/awx/ui/client/src/job-results/main.js +++ b/awx/ui/client/src/job-results/main.js @@ -16,8 +16,6 @@ import jobResultsService from './job-results.service'; import eventQueueService from './event-queue.service'; import parseStdoutService from './parse-stdout.service'; -import durationFilter from './duration.filter'; - export default angular.module('jobResults', [hostStatusBar.name, jobResultsStdOut.name, hostEvent.name]) .run(['$stateExtender', function($stateExtender) { @@ -26,5 +24,4 @@ export default .controller('jobResultsController', jobResultsController) .service('jobResultsService', jobResultsService) .service('eventQueue', eventQueueService) - .service('parseStdoutService', parseStdoutService) - .filter('duration', durationFilter); + .service('parseStdoutService', parseStdoutService); diff --git a/awx/ui/client/src/shared/main.js b/awx/ui/client/src/shared/main.js index 8db9dd8172..5b68027dde 100644 --- a/awx/ui/client/src/shared/main.js +++ b/awx/ui/client/src/shared/main.js @@ -20,6 +20,7 @@ import templateUrl from './template-url/main'; import RestServices from '../rest/main'; import stateDefinitions from './stateDefinitions.factory'; import apiLoader from './api-loader'; +import 'angular-duration-format'; export default angular.module('shared', [listGenerator.name, @@ -36,6 +37,7 @@ angular.module('shared', [listGenerator.name, RestServices.name, apiLoader.name, require('angular-cookies'), + 'angular-duration-format' ]) .factory('stateDefinitions', stateDefinitions) .factory('lodashAsPromised', lodashAsPromised) diff --git a/awx/ui/npm-shrinkwrap.json b/awx/ui/npm-shrinkwrap.json index 882ba5a78e..240a18f337 100644 --- a/awx/ui/npm-shrinkwrap.json +++ b/awx/ui/npm-shrinkwrap.json @@ -86,6 +86,11 @@ "from": "leigh-johnson/angular-drag-and-drop-lists#1.4.0", "resolved": "git://github.com/leigh-johnson/angular-drag-and-drop-lists.git#4d32654ab7159689a7767b9be8fc85f9812ca5a8" }, + "angular-duration-format": { + "version": "1.0.1", + "from": "angular-duration-format@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/angular-duration-format/-/angular-duration-format-1.0.1.tgz" + }, "angular-filters": { "version": "1.1.2", "from": "angular-filters@>=1.1.2 <2.0.0", @@ -102,9 +107,9 @@ "resolved": "https://registry.npmjs.org/angular-gettext-tools/-/angular-gettext-tools-2.3.0.tgz", "dependencies": { "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" } } }, @@ -113,6 +118,11 @@ "from": "angular-md5@>=0.1.8 <0.2.0", "resolved": "https://registry.npmjs.org/angular-md5/-/angular-md5-0.1.10.tgz" }, + "angular-mocks": { + "version": "1.5.8", + "from": "angular-mocks@>=1.5.8 <2.0.0", + "resolved": "https://registry.npmjs.org/angular-mocks/-/angular-mocks-1.5.8.tgz" + }, "angular-moment": { "version": "0.10.3", "from": "angular-moment@>=0.10.1 <0.11.0", @@ -207,9 +217,9 @@ "resolved": "https://registry.npmjs.org/archiver/-/archiver-1.1.0.tgz", "dependencies": { "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.8.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" } } }, @@ -219,9 +229,9 @@ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz", "dependencies": { "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.8.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" } } }, @@ -311,9 +321,9 @@ "resolved": "https://registry.npmjs.org/async/-/async-2.1.2.tgz", "dependencies": { "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.14.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" } } }, @@ -333,9 +343,9 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" }, "autoprefixer": { - "version": "6.5.1", + "version": "6.5.3", "from": "autoprefixer@>=6.0.0 <7.0.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.5.1.tgz" + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.5.3.tgz" }, "aws-sign2": { "version": "0.6.0", @@ -358,9 +368,9 @@ "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.18.2.tgz", "dependencies": { "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" }, "source-map": { "version": "0.5.6", @@ -375,9 +385,9 @@ "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.18.0.tgz", "dependencies": { "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" }, "source-map": { "version": "0.5.6", @@ -397,9 +407,9 @@ "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.18.0.tgz", "dependencies": { "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" } } }, @@ -429,9 +439,9 @@ "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.18.0.tgz", "dependencies": { "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" } } }, @@ -445,6 +455,60 @@ "from": "babel-helpers@>=6.16.0 <7.0.0", "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.16.0.tgz" }, + "babel-istanbul": { + "version": "0.11.0", + "from": "babel-istanbul@>=0.11.0 <0.12.0", + "resolved": "https://registry.npmjs.org/babel-istanbul/-/babel-istanbul-0.11.0.tgz", + "dependencies": { + "async": { + "version": "1.5.2", + "from": "async@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" + }, + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + }, + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + }, + "source-map": { + "version": "0.4.4", + "from": "source-map@>=0.4.0 <0.5.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz" + }, + "supports-color": { + "version": "3.1.2", + "from": "supports-color@>=3.1.0 <3.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz" + }, + "wordwrap": { + "version": "1.0.0", + "from": "wordwrap@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" + } + } + }, + "babel-loader": { + "version": "6.2.7", + "from": "babel-loader@>=6.2.4 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-6.2.7.tgz", + "dependencies": { + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + }, + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.5.1 <0.6.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + } + } + }, "babel-messages": { "version": "6.8.0", "from": "babel-messages@>=6.8.0 <7.0.0", @@ -455,6 +519,11 @@ "from": "babel-plugin-check-es2015-constants@>=6.3.13 <7.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.8.0.tgz" }, + "babel-plugin-istanbul": { + "version": "2.0.3", + "from": "babel-plugin-istanbul@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-2.0.3.tgz" + }, "babel-plugin-transform-es2015-arrow-functions": { "version": "6.8.0", "from": "babel-plugin-transform-es2015-arrow-functions@>=6.3.13 <7.0.0", @@ -471,9 +540,9 @@ "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.18.0.tgz", "dependencies": { "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" } } }, @@ -582,15 +651,20 @@ "from": "babel-plugin-transform-strict-mode@>=6.18.0 <7.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.18.0.tgz" }, + "babel-preset-es2015": { + "version": "6.18.0", + "from": "babel-preset-es2015@>=6.9.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.18.0.tgz" + }, "babel-register": { "version": "6.18.0", "from": "babel-register@>=6.18.0 <7.0.0", "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.18.0.tgz", "dependencies": { "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" }, "minimist": { "version": "0.0.8", @@ -615,9 +689,9 @@ "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.16.0.tgz", "dependencies": { "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" } } }, @@ -627,9 +701,9 @@ "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.18.0.tgz", "dependencies": { "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" } } }, @@ -639,9 +713,9 @@ "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.18.0.tgz", "dependencies": { "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.2.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" } } }, @@ -691,9 +765,9 @@ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz" }, "beeper": { - "version": "1.1.0", + "version": "1.1.1", "from": "beeper@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.0.tgz" + "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz" }, "benchmark": { "version": "1.0.0", @@ -795,9 +869,9 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz" }, "browser-sync": { - "version": "2.17.5", + "version": "2.17.6", "from": "browser-sync@>=2.14.0 <3.0.0", - "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.17.5.tgz" + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.17.6.tgz" }, "browser-sync-client": { "version": "2.4.3", @@ -820,9 +894,9 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.4.0.tgz" }, "bs-recipes": { - "version": "1.2.3", - "from": "bs-recipes@1.2.3", - "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.2.3.tgz" + "version": "1.3.2", + "from": "bs-recipes@1.3.2", + "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.2.tgz" }, "buffer": { "version": "4.9.1", @@ -872,9 +946,9 @@ } }, "caniuse-db": { - "version": "1.0.30000576", - "from": "caniuse-db@>=1.0.30000554 <2.0.0", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000576.tgz" + "version": "1.0.30000581", + "from": "caniuse-db@>=1.0.30000578 <2.0.0", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000581.tgz" }, "caseless": { "version": "0.11.0", @@ -947,9 +1021,9 @@ "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz", "dependencies": { "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.5.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" } } }, @@ -1483,6 +1557,11 @@ "from": "expand-range@>=1.8.1 <2.0.0", "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz" }, + "expose-loader": { + "version": "0.7.1", + "from": "expose-loader@>=0.7.1 <0.8.0", + "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-0.7.1.tgz" + }, "express": { "version": "2.5.11", "from": "express@>=2.5.0 <2.6.0", @@ -2343,9 +2422,9 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz" }, "globals": { - "version": "9.12.0", + "version": "9.13.0", "from": "globals@>=9.0.0 <10.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.12.0.tgz" + "resolved": "https://registry.npmjs.org/globals/-/globals-9.13.0.tgz" }, "globule": { "version": "1.1.0", @@ -2369,6 +2448,33 @@ "from": "graceful-readlink@>=1.0.0", "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" }, + "grunt": { + "version": "1.0.1", + "from": "grunt@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.0.1.tgz", + "dependencies": { + "glob": { + "version": "7.0.6", + "from": "glob@>=7.0.0 <7.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz" + }, + "js-yaml": { + "version": "3.5.5", + "from": "js-yaml@>=3.5.2 <3.6.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.5.5.tgz" + }, + "rimraf": { + "version": "2.2.8", + "from": "rimraf@>=2.2.8 <2.3.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz" + } + } + }, + "grunt-angular-gettext": { + "version": "2.3.0", + "from": "grunt-angular-gettext@>=2.2.3 <3.0.0", + "resolved": "https://registry.npmjs.org/grunt-angular-gettext/-/grunt-angular-gettext-2.3.0.tgz" + }, "grunt-browser-sync": { "version": "2.2.0", "from": "grunt-browser-sync@>=2.2.0 <3.0.0", @@ -2379,6 +2485,64 @@ "from": "grunt-cli@>=1.2.0 <2.0.0", "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz" }, + "grunt-concurrent": { + "version": "2.3.1", + "from": "grunt-concurrent@>=2.3.0 <3.0.0", + "resolved": "https://registry.npmjs.org/grunt-concurrent/-/grunt-concurrent-2.3.1.tgz", + "dependencies": { + "async": { + "version": "1.5.2", + "from": "async@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" + } + } + }, + "grunt-contrib-clean": { + "version": "1.0.0", + "from": "grunt-contrib-clean@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-1.0.0.tgz", + "dependencies": { + "async": { + "version": "1.5.2", + "from": "async@>=1.5.2 <2.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" + } + } + }, + "grunt-contrib-concat": { + "version": "1.0.1", + "from": "grunt-contrib-concat@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-1.0.1.tgz", + "dependencies": { + "source-map": { + "version": "0.5.6", + "from": "source-map@>=0.5.3 <0.6.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" + } + } + }, + "grunt-contrib-copy": { + "version": "1.0.0", + "from": "grunt-contrib-copy@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz" + }, + "grunt-contrib-jshint": { + "version": "1.0.0", + "from": "grunt-contrib-jshint@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-jshint/-/grunt-contrib-jshint-1.0.0.tgz" + }, + "grunt-contrib-less": { + "version": "1.4.0", + "from": "grunt-contrib-less@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-less/-/grunt-contrib-less-1.4.0.tgz", + "dependencies": { + "lodash": { + "version": "4.17.1", + "from": "lodash@>=4.8.2 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" + } + } + }, "grunt-contrib-watch": { "version": "1.0.0", "from": "grunt-contrib-watch@>=1.0.0 <2.0.0", @@ -2391,6 +2555,11 @@ } } }, + "grunt-extract-sourcemap": { + "version": "0.1.19", + "from": "grunt-extract-sourcemap@>=0.1.18 <0.2.0", + "resolved": "https://registry.npmjs.org/grunt-extract-sourcemap/-/grunt-extract-sourcemap-0.1.19.tgz" + }, "grunt-known-options": { "version": "1.1.0", "from": "grunt-known-options@>=1.1.0 <1.2.0", @@ -2430,10 +2599,34 @@ } } }, + "grunt-newer": { + "version": "1.2.0", + "from": "grunt-newer@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/grunt-newer/-/grunt-newer-1.2.0.tgz", + "dependencies": { + "async": { + "version": "1.5.2", + "from": "async@>=1.5.2 <2.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" + } + } + }, + "grunt-webpack": { + "version": "1.0.18", + "from": "grunt-webpack@>=1.0.11 <2.0.0", + "resolved": "https://registry.npmjs.org/grunt-webpack/-/grunt-webpack-1.0.18.tgz", + "dependencies": { + "lodash": { + "version": "4.17.1", + "from": "lodash@>=4.7.0 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" + } + } + }, "handlebars": { - "version": "4.0.5", + "version": "4.0.6", "from": "handlebars@>=4.0.0 <4.1.0", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.5.tgz", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.6.tgz", "dependencies": { "async": { "version": "1.5.2", @@ -2579,9 +2772,9 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz" }, "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.16.2 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" } } }, @@ -2620,6 +2813,18 @@ "from": "immutable@3.8.1", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.1.tgz" }, + "imports-loader": { + "version": "0.6.5", + "from": "imports-loader@>=0.6.5 <0.7.0", + "resolved": "https://registry.npmjs.org/imports-loader/-/imports-loader-0.6.5.tgz", + "dependencies": { + "source-map": { + "version": "0.1.43", + "from": "source-map@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz" + } + } + }, "indent-string": { "version": "2.1.0", "from": "indent-string@>=2.1.0 <3.0.0", @@ -2663,9 +2868,9 @@ "resolved": "https://registry.npmjs.org/interpret/-/interpret-0.6.6.tgz" }, "invariant": { - "version": "2.2.1", + "version": "2.2.2", "from": "invariant@>=2.2.0 <3.0.0", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.1.tgz" + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz" }, "invert-kv": { "version": "1.0.0", @@ -2845,9 +3050,14 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.0.tgz" }, "istanbul-lib-instrument": { - "version": "1.2.0", + "version": "1.3.0", "from": "istanbul-lib-instrument@>=1.1.4 <2.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.2.0.tgz" + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.3.0.tgz" + }, + "jasmine-core": { + "version": "2.5.2", + "from": "jasmine-core@>=2.4.1 <3.0.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.5.2.tgz" }, "javascript-detect-element-resize": { "version": "0.5.3", @@ -2885,9 +3095,9 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-2.0.0.tgz" }, "js-yaml": { - "version": "3.6.1", + "version": "3.7.0", "from": "js-yaml@>=3.2.7 <4.0.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz" + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz" }, "jsbn": { "version": "0.1.0", @@ -2904,6 +3114,35 @@ "from": "jsesc@>=1.3.0 <2.0.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz" }, + "jshint": { + "version": "2.9.4", + "from": "jshint@>=2.9.4 <3.0.0", + "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.9.4.tgz", + "dependencies": { + "lodash": { + "version": "3.7.0", + "from": "lodash@>=3.7.0 <3.8.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.7.0.tgz" + } + } + }, + "jshint-loader": { + "version": "0.8.3", + "from": "jshint-loader@>=0.8.3 <0.9.0", + "resolved": "https://registry.npmjs.org/jshint-loader/-/jshint-loader-0.8.3.tgz", + "dependencies": { + "strip-json-comments": { + "version": "0.1.3", + "from": "strip-json-comments@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-0.1.3.tgz" + } + } + }, + "jshint-stylish": { + "version": "2.2.1", + "from": "jshint-stylish@>=2.2.0 <3.0.0", + "resolved": "https://registry.npmjs.org/jshint-stylish/-/jshint-stylish-2.2.1.tgz" + }, "json-schema": { "version": "0.2.3", "from": "json-schema@0.2.3", @@ -2944,6 +3183,178 @@ "from": "jstimezonedetect@1.0.5", "resolved": "https://registry.npmjs.org/jstimezonedetect/-/jstimezonedetect-1.0.5.tgz" }, + "karma": { + "version": "1.3.0", + "from": "karma@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/karma/-/karma-1.3.0.tgz", + "dependencies": { + "accepts": { + "version": "1.1.4", + "from": "accepts@1.1.4", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.1.4.tgz" + }, + "base64-arraybuffer": { + "version": "0.1.2", + "from": "base64-arraybuffer@0.1.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.2.tgz" + }, + "component-emitter": { + "version": "1.2.0", + "from": "component-emitter@1.2.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.0.tgz" + }, + "engine.io": { + "version": "1.6.10", + "from": "engine.io@1.6.10", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.6.10.tgz" + }, + "engine.io-client": { + "version": "1.6.9", + "from": "engine.io-client@1.6.9", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.6.9.tgz", + "dependencies": { + "component-emitter": { + "version": "1.1.2", + "from": "component-emitter@1.1.2", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz" + } + } + }, + "engine.io-parser": { + "version": "1.2.4", + "from": "engine.io-parser@1.2.4", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.2.4.tgz", + "dependencies": { + "has-binary": { + "version": "0.1.6", + "from": "has-binary@0.1.6", + "resolved": "https://registry.npmjs.org/has-binary/-/has-binary-0.1.6.tgz" + } + } + }, + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "mime": { + "version": "1.3.4", + "from": "mime@>=1.3.4 <2.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz" + }, + "mime-db": { + "version": "1.12.0", + "from": "mime-db@>=1.12.0 <1.13.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz" + }, + "mime-types": { + "version": "2.0.14", + "from": "mime-types@>=2.0.4 <2.1.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz" + }, + "negotiator": { + "version": "0.4.9", + "from": "negotiator@0.4.9", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.4.9.tgz" + }, + "socket.io": { + "version": "1.4.7", + "from": "socket.io@1.4.7", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.4.7.tgz" + }, + "socket.io-client": { + "version": "1.4.6", + "from": "socket.io-client@1.4.6", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.4.6.tgz" + }, + "source-map": { + "version": "0.5.6", + "from": "source-map@>=0.5.3 <0.6.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" + }, + "ws": { + "version": "1.0.1", + "from": "ws@1.0.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.0.1.tgz" + } + } + }, + "karma-chrome-launcher": { + "version": "1.0.1", + "from": "karma-chrome-launcher@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-1.0.1.tgz" + }, + "karma-coverage": { + "version": "1.1.1", + "from": "karma-coverage@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-1.1.1.tgz", + "dependencies": { + "source-map": { + "version": "0.5.6", + "from": "source-map@>=0.5.1 <0.6.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" + } + } + }, + "karma-firefox-launcher": { + "version": "1.0.0", + "from": "karma-firefox-launcher@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-1.0.0.tgz" + }, + "karma-html2js-preprocessor": { + "version": "1.1.0", + "from": "karma-html2js-preprocessor@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/karma-html2js-preprocessor/-/karma-html2js-preprocessor-1.1.0.tgz" + }, + "karma-jasmine": { + "version": "1.0.2", + "from": "karma-jasmine@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.0.2.tgz" + }, + "karma-junit-reporter": { + "version": "1.1.0", + "from": "karma-junit-reporter@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/karma-junit-reporter/-/karma-junit-reporter-1.1.0.tgz" + }, + "karma-phantomjs-launcher": { + "version": "1.0.2", + "from": "karma-phantomjs-launcher@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.2.tgz", + "dependencies": { + "lodash": { + "version": "4.17.1", + "from": "lodash@>=4.0.1 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" + } + } + }, + "karma-sauce-launcher": { + "version": "1.1.0", + "from": "karma-sauce-launcher@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/karma-sauce-launcher/-/karma-sauce-launcher-1.1.0.tgz" + }, + "karma-sourcemap-loader": { + "version": "0.3.7", + "from": "karma-sourcemap-loader@>=0.3.7 <0.4.0", + "resolved": "https://registry.npmjs.org/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz" + }, + "karma-webpack": { + "version": "1.8.0", + "from": "karma-webpack@>=1.8.0 <2.0.0", + "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-1.8.0.tgz", + "dependencies": { + "async": { + "version": "0.9.2", + "from": "async@>=0.9.0 <0.10.0", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz" + }, + "source-map": { + "version": "0.1.43", + "from": "source-map@>=0.1.41 <0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz" + } + } + }, "kew": { "version": "0.7.0", "from": "kew@>=0.7.0 <0.8.0", @@ -3011,6 +3422,11 @@ } } }, + "less-plugin-autoprefix": { + "version": "1.5.1", + "from": "less-plugin-autoprefix@>=1.4.2 <2.0.0", + "resolved": "https://registry.npmjs.org/less-plugin-autoprefix/-/less-plugin-autoprefix-1.5.1.tgz" + }, "levn": { "version": "0.3.0", "from": "levn@>=0.3.0 <0.4.0", @@ -3026,6 +3442,16 @@ "from": "livereload-js@>=2.2.0 <3.0.0", "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.2.2.tgz" }, + "load-grunt-configs": { + "version": "1.0.0", + "from": "load-grunt-configs@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/load-grunt-configs/-/load-grunt-configs-1.0.0.tgz" + }, + "load-grunt-tasks": { + "version": "3.5.2", + "from": "load-grunt-tasks@>=3.5.0 <4.0.0", + "resolved": "https://registry.npmjs.org/load-grunt-tasks/-/load-grunt-tasks-3.5.2.tgz" + }, "load-json-file": { "version": "1.1.0", "from": "load-json-file@>=1.0.0 <2.0.0", @@ -3186,9 +3612,9 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" }, "moment": { - "version": "2.15.2", + "version": "2.16.0", "from": "moment@>=2.10.2 <3.0.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.15.2.tgz" + "resolved": "https://registry.npmjs.org/moment/-/moment-2.16.0.tgz" }, "ms": { "version": "0.7.1", @@ -3760,9 +4186,9 @@ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz" }, "readable-stream": { - "version": "2.1.5", + "version": "2.2.2", "from": "readable-stream@>=2.0.2 <3.0.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz" + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.2.tgz" }, "readdirp": { "version": "2.1.0", @@ -3797,9 +4223,9 @@ "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz" }, "regenerate": { - "version": "1.3.1", + "version": "1.3.2", "from": "regenerate@>=1.2.1 <2.0.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.1.tgz" + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.2.tgz" }, "regenerator-runtime": { "version": "0.9.6", @@ -4174,9 +4600,9 @@ } }, "statuses": { - "version": "1.3.0", + "version": "1.3.1", "from": "statuses@>=1.3.0 <1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.0.tgz" + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz" }, "stream-browserify": { "version": "1.0.0", @@ -4302,6 +4728,11 @@ } } }, + "time-grunt": { + "version": "1.4.0", + "from": "time-grunt@>=1.4.0 <2.0.0", + "resolved": "https://registry.npmjs.org/time-grunt/-/time-grunt-1.4.0.tgz" + }, "time-zone": { "version": "0.1.0", "from": "time-zone@>=0.1.0 <0.2.0", @@ -4604,6 +5035,33 @@ } } }, + "webpack": { + "version": "1.13.3", + "from": "webpack@>=1.13.1 <2.0.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-1.13.3.tgz", + "dependencies": { + "async": { + "version": "1.5.2", + "from": "async@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" + }, + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + }, + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + }, + "supports-color": { + "version": "3.1.2", + "from": "supports-color@>=3.1.0 <4.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz" + } + } + }, "webpack-core": { "version": "0.6.8", "from": "webpack-core@>=0.6.0 <0.7.0", @@ -4666,9 +5124,9 @@ "resolved": "https://registry.npmjs.org/weinre/-/weinre-2.0.0-pre-I0Z7U9OV.tgz" }, "which": { - "version": "1.2.11", + "version": "1.2.12", "from": "which@>=1.2.0 <1.3.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.2.11.tgz" + "resolved": "https://registry.npmjs.org/which/-/which-1.2.12.tgz" }, "which-module": { "version": "1.0.0", @@ -4765,9 +5223,9 @@ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.1.0.tgz", "dependencies": { "lodash": { - "version": "4.16.6", + "version": "4.17.1", "from": "lodash@>=4.8.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.1.tgz" } } } diff --git a/awx/ui/package.json b/awx/ui/package.json index 4cba5b9df9..737eaed35c 100644 --- a/awx/ui/package.json +++ b/awx/ui/package.json @@ -84,6 +84,7 @@ "angular-codemirror": "chouseknecht/angular-codemirror#1.0.4", "angular-cookies": "^1.4.3", "angular-drag-and-drop-lists": "leigh-johnson/angular-drag-and-drop-lists#1.4.0", + "angular-duration-format": "^1.0.1", "angular-gettext": "^2.3.5", "angular-md5": "^0.1.8", "angular-moment": "^0.10.1", diff --git a/awx/ui/webpack.config.js b/awx/ui/webpack.config.js index 067ba50dca..6c8a7b1ffe 100644 --- a/awx/ui/webpack.config.js +++ b/awx/ui/webpack.config.js @@ -18,6 +18,7 @@ var vendorPkgs = [ 'angular-codemirror', 'angular-cookies', 'angular-drag-and-drop-lists', + 'angular-duration-format', 'angular-gettext', 'angular-md5', 'angular-moment', From 53d7a822bd80a014e437a80a4bce3bbb4b422207 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 15 Nov 2016 15:59:00 -0500 Subject: [PATCH 43/57] job results cleanup 1 of 2 --- .../src/job-results/event-queue.service.js | 327 ++++++++---------- .../job-results-stdout.block.less | 4 +- .../src/job-results/job-results.block.less | 3 +- .../src/job-results/job-results.controller.js | 5 + .../src/job-results/job-results.service.js | 1 + 5 files changed, 160 insertions(+), 180 deletions(-) diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js index b8796c361e..3737de2258 100644 --- a/awx/ui/client/src/job-results/event-queue.service.js +++ b/awx/ui/client/src/job-results/event-queue.service.js @@ -7,162 +7,155 @@ export default ['jobResultsService', 'parseStdoutService', '$q', function(jobResultsService, parseStdoutService, $q){ var val = {}; - // Get the count of the last event - var getPreviousCount = function(counter, type) { - var countAttr; - - if (type === 'play') { - countAttr = 'playCount'; - } else if (type === 'task') { - countAttr = 'taskCount'; - } else { - countAttr = 'count'; - } - - var previousCount = $q.defer(); - - // iteratively find the last count - var findCount = function(counter) { - if (counter === 0) { - // if counter is 0, no count has been initialized - // initialize one! - - if (countAttr === 'count') { - previousCount.resolve({ - ok: 0, - skipped: 0, - unreachable: 0, - failures: 0, - changed: 0 - }); - } else { - previousCount.resolve(0); - } - - } else if (val.queue[counter] && val.queue[counter][countAttr] !== undefined) { - // this event has a count, resolve! - previousCount.resolve(_.clone(val.queue[counter][countAttr])); - } else { - // this event doesn't have a count, decrement to the - // previous event and check it - findCount(counter - 1); - } - }; - - if (val.queue[counter - 1]) { - // if the previous event has been resolved, start the iterative - // get previous count process - findCount(counter - 1); - } else if (val.populateDefers[counter - 1]){ - // if the previous event has not been resolved, wait for it to - // be and then start the iterative get previous count process - val.populateDefers[counter - 1].promise.then(function() { - findCount(counter - 1); - }); - } - - return previousCount.promise; - }; - - // munge the raw event from the backend into the event_queue's format - var mungeEvent = function(event) { - var mungedEventDefer = $q.defer(); - - // 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.changes.push('stdout'); - } - - // for different types of events, you need different types of data - if (event.event_name === 'playbook_on_start') { - mungedEvent.count = { - ok: 0, - skipped: 0, - unreachable: 0, - failures: 0, - changed: 0 - }; - mungedEvent.startTime = event.modified; - mungedEvent.changes.push('count'); - mungedEvent.changes.push('startTime'); - } else if (event.event_name === 'playbook_on_play_start') { - getPreviousCount(mungedEvent.counter, "play") - .then(count => { - mungedEvent.playCount = count + 1; - mungedEvent.changes.push('playCount'); - }); - } else if (event.event_name === 'playbook_on_task_start') { - getPreviousCount(mungedEvent.counter, "task") - .then(count => { - mungedEvent.taskCount = count + 1; - mungedEvent.changes.push('taskCount'); - }); - } else if (event.event_name === 'runner_on_ok' || - event.event_name === 'runner_on_async_ok') { - getPreviousCount(mungedEvent.counter) - .then(count => { - mungedEvent.count = count; - mungedEvent.count.ok++; - mungedEvent.changes.push('count'); - }); - } else if (event.event_name === 'runner_on_skipped') { - getPreviousCount(mungedEvent.counter) - .then(count => { - mungedEvent.count = count; - mungedEvent.count.skipped++; - mungedEvent.changes.push('count'); - }); - } else if (event.event_name === 'runner_on_unreachable') { - getPreviousCount(mungedEvent.counter) - .then(count => { - mungedEvent.count = count; - mungedEvent.count.unreachable++; - mungedEvent.changes.push('count'); - }); - } else if (event.event_name === 'runner_on_error' || - event.event_name === 'runner_on_async_failed') { - getPreviousCount(mungedEvent.counter) - .then(count => { - mungedEvent.count = count; - mungedEvent.count.failed++; - mungedEvent.changes.push('count'); - }); - } else 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'); - } - - mungedEventDefer.resolve(mungedEvent); - - return mungedEventDefer.promise; - }; - val = { populateDefers: {}, queue: {}, + // Get the count of the last event + getPreviousCount: function(counter, type) { + var countAttr; + + if (type === 'play') { + countAttr = 'playCount'; + } else if (type === 'task') { + countAttr = 'taskCount'; + } else { + countAttr = 'count'; + } + + var previousCount = $q.defer(); + + // iteratively find the last count + var findCount = function(counter) { + if (counter === 0) { + // if counter is 0, no count has been initialized + // initialize one! + + if (countAttr === 'count') { + previousCount.resolve({ + ok: 0, + skipped: 0, + unreachable: 0, + failures: 0, + changed: 0 + }); + } else { + previousCount.resolve(0); + } + + } else if (val.queue[counter] && val.queue[counter][countAttr] !== undefined) { + // this event has a count, resolve! + previousCount.resolve(_.clone(val.queue[counter][countAttr])); + } else { + // this event doesn't have a count, decrement to the + // previous event and check it + findCount(counter - 1); + } + }; + + if (val.queue[counter - 1]) { + // if the previous event has been resolved, start the iterative + // get previous count process + findCount(counter - 1); + } else if (val.populateDefers[counter - 1]){ + // if the previous event has not been resolved, wait for it to + // be and then start the iterative get previous count process + val.populateDefers[counter - 1].promise.then(function() { + findCount(counter - 1); + }); + } + + return previousCount.promise; + }, + // munge the raw event from the backend into the event_queue's format + munge: function(event) { + var mungedEventDefer = $q.defer(); + + // 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.changes.push('stdout'); + } + + // for different types of events, you need different types of data + if (event.event_name === 'playbook_on_start') { + mungedEvent.count = { + ok: 0, + skipped: 0, + unreachable: 0, + failures: 0, + changed: 0 + }; + mungedEvent.startTime = event.modified; + mungedEvent.changes.push('count'); + mungedEvent.changes.push('startTime'); + } else if (event.event_name === 'playbook_on_play_start') { + val.getPreviousCount(mungedEvent.counter, "play") + .then(count => { + mungedEvent.playCount = count + 1; + mungedEvent.changes.push('playCount'); + }); + } else if (event.event_name === 'playbook_on_task_start') { + val.getPreviousCount(mungedEvent.counter, "task") + .then(count => { + mungedEvent.taskCount = count + 1; + mungedEvent.changes.push('taskCount'); + }); + } else if (event.event_name === 'runner_on_ok' || + event.event_name === 'runner_on_async_ok') { + val.getPreviousCount(mungedEvent.counter) + .then(count => { + mungedEvent.count = count; + mungedEvent.count.ok++; + mungedEvent.changes.push('count'); + }); + } else if (event.event_name === 'runner_on_skipped') { + val.getPreviousCount(mungedEvent.counter) + .then(count => { + mungedEvent.count = count; + mungedEvent.count.skipped++; + mungedEvent.changes.push('count'); + }); + } else if (event.event_name === 'runner_on_unreachable') { + val.getPreviousCount(mungedEvent.counter) + .then(count => { + mungedEvent.count = count; + mungedEvent.count.unreachable++; + mungedEvent.changes.push('count'); + }); + } else if (event.event_name === 'runner_on_error' || + event.event_name === 'runner_on_async_failed') { + val.getPreviousCount(mungedEvent.counter) + .then(count => { + mungedEvent.count = count; + mungedEvent.count.failed++; + mungedEvent.changes.push('count'); + }); + } else 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'); + } + + mungedEventDefer.resolve(mungedEvent); + + return mungedEventDefer.promise; + }, // reinitializes the event queue value for the job results page - // - // TODO: implement some sort of local storage scheme - // to make viewing job details that the user has - // previous clicked on super quick, this would be where you grab - // from local storage initialize: function() { val.queue = {}; val.populateDefers = {}; @@ -175,6 +168,8 @@ export default ['jobResultsService', 'parseStdoutService', '$q', function(jobRes val.populateDefers[event.counter] = $q.defer(); } + // make sure not to send duplicate events over to the + // controller if (val.queue[event.counter] && val.queue[event.counter].processed) { val.populateDefers.reject("duplicate event: " + @@ -185,11 +180,6 @@ export default ['jobResultsService', 'parseStdoutService', '$q', function(jobRes var resolvePopulation = function(event) { // to resolve, put the event on the queue and // then resolve the deferred value - // - // TODO: implement some sort of local storage scheme - // to make viewing job details that the user has - // previous clicked on super quick, this would be - // where you put in local storage val.queue[event.counter] = event; val.populateDefers[event.counter].resolve(event); }; @@ -197,7 +187,7 @@ export default ['jobResultsService', 'parseStdoutService', '$q', function(jobRes if (event.counter === 1) { // for the first event, go ahead and munge and // resolve - mungeEvent(event).then(event => { + val.munge(event).then(event => { resolvePopulation(event); }); } else { @@ -214,15 +204,11 @@ export default ['jobResultsService', 'parseStdoutService', '$q', function(jobRes } // you can start the munging process... - mungeEvent(event).then(event => { + val.munge(event).then(event => { // ...but wait until the previous event has // been resolved before resolving this one and // doing stuff in the ui (that's why we - // needed that previous conditional). this - // could be done in a more asynchronous nature - // if we need a performance boost. See the - // todo note in the markProcessed function - // for an idea + // needed that previous conditional). val.populateDefers[event.counter - 1].promise .then(() => { resolvePopulation(event); @@ -245,19 +231,6 @@ export default ['jobResultsService', 'parseStdoutService', '$q', function(jobRes // the event has now done it's work in the UI, record // that! val.queue[event.counter].processed = true; - - // TODO: we can actually record what has been done in the - // UI and at what event too! (something like "resolved - // the count on event 63)". - // - // if we do something like this, we actually wouldn't - // have to wait until the previous events had completed - // before resolving and returning to the controller - // in populate()... - // in other words, we could send events out of order to - // the controller, but let the controller know that it's - // an older event that what is in the view so we don't - // need to do anything }; if (!val.queue[event.counter]) { 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 index 3505f1ce7d..c5763754c7 100644 --- 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 @@ -1,5 +1,7 @@ @import '../../shared/branding/colors.default.less'; +@breakpoint-md: 1200px; + .JobResultsStdOut { height: ~"calc(100% - 70px)"; } @@ -204,7 +206,7 @@ color: @default-interface-txt; } -@media (max-width: 1199px) { +@media (max-width: @breakpoint-md) { .JobResultsStdOut-numberColumnPreload { display: none; } diff --git a/awx/ui/client/src/job-results/job-results.block.less b/awx/ui/client/src/job-results/job-results.block.less index 255e4d26d5..37137dde6d 100644 --- a/awx/ui/client/src/job-results/job-results.block.less +++ b/awx/ui/client/src/job-results/job-results.block.less @@ -3,7 +3,6 @@ @import '../shared/layouts/one-plus-two.less'; @breakpoint-md: 1200px; -@breakpoint-sm: 623px; .JobResults { .OnePlusTwo-container(100%, @breakpoint-md); @@ -115,7 +114,7 @@ margin-left: 20px; } -@media (max-width: 1199px) { +@media (max-width: @breakpoint-md) { .JobResults-detailsPanel { overflow-y: auto; } diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index d157d15849..ce0358fed3 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -144,14 +144,19 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count' } if(change === 'stdout'){ + // put stdout elements in stdout container angular .element(".JobResultsStdOut-stdoutContainer") .append($compile(mungedEvent .stdout)($scope)); + // move the followAnchor to the bottom of the + // container $(".JobResultsStdOut-followAnchor") .appendTo(".JobResultsStdOut-stdoutContainer"); + // if follow is engaged, + // scroll down to the followAnchor if ($scope.followEngaged) { $scope.followScroll(); } diff --git a/awx/ui/client/src/job-results/job-results.service.js b/awx/ui/client/src/job-results/job-results.service.js index 3ca6019587..07d442f3d7 100644 --- a/awx/ui/client/src/job-results/job-results.service.js +++ b/awx/ui/client/src/job-results/job-results.service.js @@ -75,6 +75,7 @@ export default ['$q', 'Prompt', '$filter', 'Wait', 'Rest', '$state', 'ProcessErr return count; }, + // rest call to grab previously complete job_events getEvents: function(url) { var val = $q.defer(); From 86cf09ec71c4b69b490e63651b375a6ede4da757 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 15 Nov 2016 16:33:14 -0500 Subject: [PATCH 44/57] job results cleanup 1.5 of 2 parse standard out service is commented --- .../src/job-results/parse-stdout.service.js | 75 +++++++++++++++---- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/awx/ui/client/src/job-results/parse-stdout.service.js b/awx/ui/client/src/job-results/parse-stdout.service.js index 12620d3197..8799a152ac 100644 --- a/awx/ui/client/src/job-results/parse-stdout.service.js +++ b/awx/ui/client/src/job-results/parse-stdout.service.js @@ -4,12 +4,15 @@ * All Rights Reserved *************************************************/ -export default [function(){ +export default ['$log', function($log){ var val = { + // parses stdout string from api and formats various codes to the + // correct dom structure prettify: function(line){ + // TODO: remove once Chris's fixes to the [K lines comes in if (line.indexOf("[K") > -1) { - console.log(line); + $log.error(line); } line = line.replace(/u001b/g, ''); @@ -30,45 +33,60 @@ export default [function(){ line = line.replace(/\[0m/g, ''); return line; }, + // adds anchor tags and tooltips to host status lines getAnchorTags: function(event, line){ if(event.event.indexOf("runner_") === -1){ return line; } else{ - var str = ``, - str2 = ''; - return str.concat(line).concat(str2); + return `${line}`; } }, - getCollapseClasses: function(event, line, lineNum) { + // this adds classes based on event data to the + // .JobResultsStdOut-aLineOfStdOut element + getLineClasses: function(event, line, lineNum) { var string = ""; if (event.event_name === "playbook_on_play_start") { + // play header classes string += " header_play"; string += " header_play_" + event.event_data.play_uuid; if (line) { 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; - if (event.event_data.play_uuid) { - string += " play_" + event.event_data.play_uuid; - } if (line) { string += " actual_header"; } - } else { + + // 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 { + // 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; }, + // used to add expand/collapse icon next to line numbers of headers getCollapseIcon: function(event, line) { var clickClass, expanderizerSpecifier; @@ -81,19 +99,26 @@ export default [function(){ 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; } - return ` + + var expandDom = ` `; + console.log(expandDom); + return expandDom; } else { + // non-header lines don't get an expander return emptySpan; } }, + // public function that provides the parsed stdout line, given a + // job_event parseStdout: function(event){ - return _ + // this utilizes the start/end lines and stdout blob + // to create an array in the format: + // [ + // [lineNum: lineText], + // [lineNum: lineText], + // ] + var lineArr = _ .zip(_.range(event.start_line + 1, event.end_line + 1), - event.stdout.replace("\t", " ").split("\r\n").slice(0, -1)) + event.stdout.replace("\t", " ").split("\r\n").slice(0, -1)); + + // 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]}
${this.getAnchorTags(event, this.prettify(lineArr[1]))}
`; - }).join(""); + }); + + // this joins all the lines for this job_event together and + // returns to the mungeEvent function + return lineArr.join(""); } }; return val; From 96952e78419b8d4556200f7f01ed7060436ee9e4 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Tue, 15 Nov 2016 16:39:50 -0800 Subject: [PATCH 45/57] Fixing up host event modal Moving the details up from a nav pane and into the main body of hte modal. --- .../host-event/host-event.block.less | 10 ++-- .../host-event-details.partial.html | 46 ------------------- .../host-event/host-event-modal.partial.html | 31 +++++++++++-- .../host-event/host-event.block.less | 6 ++- .../host-event/host-event.controller.js | 28 ++--------- .../host-event/host-event.route.js | 11 +---- .../client/src/job-results/host-event/main.js | 3 +- .../src/job-results/parse-stdout.service.js | 35 +++++++++++++- 8 files changed, 75 insertions(+), 95 deletions(-) delete mode 100644 awx/ui/client/src/job-results/host-event/host-event-details.partial.html diff --git a/awx/ui/client/src/job-detail/host-event/host-event.block.less b/awx/ui/client/src/job-detail/host-event/host-event.block.less index fc84890856..eafa9678fd 100644 --- a/awx/ui/client/src/job-detail/host-event/host-event.block.less +++ b/awx/ui/client/src/job-detail/host-event/host-event.block.less @@ -101,11 +101,11 @@ font-weight: 600; margin-bottom: 8px; } -.HostEvent .modal-body{ - max-height: 500px; - overflow-y: auto; - padding: 20px; -} +// .HostEvent .modal-body{ +// max-height: 500px; +// overflow-y: auto; +// padding: 20px; +// } .HostEvent-nav{ padding-top: 12px; padding-bottom: 12px; diff --git a/awx/ui/client/src/job-results/host-event/host-event-details.partial.html b/awx/ui/client/src/job-results/host-event/host-event-details.partial.html deleted file mode 100644 index 60cd44a9b7..0000000000 --- a/awx/ui/client/src/job-results/host-event/host-event-details.partial.html +++ /dev/null @@ -1,46 +0,0 @@ -
-
EVENT
- -
- STATUS - - - - - {{processEventStatus(event).status || "No result found"}} - -
-
- ID - {{event.id || "No result found"}} -
-
- CREATED - {{event.created || "No result found"}} -
-
- PLAY - {{event.play || "No result found"}} -
-
- TASK - {{event.task || "No result found"}} -
-
- MODULE - {{event.event_data.res.invocation.module_name || "No result found"}} -
-
-
-
RESULTS
- -
- {{key}} - {{value}} -
-
- diff --git a/awx/ui/client/src/job-results/host-event/host-event-modal.partial.html b/awx/ui/client/src/job-results/host-event/host-event-modal.partial.html index db236894e8..7f5b750eaa 100644 --- a/awx/ui/client/src/job-results/host-event/host-event-modal.partial.html +++ b/awx/ui/client/src/job-results/host-event/host-event-modal.partial.html @@ -4,15 +4,39 @@