http://git-wip-us.apache.org/repos/asf/mesos/blob/5852eb93/src/webui/master/static/js/jquery.pailer.js ---------------------------------------------------------------------- diff --git a/src/webui/master/static/js/jquery.pailer.js b/src/webui/master/static/js/jquery.pailer.js new file mode 100644 index 0000000..4edc5b4 --- /dev/null +++ b/src/webui/master/static/js/jquery.pailer.js @@ -0,0 +1,335 @@ +// A jQuery plugin for PAging and taILing data (i.e., a +// 'PAILer'). Paging occurs when scrolling reaches the "top" and +// tailing occurs when scrolling has reached the "bottom". + +// A 'read' function must be provided for reading the data (in +// bytes). This function should expect an "options" object with the +// fields 'offset' and 'length' set for reading the data. The result +// from of the function should be a "promise" like value which has a +// 'success' and 'error' callback which each take a function. An +// object with at least two fields defined ('offset' and 'data') is +// expected on success. The length of 'data' may be smaller than the +// amount requested. If the offset requested is greater than the +// available offset, the result should be an object with the 'offset' +// field set to the available offset (i.e., the total length of the +// data) with an empty 'data' field. + +// The plugin prepends, appends, and updates the "html" component of +// the elements specified in the jQuery selector (e.g., doing +// $('#data').pailer(...) means that data will be updated within +// $('#data') via $('#data').prepend(...) and $('#data').append(...) +// and $('#data').html(...) calls). + +// An indicator paragraph element (i.e., <p>) can be specified that +// the plugin will write text to describing any status/errors that +// have been encountered. + +// Data will automagically get truncated at some specified length, +// configurable via the 'truncate-length' option. Likewise, the amount +// of data paged in at a time can be configured via the 'page-size' +// option. + +// Example: +// HTML: +// <div id="data" style="white-space:pre-wrap;"></div> +// +// <div style="position: absolute; left: 5px; top: 0px;"> +// <p id="indicator"></p> +// </div> +// Javascript: +// $('#data').pailer({ +// 'read': function(options) { +// var settings = $.extend({ +// 'offset': -1, +// 'length': -1 +// }, options); +// var url = '/url/for/data' +// + '?offset=' + settings.offset +// + '&length=' + settings.length; +// return $.getJSON(url); +// }, +// 'indicator': $('#indicator') +// }); + +(function($) { + // Helper for escaping html, based on _.escape from underscore.js. + function escapeHTML(string) { + if (string == null) { + return ''; + } + + var escapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/' + }; + var regex = new RegExp('[' + Object.keys(escapes).join('') + ']', 'g'); + + return ('' + string).replace(regex, function (match) { + return escapes[match]; + }); + } + + function Pailer(read, element, indicator, page_size, truncate_length) { + var this_ = this; + + this_.read = read; + this_.element = element; + this_.indicator = indicator; + this_.initialized = false; + this_.start = -1; + this_.end = -1; + this_.paging = false; + this_.tailing = true; + + page_size || $.error('Expecting page_size to be defined'); + truncate_length || $.error('Expecting truncate_length to be defined'); + + this_.page_size = page_size; + this_.truncate_length = truncate_length; + + this_.element.css('overflow', 'auto'); + + this_.element.scroll(function () { + var scrollTop = this_.element.scrollTop(); + var height = this_.element.height(); + var scrollHeight = this_.element[0].scrollHeight; + + if (scrollTop == 0) { + this_.page(); + } else if (scrollTop + height >= scrollHeight) { + if (!this_.tailing) { + this_.tailing = true; + this_.tail(); + } + } else { + this_.tailing = false; + } + }); + } + + + Pailer.prototype.initialize = function() { + var this_ = this; + + // Set an indicator while we load the data. + this_.indicate('(LOADING)'); + + this_.read({'offset': -1}) + .success(function(data) { + this_.indicate(''); + + // Get the last page of data. + if (data.offset > this_.page_size) { + this_.start = this_.end = data.offset - this_.page_size; + } else { + this_.start = this_.end = 0; + } + + this_.initialized = true; + this_.element.html(''); + setTimeout(function() { this_.tail(); }, 0); + }) + .error(function() { + this_.indicate('(FAILED TO INITIALIZE ... RETRYING)'); + setTimeout(function() { + this_.indicate(''); + this_.initialize(); + }, 1000); + }); + } + + + Pailer.prototype.page = function() { + var this_ = this; + + if (!this_.initialized) { + return; + } + + if (this_.paging) { + return; + } + + this_.paging = true; + this_.indicate('(PAGING)'); + + if (this_.start == 0) { + this_.paging = false; + this_.indicate('(AT BEGINNING OF FILE)'); + setTimeout(function() { this_.indicate(''); }, 1000); + return; + } + + var offset = this_.start - this_.page_size; + var length = this_.page_size; + + if (offset < 0) { + offset = 0; + length = this_.start; + } + + // Buffer the data in case what gets read is less than 'length'. + var buffer = ''; + + var read = function(offset, length) { + this_.read({'offset': offset, 'length': length}) + .success(function(data) { + if (data.data.length < length) { + buffer += data.data; + read(offset + data.data.length, length - data.data.length); + } else if (data.data.length > 0) { + this_.indicate('(PAGED)'); + setTimeout(function() { this_.indicate(''); }, 1000); + + // Prepend buffer onto data. + data.offset -= buffer.length; + data.data = buffer + data.data; + + // Truncate to the first newline (unless this is the beginning). + if (data.offset != 0) { + var index = data.data.indexOf('\n') + 1; + data.offset += index; + data.data = data.data.substring(index); + } + + this_.start = data.offset; + + var scrollTop = this_.element.scrollTop(); + var scrollHeight = this_.element[0].scrollHeight; + + this_.element.prepend(escapeHTML(data.data)); + + scrollTop += this_.element[0].scrollHeight - scrollHeight; + this_.element.scrollTop(scrollTop); + + this_.paging = false; + } + }) + .error(function() { + this_.indicate('(FAILED TO PAGE ... RETRYING)'); + setTimeout(function() { + this_.indicate(''); + this_.page(); + }, 1000); + }); + } + + read(offset, length); + } + + + Pailer.prototype.tail = function() { + var this_ = this; + + if (!this_.initialized) { + return; + } + + this_.read({'offset': this_.end, 'length': this_.truncate_length}) + .success(function(data) { + var scrollTop = this_.element.scrollTop(); + var height = this_.element.height(); + var scrollHeight = this_.element[0].scrollHeight; + + // Check if we are still at the bottom (since this event might + // have fired before the scroll event has been dispatched). + if (scrollTop + height < scrollHeight) { + this_.tailing = false; + return; + } + + if (data.data.length > 0) { + // Truncate to the first newline if this is the first time + // (and we aren't reading from the beginning of the log). + if (this_.start == this_.end && data.offset != 0) { + var index = data.data.indexOf('\n') + 1; + data.offset += index; + data.data = data.data.substring(index); + this_.start = data.offset; // Adjust the actual start too! + } + + this_.end = data.offset + data.data.length; + + this_.element.append(escapeHTML(data.data)); + + scrollTop += this_.element[0].scrollHeight - scrollHeight; + this_.element.scrollTop(scrollTop); + + // Also, only if we're at the bottom, truncate data so that we + // don't consume too much memory. TODO(benh): Only do + // truncations if we've been at the bottom for a while. + this_.truncate(); + } + + // Tail immediately if we got as much data as requested (since + // this probably means we've waited around a while). The + // alternative here would be to not get data in chunks, but the + // potential issue here is that we might end up requesting GB of + // log data at a time ... the right solution here might be to do + // a request to determine the new ending offset and then request + // the proper length. + if (data.data.length == this_.truncate_length) { + setTimeout(function() { this_.tail(); }, 0); + } else { + setTimeout(function() { this_.tail(); }, 1000); + } + }) + .error(function() { + this_.indicate('(FAILED TO TAIL ... RETRYING)'); + this_.initialized = false; + setTimeout(function() { + this_.indicate(''); + this_.initialize(); + }, 1000); + }); + } + + + Pailer.prototype.indicate = function(text) { + var this_ = this; + + if (this_.indicator) { + this_.indicator.text(text); + } + } + + + Pailer.prototype.truncate = function() { + var this_ = this; + + var length = this_.element.html().length; + if (length >= this_.truncate_length) { + var index = length - this_.truncate_length; + this_.start = this_.end - this_.truncate_length; + this_.element.html(this_.element.html().substring(index)); + } + } + + $.fn.pailer = function(options) { + var settings = $.extend({ + 'read': function() { return { + 'success': function() {}, + 'error': function(f) { f(); } + }}, + 'page_size': 8 * 4096, // 8 "pages". + 'truncate_length': 50000 + }, options); + + this.each(function() { + var pailer = $.data(this, 'pailer'); + if (!pailer) { + pailer = new Pailer(settings.read, + $(this), + settings.indicator, + settings.page_size, + settings.truncate_length); + $.data(this, 'pailer', pailer); + pailer.initialize(); + } + }); + } +})(jQuery);
http://git-wip-us.apache.org/repos/asf/mesos/blob/5852eb93/src/webui/master/static/js/relative-date.js ---------------------------------------------------------------------- diff --git a/src/webui/master/static/js/relative-date.js b/src/webui/master/static/js/relative-date.js new file mode 100644 index 0000000..b33ada5 --- /dev/null +++ b/src/webui/master/static/js/relative-date.js @@ -0,0 +1,48 @@ +// Retrieved from: +// https://github.com/azer/relative-date/blob/master/lib/relative-date.js. + +var relativeDate = (function(undefined){ + + var SECOND = 1000, + MINUTE = 60 * SECOND, + HOUR = 60 * MINUTE, + DAY = 24 * HOUR, + WEEK = 7 * DAY, + YEAR = DAY * 365, + MONTH = YEAR / 12; + + var formats = [ + [ 0.7 * MINUTE, 'just now' ], + [ 1.5 * MINUTE, 'a minute ago' ], + [ 60 * MINUTE, 'minutes ago', MINUTE ], + [ 1.5 * HOUR, 'an hour ago' ], + [ DAY, 'hours ago', HOUR ], + [ 2 * DAY, 'yesterday' ], + [ 7 * DAY, 'days ago', DAY ], + [ 1.5 * WEEK, 'a week ago'], + [ MONTH, 'weeks ago', WEEK ], + [ 1.5 * MONTH, 'a month ago' ], + [ YEAR, 'months ago', MONTH ], + [ 1.5 * YEAR, 'a year ago' ], + [ Number.MAX_VALUE, 'years ago', YEAR ] + ]; + + function relativeDate(input,reference){ + !reference && ( reference = (new Date).getTime() ); + reference instanceof Date && ( reference = reference.getTime() ); + input instanceof Date && ( input = input.getTime() ); + + var delta = reference - input, + format, i, len; + + for(i = -1, len=formats.length; ++i < len; ){ + format = formats[i]; + if(delta < format[0]){ + return format[2] == undefined ? format[1] : Math.round(delta/format[2]) + ' ' + format[1]; + } + }; + } + + return relativeDate; + +})();