ManageIQ/manageiq-ui-classic

View on GitHub
app/javascript/oldjs/miq_application.js

Summary

Maintainability
F
1 wk
Test Coverage
/* global add_flash getChartColumnDataValues getChartFormatedValue miqBrowserDetect miqExpressionPrefill miqFlashLater miqFlashSaved miqGridCheckAll miqGridGetCheckedRows miqMenu miqTreeObject miqValueStylePrefill recalculateChartYAxisLabels */

// MIQ specific JS functions

// Things to be done on page loads
require('./miq_global.js');

window.miqOnLoad = function() {
  // controller to be used in url in miqDropComplete method
  ManageIQ.widget.dashboardUrl = 'dashboard/widget_dd_done';

  // Initialize the dashboard column sortables
  if (miqDomElementExists('col1')) {
    miqInitDashboardCols();
  }

  // Track the mouse coordinates for popup menus
  $(document).mousemove((e) => {
    ManageIQ.mouse.x = e.pageX;
    ManageIQ.mouse.y = e.pageY;
  });

  miqBuildCalendar();

  // Initialize the dashboard widget pulldown
  if (miqDomElementExists('widget_select_div')) {
    miqInitWidgetPulldown();
  }

  // Refresh the myCodeMirror editor
  if (ManageIQ.editor !== null) {
    ManageIQ.editor.refresh();
  }

  // Run MIQ after onload code if present
  if (ManageIQ.afterOnload) {
    const data = ManageIQ.afterOnload;

    if (typeof data === 'object' && data.action) {
      data.action.call();
    }
  }

  // Focus on search box, if it's there and allows focus
  if (miqDomElementExists('search_text')) {
    try {
      $('#search_text').focus();
    } catch (_e) {}
  }

  miqInitAccordions();
  miqInitMainContent();
  miqFlashSaved();
};

window.miqPrepRightCellForm = function(tree) {
  if (miqDomElementExists('adv_searchbox_div')) {
    $('#adv_searchbox_div').hide();
  }
  $('#toolbar').hide();
  miqTreeObject(tree).disableAll({ silent: true, keepState: true });
  miqDimDiv(`${tree}_div`, true);
};

window.miqCalendarDateConversion = function(server_offset) {
  return moment().utcOffset(Number(server_offset) / 60).toDate();
};

// The expressions variable is used only in the following two functions
// TODO: Remove this scope wrapper after the expressions were moved to Ruby
(function() {
  // TODO: This probably should be moved into the Ruby code
  const expressions = {
    boolean:     __('true/false'),
    bytes:       __('Number (Bytes)'),
    date:        __('Date'),
    datetime:    __('Date/Time'),
    decimal:     __('Integer'),
    fixnum:      __('Integer'),
    float:       __('Number'),
    gigabytes:   __('Number (GB)'),
    integer:     __('Integer'),
    kbps:        __('KBps'),
    kilobytes:   __('Number (kB)'),
    megabytes:   __('Number (MB)'),
    mhz:         __('MHz'),
    mhz_avg:     __('MHz'),
    numeric_set: __('Number List'),
    percent:     __('Percent'),
    regex:       __('Text (REGEX)'),
    ruby:        __('Ruby Script'),
    string:      __('Text'),
    string_set:  __('String List'),
    text:        __('Text'),
  };

  // Prefill expression value text entry fields when blank
  window.miqExpressionPrefill = function(expEditor, noPrefillCount) {
    let title;

    if ($('#chosen_value[type=text]').length) {
      $('#chosen_value').prop('placeholder', expressions[expEditor.first.type]);
      $('#chosen_value').prop('title', expEditor.first.title);
      $('#chosen_value').prop('alt', expEditor.first.title);
    }
    if ($('#chosen_cvalue[type=text]').length) {
      $('#chosen_cvalue').prop('placeholder', expressions[expEditor.second.type]);
      $('#chosen_cvalue').prop('title', expEditor.second.title);
      $('#chosen_cvalue').prop('alt', expEditor.second.title);
    }
    if ($('#chosen_regkey[type=text]').length) {
      title = __('Registry Key');
      $('#chosen_regkey').prop('placeholder', expressions.string);
      $('#chosen_regkey').prop('title', title);
      $('#chosen_regkey').prop('alt', title);
    }
    if ($('#chosen_regval[type=text]').length) {
      title = __('Registry Key Value');
      $('#chosen_regval').prop('placeholder', expressions.string);
      $('#chosen_regval').prop('title', title);
      $('#chosen_regval').prop('alt', title);
    }
    if ($('#miq_date_1_0[type=text]').length) {
      $('#miq_date_1_0').prop('placeholder', expressions[expEditor.first.type]);
      $('#miq_date_1_0').prop('title', expEditor.first.title);
      $('#miq_date_1_0').prop('alt', expEditor.first.title);
    }
    if ($('#miq_date_1_1[type=text]').length) {
      $('#miq_date_1_1').prop('placeholder', expressions[expEditor.first.type]);
      $('#miq_date_1_1').prop('title', expEditor.first.title);
      $('#miq_date_1_1').prop('alt', expEditor.first.title);
    }
    if ($('#miq_date_2_0[type=text]').length) {
      $('#miq_date_2_0').prop('placeholder', expressions[expEditor.second.type]);
      $('#miq_date_2_0').prop('title', expEditor.second.title);
      $('#miq_date_2_0').prop('alt', expEditor.second.title);
    }
    if ($('#miq_date_2_1[type=text]').length) {
      $('#miq_date_2_1').prop('placeholder', expressions[expEditor.second.type]);
      $('#miq_date_2_1').prop('title', expEditor.second.title);
      $('#miq_date_2_1').prop('alt', expEditor.second.title);
    }
    if (noPrefillCount) {
      expEditor.prefillCount = 0;
      setTimeout(() => {
        miqExpressionPrefill(expEditor, false);
      }, 200);
    } else {
      if (++expEditor.prefillCount > 100) {
        expEditor.prefillCount = 0;
      }
      setTimeout(() => {
        miqExpressionPrefill(expEditor, false);
      }, 200);
    }
  };

  // Prefill report editor style value text entry fields when blank
  // (written more generic for reuse, just have to build
  // the ManageIQ.reportEditor.valueStyles hash)
  window.miqValueStylePrefill = function(count) {
    let found = false;

    for (const field in ManageIQ.reportEditor.valueStyles) {
      if ($(field).length) {
        $(field).prop('placeholder', expressions[ManageIQ.reportEditor.valueStyles[field]]);
        found = true;
      }
    }
    if (found) {
      if (typeof count === 'undefined') {
        ManageIQ.reportEditor.prefillCount = 0;
        setTimeout(() => {
          miqValueStylePrefill(ManageIQ.reportEditor.prefillCount);
        }, 200);
      } else if (count === ManageIQ.reportEditor.prefillCount) {
        if (++ManageIQ.reportEditor.prefillCount > 100) {
          ManageIQ.reportEditor.prefillCount = 0;
        }
        setTimeout(() => {
          miqValueStylePrefill(ManageIQ.reportEditor.prefillCount);
        }, 200);
      }
    }
  };
}());

// Get user's time zone offset
window.miqGetTZO = function() {
  if (miqDomElementExists('user_TZO')) {
    $('#user_TZO').val(moment().utcOffset() / 60);
  }
};

// Get user's browswer info
window.miqGetBrowserInfo = function() {
  const bd = miqBrowserDetect();

  if (miqDomElementExists('browser_name')) {
    $('#browser_name').val(bd.browser);
  }
  if (miqDomElementExists('browser_version')) {
    $('#browser_version').val(bd.version);
  }
  if (miqDomElementExists('browser_os')) {
    $('#browser_os').val(bd.OS);
  }
};

// Turn highlight on or off
window.miqHighlight = function(elem, status) {
  if ($(elem).length) {
    return;
  }

  if (status) {
    $(elem).addClass('active');
  } else {
    $(elem).removeClass('active');
  }
};

// Turn on activity indicator
window.miqSparkle = function(status) {
  if (status) {
    // Make sure an ajax request is active before sparkling
    if ($.active) {
      miqSparkleOn();
    }
  } else if ($.active < 2) {
    // Make sure all but 1 ajax request is done
    miqSparkleOff();
  }
};

window.miqSparkleOn = function() {
  if (miqDomElementExists('advsearchModal')
      && ($('#advsearchModal').hasClass('modal fade in'))) {
    if (miqDomElementExists('searching_spinner_center')) {
      miqSearchSpinner(true);
    }
    miqSpinner(false);
    if (miqDomElementExists('notification')) {
      $('#notification').hide();
    }
  } else {
    if (miqDomElementExists('notification')) {
      $('#notification').show();
    }
    miqSpinner(true);
  }
};

window.miqSparkleOff = function() {
  // this prevents ajax requests on GTL screens from disabling the spinner too early
  if (ManageIQ.gtl.loading) {
    return;
  }

  miqSpinner(false);
  if (miqDomElementExists('searching_spinner_center')) {
    miqSearchSpinner(false);
  }
  if (miqDomElementExists('notification')) {
    $('#notification').hide();
  }
  if (miqDomElementExists('rep_notification')) {
    $('#rep_notification').hide();
  }
};

// dim/un-dim a div: pass divname and status (true to dim, false to un-dim)
window.miqDimDiv = function(divname, status) {
  if ($(divname).length) {
    if (status) {
      $(divname).addClass('dimmed');
    } else {
      $(divname).removeClass('dimmed');
    }
  }
};

// Check for changes and prompt
window.miqCheckForChanges = function() {
  let type = 'old'; // 'old' | 'angular' | 'tagging' | 'react'
  let dirty = false;
  const ignore = miqDomElementExists('ignore_form_changes');

  if (ManageIQ.angular.scope && ManageIQ.angular.scope.angularForm) {
    type = 'angular';
  }

  if (ManageIQ.redux.store.getState().FormButtons && ManageIQ.redux.store.getState().FormButtons.in_a_form) {
    type = 'react';
  }

  // FIXME: this should not be a special case here, it should use the react reducer
  if (ManageIQ.redux.store.getState().tagging) {
    type = 'tagging';
  }

  switch (type) {
    case 'old':
      dirty = $('#buttons_on').is(':visible');
      break;

    case 'angular':
      dirty = ManageIQ.angular.scope.angularForm.$dirty;
      break;

    case 'react':
      dirty = !ManageIQ.redux.store.getState().FormButtons.pristine;
      break;

    case 'tagging':
      var taggingStore = ManageIQ.redux.store.getState().tagging;
      dirty = !_.isEqual(taggingStore.appState.assignedTags, taggingStore.initialState.assignedTags);
      break;
  }

  if (dirty && !ignore) {
    return confirm(__('Abandon changes?'));
  }

  // not in a form => abandon anything
  return true;
};

// Hide/show form buttons
window.miqButtons = function(h_or_s, prefix) {
  $('#flash_msg_div').hide();

  const on = h_or_s === 'show' ? 'on' : 'off';
  const off = h_or_s === 'show' ? 'off' : 'on';

  const buttonPrefix = (typeof prefix === 'undefined' || prefix === '') ? '' : (`${prefix}_`);

  $(`#${buttonPrefix}buttons_${on}`).show();
  $(`#${buttonPrefix}buttons_${off}`).hide();
};

// Hide/show form validate buttons
window.miqValidateButtons = function(h_or_s, prefix) {
  const buttonPrefix = prefix || '';
  const buttonsOnId = `${buttonPrefix}validate_buttons_on`;
  const buttonsOffId = `${buttonPrefix}validate_buttons_off`;

  $('#flash_msg_div').hide();

  if (h_or_s === 'show') {
    if (miqDomElementExists(buttonsOnId)) {
      $(`#${buttonsOnId}`).show();
    }
    if (miqDomElementExists(buttonsOffId)) {
      $(`#${buttonsOffId}`).hide();
    }
  } else {
    if (miqDomElementExists(buttonsOffId)) {
      $(`#${buttonsOffId}`).show();
    }
    if (miqDomElementExists(buttonsOnId)) {
      $(`#${buttonsOnId}`).hide();
    }
  }
};

// update all checkboxes on a form when the masterToggle checkbox is changed
// parms: button_div=<id of div with buttons to update>
window.miqUpdateAllCheckboxes = function(button_div) {
  if (!miqDomElementExists('masterToggle')) {
    return;
  }

  const state = $('#masterToggle').prop('checked');

  if (ManageIQ.grids.gtl_list_grid) {
    miqGridCheckAll(state);
    const crows = miqGridGetCheckedRows();

    ManageIQ.gridChecks = crows;
    miqSetButtons(crows.length, button_div);
  } else if ($('input.listcheckbox').length) {
    // No list_grid on the screen
    const cbs = $('input.listcheckbox')
      .prop('checked', state)
      .trigger('change');

    miqUpdateButtons(cbs[0], button_div);
  } else if ($("input[id^='storage_cb']").length) {
    // to handle check/uncheck all for C&U collection
    $("input[id^='storage_cb']")
      .prop('checked', state)
      .trigger('change');
    miqJqueryRequest(miqPassFields(
      '/configuration/form_field_changed',
      { storage_cb_all: state }
    ));
  }
};

// Update buttons based on number of checkboxes that are checked
// parms: obj=<checkbox element>, button_div=<id of div with buttons to update>
window.miqUpdateButtons = function(obj, button_div) {
  let count = 0;

  sendDataWithRx({ rowSelect: obj });

  if (typeof obj.id !== 'undefined') {
    $("input[id^='check_']").each(function() {
      if (this.checked && !this.disabled) {
        count++;
      }
      if (count > 1) {
        return false;
      }
    });
  // Check for number object, as passed from snapshot tree
  } else if (typeof obj === 'number') {
    count = 1;
  }
  miqSetButtons(count, button_div);
};

window.miqSetToolbarCount = function(count) {
  sendDataWithRx({
    eventType: 'updateToolbarCount',
    countSelected: count,
  });
};

// Set the buttons in a div based on the count of checked items passed in
window.miqSetButtons = function(count, button_div) {
  if (button_div.match('_tb$') && count === 0) {
    // FIXME: this should be happening regardless of `count === 0`
    // ..but that needs more refactoring around miqUpdateAllCheckboxes, miqUpdateButtons, etc.
    miqSetToolbarCount(count);
    return;
  }

  if (miqDomElementExists(button_div) && button_div.match('_buttons$')) { // Handle buttons that are not part of miq toolbars
    if (count === 0) {
      $(`#${button_div} button[id$=on_1]`).prop('disabled', true);
    } else {
      $(`#${button_div} button[id$=on_1]`).prop('disabled', false);
    }
  }
};

// go to the specified URL when a table cell is clicked
window.DoNav = function(theUrl) {
  document.location.href = theUrl;
};

// Routines to get the size of the window
window.miqResetSizeTimer = function() {
  const height = window.innerHeight;
  const offset = 427;
  let h = height - offset;

  if (h < 200) {
    h = 200;
  }

  // Adjust certain elements, if present
  if (miqDomElementExists('list_grid')) {
    $('#list_grid').css({ height: `${h}px` });
  } else if (miqDomElementExists('logview')) {
    $('#logview').css({ height: `${h}px` });
  }
};

// Pass fields to server given a URL and fields in name/value pairs
window.miqPassFields = function(url, args) {
  return `${url}?${$.param(args)}`;
};

window.miqChartLinkData = function(col, row, value, category, series, id, message) {
  // Create the context menu
  if (typeof miqMenu !== 'undefined') {
    miqMenu.hideContextMenu();
  }
  if (category.indexOf('<Other(') === 0) {
    // No menus for <Other> category
    return;
  }
  // Added delay before showing menus to get it work in version 3.5
  setTimeout(() => {
    miqBuildChartMenu(col, row, value, category, series, id, message);
  }, 250);
};

window.miqBuildChartMenu = function(col, row, _value, category, series, id, _message) {
  const set = id.split('_')[1]; // Get the chart set
  const idx = id.split('_')[2]; // Get the chart index
  const chart_data = ManageIQ.charts.chartData[set];
  const chart_el_id = id.replace(/^miq_/, 'miq_chart_');
  const chartmenu_el_id = id.replace(/^miq_/, 'miq_chartmenu_');

  if (chart_data[idx].menu != null && chart_data[idx].menu.length) {
    const rowcolidx = `_${row}-${col}-${idx}`;

    for (let i = 0; i < chart_data[idx].menu.length; i++) {
      const menu_id = chart_data[idx].menu[i].split(':')[0] + rowcolidx;
      const pid = menu_id.split('-')[0];

      if ($(`#${chartmenu_el_id}`).find(`#${pid}`).length === 0) {
        $(`#${chartmenu_el_id}`).append(`${"<li class='dropdown-submenu'>"
          + "<a tabindex='-1' href='#'>"}${pid}</a>`
          + `<ul id='' + pid + '' class='dropdown-menu'></ul></li>`);
      }

      let menu_title = chart_data[idx].menu[i].split(':')[1];
      menu_title = menu_title.replace('<series>', series);
      menu_title = menu_title.replace('<category>', category);
      $(`#${pid}`).append(`<li><a id='${menu_id
      }' href='#' onclick='miqChartMenuClick(this.id)'>${menu_title}</a></li>`);
    }

    $(`#${chartmenu_el_id}`).css({ left: ManageIQ.mouse.x, top: ManageIQ.mouse.y });
    $(`#${chartmenu_el_id}`).dropdown('toggle');
    $(`#${chart_el_id}`).find('.overlay').show();
  }
};

window.miqChartBindEvents = function() {
  if (ManageIQ.charts.provider === 'c3') {
    // noop
  }
};

window.miqBuildChartMenuEx = function(col, row, _value, category, series, chart_set, chart_index) {
  const chart_data = ManageIQ.charts.chartData[chart_set];
  const chart_el = $(`#miq_chart_parent_${chart_set}_${chart_index}`);
  const chartmenu_el = $(`#miq_chartmenu_${chart_set}_${chart_index}`);
  chartmenu_el.empty();

  if (chart_data[chart_index].menu != null && chart_data[chart_index].menu.length) {
    const row_col_chart_index = {
      row,
      column: col,
      chart_index,
    };
    for (let i = 0; i < chart_data[chart_index].menu.length; i++) {
      row_col_chart_index.chart_name = chart_data[chart_index].menu[i].split(':')[0];
      const pid = row_col_chart_index.chart_name.split('-')[0];
      if (chartmenu_el.find(`#${pid}`).length === 0) {
        chartmenu_el.append(`${"<li class='dropdown-submenu'>"
          + "<a tabindex='-1' href='#'>"}${pid}</a>`
          + `<ul id='${pid}' class='dropdown-menu'></ul></li>`);
      }

      let menu_title = chart_data[chart_index].menu[i].split(':')[1];
      menu_title = menu_title.replace('<series>', series);
      menu_title = menu_title.replace('<category>', category);
      $(`#${pid}`).append(`<li><a id='${btoa(JSON.stringify(row_col_chart_index))}' href='#' onclick='miqChartMenuClick(this.id)'>${menu_title}</a></li>`);
    }

    // chart menu has min-width: 160 a has two levels
    const x_position = (ManageIQ.mouse.x > $(window).width() - 320) ? $(window).width() - 320 : ManageIQ.mouse.x;
    chartmenu_el.css({ left: x_position, top: ManageIQ.mouse.y });
    chartmenu_el.dropdown('toggle');
    chart_el.find('.overlay').show();
  }
};

// Handle chart context menu clicks
window.miqChartMenuClick = function(itemId) {
  if (miqDomElementExists('menu_div')) {
    $('#menu_div').hide();
  }
  if (itemId !== 'cancel') {
    miqAsyncAjax(`?menu_click=${itemId}`);
  }
};

window.miqRESTAjaxButton = function(url, button, dataType, data) {
  const form = $(button).parents('form:first')[0];
  if (form) {
    $(form).submit((e) => {
      e.preventDefault();
      return false;
    });
    let formData;
    if (data) {
      formData = data;
    } else {
      formData = $(form).serialize();
    }
    return miqJqueryRequest(url, {
      beforeSend: true,
      complete: true,
      data: formData,
      dataType,
    });
  }

  miqAjaxButton(url, true);
};

// Handle an ajax form button press (i.e. Submit) by starting the spinning Q,
// then waiting for .7 seconds for observers to finish
window.miqAjaxButton = function(url, serialize_fields, options) {
  if (typeof serialize_fields === 'undefined') {
    serialize_fields = false;
  }
  if (miqDomElementExists('notification')) {
    $('#notification').show();
  }

  setTimeout(() => {
    miqAjaxButtonSend(url, serialize_fields, options);
  }, 700);
};

// Send ajax url after any outstanding ajax requests, wait longer if needed
window.miqAjaxButtonSend = function(url, serialize_fields, options) {
  if ($.active) {
    setTimeout(() => {
      miqAjaxButtonSend(url, serialize_fields, options);
    }, 700);
  } else {
    miqAjax(url, serialize_fields, options);
  }
};

// Function to generate an Ajax request
window.miqAjax = function(url, serialize_fields, options) {
  let data;

  if (serialize_fields === true) {
    data = miqSerializeForm('form_div');
  } else if (serialize_fields) { // object or possibly FormData
    data = serialize_fields;
  }

  const defaultOptions = {
    beforeSend: true,
    complete: true,
  };

  // miqAjaxButton with { observeQueue: true } will queue the request instead of sending directly
  let requestFn = miqJqueryRequest;
  if (options && options.observeQueue) {
    delete options.observeQueue;
    requestFn = miqObserveRequest;
  }

  return requestFn(url, _.extend(defaultOptions, options || {}, { data }))
    .catch((err) => {
      add_flash(__('Error requesting data from server'), 'error');
      console.log(err);
      return Promise.reject(err);
    });
};

// Function to generate an Ajax request for EVM async processing
window.miqAsyncAjax = function(url) {
  miqJqueryRequest(url, { beforeSend: true });
};

ManageIQ.oneTransition.oneTrans = 0;

// Function to generate an Ajax request, but only once for a drawn screen
window.miqSendOneTrans = function(url, observe) {
  if (ManageIQ.oneTransition.oneTrans) {
    return;
  }

  ManageIQ.oneTransition.oneTrans = 1;

  if (observe && observe.observe) {
    return miqObserveRequest(url, { done: observe.done });
  }
  return miqJqueryRequest(url);
};

// Check max length on a text area and set remaining chars
window.miqCheckMaxLength = function(obj) {
  const ml = obj.getAttribute ? parseInt(obj.getAttribute('maxlength'), 10) : '';
  const counter = obj.getAttribute ? obj.getAttribute('counter') : '';

  if (obj.getAttribute && obj.value.length > ml) {
    obj.value = obj.value.substring(0, ml);
  }
  if (counter) {
    $(`#${counter}`).text(obj.value.length);
  }
};

// Check for enter key pressed
window.miqEnterPressed = function(e) {
  let keycode;

  if (window.event) {
    keycode = window.event.keyCode;
  } else if (e) {
    keycode = e.which;
  } else {
    return false;
  }
  return (keycode === 13);
};

// Send login authentication via ajax
window.miqAjaxAuth = function(url) {
  miqEnableLoginFields(false);

  return miqJqueryRequest(url || '/dashboard/authenticate', {
    beforeSend: true,
    data: miqSerializeForm('login_div'),
  }).then(null, (err) => {
    // HTTP failures only, authentication failures come as 200, with their own javascript
    let message = __('Incorrect username or password');
    if (err && err.status && err.status !== 200 && err.statusText) {
      message = `${__('Login failed:')} ${err.statusText}`;
    }

    clearFlash();
    add_flash(message, 'error', { id: 'auth_failed' });

    miqAjaxAuthFail();
    miqSparkleOff();
  });
};

window.miqAjaxAuthFail = function() {
  miqClearLoginFields();
  miqEnableLoginFields(true);
};

// Send SSO login authentication via ajax
window.miqAjaxAuthSso = function(url) {
  miqEnableLoginFields(false);
  miqSparkleOn();

  miqJqueryRequest(url || '/dashboard/kerberos_authenticate', {
    beforeSend: true,
  });
};

// Send External Authentication via ajax
window.miqAjaxExtAuth = function(url) {
  miqEnableLoginFields(false);
  miqSparkleOn();

  miqJqueryRequest(url || '/dashboard/external_authenticate', {
    beforeSend: true,
    data: miqSerializeForm('login_div'),
  });
};

window.miqEnableLoginFields = function(enabled) {
  $('#user_name').prop('readonly', !enabled);
  $('#user_password').prop('readonly', !enabled);
  if (miqDomElementExists('user_new_password')) {
    $('#user_new_password').prop('readonly', !enabled);
  }
  if (miqDomElementExists('user_verify_password')) {
    $('#user_verify_password').prop('readonly', !enabled);
  }
};

// reset form fields on login failure
window.miqClearLoginFields = function() {
  $('#user_name').val('').focus();
  $('#user_password').val('');
};

// Initialize dashboard column jQuery sortables
window.miqInitDashboardCols = function() {
  if (miqDomElementExists('col1')) {
    $('#col1').sortable({
      connectWith: '#col2',
      handle: '.sortable-handle',
      helper: 'clone',
      placeholder: 'sortable-placeholder',
      forcePlaceholderSize: true,
    });
    $('#col1').off('sortupdate');
    $('#col1').on('sortupdate', miqDropComplete);
  }
  if (miqDomElementExists('col2')) {
    $('#col2').sortable({
      connectWith: '#col1',
      handle: '.sortable-handle',
      helper: 'clone',
      placeholder: 'sortable-placeholder',
      forcePlaceholderSize: true,
    });
    $('#col2').off('sortupdate');
    $('#col2').on('sortupdate', miqDropComplete);
  }
};

// Send the updated sortable order after jQuery drag/drop
window.miqDropComplete = function(_event, _ui) {
  const el = $(this);
  let url = `/${ManageIQ.widget.dashboardUrl}?${el.sortable(
    'serialize', { key: `${el.attr('id')}[]` }
  ).toString()}`;
  // Adding id of record being edited to be used by load_edit call
  if (ManageIQ.record.recordId !== null) {
    url += `&id=${ManageIQ.record.recordId}`;
  }
  miqJqueryRequest(url);
};

// Attach a calendar control to all text boxes that start with miq_date_
window.miqBuildCalendar = function() {
  // Get all of the input boxes with ids starting with "miq_date_"
  const all = $('input[id^=miq_date_]');

  all.each(function() {
    const element = $(this);
    let observeDateBackup = null;

    if (!element.data('datepicker')) {
      observeDateBackup = ManageIQ.observeDate;
      ManageIQ.observeDate = function() {};
      element.datepicker();
    }

    const startDate = element.attr('data_date_start');
    if (startDate) {
      element.datepicker('setStartDate', new Date(startDate));
    } else if (ManageIQ.calendar.calDateFrom) {
      element.datepicker('setStartDate', ManageIQ.calendar.calDateFrom);
    }

    if (ManageIQ.calendar.calDateTo) {
      element.datepicker('setEndDate', ManageIQ.calendar.calDateTo);
    }

    if (ManageIQ.calendar.calSkipDays) {
      element.datepicker('setDaysOfWeekDisabled', ManageIQ.calendar.calSkipDays);
    }

    if (observeDateBackup != null) {
      ManageIQ.observeDate = observeDateBackup;
    }
  });
};

window.miqSendDateRequest = function(el) {
  const parms = $.parseJSON(el.attr('data-miq_observe_date'));
  const { url } = parms;
  //  tack on the id and value to the URL
  const urlstring = `${url}?${el.prop('id')}=${el.val()}`;

  const options = {
    beforeSend: !!el.attr('data-miq_sparkle_on'),
    complete: !!el.attr('data-miq_sparkle_off'),
  };

  return miqObserveRequest(urlstring, options);
};

// common function to pass ajax request to server
window.miqAjaxRequest = function(itemId, path) {
  if (miqCheckForChanges()) {
    miqJqueryRequest(
      miqPassFields(path, { id: itemId }),
      { beforeSend: true, complete: true }
    );
    return true;
  }
  return false;
};

// Handle an element onclick to open href in a new window with optional confirmation
window.miqClickAndPop = function(el) {
  const conmsg = el.getAttribute('data-miq_confirm');

  if (conmsg == null || confirm(conmsg)) {
    window.open(el.href);
  }
  // no default browser reaction for onclick
  return false;
};

window.miq_tabs_init = function(id, url, parms) {
  $(`${id} > ul.nav-tabs a[data-toggle="tab"]`).on('show.bs.tab', (e) => {
    if ($(e.target).parent().hasClass('disabled')) {
      e.preventDefault();
      return false;
    } if (typeof url !== 'undefined') {
      // Load remote tab if an URL is specified
      const currTabTarget = $(e.target).attr('href').substring(1);
      const urlParams = _.reduce(parms || [], (sum, value, key) => `${sum}&${key}=${value}`, `?tab_id=${currTabTarget}`);

      const editMode = typeof parms !== 'undefined' && parms.edit_mode !== 'undefined' && parms.edit_mode === 'true';
      if (editMode || miqCheckForChanges()) {
        miqObserveRequest(url + urlParams, { beforeSend: true })
          .catch((err) => {
            add_flash(__('Error requesting data from server'), 'error');
            console.error(err);
            return Promise.reject(err);
          });
      } else {
        return false;
      }
    }
  });

  $(`${id} > ul.nav-tabs a[data-toggle="tab"]`).on('shown.bs.tab', (e) => {
    // Refresh CodeMirror when its tab is toggled
    if ($($(e.target).attr('href')).hasClass('cm-tab') && typeof ManageIQ.editor !== 'undefined') {
      miq_refresh_code_mirror();
    }
  });

  // If no active tab is present, set the first tab as active
  const active_tabs = $(`${id} > ul.nav-tabs li.active:not(.hidden)`).length;
  let tab;
  if (active_tabs > 1) {
    tab = $(`${id} > ul.nav-tabs li:not(.hidden)`).first().removeClass('active');
    $(tab.find('a').attr('href')).removeClass('active');
  } else if (active_tabs !== 1) {
    tab = $(`${id} > ul.nav-tabs li:not(.hidden)`).first().addClass('active');
    $(tab.find('a').attr('href')).addClass('active');
  }

  // Hide the tab header when there is only one visible tab available
  if ($(`${id} > ul.nav-tabs > li:not(.hidden)`).length === 1) {
    $(`${id} > ul.nav-tabs`).hide();
  } else if ($(`${id} > ul.nav-tabs > li:not(.hidden)`).length > 1) {
    $(`${id} > ul.nav-tabs`).show();
  }
};

// refresh multiple/single code mirror textboxes on screen
window.miq_refresh_code_mirror = function() {
  $('.CodeMirror').each((_i, el) => {
    el.CodeMirror.refresh();
  });
};

window.miq_tabs_disable_inactive = function(id) {
  $(`${id} ul.nav-tabs > li:not(.active)`).addClass('disabled');
};

window.miq_tabs_show_hide = function(tab_id, show) {
  $(tab_id).toggleClass('hidden', !show);
};

// Send explorer search by name via ajax
window.miqSearchByName = function(button) {
  if (button == null) {
    miqJqueryRequest('x_search_by_name', { beforeSend: true, data: miqSerializeForm('searchbox') });
  }
};

// Send transaction to server so automate tree selection box can be made active
// and rest of the screen can be blocked
window.miqShowAE_Tree = function(typ) {
  const ae_url = `/${ManageIQ.controller}/ae_tree_select_toggle`;
  miqJqueryRequest(miqPassFields(ae_url, { typ }));
  return true;
};

/** Function to open the Workflows dialog box.
 * Params are set from automation_mixin.rb
 * fields = :fqname || :retire_fqname || :reconfigure_fqname
 * key    = 'provision_configuration_script_id' || 'retire_configuration_script_id' || 'configure_configuration_script_id'
 * type   = 'provision' || 'retire' || 'configure'
 */
window.miqShowEmbeddedWorkflowsModal = function(field, key, type) {
  const url = `/${ManageIQ.controller}/embedded_workflows_modal`;
  const selected = document.getElementById(key).value;
  miqJqueryRequest(miqPassFields(url, { field, selected, type }));
  return true;
};

// Toggle the user options div in the page header (:onclick from layouts/user_options)
window.miqChangeGroup = function(id) {
  miqSparkleOn();

  // prevent login redirect once current requests fail after the group gets changed
  ManageIQ.logoutInProgress = true;

  miqJqueryRequest(miqPassFields('/dashboard/change_group', { to_group: id }));
};

// Check for enter/escape on quick search box
window.miqQsEnterEscape = function(e) {
  let keycode;

  if (window.event) {
    keycode = window.event.keyCode;
  } else if (e) {
    keycode = e.keyCode;
  } else {
    return false;
  }

  if (keycode === 13) {
    if ($('#apply_button').is(':visible')) {
      miqAjaxButton('quick_search?button=apply');
      $('#quicksearchbox').modal('hide');
    }
  }

  if (keycode === 27) {
    miqAjaxButton('quick_search?button=cancel');
  }
};

// Start/stop the JS spinner
window.miqSpinner = function(status) {
  if (!miqSpinner.spinner) {
    miqSpinner.spinner = new Spinner({
      lines: 15, // The number of lines to draw
      length: 18, // The length of each line
      width: 4, // The line thickness
      radius: 25, // The radius of the inner circle
      color: '#fff', // #rgb or #rrggbb
      trail: 60, // Afterglow percentage
      className: 'miq-spinner', // The CSS class to assign to the spinner
    });
  }

  if (status) {
    const target = document.querySelector('#spinner_div');
    miqSpinner.spinner.spin(target);
  } else {
    miqSpinner.spinner.stop();
  }
};

// Start/stop the search spinner
window.miqSearchSpinner = function(status) {
  if (!miqSearchSpinner.spinner) {
    miqSearchSpinner.spinner = new Spinner({
      lines: 13, // The number of lines to draw
      length: 20, // The length of each line
      width: 10, // The line thickness
      radius: 30, // The radius of the inner circle
      color: '#000', // #rgb or #rrggbb or array of colors
      trail: 60, // Afterglow percentage
      className: 'miq-spinner', // The CSS class to assign to the spinner
    });
  }

  $('#search_notification').toggle(!!status);

  if (status) {
    const target = document.querySelector('#searching_spinner_center');
    miqSearchSpinner.spinner.spin(target);
  } else {
    miqSearchSpinner.spinner.stop();
  }
};

window.miqProcessObserveQueue = function() {
  if (!ManageIQ.observe.queue.length) {
    return;
  }

  if (ManageIQ.observe.processing) {
    setTimeout(miqProcessObserveQueue, 700);
    return;
  }

  ManageIQ.observe.processing = true;

  const request = ManageIQ.observe.queue.shift();

  miqJqueryRequest(request.url, request.options)
    .then((arg) => {
      ManageIQ.observe.processing = false;
      request.deferred.resolve(arg);
    }, (err) => {
      ManageIQ.observe.processing = false;
      add_flash(__('Error requesting data from server'), 'error');
      console.log(err);
      request.deferred.reject(err);
    });
};

window.miqDeferred = function() {
  const deferred = {};

  deferred.promise = new Promise((resolve, reject) => {
    deferred.resolve = resolve;
    deferred.reject = reject;
  });

  return deferred;
};

window.miqObserveRequest = function(url, options) {
  options = _.cloneDeep(options || {});
  options.observe = true;

  const deferred = miqDeferred();

  url && ManageIQ.observe.queue.push({
    url,
    options,
    deferred,
  });

  miqProcessObserveQueue();

  return deferred.promise;
};

window.miqJqueryRequest = function(url, options) {
  if ((ManageIQ.observe.processing || ManageIQ.observe.queue.length) && (!options || !options.observe)) {
    console.debug('Postponing miqJqueryRequest - waiting for the observe queue to empty first');

    return new Promise((resolve, reject) => {
      setTimeout(() => {
        miqJqueryRequest(url, options)
          .then(resolve, reject);
      }, 700);
    });
  }

  options = options || {};
  const ajax_options = {
    type: 'POST',
  };

  if (options.dataType === undefined) {
    ajax_options.accepts = { script: `*/*;q=0.5, ${$.ajaxSettings.accepts.script}` };
    ajax_options.dataType = 'script';
  }

  // copy selected options over
  _.extend(ajax_options, _.pick(options, [
    'data',
    'dataType',
    'contentType',
    'processData',
    'cache',
  ]));

  if (options.beforeSend) {
    ajax_options.beforeSend = function(_request) {
      miqSparkle(true);
    };
  }

  const complete = [];
  if (options.complete) {
    complete.push((_request) => {
      miqSparkle(false);
    });
  }

  if (options.done) {
    complete.push(options.done);
  }

  if (complete.length) {
    ajax_options.complete = complete;
  }

  return new Promise((resolve, reject) => {
    $.ajax(options.no_encoding ? url : encodeURI(url), ajax_options)
      .then(resolve, reject);
  });
};

window.miqDomElementExists = function(element) {
  return $(`#${element}`).length;
};

window.miqSerializeForm = function(element) {
  return $(`#${element}`).find('input,select,textarea').serialize().replace(/%0D%0A/g, '%0A');
};

window.miqSerializeField = function(element, fieldName) {
  return $(`#${element} :input[id=${fieldName}]`).serialize();
};

window.miqInitSelectPicker = function(selector, options) {
  $(selector || '.selectpicker').selectpicker({
    size: 10,
    dropupAuto: false,
    noneSelectedText: __('Nothing selected'),
    ...options || {},
  });
  $('.bootstrap-select > button[title]').not('.selectpicker').tooltip({ container: 'none' });
};

window.miqInitCodemirror = function(options) {
  if (!miqDomElementExists(options.text_area_id)) {
    return;
  }

  const textarea = $(`#${options.text_area_id}`)[0];

  ManageIQ.editor = CodeMirror.fromTextArea(textarea, {
    mode: options.mode,
    lineNumbers: options.line_numbers,
    matchBrackets: true,
    theme: 'eclipse',
    readOnly: options.read_only ? 'nocursor' : false,
    inputStyle: 'contenteditable',
    viewportMargin: Infinity,
  });

  ManageIQ.editor.on('change', () => {
    if (options.angular) {
      ManageIQ.editor.save();
      $(textarea).trigger('change');
    } else {
      miqSendOneTrans(options.url);
    }
  });

  ManageIQ.editor.on('blur', () => {
    ManageIQ.editor.save();
  });

  $('.CodeMirror').css('height', options.height);
  $('.CodeMirror').css('width', options.width);

  if (!options.no_focus && !options.read_only) {
    ManageIQ.editor.focus();
  }
};

window.miqSelectPickerEvent = function(element, url, options) {
  options = options || {};
  options.no_encoding = true;
  const firstarg = !url.includes('?');

  $(`#${element}`).on('change', _.debounce(function() {
    const selected = $(this).val();
    const finalUrl = `${url + (firstarg ? '?' : '&') + element}=${encodeURIComponent(selected)}`;

    if (typeof $(this).attr('data-miq_sparkle_on') !== 'undefined') {
      options.beforeSend = $(this).attr('data-miq_sparkle_on') === 'true';
    }

    if (typeof $(this).attr('data-miq_sparkle_off') !== 'undefined') {
      options.complete = $(this).attr('data-miq_sparkle_off') === 'true';
    }

    if (options.callback) {
      options.done = function() {
        options.callback();
      };
    }

    miqObserveRequest(finalUrl, options);

    return true;
  }, 700, { leading: true, trailing: true }));
};

window.miqAccordSelect = function(e) {
  if (ManageIQ.noCollapseEvent) { // implicitly return true when the noCollapseEvent is set
    return true;
  }
  if (!miqCheckForChanges()) {
    return false;
  }

  // No need to load anything if only a single accordion is present
  if ($('#accordion > .panel').length > 1) {
    const url = `/${$('body').data('controller')}/accordion_select?id=${$(e.target).attr('id')}`;
    miqJqueryRequest(url, { beforeSend: true, complete: true });
  }
  return true;
};

window.miqInitBootstrapSwitch = function(element, url, options) {
  $(`[name=${element}]`).bootstrapSwitch();

  $(`#${element}`).on('switchChange.bootstrapSwitch', (_event, state) => {
    options = typeof options !== 'undefined' ? options : {};
    options.no_encoding = true;

    const firstarg = !url.includes('?');
    miqObserveRequest(`${url + (firstarg ? '?' : '&') + element}=${state}`, options);

    return true;
  });
};

// Function to expand/collapse a pair of accordions
window.miqAccordionSwap = function(_collapse, expand) {
  /*
   * Blocked by: https://github.com/twbs/bootstrap/issues/18418
   * TODO: uncomment this and delete below when the issue is fixed
   *
   * // Fire an one-time event after the collapse is done
   * $(collapse).one('hidden.bs.collapse', function () {
   *   $(expand).collapse('show');
   * });
   * // Fire an one-time event fater the expand is done
   * $(expand).one('shown.bs.collapse', function () {
   *   ManageIQ.noCollapseEvent = false;
   * })
   * ManageIQ.noCollapseEvent = true;
   * $(collapse).collapse('hide');
   *
   */
  ManageIQ.noCollapseEvent = true;
  $(expand).parent().find('.panel-heading a').trigger('click');
  ManageIQ.noCollapseEvent = false;
};

window.miqInitAccordions = function() {
  const height = $('#left_div').height() - $('#toolbar').outerHeight() - $('#breadcrumbs').outerHeight();
  const panel = $('#left_div .panel-heading').outerHeight();
  const count = $('#accordion:visible > .panel .panel-body').length;
  $('#accordion:visible > .panel .panel-body').each((_k, v) => {
    if (window.matchMedia('(max-width: 768px)').matches) {
      $(v).css('max-height', '');
      $(v).css('overflow-y', 'none');
    } else {
      $(v).css('max-height', `${height - count * panel}px`);
      $(v).css('overflow-y', 'auto');
    }
    $(v).css('overflow-x', 'hidden');
  });
};

// Function to resize the main content for best fit between the toolbar & footer
window.miqInitMainContent = function() {
  const toolbar = $('#toolbar');
  const breadcrumbs = $('#breadcrumbs');
  const footer = $('#paging_div');
  const buttons = $('#form_buttons_div');
  let height = 0;
  if (footer.find('*:visible').length > 0) {
    height += footer.outerHeight();
  } else if (buttons.find('*:visible').length > 0) {
    height += buttons.outerHeight() + 5; // TODO: the styling of #form_buttons_div should be revisited
  }
  if (toolbar.find('*:visible').length > 0) {
    height += toolbar.outerHeight();
  }
  height += breadcrumbs.outerHeight();

  $('#main-content').css('height', `calc(100% - ${height}px)`);
};

window.miqHideSearchClearButton = function(explorer) {
  // Hide the clear button if the search input is empty
  $('.search-pf .has-clear .clear').each(function() {
    if (!$(this).prev('.form-control').val()) {
      $(this).hide();
    }
  });
  // Show the clear button upon entering text in the search input
  $('.search-pf .has-clear .form-control').keyup(function() {
    const t = $(this);
    t.nextAll('button.clear').toggle(Boolean(t.val()));
  });
  // Upon clicking the clear button, empty the entered text and hide the clear button
  $('.search-pf .has-clear .clear').click(function() {
    $(this).prev('.form-control').val('').focus();
    $(this).hide();
    // Clear Search text values as well
    const url = `/${ManageIQ.controller}/search_clear` + `?in_explorer=${explorer}`;
    ManageIQ.gridChecks = [];
    miqJqueryRequest(url);
  });
};

window.toggle_expansion = function(link) {
  link = $(link);
  link.find('i').toggleClass('fa-angle-right fa-angle-down');
  link.closest('td').children(0).toggleClass('expanded');
};

window.check_for_ellipsis = function() {
  const $element = $('.expand');
  $.each($element, (_i, value) => {
    const $val = $(value);
    const $c = $val.clone().css('overflow', 'initial').appendTo('body');
    if ($c.width() > $val.width() && $val.parent().find('i.fa-angle-right').length === 0) {
      add_expanding_icon($val.parent());
    }
    $c.remove();
  });
};

window.add_expanding_icon = function(element) {
  element.find('.pull-right').append("<a onclick='toggle_expansion(this)'> <i class='fa fa-angle-right'></i>");
};

window.rbacGroupLoadTab = function(id) {
  const lazy = $(`#${id}`).hasClass('lazy');
  if (!lazy) {
    // already loaded
    return;
  }

  $(`#${id}`).removeClass('lazy');

  miqJqueryRequest(`/ops/rbac_group_load_tab?tab_id=${id}`, {
    beforeSend: true,
    complete: true,
  });
};

window.chartData = function(type, data, data2) {
  if (type === undefined) {
    return emptyChart();
  }

  const config = _.cloneDeep(ManageIQ.charts.c3config[type]);
  if (config === undefined) {
    return emptyChart();
  }

  if (_.isObject(data.miq)) {
    if (data.miq.empty) {
      return _.defaultsDeep({}, data, data2);
    }
    customizeChart(data);
  }

  // set formating function for tooltip and y tick labels
  if (validateChartAxis(data.axis)) {
    let { format } = data.axis.y.tick;
    const titleFormat = _.cloneDeep(format);
    const max = _.max(getChartColumnDataValues(data.data.columns));
    let min = _.min(getChartColumnDataValues(data.data.columns));
    const maxShowed = getChartFormatedValue(format, max);
    const minShowed = getChartFormatedValue(format, min);
    let changeFormat = true;

    const tmp = validateMinMax(min, max, minShowed, maxShowed);
    changeFormat = !tmp.invalid;
    min = tmp.min;

    if (changeFormat) {
      // if min and max are close, labels should be more precise
      const recalculated = recalculatePrecision(minShowed, maxShowed, format, min, max);
      format = recalculated.format;
    }
    data.axis.y.tick.format = ManageIQ.charts.formatters[format.function].c3(format.options);
    data.miq.format = format;
    data.legend.item = {
      onclick: recalculateChartYAxisLabels,
    };

    data.tooltip.format.value = function(value, _ratio, _id) {
      const formatFunction = ManageIQ.charts.formatters[titleFormat.function].c3(titleFormat.options);
      return formatFunction(value);
    };
  }

  correctPatternflyOptions(config);
  return _.defaultsDeep({}, data, config, data2);
};

window.validateChartAxis = function(axis) {
  return _.isObject(axis)
         && _.isObject(axis.y)
         && _.isObject(axis.y.tick)
         && _.isObject(axis.y.tick.format)
         && axis.y.tick.format.function;
};

window.emptyChart = function() {
  return {
    data: {
      columns: [],
    },
  };
};

window.customizeChart = function(data) {
  // set maximum count of x axis tick labels for C&U charts
  if (data.miq.performance_chart) {
    data.axis.x.tick.centered = true;
    data.axis.x.tick.culling = { max: 5 };
  }

  // small C&U charts have very limited height
  if (data.miq.flat_chart) {
    const max = _.max(getChartColumnDataValues(data.data.columns));
    data.axis.y.tick.values = [0, max];
  }

  if (data.miq.expand_tooltip) {
    data.tooltip.format.name = function(_name, _ratio, id, _index) {
      return data.miq.name_table[id];
    };

    data.tooltip.format.title = function(x) {
      return data.miq.category_table[x];
    };
  }
  if (data.miq.zoomed) {
    data.size = { height: $('#lightbox-panel').height() - 200 };
    data.data.names = data.miq.name_table;
    data.legend = { position: 'bottom' };
  }
};

window.correctPatternflyOptions = function(config) {
  // some PatternFly default configs define contents function, but it breaks formatting
  if (_.isObject(config.tooltip)) {
    config.tooltip.contents = undefined;
  }
  // some PatternFly default configs define size of chart
  config.size = {};
};

$(() => {
  if (window.__testing__) {
    // in a jest test
    return;
  }

  $(window).on('resize', miqInitAccordions);
  $(window).on('resize', miqInitMainContent);
  $(window).on('resize', _.debounce(miqResetSizeTimer, 1000));

  check_for_ellipsis();
});

window.miqScrollToSelected = function(div_name) {
  const rowpos = $('tr.selected').position();
  if (rowpos) {
    $(`#${div_name}`).scrollTop(rowpos.top);
  }
};

window.miqFormatNotification = function(text, bindings) {
  if (!text) {
    // prevent Jed exceptions when a notification has missing text (__(undefined))
    return '';
  }

  let str = __(text);
  _.each(bindings, (value, key) => {
    str = str.replace(new RegExp(`%{${key}}`, 'g'), value.text);
  });
  return str;
};

window.fontIconChar = _.memoize((klass) => {
  const tmp = document.createElement('i');
  tmp.className = `hidden ${klass}`;
  document.body.appendChild(tmp);
  const char = window.getComputedStyle(tmp, ':before').content.replace(/'|"/g, '');
  const font = window.getComputedStyle(tmp, ':before').fontFamily;
  tmp.remove();
  return { font, char };
});

window.redirectLogin = function(msg) {
  if (ManageIQ.logoutInProgress) {
    return; // prevent double redirect after pressing the Logout button or when changing group
  }

  add_flash(msg, 'warning');
  window.document.location.href = '/dashboard/login?timeout=true';
};

window.camelizeQuadicon = function(quad) {
  return _.reduce(quad, (result, current, key) => {
    const item = {};
    item[_.camelCase(key)] = current;
    return Object.assign(result, item);
  }, {});
};