/* Namespace for core functionality related to DataTables. */ horizon.datatables = { update: function () { var $rows_to_update = $('tr.warning.ajax-update'); var $table = $rows_to_update.closest('table'); var interval = $rows_to_update.attr('data-update-interval'); var decay_constant = $table.attr('decay_constant'); var requests = []; // do nothing if there are no rows to update. if($rows_to_update.length <= 0) { return; } // 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; } $rows_to_update.each(function() { var $row = $(this); var $table = $row.closest('table.datatable'); requests.push( horizon.ajax.queue({ url: $row.attr('data-update-url'), error: function (jqXHR) { 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 row_count, colspan, template, params; // 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('.table_column_header th').length; template = horizon.templates.compiled_templates["#empty_row_template"]; params = { "colspan": colspan, no_items_label: gettext("No items to display.") }; var empty_row = template.render(params); $row.replaceWith(empty_row); } else { $row.remove(); } // Reset tablesorter's data cache. $table.trigger("update"); // Enable launch action if quota is not exceeded horizon.datatables.update_actions(); break; default: console.log(gettext("An error occurred while updating.")); $row.removeClass("ajax-update"); $row.find("i.ajax-updating").remove(); break; } }, success: function (data) { var $new_row = $(data); if ($new_row.hasClass('warning')) { var $container = $(document.createElement('div')) .addClass('progress-text horizon-loading-bar'); var $progress = $(document.createElement('div')) .addClass('progress progress-striped active') .appendTo($container); $(document.createElement('div')) .addClass('progress-bar') .appendTo($progress); // if action/confirm is required, show progress-bar with "?" // icon to indicate user action is required if ($new_row.find('.btn-action-required').length > 0) { $(document.createElement('span')) .addClass('fa fa-question-circle progress-bar-text') .appendTo($container); } $new_row.find("td.warning:last").prepend($container); } // Only replace row if the html content has changed if($new_row.html() !== $row.html()) { // Directly accessing the checked property of the element // is MUCH faster than using jQuery's helper method var $checkbox = $row.find('.table-row-multi-select'); if($checkbox.length && $checkbox[0].checked) { // Preserve the checkbox if it's already clicked $new_row.find('.table-row-multi-select').prop('checked', true); } $row.replaceWith($new_row); // TODO(matt-borland, tsufiev): ideally we should solve the // problem with not-working angular actions in a content added // by jQuery via replacing jQuery insert with Angular insert. // Should address this in Newton release recompileAngularContent($table); // Reset tablesorter's data cache. $table.trigger("update"); // Reset decay constant. $table.removeAttr('decay_constant'); // Check that quicksearch is enabled for this table // Reset quicksearch's data cache. if ($table.attr('id') in horizon.datatables.qs) { horizon.datatables.qs[$table.attr('id')].cache(); } } }, complete: function () { // Revalidate the button check for the updated table horizon.datatables.validate_button(); } }) ); }); $.when.apply($, requests).always(function() { decay_constant = decay_constant || 0; decay_constant++; $table.attr('decay_constant', decay_constant); var 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); }); }, update_actions: function() { var $actions_to_update = $('.btn-launch.ajax-update, .btn-create.ajax-update'); $actions_to_update.each(function() { var $action = $(this); horizon.ajax.queue({ url: $action.attr('data-update-url'), error: function () { console.log(gettext("An error occurred while updating.")); }, success: function (data) { var $new_action = $(data); // Only replace row if the html content has changed if($new_action.html() != $action.html()) { $action.replaceWith($new_action); } } }); }); }, validate_button: function ($form, disable_button) { // Enable or disable table batch action buttons based on row selection. $form = $form || $(".table_wrapper > form"); $form.each(function () { var $this = $(this); var $action_buttons = $this.find('.table_actions button[data-batch-action="true"]'); if (disable_button === undefined) { disable_button = $this.find(".table-row-multi-select").filter(":checked").length == 0; } $action_buttons.toggleClass("disabled", disable_button); }); }, initialize_checkboxes_behavior: function() { // Bind the "select all" checkbox action. $('.table_wrapper, #modal_wrapper') .on('change', '.table-row-multi-select', function() { var $this = $(this); var $table = $this.closest('table'); var is_checked = $this.prop('checked'); if ($this.hasClass('multi-select-header')) { // Only select / deselect the visible rows $table.find('tbody tr:visible .table-row-multi-select') .prop('checked', is_checked); } else { // Find the master checkbox var $multi_select_checkbox = $table.find('.multi-select-header'); // Determine if there are any unchecked checkboxes in the table var $checkboxes = $table.find('tbody .table-row-multi-select'); var not_checked = $checkboxes.not(':checked').length; is_checked = $checkboxes.length != not_checked; // If there are none, then check the master checkbox $multi_select_checkbox.prop('checked', not_checked == 0); } // Pass in whether it should be visible, no point in doing this twice horizon.datatables.validate_button($this.closest('form'), !is_checked); }); }, initialize_table_tooltips: function() { $('div.table_wrapper').tooltip({selector: '[data-toggle="tooltip"]', container: 'body'}); }, disable_actions_on_submit: function($form) { // This applies changes to the table form when a user takes an action that // submits the form. It relies on the form being re-rendered after the // submit is completed to remove these changes. $form = $form || $(".table_wrapper > form"); $form.on("submit", function () { var $this = $(this); // Add the 'submitted' flag to the form so the row update interval knows // not to update the row and therefore re-enable the actions that we are // disabling here. $this.attr('data-submitted', 'true'); // Disable row action buttons. This prevents multiple form submission. $this.find('td.actions_column button[type="submit"]').addClass("disabled"); // Use CSS to update the cursor so it's very clear that an action is // in progress. $this.addClass('wait'); }); } }; /* Generates a confirmation modal dialog for the given action. */ horizon.datatables.confirm = function(action) { var $action = $(action); if ($action.hasClass("disabled")) { return; } var $modal_parent = $action.closest('.modal'); var name_array = []; var action_string = $action.text(); var help_text = $action.attr("help_text") || ""; var name_string = ""; // Add the display name defined by table.get_object_display(datum) var $closest_table = $action.closest("table"); // Check if data-display attribute is available var $data_display = $closest_table.find('tr[data-display]'); if ($data_display.length > 0) { var $actions_div = $action.closest("div"); if ($actions_div.hasClass("table_actions") || $actions_div.hasClass("table_actions_menu")) { // One or more checkboxes selected $data_display.has(".table-row-multi-select:checked").each(function() { name_array.push(" \"" + $(this).attr("data-display") + "\""); }); name_string = name_array.join(", "); } else { // If no checkbox is selected name_string = " \"" + $action.closest("tr").attr("data-display") + "\""; name_array = [name_string]; } } var title = interpolate(gettext("Confirm %s"), [action_string]); // compose the action string using a template that can be overridden var template = horizon.templates.compiled_templates["#confirm_modal"], params = { selection: name_string, selection_list: name_array, help: help_text }; var body; try { body = $(template.render(params)).html(); } catch (e) { body = name_string + gettext("Please confirm your selection. ") + help_text; } var 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 () { var $form = $action.closest('form'); var el = document.createElement("input"); el.type = 'hidden'; el.name = $action.attr('name'); el.value = $action.attr('value'); $form .append(el) .submit(); modal.modal('hide'); horizon.modals.modal_spinner(gettext("Working")); return false; }); return modal; }; $.tablesorter.addParser({ // set a unique id id: 'sizeSorter', is: function() { // 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() { // Not an auto-detected parser return false; }, // compare int values format: function(s, table, cell) { return $(cell).find('span').data('seconds'); }, type: 'numeric' }); $.tablesorter.addParser({ id: "timestampSorter", is: function() { return false; }, format: function(s) { s = s.replace(/\-/g, " ").replace(/:/g, " "); s = s.replace("T", " ").replace("Z", " "); s = s.split(" "); return new Date(s[0], s[1], s[2], s[3], s[4], s[5]).getTime(); }, type: "numeric" }); $.tablesorter.addParser({ id: 'IPv4Address', is: function(s, table, cell) { // The first arg to this function is a string of all the cell's innertext smashed together // with no delimiters, so to make this work with the Instances and Ports tables where the // IP address cell content is an unordered list we need to check the content of the first //