blob: 737399a26f3edd236d41745d481d33d9feae70ba [file] [log] [blame]
/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 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 'length') is
// expected on success. A third field, 'data' may be provided if
// 'length' is greater than 0. 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 length of the
// data) and the 'length' field set to 0.
// 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($) {
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.length < length) {
buffer += data.data;
read(offset + data.length, length - data.length);
} else if (data.length > 0) {
this_.indicate('(PAGED)');
setTimeout(function() { this_.indicate(''); }, 1000);
// Prepend buffer onto data.
data.offset -= buffer.length;
data.data = buffer + data.data;
data.length = data.data.length;
// 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);
data.length -= index;
}
this_.start = data.offset;
var scrollTop = this_.element.scrollTop();
var scrollHeight = this_.element[0].scrollHeight;
this_.element.prepend(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.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);
data.length -= index;
this_.start = data.offset; // Adjust the actual start too!
}
this_.end = data.offset + data.length;
this_.element.append(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.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);