Job stdout

Replaced scrolling widget with new angular directive and simplified approach to endless scroll. Fixed non-response to job failing before any job_events actually received. Now if playbook run completely fails, the stdout gets displayed without having to refresh browser.
This commit is contained in:
Chris Houseknecht
2014-07-03 12:36:05 -04:00
parent 1d1137a8d8
commit ac3e76236a
5 changed files with 502 additions and 544 deletions

View File

@@ -16,37 +16,23 @@ function JobStdoutController ($log, $rootScope, $scope, $compile, $routeParams,
stdout_url, stdout_url,
current_range, current_range,
event_socket, event_socket,
first_time_up = 0, status_socket,
first_time_down = 0,
loaded_sections = [], loaded_sections = [],
event_queue = 0, event_queue = 0,
auto_scroll_down=true, // programmatic scroll to bottom auto_scroll_down=true, // programmatic scroll to bottom
live_event_processing = true, live_event_processing = true,
should_apply_live_events = true, should_apply_live_events = true,
prior_mcs, page_size = 500,
page_size = 500; lastScrollTop = 0,
st,
direction;
event_socket = Socket({ status_socket = Socket({
scope: $scope, scope: $scope,
endpoint: "job_events" endpoint: "jobs"
}); });
status_socket.init();
Wait('start'); status_socket.on("status_changed", function(data) {
event_socket.init();
event_socket.on("job_events-" + job_id, function() {
if (api_complete) {
event_queue++;
}
});
if ($rootScope.removeJobStatusChange) {
$rootScope.removeJobStatusChange();
}
$rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange', function(e, data) {
// if we receive a status change event for the current job indicating the job
// is finished, stop event queue processing and reload
if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10) && $scope.job) { if (parseInt(data.unified_job_id, 10) === parseInt(job_id,10) && $scope.job) {
$scope.job.status = data.status; $scope.job.status = data.status;
if (data.status === 'failed' || data.status === 'canceled' || if (data.status === 'failed' || data.status === 'canceled' ||
@@ -55,13 +41,29 @@ function JobStdoutController ($log, $rootScope, $scope, $compile, $routeParams,
window.clearInterval($rootScope.jobStdOutInterval); window.clearInterval($rootScope.jobStdOutInterval);
} }
if (live_event_processing) { if (live_event_processing) {
if (loaded_sections.length === 0) {
$scope.$emit('LoadStdout');
}
else {
getNextSection(); getNextSection();
} }
}
live_event_processing = false; live_event_processing = false;
} }
} }
}); });
event_socket = Socket({
scope: $scope,
endpoint: "job_events"
});
event_socket.init();
event_socket.on("job_events-" + job_id, function() {
if (api_complete) {
event_queue++;
}
});
$rootScope.jobStdOutInterval = setInterval( function() { $rootScope.jobStdOutInterval = setInterval( function() {
if (event_queue > 0) { if (event_queue > 0) {
// events happened since the last check // events happened since the last check
@@ -94,9 +96,8 @@ function JobStdoutController ($log, $rootScope, $scope, $compile, $routeParams,
start: (data.range.start < 0) ? 0 : data.range.start, start: (data.range.start < 0) ? 0 : data.range.start,
end: data.range.end end: data.range.end
}); });
setTimeout(function() { $('#pre-container').scrollTop($('#pre-container').prop("scrollHeight"));
$('#pre-container').mCustomScrollbar("scrollTo", "bottom"); //console.log($('#pre-container-content').prop("scrollHeight"));
}, 300);
} }
else { else {
api_complete = true; api_complete = true;
@@ -108,11 +109,21 @@ function JobStdoutController ($log, $rootScope, $scope, $compile, $routeParams,
}); });
}); });
function detectDirection() {
st = $('#pre-container').scrollTop();
if (st > lastScrollTop) {
direction = "down";
} else {
direction = "up";
}
lastScrollTop = st;
return direction;
}
function resizeToFit() { function resizeToFit() {
available_height = $(window).height() - $('#main-menu-container .navbar').outerHeight() - $('#job-status').outerHeight() - available_height = $(window).height() - $('#main-menu-container .navbar').outerHeight() - $('#job-status').outerHeight() -
$('#breadcrumb-container').outerHeight() - 30; $('#breadcrumb-container').outerHeight() - 60;
$('#pre-container').height(available_height); $('#pre-container').height(available_height);
$('#pre-container').mCustomScrollbar("update");
} }
resizeToFit(); resizeToFit();
@@ -120,6 +131,12 @@ function JobStdoutController ($log, $rootScope, $scope, $compile, $routeParams,
resizeToFit(); resizeToFit();
}, 500)); }, 500));
$('#pre-container').bind('scroll', function() {
if (detectDirection() === "up") {
should_apply_live_events = false;
}
});
Rest.setUrl(GetBasePath('jobs') + job_id + '/'); Rest.setUrl(GetBasePath('jobs') + job_id + '/');
Rest.get() Rest.get()
.success(function(data) { .success(function(data) {
@@ -139,56 +156,7 @@ function JobStdoutController ($log, $rootScope, $scope, $compile, $routeParams,
}); });
$scope.onTotalScroll = function() { $scope.stdOutScrollToTop = function() {
// scroll forward or into the future toward the end of the file
var start, url;
if ((live_event_processing === false || (live_event_processing && should_apply_live_events === false)) &&
auto_scroll_down === false) {
if (loaded_sections.length > 0) {
start = loaded_sections[loaded_sections.length - 1].end + 1;
}
else {
start = 0;
}
url = stdout_url + '?format=json&start_line=' + start + '&end_line=' + (start + page_size);
first_time_down++;
Wait('start');
Rest.setUrl(url);
Rest.get()
.success( function(data) {
Wait('stop');
if (loaded_sections.indexOf(start) < 0) {
if (data.content) {
$('#pre-container-content').append(data.content);
loaded_sections.push({
start: (data.range.start < 0) ? 0 : data.range.start,
end: data.range.end
});
current_range = data.range;
}
}
if (data.range.end === data.range.absolute_end) {
should_apply_live_events = true; //we're at the bottom
$log.debug('at the end. turned on live events');
}
auto_scroll_down = true;
if (first_time_down === 1) {
$('#pre-container').mCustomScrollbar("update");
}
$("#pre-container").mCustomScrollbar("scrollTo", "bottom", {scrollInertia:0});
})
.error(function(data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to retrieve stdout for job: ' + job_id + '. GET returned: ' + status });
});
}
else {
auto_scroll_down = false;
}
};
$scope.onTotalScrollBack = function() {
// scroll up or back in time toward the beginning of the file // scroll up or back in time toward the beginning of the file
var start, end, url; var start, end, url;
if (loaded_sections.length > 0 && loaded_sections[0].start > 0) { if (loaded_sections.length > 0 && loaded_sections[0].start > 0) {
@@ -200,26 +168,26 @@ function JobStdoutController ($log, $rootScope, $scope, $compile, $routeParams,
end = page_size; end = page_size;
} }
if (start !== undefined && end !== undefined) { if (start !== undefined && end !== undefined) {
$('#stdoutMoreRowsTop').fadeIn();
url = stdout_url + '?format=json&start_line=' + start + '&end_line=' + end; url = stdout_url + '?format=json&start_line=' + start + '&end_line=' + end;
first_time_up++;
Wait('start');
Rest.setUrl(url); Rest.setUrl(url);
Rest.get() Rest.get()
.success( function(data) { .success( function(data) {
Wait('stop'); //var currentPos = $('#pre-container').scrollTop();
var oldContentHeight, heightDiff; var newSH, oldSH = $('#pre-container').prop('scrollHeight'),
oldContentHeight=$("#pre-container .mCSB_container").innerHeight(); st = $('#pre-container').scrollTop();
$('#pre-container-content').prepend(data.content); $('#pre-container-content').prepend(data.content);
newSH = $('#pre-container').prop('scrollHeight');
$('#pre-container').scrollTop(newSH - oldSH + st);
loaded_sections.unshift({ loaded_sections.unshift({
start: (data.range.start < 0) ? 0 : data.range.start, start: (data.range.start < 0) ? 0 : data.range.start,
end: data.range.end end: data.range.end
}); });
current_range = data.range; current_range = data.range;
heightDiff=$("#pre-container .mCSB_container").innerHeight() - oldContentHeight; $('#stdoutMoreRowsTop').fadeOut(400);
if (first_time_up === 1) {
$('#pre-container').mCustomScrollbar("update");
}
$("#pre-container").mCustomScrollbar("scrollTo", heightDiff, {scrollInertia:0});
}) })
.error(function(data, status) { .error(function(data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!', ProcessErrors($scope, data, status, null, { hdr: 'Error!',
@@ -228,42 +196,11 @@ function JobStdoutController ($log, $rootScope, $scope, $compile, $routeParams,
} }
}; };
$scope.whileScrolling = function(mcs) {
var direction;
if (prior_mcs !== undefined) {
if (mcs.topPct < prior_mcs.topPct && prior_mcs.topPct !== 100) {
direction = "up";
}
else if (mcs.topPct > prior_mcs.topPct) {
direction = "down";
}
else {
direction = "none";
}
}
prior_mcs = mcs;
if (direction === "up") {
// user is scrollin up or back in time
$log.debug('user scrolled up. turned off live events.');
should_apply_live_events = false;
}
};
$scope.scrollStarted = function() {
// user touched the scroll bar. stop applying live events and forcing
//if (auto_scroll_down === false) {
// should_apply_live_events = false;
// $log.debug('turned off live events');
//}
//else {
// auto_scroll_down = false;
//}
};
function getNextSection() { function getNextSection() {
// get the next range of data from the API // get the next range of data from the API
var start = loaded_sections[loaded_sections.length - 1].end + 1, url; var start = loaded_sections[loaded_sections.length - 1].end + 1, url;
url = stdout_url + '?format=json&start_line=' + start + '&end_line=' + (start + page_size); url = stdout_url + '?format=json&start_line=' + start + '&end_line=' + (start + page_size);
$('#stdoutMoreRowsBottom').fadeIn();
Rest.setUrl(url); Rest.setUrl(url);
Rest.get() Rest.get()
.success( function(data) { .success( function(data) {
@@ -276,12 +213,9 @@ function JobStdoutController ($log, $rootScope, $scope, $compile, $routeParams,
// if user has not disabled live event view by scrolling upward, then scroll down to the new content // if user has not disabled live event view by scrolling upward, then scroll down to the new content
current_range = data.range; current_range = data.range;
auto_scroll_down = true; // prevent auto load from happening auto_scroll_down = true; // prevent auto load from happening
first_time_down++; $('#pre-container-content').scrollTop($('#pre-container-content').prop("scrollHeight"));
if (first_time_down === 1) {
$('#pre-container').mCustomScrollbar("update");
}
$("#pre-container").mCustomScrollbar("scrollTo", "bottom", {scrollInertia:0});
} }
$('#stdoutMoreRowsBottom').fadeOut(400);
}) })
.error(function(data, status) { .error(function(data, status) {
ProcessErrors($scope, data, status, null, { hdr: 'Error!', ProcessErrors($scope, data, status, null, { hdr: 'Error!',

View File

@@ -13,6 +13,9 @@
@skipped-hosts-color: @skipped; @skipped-hosts-color: @skipped;
@unreachable-hosts-color: @unreachable; @unreachable-hosts-color: @unreachable;
#jobs-detail {
.job_summary { .job_summary {
.table { .table {
margin-bottom: 0; margin-bottom: 0;
@@ -54,6 +57,7 @@
color:#000; color:#000;
float:right; float:right;
} }
#hostResultsMoreRows.scroll-spinner { #hostResultsMoreRows.scroll-spinner {
padding-top: 0; padding-top: 0;
position: relative; position: relative;
@@ -482,4 +486,4 @@ svg text.percent{
} }
} }
} }
}

View File

@@ -6,12 +6,33 @@
*/ */
#jobs-stdout { #jobs-stdout {
margin-bottom: 20px;
#job-status { #job-status {
label { label {
margin-right: 15px; margin-right: 15px;
} }
margin-bottom: 15px; margin-bottom: 15px;
} }
.scroll-spinner {
display: none;
background-color: transparent;
color:#000;
}
#stdoutMoreRowsTop {
position: absolute;
top: 10px;
right: 20px;
}
#stdoutMoreRowsBottom {
float: right;
}
#pre-container {
overflow-x: scroll;
overflow-y: auto;
}
} }
.body_foreground { color: #AAAAAA; } .body_foreground { color: #AAAAAA; }

View File

@@ -2,7 +2,7 @@
'use strict'; 'use strict';
var module = ng.module('lrInfiniteScroll', []); var module = ng.module('lrInfiniteScroll', []);
module.directive('lrInfiniteScroll', ['$timeout', function (timeout) { module.directive('lrInfiniteScroll', ['$log', '$timeout', function ($log, timeout) {
return{ return{
link: function (scope, element, attr) { link: function (scope, element, attr) {
var var
@@ -21,9 +21,7 @@
} }
element.bind('scroll', function () { element.bind('scroll', function () {
var remaining = var remaining = (direction === 'down') ? element[0].scrollHeight - (element[0].clientHeight + element[0].scrollTop) : element[0].scrollTop;
(direction === 'down') ? element[0].scrollHeight - (element[0].clientHeight + element[0].scrollTop) : element[0].scrollTop;
// if we have reached the threshold and we scroll down // if we have reached the threshold and we scroll down
if ((direction === 'down' && remaining < lengthThreshold && (remaining - lastRemaining) < 0) || if ((direction === 'down' && remaining < lengthThreshold && (remaining - lastRemaining) < 0) ||
direction === 'up' && remaining < lengthThreshold) { direction === 'up' && remaining < lengthThreshold) {

View File

@@ -15,12 +15,13 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div id="job-status"><label>Job Status</label> <i class="fa icon-job-{{ job.status }}"></i> {{ job.status }}</div> <div id="job-status"><label>Job Status</label> <i class="fa icon-job-{{ job.status }}"></i> {{ job.status }}</div>
<div id="pre-container" class="body_background body_foreground pre mono-space" aw-custom-scroll data-scroll-theme="light-thin" <div class="scroll-spinner" id="stdoutMoreRowsTop"><i class="fa fa-cog fa-spin"></i></div>
data-scroll-inertia="500" data-on-total-scroll="onTotalScroll" data-on-total-scroll-back="onTotalScrollBack" data-on-scroll-start="scrollStarted" <div id="pre-container" class="body_background body_foreground pre mono-space" lr-infinite-scroll="stdOutScrollToTop"
data-while-scrolling="whileScrolling" data-on-total-scroll-back-offset="100" data-scroll-axis="yx"> scroll-threshold="300" data-direction="up" time-threshold="500">
<div id="pre-container-content"></div> <div id="pre-container-content"></div>
</div> </div>
</div> </div>
<div class="scroll-spinner" id="stdoutMoreRowsBottom"><i class="fa fa-cog fa-spin"></i></div>
</div> </div>
</div> </div>
</div> </div>