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 )