diff --git a/awx/ui/client/src/about/about.route.js b/awx/ui/client/src/about/about.route.js
index 5f8b5e9220..475cf1aea0 100644
--- a/awx/ui/client/src/about/about.route.js
+++ b/awx/ui/client/src/about/about.route.js
@@ -8,5 +8,10 @@ export default {
ncyBreadcrumb: {
label: "ABOUT"
},
+ onExit: function(){
+ // hacky way to handle user browsing away via URL bar
+ $('.modal-backdrop').remove();
+ $('body').removeClass('modal-open');
+ },
templateUrl: templateUrl('about/about')
};
diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js
index cbf50b22b6..48d7d07019 100644
--- a/awx/ui/client/src/app.js
+++ b/awx/ui/client/src/app.js
@@ -177,7 +177,6 @@ var tower = angular.module('Tower', [
'StandardOutHelper',
'LogViewerOptionsDefinition',
'EventViewerHelper',
- 'HostEventsViewerHelper',
'JobDetailHelper',
'SocketIO',
'lrInfiniteScroll',
diff --git a/awx/ui/client/src/helpers.js b/awx/ui/client/src/helpers.js
index b298a635ef..e8190ea50e 100644
--- a/awx/ui/client/src/helpers.js
+++ b/awx/ui/client/src/helpers.js
@@ -12,7 +12,6 @@ import Credentials from "./helpers/Credentials";
import EventViewer from "./helpers/EventViewer";
import Events from "./helpers/Events";
import Groups from "./helpers/Groups";
-import HostEventsViewer from "./helpers/HostEventsViewer";
import Hosts from "./helpers/Hosts";
import JobDetail from "./helpers/JobDetail";
import JobSubmission from "./helpers/JobSubmission";
@@ -46,7 +45,6 @@ export
EventViewer,
Events,
Groups,
- HostEventsViewer,
Hosts,
JobDetail,
JobSubmission,
diff --git a/awx/ui/client/src/helpers/HostEventsViewer.js b/awx/ui/client/src/helpers/HostEventsViewer.js
deleted file mode 100644
index e8fc5a940a..0000000000
--- a/awx/ui/client/src/helpers/HostEventsViewer.js
+++ /dev/null
@@ -1,287 +0,0 @@
-/*************************************************
- * Copyright (c) 2015 Ansible, Inc.
- *
- * All Rights Reserved
- *************************************************/
-
- /**
- * @ngdoc function
- * @name helpers.function:HostEventsViewer
- * @description view a list of events for a given job and host
-*/
-
-export default
- angular.module('HostEventsViewerHelper', ['ModalDialog', 'Utilities', 'EventViewerHelper'])
-
- .factory('HostEventsViewer', ['$log', '$compile', 'CreateDialog', 'Wait', 'GetBasePath', 'Empty', 'GetEvents', 'EventViewer',
- function($log, $compile, CreateDialog, Wait, GetBasePath, Empty, GetEvents, EventViewer) {
- return function(params) {
- var parent_scope = params.scope,
- scope = parent_scope.$new(true),
- job_id = params.job_id,
- url = params.url,
- title = params.title, //optional
- fixHeight, buildTable,
- lastID, setStatus, buildRow, status;
-
- // initialize the status dropdown
- scope.host_events_status_options = [
- { value: "all", name: "All" },
- { value: "changed", name: "Changed" },
- { value: "failed", name: "Failed" },
- { value: "ok", name: "OK" },
- { value: "unreachable", name: "Unreachable" }
- ];
- scope.host_events_search_name = params.name;
- status = (params.status) ? params.status : 'all';
- scope.host_events_status_options.every(function(opt, idx) {
- if (opt.value === status) {
- scope.host_events_search_status = scope.host_events_status_options[idx];
- return false;
- }
- return true;
- });
- if (!scope.host_events_search_status) {
- scope.host_events_search_status = scope.host_events_status_options[0];
- }
-
- $log.debug('job_id: ' + job_id + ' url: ' + url + ' title: ' + title + ' name: ' + name + ' status: ' + status);
-
- scope.eventsSearchActive = (scope.host_events_search_name) ? true : false;
-
- if (scope.removeModalReady) {
- scope.removeModalReady();
- }
- scope.removeModalReady = scope.$on('ModalReady', function() {
- scope.hostViewSearching = false;
- $('#host-events-modal-dialog').dialog('open');
- });
-
- if (scope.removeJobReady) {
- scope.removeJobReady();
- }
- scope.removeEventReady = scope.$on('EventsReady', function(e, data, maxID) {
- var elem, html;
-
- lastID = maxID;
- html = buildTable(data);
- $('#host-events').html(html);
- elem = angular.element(document.getElementById('host-events-modal-dialog'));
- $compile(elem)(scope);
-
- CreateDialog({
- scope: scope,
- width: 675,
- height: 600,
- minWidth: 450,
- callback: 'ModalReady',
- id: 'host-events-modal-dialog',
- onResizeStop: fixHeight,
- title: ( (title) ? title : 'Host Events' ),
- onClose: function() {
- try {
- scope.$destroy();
- }
- catch(e) {
- //ignore
- }
- },
- onOpen: function() {
- fixHeight();
- }
- });
- });
-
- if (scope.removeRefreshHTML) {
- scope.removeRefreshHTML();
- }
- scope.removeRefreshHTML = scope.$on('RefreshHTML', function(e, data) {
- var elem, html = buildTable(data);
- $('#host-events').html(html);
- scope.hostViewSearching = false;
- elem = angular.element(document.getElementById('host-events'));
- $compile(elem)(scope);
- });
-
- setStatus = function(result) {
- var msg = '', status = 'ok', status_text = 'OK';
- if (!result.task && result.event_data && result.event_data.res && result.event_data.res.ansible_facts) {
- result.task = "Gathering Facts";
- }
- if (result.event === "runner_on_no_hosts") {
- msg = "No hosts remaining";
- }
- if (result.event === 'runner_on_unreachable') {
- status = 'unreachable';
- status_text = 'Unreachable';
- }
- else if (result.failed) {
- status = 'failed';
- status_text = 'Failed';
- }
- else if (result.changed) {
- status = 'changed';
- status_text = 'Changed';
- }
- if (result.event_data.res && result.event_data.res.msg) {
- msg = result.event_data.res.msg;
- }
- result.msg = msg;
- result.status = status;
- result.status_text = status_text;
- return result;
- };
-
- buildRow = function(res) {
- var html = '';
- html += "
\n";
- html += "| " + res.status_text + " | \n";
- html += "" + res.host_name + " | \n";
- html += "" + res.play + " | \n";
- html += "" + res.task + " | \n";
- html += "
";
- return html;
- };
-
- buildTable = function(data) {
- var html = "\n";
- html += "\n";
- data.results.forEach(function(result) {
- var res = setStatus(result);
- html += buildRow(res);
- });
- html += "\n";
- html += "
\n";
- return html;
- };
-
- fixHeight = function() {
- var available_height = $('#host-events-modal-dialog').height() - $('#host-events-modal-dialog #search-form').height() - $('#host-events-modal-dialog #fixed-table-header').height();
- $('#host-events').height(available_height);
- $log.debug('set height to: ' + available_height);
- // Check width and reset search fields
- if ($('#host-events-modal-dialog').width() <= 450) {
- $('#host-events-modal-dialog #status-field').css({'margin-left': '7px'});
- }
- else {
- $('#host-events-modal-dialog #status-field').css({'margin-left': '15px'});
- }
- };
-
- GetEvents({
- url: url,
- scope: scope,
- callback: 'EventsReady'
- });
-
- scope.modalOK = function() {
- $('#host-events-modal-dialog').dialog('close');
- scope.$destroy();
- };
-
- scope.searchEvents = function() {
- scope.eventsSearchActive = (scope.host_events_search_name) ? true : false;
- GetEvents({
- scope: scope,
- url: url,
- callback: 'RefreshHTML'
- });
- };
-
- scope.searchEventKeyPress = function(e) {
- if (e.keyCode === 13) {
- scope.searchEvents();
- }
- };
-
- scope.showDetails = function(id) {
- EventViewer({
- scope: parent_scope,
- url: GetBasePath('jobs') + job_id + '/job_events/?id=' + id,
- });
- };
-
- if (scope.removeEventsScrollDownBuild) {
- scope.removeEventsScrollDownBuild();
- }
- scope.removeEventsScrollDownBuild = scope.$on('EventScrollDownBuild', function(e, data, maxID) {
- var elem, html = '';
- lastID = maxID;
- data.results.forEach(function(result) {
- var res = setStatus(result);
- html += buildRow(res);
- });
- if (html) {
- $('#host-events table tbody').append(html);
- elem = angular.element(document.getElementById('host-events'));
- $compile(elem)(scope);
- }
- });
-
- scope.hostEventsScrollDown = function() {
- GetEvents({
- scope: scope,
- url: url,
- gt: lastID,
- callback: 'EventScrollDownBuild'
- });
- };
-
- };
- }])
-
- .factory('GetEvents', ['Rest', 'ProcessErrors', function(Rest, ProcessErrors) {
- return function(params) {
- var url = params.url,
- scope = params.scope,
- gt = params.gt,
- callback = params.callback;
-
- if (scope.host_events_search_name) {
- url += '?host_name=' + scope.host_events_search_name;
- }
- else {
- url += '?host_name__isnull=false';
- }
-
- if (scope.host_events_search_status.value === 'changed') {
- url += '&event__icontains=runner&changed=true';
- }
- else if (scope.host_events_search_status.value === 'failed') {
- url += '&event__icontains=runner&failed=true';
- }
- else if (scope.host_events_search_status.value === 'ok') {
- url += '&event=runner_on_ok&changed=false';
- }
- else if (scope.host_events_search_status.value === 'unreachable') {
- url += '&event=runner_on_unreachable';
- }
- else if (scope.host_events_search_status.value === 'all') {
- url += '&event__icontains=runner¬__event=runner_on_skipped';
- }
-
- if (gt) {
- // used for endless scroll
- url += '&id__gt=' + gt;
- }
-
- url += '&page_size=50&order=id';
-
- scope.hostViewSearching = true;
- Rest.setUrl(url);
- Rest.get()
- .success(function(data) {
- var lastID;
- scope.hostViewSearching = false;
- if (data.results.length > 0) {
- lastID = data.results[data.results.length - 1].id;
- }
- scope.$emit(callback, data, lastID);
- })
- .error(function(data, status) {
- scope.hostViewSearching = false;
- ProcessErrors(scope, data, status, null, { hdr: 'Error!',
- msg: 'Failed to get events ' + url + '. GET returned: ' + status });
- });
- };
- }]);
diff --git a/awx/ui/client/src/job-detail/host-event/host-event.route.js b/awx/ui/client/src/job-detail/host-event/host-event.route.js
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/awx/ui/client/src/job-detail/host-events/host-events.block.less b/awx/ui/client/src/job-detail/host-events/host-events.block.less
new file mode 100644
index 0000000000..bde3fe72fd
--- /dev/null
+++ b/awx/ui/client/src/job-detail/host-events/host-events.block.less
@@ -0,0 +1,82 @@
+@import "awx/ui/client/src/shared/branding/colors.less";
+@import "awx/ui/client/src/shared/branding/colors.default.less";
+
+.HostEvents .modal-footer{
+ border: 0;
+ margin-top: 0px;
+ padding-top: 5px;
+}
+.HostEvents-status--ok{
+ color: @green;
+}
+.HostEvents-status--unreachable{
+ color: @unreachable;
+}
+.HostEvents-status--changed{
+ color: @changed;
+}
+.HostEvents-status--failed{
+ color: @warning;
+}
+.HostEvents-status--skipped{
+ color: @skipped;
+}
+.HostEvents-search--form{
+ max-width: 420px;
+ display: inline-block;
+}
+.HostEvents-close{
+ width: 70px;
+}
+.HostEvents-filter--form{
+ padding-top: 15px;
+ padding-bottom: 15px;
+ float: right;
+ display: inline-block;
+}
+.HostEvents .modal-body{
+ padding: 20px;
+}
+.HostEvents .select2-container{
+ text-transform: capitalize;
+ max-width: 220px;
+ float: right;
+}
+.HostEvents-form--container{
+ padding-top: 15px;
+ padding-bottom: 15px;
+}
+.HostEvents-title{
+ color: @default-interface-txt;
+ font-weight: 600;
+}
+.HostEvents-status i {
+ padding-right: 10px;
+}
+.HostEvents-table--header {
+ height: 30px;
+ font-size: 14px;
+ font-weight: normal;
+ text-transform: uppercase;
+ color: #848992;
+ background-color: #EBEBEB;
+ padding-left: 15px;
+ padding-right: 15px;
+ border-bottom-width: 0px;
+}
+.HostEvents-table--header:first-of-type{
+ border-top-left-radius: 5px;
+}
+.HostEvents-table--header:last-of-type{
+ border-top-right-radius: 5px;
+}
+.HostEvents-table--row{
+ color: @default-data-txt;
+ border: 0 !important;
+}
+.HostEvents-table--row:nth-child(odd){
+ background: @default-tertiary-bg;
+}
+.HostEvents-table--cell{
+ border: 0 !important;
+}
diff --git a/awx/ui/client/src/job-detail/host-events/host-events.controller.js b/awx/ui/client/src/job-detail/host-events/host-events.controller.js
new file mode 100644
index 0000000000..45a7268ed7
--- /dev/null
+++ b/awx/ui/client/src/job-detail/host-events/host-events.controller.js
@@ -0,0 +1,184 @@
+/*************************************************
+ * Copyright (c) 2016 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+ export default
+ ['$stateParams', '$scope', '$rootScope', '$state', 'Wait',
+ 'JobDetailService', 'CreateSelect2', 'PaginateInit',
+ function($stateParams, $scope, $rootScope, $state, Wait,
+ JobDetailService, CreateSelect2, PaginateInit){
+
+
+ $scope.search = function(){
+ Wait('start');
+ if ($scope.searchStr == undefined){
+ return
+ }
+ // The API treats params as AND query
+ // We should discuss the possibility of an OR array
+
+ // search play description
+ /*
+ JobDetailService.getRelatedJobEvents($stateParams.id, {
+ play: $scope.searchStr})
+ .success(function(res){
+ results.push(res.results);
+ });
+ */
+ // search host name
+ JobDetailService.getRelatedJobEvents($stateParams.id, {
+ host_name: $scope.searchStr})
+ .success(function(res){
+ $scope.results = res.results;
+ Wait('Stop')
+ });
+ // search task
+ /*
+ JobDetailService.getRelatedJobEvents($stateParams.id, {
+ task: $scope.searchStr})
+ .success(function(res){
+ results.push(res.results);
+ });
+ */
+ };
+
+ $scope.filters = ['all', 'changed', 'failed', 'ok', 'unreachable', 'skipped'];
+
+ var filter = function(filter){
+ Wait('start');
+ if (filter == 'all'){
+ return JobDetailService.getRelatedJobEvents($stateParams.id, {host_name: $stateParams.hostName})
+ .success(function(res){
+ $scope.results = res.results;
+ Wait('stop');
+ });
+ }
+ // handle runner cases
+ if (filter == 'skipped'){
+ return JobDetailService.getRelatedJobEvents($stateParams.id, {
+ host_name: $stateParams.hostName,
+ event: 'runner_on_skipped'})
+ .success(function(res){
+ $scope.results = res.results;
+ Wait('stop');
+ });
+ }
+ if (filter == 'unreachable'){
+ return JobDetailService.getRelatedJobEvents($stateParams.id, {
+ host_name: $stateParams.hostName,
+ event: 'runner_on_unreachable'})
+ .success(function(res){
+ $scope.results = res.results;
+ Wait('stop');
+ });
+ }
+ if (filter == 'ok'){
+ return JobDetailService.getRelatedJobEvents($stateParams.id, {
+ host_name: $stateParams.hostName,
+ event: 'runner_on_ok'
+ // add param changed: false if 'ok' shouldn't display changed hosts
+ })
+ .success(function(res){
+ $scope.results = res.results;
+ Wait('stop');
+ });
+ }
+ // handle convience properties .changed .failed
+ if (filter == 'changed'){
+ return JobDetailService.getRelatedJobEvents($stateParams.id, {
+ host_name: $stateParams.hostName,
+ changed: true})
+ .success(function(res){
+ $scope.results = res.results;
+ Wait('stop');
+ });
+ }
+ if (filter == 'failed'){
+ return JobDetailService.getRelatedJobEvents($stateParams.id, {
+ host_name: $stateParams.hostName,
+ failed: true})
+ .success(function(res){
+ $scope.results = res.results;
+ Wait('stop');
+ });
+ }
+ };
+
+ // watch select2 for changes
+ $('.HostEvents-select').on("select2:select", function (e) {
+ filter($('.HostEvents-select').val());
+ });
+
+ $scope.processStatus = function(event, $index){
+ // the stack for which status to display is
+ // unreachable > failed > changed > ok
+ // uses the API's runner events and convenience properties .failed .changed to determine status.
+ // see: job_event_callback.py
+ if (event.event == 'runner_on_unreachable'){
+ $scope.results[$index].status = 'Unreachable';
+ return 'HostEvents-status--unreachable'
+ }
+ // equiv to 'runner_on_error' && 'runner on failed'
+ if (event.failed){
+ $scope.results[$index].status = 'Failed';
+ return 'HostEvents-status--failed'
+ }
+ // catch the changed case before ok, because both can be true
+ if (event.changed){
+ $scope.results[$index].status = 'Changed';
+ return 'HostEvents-status--changed'
+ }
+ if (event.event == 'runner_on_ok'){
+ $scope.results[$index].status = 'OK';
+ return 'HostEvents-status--ok'
+ }
+ if (event.event == 'runner_on_skipped'){
+ $scope.results[$index].status = 'Skipped';
+ return 'HostEvents-status--skipped'
+ }
+ else{
+ // study a case where none of these apply
+ }
+ };
+
+
+ var init = function(){
+ // create filter dropdown
+ CreateSelect2({
+ element: '.HostEvents-select',
+ multiple: false
+ });
+ // process the filter if one was passed
+ if ($stateParams.filter){
+ filter($stateParams.filter).success(function(res){
+ $scope.results = res.results;
+ PaginateInit({ scope: $scope, list: defaultUrl });
+ Wait('stop');
+ $('#HostEvents').modal('show');
+
+
+ });;
+ }
+ else{
+ Wait('start');
+ JobDetailService.getRelatedJobEvents($stateParams.id, {host_name: $stateParams.hostName})
+ .success(function(res){
+ $scope.results = res.results;
+ Wait('stop');
+ $('#HostEvents').modal('show');
+
+ });
+ }
+ };
+
+ $scope.goBack = function(){
+ // go back to the job details state
+ // we're leaning on $stateProvider's onExit to close the modal
+ $state.go('jobDetail');
+ };
+
+ init();
+
+ }];
\ No newline at end of file
diff --git a/awx/ui/client/src/job-detail/host-events/host-events.partial.html b/awx/ui/client/src/job-detail/host-events/host-events.partial.html
new file mode 100644
index 0000000000..a0ee6956bb
--- /dev/null
+++ b/awx/ui/client/src/job-detail/host-events/host-events.partial.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+ {{event.status}}
+ |
+ {{event.host_name}} |
+ {{event.play}} |
+ {{event.task}} |
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awx/ui/client/src/job-detail/host-events/host-events.route.js b/awx/ui/client/src/job-detail/host-events/host-events.route.js
new file mode 100644
index 0000000000..5365fea95c
--- /dev/null
+++ b/awx/ui/client/src/job-detail/host-events/host-events.route.js
@@ -0,0 +1,27 @@
+/*************************************************
+ * Copyright (c) 2015 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+import {templateUrl} from '../../shared/template-url/template-url.factory';
+
+export default {
+ name: 'jobDetail.hostEvents',
+ url: '/host-events/:hostName?:filter',
+ controller: 'HostEventsController',
+ templateUrl: templateUrl('job-detail/host-events/host-events'),
+ onExit: function(){
+ // close the modal
+ // using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
+ $('#HostEvents').modal('hide');
+ // hacky way to handle user browsing away via URL bar
+ $('.modal-backdrop').remove();
+ $('body').removeClass('modal-open');
+ },
+ resolve: {
+ features: ['FeaturesService', function(FeaturesService) {
+ return FeaturesService.get();
+ }]
+ }
+};
diff --git a/awx/ui/client/src/job-detail/host-events/main.js b/awx/ui/client/src/job-detail/host-events/main.js
new file mode 100644
index 0000000000..8a9487aec4
--- /dev/null
+++ b/awx/ui/client/src/job-detail/host-events/main.js
@@ -0,0 +1,15 @@
+/*************************************************
+ * Copyright (c) 2016 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+import route from './host-events.route';
+import controller from './host-events.controller';
+
+export default
+ angular.module('jobDetail.hostEvents', [])
+ .controller('HostEventsController', controller)
+ .run(['$stateExtender', function($stateExtender){
+ $stateExtender.addState(route)
+ }]);
\ No newline at end of file
diff --git a/awx/ui/client/src/job-detail/job-detail.controller.js b/awx/ui/client/src/job-detail/job-detail.controller.js
index e36dbb13de..1383f04c35 100644
--- a/awx/ui/client/src/job-detail/job-detail.controller.js
+++ b/awx/ui/client/src/job-detail/job-detail.controller.js
@@ -15,7 +15,7 @@ export default
'$stateParams', '$log', 'ClearScope', 'GetBasePath', 'Wait',
'ProcessErrors', 'SelectPlay', 'SelectTask', 'Socket', 'GetElapsed',
'DrawGraph', 'LoadHostSummary', 'ReloadHostSummaryList',
- 'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'PlaybookRun', 'HostEventsViewer',
+ 'JobIsFinished', 'SetTaskStyles', 'DigestEvent', 'UpdateDOM', 'DeleteJob', 'PlaybookRun',
'LoadPlays', 'LoadTasks', 'LoadHosts', 'HostsEdit',
'ParseVariableString', 'GetChoices', 'fieldChoices', 'fieldLabels',
'EditSchedule', 'ParseTypeChange', 'JobDetailService', 'EventViewer',
@@ -25,7 +25,7 @@ export default
SelectPlay, SelectTask, Socket, GetElapsed, DrawGraph,
LoadHostSummary, ReloadHostSummaryList, JobIsFinished,
SetTaskStyles, DigestEvent, UpdateDOM, DeleteJob,
- PlaybookRun, HostEventsViewer, LoadPlays, LoadTasks, LoadHosts,
+ PlaybookRun, LoadPlays, LoadTasks, LoadHosts,
HostsEdit, ParseVariableString, GetChoices, fieldChoices,
fieldLabels, EditSchedule, ParseTypeChange, JobDetailService, EventViewer
) {
@@ -43,7 +43,7 @@ export default
scope.parseType = 'yaml';
scope.previousTaskFailed = false;
$scope.stdoutFullScreen = false;
-
+
scope.$watch('job_status', function(job_status) {
if (job_status && job_status.explanation && job_status.explanation.split(":")[0] === "Previous Task Failed") {
scope.previousTaskFailed = true;
@@ -1400,17 +1400,6 @@ export default
}
};
- scope.hostEventsViewer = function(id, name, status) {
- HostEventsViewer({
- scope: scope,
- id: id,
- name: name,
- url: scope.job.related.job_events,
- job_id: scope.job.id,
- status: status
- });
- };
-
scope.refresh = function(){
$scope.$emit('LoadJob');
};
diff --git a/awx/ui/client/src/job-detail/job-detail.partial.html b/awx/ui/client/src/job-detail/job-detail.partial.html
index 3ff7262d1c..aed0a5446e 100644
--- a/awx/ui/client/src/job-detail/job-detail.partial.html
+++ b/awx/ui/client/src/job-detail/job-detail.partial.html
@@ -1,9 +1,8 @@