horizon/horizon/static/horizon/js/horizon.tables.js

419 lines
16 KiB
JavaScript

/* Namespace for core functionality related to DataTables. */
horizon.datatables = {
update: function () {
var $rows_to_update = $('tr.status_unknown.ajax-update');
if ($rows_to_update.length) {
var interval = $rows_to_update.attr('data-update-interval'),
$table = $rows_to_update.closest('table'),
decay_constant = $table.attr('decay_constant');
// Do not update this row if the action column is expanded
if ($rows_to_update.find('.actions_column .btn-group.open').length) {
// Wait and try to update again in next interval instead
setTimeout(horizon.datatables.update, interval);
// Remove interval decay, since this will not hit server
$table.removeAttr('decay_constant');
return;
}
// Trigger the update handlers.
$rows_to_update.each(function(index, row) {
var $row = $(this),
$table = $row.closest('table.datatable');
horizon.ajax.queue({
url: $row.attr('data-update-url'),
error: function (jqXHR, textStatus, errorThrown) {
switch (jqXHR.status) {
// A 404 indicates the object is gone, and should be removed from the table
case 404:
// Update the footer count and reset to default empty row if needed
var $footer, row_count, footer_text, colspan, template, params, $empty_row;
// existing count minus one for the row we're removing
row_count = horizon.datatables.update_footer_count($table, -1);
if(row_count === 0) {
colspan = $table.find('th[colspan]').attr('colspan');
template = horizon.templates.compiled_templates["#empty_row_template"];
params = {"colspan": colspan};
empty_row = template.render(params);
$row.replaceWith(empty_row);
} else {
$row.remove();
}
// Reset tablesorter's data cache.
$table.trigger("update");
break;
default:
horizon.utils.log(gettext("An error occurred while updating."));
$row.removeClass("ajax-update");
$row.find("i.ajax-updating").remove();
break;
}
},
success: function (data, textStatus, jqXHR) {
var $new_row = $(data);
if ($new_row.hasClass('status_unknown')) {
var spinner_elm = $new_row.find("td.status_unknown:last");
if ($new_row.find('a.btn-action-required').length > 0) {
spinner_elm.prepend(
$("<div />")
.addClass("action_required_img")
.append(
$("<img />")
.attr("src", "/static/dashboard/img/action_required.png")));
} else {
// Replacing spin.js here with an animated gif to reduce CPU
spinner_elm.prepend(
$("<div />")
.addClass("loading_gif")
.append(
$("<img />")
.attr("src", "/static/dashboard/img/loading.gif")));
}
}
// Only replace row if the html content has changed
if($new_row.html() != $row.html()) {
if($row.find('.table-row-multi-select:checkbox').is(':checked')) {
// Preserve the checkbox if it's already clicked
$new_row.find('.table-row-multi-select:checkbox').prop('checked', true);
}
$row.replaceWith($new_row);
// Reset tablesorter's data cache.
$table.trigger("update");
// Reset decay constant.
$table.removeAttr('decay_constant');
}
},
complete: function (jqXHR, textStatus) {
// Revalidate the button check for the updated table
horizon.datatables.validate_button();
// Set interval decay to this table, and increase if it already exist
if(decay_constant === undefined) {
decay_constant = 1;
} else {
decay_constant++;
}
$table.attr('decay_constant', decay_constant);
// Poll until there are no rows in an "unknown" state on the page.
next_poll = interval * decay_constant;
// Limit the interval to 30 secs
if(next_poll > 30 * 1000) next_poll = 30 * 1000;
setTimeout(horizon.datatables.update, next_poll);
}
});
});
}
},
validate_button: function () {
// Disable form button if checkbox are not checked
$("form").each(function (i) {
var checkboxes = $(this).find(".table-row-multi-select:checkbox");
if(!checkboxes.length) {
// Do nothing if no checkboxes in this form
return;
}
if(!checkboxes.filter(":checked").length) {
$(this).find(".table_actions button.btn-danger").addClass("disabled");
}
});
}
};
/* Generates a confirmation modal dialog for the given action. */
horizon.datatables.confirm = function (action) {
var $action = $(action),
$modal_parent = $(action).closest('.modal'),
name_array = [],
closest_table_id, action_string, name_string,
title, body, modal, form;
if($action.hasClass("disabled")) {
return;
}
action_string = $action.text();
name_string = "";
// Add the display name defined by table.get_object_display(datum)
closest_table_id = $(action).closest("table").attr("id");
// Check if data-display attribute is available
if ($("#"+closest_table_id+" tr[data-display]").length > 0) {
if($(action).closest("div").hasClass("table_actions")) {
// One or more checkboxes selected
$("#"+closest_table_id+" tr[data-display]").has(".table-row-multi-select:checkbox:checked").each(function() {
name_array.push(" \"" + $(this).attr("data-display") + "\"");
});
name_array.join(", ");
name_string = name_array.toString();
} else {
// If no checkbox is selected
name_string = " \"" + $(action).closest("tr").attr("data-display") + "\"";
}
name_string = interpolate(gettext("You have selected %s. "), [name_string]);
}
title = interpolate(gettext("Confirm %s"), [action_string]);
body = name_string + gettext("Please confirm your selection. This action cannot be undone.");
modal = horizon.modals.create(title, body, action_string);
modal.modal();
if($modal_parent.length) {
var child_backdrop = modal.next('.modal-backdrop');
// re-arrange z-index for these stacking modal
child_backdrop.css('z-index', $modal_parent.css('z-index')+10);
modal.css('z-index', child_backdrop.css('z-index')+10);
}
modal.find('.btn-primary').click(function (evt) {
form = $action.closest('form');
form.append("<input type='hidden' name='" + $action.attr('name') + "' value='" + $action.attr('value') + "'/>");
form.submit();
modal.modal('hide');
horizon.modals.modal_spinner(gettext("Working"));
return false;
});
return modal;
};
$.tablesorter.addParser({
// set a unique id
id: 'sizeSorter',
is: function(s) {
// Not an auto-detected parser
return false;
},
// compare int values
format: function(s) {
var sizes = {BYTE: 0, B: 0, KB: 1, MB: 2,
GB: 3, TB: 4, PB: 5};
var regex = /([\d\.,]+)\s*(byte|B|KB|MB|GB|TB|PB)+/i;
var match = s.match(regex);
if (match && match.length === 3){
return parseFloat(match[1]) *
Math.pow(1024, sizes[match[2].toUpperCase()]);
}
return parseInt(s, 10);
},
type: 'numeric'
});
$.tablesorter.addParser({
// set a unique id
id: 'timesinceSorter',
is: function(s) {
// Not an auto-detected parser
return false;
},
// compare int values
format: function(s, table, cell, cellIndex) {
return $(cell).find('span').data('seconds');
},
type: 'numeric'
});
horizon.datatables.disable_buttons = function() {
$("table .table_actions").on("click", ".btn.disabled", function(event){
event.preventDefault();
event.stopPropagation();
});
};
horizon.datatables.update_footer_count = function (el, modifier) {
var $el = $(el),
$browser, $footer, row_count, footer_text_template, footer_text;
if (!modifier) {
modifier = 0;
}
// code paths for table or browser footers...
$browser = $el.closest("#browser_wrapper");
if ($browser.length) {
$footer = $browser.find('.tfoot span.content_table_count');
}
else {
$footer = $el.find('tfoot span.table_count');
}
row_count = $el.find('tbody tr:visible').length + modifier - $el.find('.empty').length;
footer_text_template = ngettext("Displaying %s item", "Displaying %s items", row_count);
footer_text = interpolate(footer_text_template, [row_count]);
$footer.text(footer_text);
return row_count;
};
horizon.datatables.add_no_results_row = function (table) {
// Add a "no results" row if there are no results.
template = horizon.templates.compiled_templates["#empty_row_template"];
if (!table.find("tbody tr:visible").length && typeof(template) !== "undefined") {
colspan = table.find("th[colspan]").attr('colspan');
params = {"colspan": colspan};
table.find("tbody").append(template.render(params));
}
};
horizon.datatables.remove_no_results_row = function (table) {
table.find("tr.empty").remove();
};
/*
* Fixes the striping of the table after filtering results.
**/
horizon.datatables.fix_row_striping = function (table) {
table.trigger('applyWidgetId', ['zebra']);
};
horizon.datatables.set_table_sorting = function (parent) {
// Function to initialize the tablesorter plugin strictly on sortable columns.
$(parent).find("table.datatable").each(function () {
var $table = $(this),
header_options = {};
// Disable if not sortable or has <= 1 item
if ($table.find('tbody tr').not('.empty').length > 1){
$table.find("thead th[class!='table_header']").each(function (i, val) {
$th = $(this);
if (!$th.hasClass('sortable')) {
header_options[i] = {sorter: false};
} else if ($th.data('type') == 'size'){
header_options[i] = {sorter: 'sizeSorter'};
} else if ($th.data('type') == 'ip'){
header_options[i] = {sorter: 'ipAddress'};
} else if ($th.data('type') == 'timesince'){
header_options[i] = {sorter: 'timesinceSorter'};
}
});
$table.tablesorter({
headers: header_options,
widgets: ['zebra'],
selectorHeaders: "thead th[class!='table_header']",
cancelSelection: false
});
}
});
};
horizon.datatables.add_table_checkboxes = function(parent) {
$(parent).find('table thead .multi_select_column').each(function(index, thead) {
if (!$(thead).find('.table-row-multi-select:checkbox').length &&
$(thead).parents('table').find('tbody .table-row-multi-select:checkbox').length) {
$(thead).append('<input type="checkbox">');
}
});
};
horizon.datatables.set_table_query_filter = function (parent) {
$(parent).find('table').each(function (index, elm) {
var input = $($(elm).find('div.table_search input')),
table_selector;
if (input.length > 0) {
// Disable server-side searcing if we have client-side searching since
// (for now) the client-side is actually superior. Server-side filtering
// remains as a noscript fallback.
// TODO(gabriel): figure out an overall strategy for making server-side
// filtering the preferred functional method.
input.on('keypress', function (evt) {
if (evt.keyCode === 13) {
return false;
}
});
input.next('button.btn-search').on('click keypress', function (evt) {
return false;
});
// Enable the client-side searching.
table_selector = 'table#' + $(elm).attr('id');
input.quicksearch(table_selector + ' tbody tr', {
'delay': 300,
'loader': 'span.loading',
'bind': 'keyup click',
'show': this.show,
'hide': this.hide,
onBefore: function () {
var table = $(table_selector);
horizon.datatables.remove_no_results_row(table);
},
onAfter: function () {
var template, table, colspan, params;
table = $(table_selector);
horizon.datatables.update_footer_count(table);
horizon.datatables.add_no_results_row(table);
horizon.datatables.fix_row_striping(table);
},
prepareQuery: function (val) {
return new RegExp(val, "i");
},
testQuery: function (query, txt, _row) {
return query.test($(_row).find('td:not(.hidden):not(.actions_column)').text());
}
});
}
});
};
horizon.datatables.set_table_fixed_filter = function (parent) {
$(parent).find('table.datatable').each(function (index, elm) {
$(elm).on('click', 'div.table_filter button', function(evt) {
var table = $(elm);
var category = $(this).val();
evt.preventDefault();
horizon.datatables.remove_no_results_row(table);
table.find('tbody tr').hide();
table.find('tbody tr.category-' + category).show();
horizon.datatables.update_footer_count(table);
horizon.datatables.add_no_results_row(table);
horizon.datatables.fix_row_striping(table);
});
$(elm).find('div.table_filter button').each(function (i, button) {
// Select the first non-empty category
if ($(button).text().indexOf(' (0)') == -1) {
$(button).addClass('active');
$(button).trigger('click');
return false;
}
});
});
};
horizon.addInitFunction(function() {
horizon.datatables.validate_button();
horizon.datatables.disable_buttons();
$('table.datatable').each(function (idx, el) {
horizon.datatables.update_footer_count($(el), 0);
});
// Bind the "select all" checkbox action.
$('div.table_wrapper, #modal_wrapper').on('click', 'table thead .multi_select_column .table-row-multi-select:checkbox', function(evt) {
var $this = $(this),
$table = $this.closest('table'),
is_checked = $this.prop('checked'),
checkboxes = $table.find('tbody .table-row-multi-select:visible:checkbox');
checkboxes.prop('checked', is_checked);
});
// Change "select all" checkbox behaviour while any checkbox is checked/unchecked.
$("div.table_wrapper, #modal_wrapper").on("click", 'table tbody .table-row-multi-select:checkbox', function (evt) {
var $table = $(this).closest('table');
var $multi_select_checkbox = $table.find('thead .multi_select_column .table-row-multi-select:checkbox');
var any_unchecked = $table.find("tbody .table-row-multi-select:checkbox").not(":checked");
$multi_select_checkbox.prop('checked', any_unchecked.length === 0);
});
// Enable dangerous buttons only if one or more checkbox is checked.
$("div.table_wrapper, #modal_wrapper").on("click", '.table-row-multi-select:checkbox', function (evt) {
var $form = $(this).closest("form");
var any_checked = $form.find("tbody .table-row-multi-select:checkbox").is(":checked");
if(any_checked) {
$form.find(".table_actions button.btn-danger").removeClass("disabled");
}else {
$form.find(".table_actions button.btn-danger").addClass("disabled");
}
});
// Trigger run-once setup scripts for tables.
horizon.datatables.add_table_checkboxes($('body'));
horizon.datatables.set_table_sorting($('body'));
horizon.datatables.set_table_query_filter($('body'));
horizon.datatables.set_table_fixed_filter($('body'));
// Also apply on tables in modal views.
horizon.modals.addModalInitFunction(horizon.datatables.add_table_checkboxes);
horizon.modals.addModalInitFunction(horizon.datatables.set_table_sorting);
horizon.modals.addModalInitFunction(horizon.datatables.set_table_query_filter);
horizon.modals.addModalInitFunction(horizon.datatables.set_table_fixed_filter);
horizon.datatables.update();
});