App-Netdisco

 view release on metacpan or  search on metacpan

share/public/javascripts/netdisco.js  view on Meta::CPAN

function retitleTooltip(element, title) {
  $(element).attr('data-bs-title', title);
  var instance = bootstrap.Tooltip.getInstance($(element)[0]);
  if (instance) { instance.dispose(); }
}

// Hide an element that may be showing a tooltip, and any tooltip-carrying
// element inside it. Bootstrap dismisses a tip when the pointer leaves its
// trigger, but macOS Chrome fires no boundary event when the trigger is hidden
// under a pointer that has not moved, so nothing dismisses the tip and popper
// then anchors it to a zero sized rectangle at the window origin. Linux
// Chromium and Firefox both fire it, which is why #1667 only ever reproduced
// on a Mac and why leaving this to the mouseleave handler below is not enough.
function hideWithTooltip(target) {
  var elements = $(target);
  elements.find('[rel=tooltip]').addBack('[rel=tooltip]').each(function () {
    var instance = bootstrap.Tooltip.getInstance(this);
    if (instance) { instance.dispose(); }
  });
  elements.hide();
}

// The widget puts its suggestion list and its live region on document.body, and
// only its own destroy takes them down. That runs from jQuery's removal path,
// which a pane emptied natively never reaches, so both outlive the field.
//
// Matching the class the widget adds, rather than netdisco's own selectors,
// covers a field a site-local template introduced. Asking for the instance is
// the guard: every other method name throws on an element it never took over.
function destroyAutocompletesIn(pane) {
  if (!pane) return;
  pane.querySelectorAll('.ui-autocomplete-input').forEach(function (field) {
    var widget = $(field).autocomplete('instance');
    if (widget) { widget.destroy(); }
  });
}

// A pointer click focuses the category, which :focus-within then holds open
// after the pointer has left. detail is 0 for a keyboard-generated click, which
// must not close what it just opened.
document.addEventListener('click', function (event) {
  var category = event.target.closest('li.dropend > a.dropdown-toggle');
  if (category && event.detail > 0) { category.blur() }
});

// Bootstrap's own Escape handler builds a Dropdown from the nested list, finds
// no toggle beside it and throws, leaving the menu open. On window rather than
// document because Bootstrap registers its delegated handlers as capture
// listeners on document and loads first, so nothing there can precede them.
window.addEventListener('keydown', function (event) {
  if (event.key !== 'Escape') { return }
  if (!event.target.closest('li.dropend > .dropdown-menu')) { return }
  event.stopPropagation();
  var toggle = event.target.closest('.nav-item.dropdown');
  toggle = toggle && toggle.querySelector(':scope > .dropdown-toggle');
  if (!toggle) { return }
  toggle.focus();
  bootstrap.Dropdown.getOrCreateInstance(toggle).hide();
}, true);

// htmx takes the indicator down when the response arrives, but the fragment's
// own script builds its table from a ready callback afterwards, so the raw
// full-length table would paint with no indicator until that finishes.
//
// Quiet DOM rather than a table library's own event, so this outlives the move
// off jQuery. Quiet is not enough on its own: the build has gaps of several
// hundred milliseconds where nothing changes because the thread is busy
// computing, and revealing in one of those shows a table that is still moving.
// A frame that took far longer than a frame should is the evidence of that, so
// both conditions have to hold, twice running.
//
// The deadline is an escape hatch for a pane that never goes quiet rather than
// a budget: it starts at the swap, so it covers only the browser's own work,
// never the fetch.
function holdUntilSettled(pane, indicator) {
  if (!pane || !indicator) return;
  pane.classList.add('nd_pane-settling');
  indicator.classList.add('nd_indicator-held');

  var SMOOTH_FRAME = 50; // ms; a 60Hz frame is 16, and a busy one runs to 800
  var settledFrames = 0;
  var mutated = false;
  var previousFrame = null;
  var deadline = Date.now() + 60000;
  var watcher = new MutationObserver(function () { mutated = true });
  watcher.observe(pane, { childList: true, subtree: true, attributes: true });

  requestAnimationFrame(function frame(now) {
    var smooth = (previousFrame !== null) && ((now - previousFrame) < SMOOTH_FRAME);
    previousFrame = now;
    settledFrames = (smooth && !mutated) ? (settledFrames + 1) : 0;
    mutated = false;

    if (settledFrames < 2 && Date.now() < deadline) { requestAnimationFrame(frame); return }
    watcher.disconnect();
    pane.classList.remove('nd_pane-settling');
    indicator.classList.remove('nd_indicator-held');
  });
}

$(document).ready(function() {
  // sidebar form fields should change colour and have bin/copy icon
  $('.nd_field-copy-icon').hide();
  hideWithTooltip('.nd_field-clear-icon');

  // activate typeahead on the main search box, for device names only
  // the backend has already filtered, and jQuery UI does no client-side
  // filtering of a function source, so no matcher is needed
  $('#nq,#nqbody').autocomplete({
    source: function (request, response) {
      return $.get( uri_base + '/ajax/data/devicename/typeahead', request, function (data) {
        return response(data);
      });
    }
    ,delay: 150
    ,minLength: 3
    // the widget these boxes used to run opened with its first row picked out,
    // so Enter took the obvious name. jQuery UI selects nothing unless asked, and
    // does not write the row into the field: it only does that when a key moved
    // the focus.
    ,autoFocus: true

share/public/javascripts/netdisco.js  view on Meta::CPAN

    ,opens: 'left'
    ,locale: { format: 'YYYY-MM-DD', separator: ' to ' }
    ,autoUpdateInput: false
  }
  ,function(start, end) {
    $('#daterange').trigger('input');
  });

  // daterangepicker 3.x writes the picker's own dates into the input on init
  // unless autoUpdateInput is off, which blanks the server-rendered value. With
  // it off, nothing updates the input when a range is applied, so do it here.
  $('#daterange').on('apply.daterangepicker', function (ev, picker) {
    $(this).val(picker.startDate.format('YYYY-MM-DD')
      + ' to ' + picker.endDate.format('YYYY-MM-DD'));
    $(this).trigger('input');
  });

  // handler for datepicker in node sidebar
  $('.nd_sidebar').on('input', '#daterange', function() {
    if ($(this).prop('value') == '') {
      $('#daterange').parent('.clearfix').removeClass('success');
    }
    else {
      $('#daterange').parent('.clearfix').addClass('success');
    }
  });
  $('#daterange').trigger('input');

  // htmx glue. Converted panes get the same empty-result, error and
  // after-swap handling do_search gives the unconverted ones, so the two
  // transports are indistinguishable to a user. Keyed on any *_pane, not just
  // admin, because later rungs convert the search and device tabs onto this.
  document.body.addEventListener('htmx:afterSwap', function (evt) {
    var target = evt.detail.target;
    if (!target.id.match(/_pane$/)) return;
    var tab = target.id.replace(/_pane$/, '');
    if (target.innerHTML === '') {
      target.innerHTML =
        '<div class="col-md-2 alert alert-info">No matching records.</div>';
      return;
    }
    holdUntilSettled(target, document.getElementById(tab + '_indicator'));
    $('div.content > div.tab-content table.nd_floatinghead').floatThead({
      top: 40
      ,position: 'fixed'
    });
    inner_view_processing(tab);
  });
  // Empty the pane for the duration of the request, so the indicator is the
  // only thing on screen. Leaving the previous results up gives an interactive
  // table that no longer answers the search being run.
  //
  // jobqueue is excluded for the reason it carries no indicator: it refreshes
  // on a timer and would blank on every tick.
  document.body.addEventListener('htmx:beforeRequest', function (evt) {
    var target = evt.detail.target;
    if (!target.id.match(/_pane$/) || target.id === 'jobqueue_pane') return;

    // force-graph renders every frame until destroyed, and emptying the pane
    // only detaches its canvas. netmap.js destroys the previous instance as
    // well, but not until the new fragment's script runs.
    if (target.id === 'netmap_pane' && window.graph && window.graph.fg
        && typeof window.graph.fg._destructor === 'function') {
      window.graph.fg._destructor();
    }

    destroyAutocompletesIn(target);
    target.innerHTML = '';
  });
  document.body.addEventListener('htmx:responseError', function (evt) {
    var target = evt.detail.target;
    if (!target.id.match(/_pane$/)) return;
    target.innerHTML =
      '<div class="col-md-5 alert alert-danger"><i class="fas fa-triangle-exclamation"></i> ' +
      'Search failed! Please contact your site administrator (server error).</div>';
  });
  document.body.addEventListener('htmx:sendError', function (evt) {
    var target = evt.detail.target;
    if (!target.id.match(/_pane$/)) return;
    target.innerHTML =
      '<div class="col-md-5 alert alert-danger"><i class="fas fa-triangle-exclamation"></i> ' +
      'Search failed! Please contact your site administrator (network error).</div>';
  });
});

// temporarily disable datatables paging
// returns [current_page_length, current_page_index]
function dataTablesDisablePaging() {
  $.fn.dataTable.ext.search.pop();
  var plen = $('#dp-data-table').DataTable().page.len();
  var pnum = $('#dp-data-table').DataTable().page();
  $('#dp-data-table').DataTable().page.len(-1).draw(true);
  return [plen, pnum];
}

// restore the datatables pagination and page number
function dataTablesRestorePage(plen, pnum) {
  $('#dp-data-table').DataTable().page.len(plen).draw(true);
  $('#dp-data-table').DataTable().page(pnum).draw(false);
}

// install our row filter for datatables row group toggle
function dataTablesPushRowGroupVisibilityFilter() {
  $.fn.dataTable.ext.search.push(
    function(settings, data, dataIndex) {
        var row = $($('#dp-data-table').DataTable().row(dataIndex).node());
        if (! row.data('collapsed-group')) { return true; }
        return row.attr('data-is-collapsed') == 'false';
    }
  );
}

// onclick handler
// toggles visibility of a group of datatables rows
// clicked element has the group name as data-collapsed-group
var dataTablesRowGroupVisibilityToggle = function () {
  var groupname = $(this).data('collapsed-group');
  var [plen, pnum] = dataTablesDisablePaging();

  // groupname is not in a class selector due to port name characters
  $('tr.nd_collapsible').each(function(index) { 



( run in 1.538 second using v1.01-cache-2.11-cpan-b16cb0d3907 )