responsive job details & extend GetBaseUrl with /api/v1/job_events endpoint. resolves #1263

This commit is contained in:
Leigh Johnson
2016-04-09 11:05:18 -04:00
parent 2a5a97fc35
commit 833f68dd7f
11 changed files with 67 additions and 87 deletions

View File

@@ -75,7 +75,7 @@ export default
scope.viewJobDetails = function(job) { scope.viewJobDetails = function(job) {
var goToJobDetails = function(state) { var goToJobDetails = function(state) {
$state.go(state, {id: job.id}); $state.go(state, {id: job.id}, {reload:true});
} }
switch(job.type) { switch(job.type) {

View File

@@ -1,4 +1,4 @@
<div id="HostEvent" class="HostEvent modal fade" role="dialog"> <div id="HostEvent" class="HostEvent modal" role="dialog">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<!-- modal body --> <!-- modal body -->

View File

@@ -10,6 +10,7 @@
function($stateParams, $scope, $state, Wait, JobDetailService, moment, event){ function($stateParams, $scope, $state, Wait, JobDetailService, moment, event){
$scope.processEventStatus = JobDetailService.processEventStatus; $scope.processEventStatus = JobDetailService.processEventStatus;
$scope.hostResults = [];
// Avoid rendering objects in the details fieldset // Avoid rendering objects in the details fieldset
// ng-if="processResults(value)" via host-event-details.partial.html // ng-if="processResults(value)" via host-event-details.partial.html
$scope.processResults = function(value){ $scope.processResults = function(value){
@@ -18,8 +19,8 @@
}; };
var codeMirror = function(el, json){ var codeMirror = function(el, json){
var el = $(el)[0]; var container = $(el)[0];
var editor = CodeMirror.fromTextArea(el, { var editor = CodeMirror.fromTextArea(container, {
lineNumbers: true, lineNumbers: true,
mode: {name: "javascript", json: true} mode: {name: "javascript", json: true}
}); });
@@ -54,17 +55,17 @@
}; };
var init = function(){ var init = function(){
console.log(event)
$scope.event = event.data.results[0]; $scope.event = event.data.results[0];
$scope.event.created = moment($scope.event.created).format(); $scope.event.created = moment($scope.event.created).format();
$scope.hostResults = $stateParams.hostResults; JobDetailService.getJobEventChildren($stateParams.taskId).success(function(res){
$scope.hostResults = res.results;
});
$scope.json = JobDetailService.processJson($scope.event); $scope.json = JobDetailService.processJson($scope.event);
if ($state.current.name == 'jobDetail.host-event.json'){ if ($state.current.name == 'jobDetail.host-event.json'){
codeMirror('#HostEvent-json', $scope.json); codeMirror('#HostEvent-json', $scope.json);
} }
try { try {
$scope.stdout = JobDetailService.processJson($scope.event.event_data.res) $scope.stdout = JobDetailService.processJson($scope.event.event_data.res)
console.log($scope.stdout)
if ($state.current.name == 'jobDetail.host-event.stdout'){ if ($state.current.name == 'jobDetail.host-event.stdout'){
codeMirror('#HostEvent-stdout', $scope.stdout); codeMirror('#HostEvent-stdout', $scope.stdout);
} }
@@ -72,9 +73,6 @@
catch(err){ catch(err){
$scope.sdout = null; $scope.sdout = null;
} }
console.log($scope)
$('#HostEvent').modal('show'); $('#HostEvent').modal('show');
}; };
init(); init();

View File

@@ -8,14 +8,8 @@
var hostEventModal = { var hostEventModal = {
name: 'jobDetail.host-event', name: 'jobDetail.host-event',
url: '/host-event/:eventId', url: '/task/:taskId/host-event/:eventId',
controller: 'HostEventController', controller: 'HostEventController',
params:{
hostResults: {
value: null,
squash: false,
}
},
templateUrl: templateUrl('job-detail/host-event/host-event-modal'), templateUrl: templateUrl('job-detail/host-event/host-event-modal'),
resolve: { resolve: {
features: ['FeaturesService', function(FeaturesService){ features: ['FeaturesService', function(FeaturesService){
@@ -23,12 +17,12 @@ var hostEventModal = {
}], }],
event: ['JobDetailService','$stateParams', function(JobDetailService, $stateParams) { event: ['JobDetailService','$stateParams', function(JobDetailService, $stateParams) {
return JobDetailService.getRelatedJobEvents($stateParams.id, { return JobDetailService.getRelatedJobEvents($stateParams.id, {
id: $stateParams.eventId id: $stateParams.eventId,
}).success(function(res){ return res.results[0]}) }).success(function(res){ return res;})
}] }]
}, },
onExit: function($state){ onExit: function($state){
// close the modal // close the modal
// using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X" // using an onExit event to handle cases where the user navs away using the url bar / back and not modal "X"
$('#HostEvent').modal('hide'); $('#HostEvent').modal('hide');
// hacky way to handle user browsing away via URL bar // hacky way to handle user browsing away via URL bar
@@ -48,12 +42,7 @@ var hostEventModal = {
name: 'jobDetail.host-event.json', name: 'jobDetail.host-event.json',
url: '/json', url: '/json',
controller: 'HostEventController', controller: 'HostEventController',
templateUrl: templateUrl('job-detail/host-event/host-event-json'), templateUrl: templateUrl('job-detail/host-event/host-event-json')
resolve: {
features: ['FeaturesService', function(FeaturesService){
return FeaturesService.get();
}]
}
}; };
var hostEventStdout = { var hostEventStdout = {

View File

@@ -1,6 +1,9 @@
@import "awx/ui/client/src/shared/branding/colors.less"; @import "awx/ui/client/src/shared/branding/colors.less";
@import "awx/ui/client/src/shared/branding/colors.default.less"; @import "awx/ui/client/src/shared/branding/colors.default.less";
.CodeMirror{
border: none;
}
.HostEvents .modal-footer{ .HostEvents .modal-footer{
border: 0; border: 0;
margin-top: 0px; margin-top: 0px;

View File

@@ -14,9 +14,10 @@
$scope.search = null; $scope.search = null;
var buildTooltips = function(hosts){ var buildTooltips = function(hosts){
// status waterfall: unreachable > failed > changed > ok > skipped
var count = { var count = {
ok : _.filter(hosts, function(o){ ok : _.filter(hosts, function(o){
return o.changed === 0 && o.ok > 0; return o.failures === 0 && o.changed === 0 && o.ok > 0;
}), }),
skipped : _.filter(hosts, function(o){ skipped : _.filter(hosts, function(o){
return o.skipped > 0; return o.skipped > 0;
@@ -43,16 +44,13 @@
// JobEvent.update_host_summary_from_stats() from /awx/main.models.jobs.py // JobEvent.update_host_summary_from_stats() from /awx/main.models.jobs.py
jobSocket.on('summary_complete', function(data) { jobSocket.on('summary_complete', function(data) {
// discard socket msgs we don't care about in this context // discard socket msgs we don't care about in this context
if ($stateParams.id === data['unified_job_id']){ if ($stateParams.id == data['unified_job_id']){
JobDetailService.getJobHostSummaries($stateParams.id, {page_size: page_size}) init()
.success(function(res){
init()
});
} }
}); });
// UnifiedJob.def socketio_emit_status() from /awx/main.models.unified_jobs.py // UnifiedJob.def socketio_emit_status() from /awx/main.models.unified_jobs.py
jobSocket.on('status_changed', function(data) { jobSocket.on('status_changed', function(data) {
if ($stateParams.id === data['unified_job_id']){ if ($stateParams.id == data['unified_job_id']){
$scope.status = data['status']; $scope.status = data['status'];
} }
}); });

View File

@@ -2,28 +2,26 @@
@import '../shared/branding/colors.less'; @import '../shared/branding/colors.less';
@import '../shared/branding/colors.default.less'; @import '../shared/branding/colors.default.less';
@import '../shared/layouts/one-plus-one.less';
@breakpoint-md: 1200px;
@breakpoint-sm: 420px;
.JobDetail{ .JobDetail{
display: flex; .OnePlusOne-container(100%, @breakpoint-md);
flex-direction: row;
} }
.JobDetail-leftSide{ .JobDetail-leftSide{
flex: 1 0 auto; .OnePlusOne-panel--left(100%, @breakpoint-md);
width: 50%;
padding-right: 20px;
} }
.JobDetail-rightSide{ .JobDetail-rightSide{
flex: 1 0 auto; .OnePlusOne-panel--right(100%, @breakpoint-md);
width: 50%;
} }
.JobDetail-panelHeader{ .JobDetail-panelHeader{
display: flex; display: flex;
height: 30px; height: 30px;
} }
.JobDetail-expandContainer{ .JobDetail-expandContainer{
flex: 1; flex: 1;
margin: 0px; margin: 0px;
@@ -62,11 +60,17 @@
flex-wrap: wrap; flex-wrap: wrap;
flex-direction: row; flex-direction: row;
padding-top: 25px; padding-top: 25px;
@media screen and(max-width: @breakpoint-sm){
flex-direction: column;
}
} }
.JobDetail-resultRow{ .JobDetail-resultRow{
width: 50%; width: 50%;
display: flex; display: flex;
@media screen and(max-width: @breakpoint-sm){
width: 100%;
}
} }
.JobDetail-resultRowLabel{ .JobDetail-resultRowLabel{
@@ -78,6 +82,9 @@
font-size: 14px; font-size: 14px;
font-weight: normal!important; font-weight: normal!important;
flex: 1 0 auto; flex: 1 0 auto;
@media screen and(max-width: @breakpoint-md){
flex: 2.5 0 auto;
}
} }
.JobDetail-resultRow--variables{ .JobDetail-resultRow--variables{
@@ -109,10 +116,16 @@
flex-direction: row; flex-direction: row;
height: 50px; height: 50px;
margin-top: 25px; margin-top: 25px;
@media screen and(max-width: @breakpoint-sm){
height: auto;
}
} }
.JobDetail-searchContainer{ .JobDetail-searchContainer{
flex: 1 0 auto; flex: 2 0 auto;
@media screen and(max-width: @breakpoint-sm){
margin-bottom: 0px;
}
} }
.JobDetail-tableToggleContainer{ .JobDetail-tableToggleContainer{

View File

@@ -272,7 +272,7 @@ export default
$rootScope.removeJobSummaryComplete = $rootScope.$on('JobSummaryComplete', function() { $rootScope.removeJobSummaryComplete = $rootScope.$on('JobSummaryComplete', function() {
// the job host summary should now be available from the API // the job host summary should now be available from the API
$log.debug('Trigging reload of job_host_summaries'); $log.debug('Trigging reload of job_host_summaries');
scope.$emit('LoadHostSummaries'); scope.$emit('InitialLoadComplete');
}); });
if (scope.removeInitialLoadComplete) { if (scope.removeInitialLoadComplete) {
@@ -315,42 +315,6 @@ export default
if (scope.removeLoadHostSummaries) { if (scope.removeLoadHostSummaries) {
scope.removeLoadHostSummaries(); scope.removeLoadHostSummaries();
} }
scope.removeHostSummaries = scope.$on('LoadHostSummaries', function() {
if(scope.job){
var params = {
page_size: scope.hostSummariesMaxRows,
order: 'host_name'
};
JobDetailService.getJobHostSummaries(scope.job.id, params)
.success(function(data) {
scope.next_host_summaries = data.next;
if (data.results.length > 0) {
// only dump what's in memory when job_host_summaries is available.
scope.jobData.hostSummaries = {};
}
data.results.forEach(function(event) {
var name;
if (event.host_name) {
name = event.host_name;
}
else {
name = "<deleted host>";
}
scope.jobData.hostSummaries[event.host] = {
id: event.host,
name: name,
ok: event.ok,
changed: event.changed,
unreachable: event.dark,
failed: event.failures,
status: (event.failed) ? 'failed' : 'successful'
};
});
scope.$emit('InitialLoadComplete');
});
}
});
if (scope.removeLoadHosts) { if (scope.removeLoadHosts) {
scope.removeLoadHosts(); scope.removeLoadHosts();
@@ -376,13 +340,13 @@ export default
} }
scope.next_host_results = data.next; scope.next_host_results = data.next;
task.hostResults = JobDetailService.processHostEvents(data.results); task.hostResults = JobDetailService.processHostEvents(data.results);
scope.$emit('LoadHostSummaries'); scope.$emit('InitialLoadComplete');
}); });
} else { } else {
scope.$emit('LoadHostSummaries'); scope.$emit('InitialLoadComplete');
} }
} else { } else {
scope.$emit('LoadHostSummaries'); scope.$emit('InitialLoadComplete');
} }
}); });
@@ -491,10 +455,10 @@ export default
msg: 'Call to ' + url + '. GET returned: ' + status }); msg: 'Call to ' + url + '. GET returned: ' + status });
}); });
} else { } else {
scope.$emit('LoadHostSummaries'); scope.$emit('InitialLoadComplete');
} }
} else { } else {
scope.$emit('LoadHostSummaries'); scope.$emit('InitialLoadComplete');
} }
}); });

View File

@@ -1,6 +1,6 @@
<div class="tab-pane" id="jobs-detail"> <div class="tab-pane" id="jobs-detail">
<div ng-cloak id="htmlTemplate" class="JobDetail"> <div ng-cloak id="htmlTemplate" class="JobDetail">
<div ui-view></div> <div ui-view class-""></div>
<!--beginning of job-detail-container (left side) --> <!--beginning of job-detail-container (left side) -->
<div id="job-detail-container" class="JobDetail-leftSide" ng-class="{'JobDetail-stdoutActionButton--active': stdoutFullScreen}"> <div id="job-detail-container" class="JobDetail-leftSide" ng-class="{'JobDetail-stdoutActionButton--active': stdoutFullScreen}">
<!--beginning of results--> <!--beginning of results-->
@@ -344,7 +344,7 @@
<tbody> <tbody>
<tr class="List-tableRow cursor-pointer" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-repeat="result in results = (hostResults) track by $index"> <tr class="List-tableRow cursor-pointer" ng-class-odd="'List-tableRow--oddRow'" ng-class-even="'List-tableRow--evenRow'" ng-repeat="result in results = (hostResults) track by $index">
<td class="List-tableCell col-lg-4 col-md-3 col-sm-3 col-xs-3 status-column"> <td class="List-tableCell col-lg-4 col-md-3 col-sm-3 col-xs-3 status-column">
<a ui-sref="jobDetail.host-event.details({eventId: result.id, hostResults: hostResults})" aw-tool-tip="Event ID: {{ result.id }}<br \>Status: {{ result.status_text }}. Click for details" data-placement="top"><i ng-show="result.status_text != 'Unreachable'" class="JobDetail-statusIcon fa icon-job-{{ result.status }}"></i><span ng-show="result.status_text != 'Unreachable'">{{ result.name }}</span><i ng-show="result.status_text == 'Unreachable'" class="JobDetail-statusIcon fa icon-job-unreachable"></i><span ng-show="result.status_text == 'Unreachable'">{{ result.name }}</span></a> <a ui-sref="jobDetail.host-event.details({eventId: result.id, taskId: selectedTask})" aw-tool-tip="Event ID: {{ result.id }}<br \>Status: {{ result.status_text }}. Click for details" data-placement="top"><i ng-show="result.status_text != 'Unreachable'" class="JobDetail-statusIcon fa icon-job-{{ result.status }}"></i><span ng-show="result.status_text != 'Unreachable'">{{ result.name }}</span><i ng-show="result.status_text == 'Unreachable'" class="JobDetail-statusIcon fa icon-job-unreachable"></i><span ng-show="result.status_text == 'Unreachable'">{{ result.name }}</span></a>
</td> </td>
<td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3 item-column">{{ result.item }}</td> <td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3 item-column">{{ result.item }}</td>
<td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3">{{ result.msg }}</td> <td class="List-tableCell col-lg-3 col-md-4 col-sm-3 col-xs-3">{{ result.msg }}</td>

View File

@@ -145,6 +145,19 @@ export default
msg: 'Call to ' + url + '. GET returned: ' + status }); msg: 'Call to ' + url + '. GET returned: ' + status });
}); });
}, },
getJobEventChildren: function(id){
var url = GetBasePath('job_events');
url = url + id + '/children/';
Rest.setUrl(url);
return Rest.get()
.success(function(data){
return data
})
.error(function(data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Call to ' + url + '. GET returned: ' + status });
});
},
// GET job host summaries related to a job run // GET job host summaries related to a job run
// e.g. ?page_size=200&order=host_name // e.g. ?page_size=200&order=host_name
getJobHostSummaries: function(id, params){ getJobHostSummaries: function(id, params){

View File

@@ -35,6 +35,8 @@ angular.module('ApiLoader', ['Utilities'])
.success(function (data) { .success(function (data) {
data.base = base; data.base = base;
$rootScope.defaultUrls = data; $rootScope.defaultUrls = data;
// tiny hack to side-step api/v1/job_events not being a visible endpoint @ GET api/v1/
$rootScope.defaultUrls['job_events'] = '/api/v1/job_events/';
Store('api', data); Store('api', data);
}) })
.error(function (data, status) { .error(function (data, status) {