Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,8 @@
* Persists toggle state to user preferences on change.
*/
function initializeTogglePrefs() {
const sysname = document.getElementById('use-sysname-toggle');
const strip = document.getElementById('strip-domain-toggle');
const sysname = document.getElementById('use-sysname-toggle-cb');
const strip = document.getElementById('strip-domain-toggle-cb');
if (sysname) sysname.addEventListener('change', function () { savePref('use_sysname', this.checked); });
if (strip) strip.addEventListener('change', function () { savePref('strip_domain', this.checked); });
}
Expand Down Expand Up @@ -1199,10 +1199,14 @@

const dismissTrigger = event.target.closest('[data-bs-dismiss="modal"]');
if (dismissTrigger) {
event.preventDefault();

// Check if it's in the HTMX modal
if (modalElement.contains(dismissTrigger)) {
// Only handle dismiss triggers whose nearest .modal ancestor IS
// the outer HTMX modal. Buttons inside nested modals (e.g. the
// Promote-to-host modal rendered inside #htmx-modal-content)
// must be left for Bootstrap's own dismiss handler so they
// close the inner modal, not the outer one.
const nearestModal = dismissTrigger.closest('.modal');
if (nearestModal === modalElement) {
event.preventDefault();
hideModal(modalElement, fallbackBackdropRef);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
{% comment %}
Reusable "Add Device Type Mapping" form for the import validation modal.

Required context:
- libre_device (dict-like with device_id, hardware)
Optional context:
- preselect_device_type: a DeviceType instance to pre-fill the typeahead
(used in the existing-device + mismatch branch so a single click maps
`libre_device.hardware` → `existing_device.device_type`).
- submit_label (defaults to "Add Mapping")
{% endcomment %}
<form hx-post="{% url 'plugins:netbox_librenms_plugin:add_device_type_mapping' device_id=libre_device.device_id %}"
hx-swap="none"
hx-include="[name=role_{{ libre_device.device_id }}], [name=rack_{{ libre_device.device_id }}], [name=cluster_{{ libre_device.device_id }}], #use-sysname-toggle, #strip-domain-toggle"
class="mt-1"
id="dt-mapping-form-{{ libre_device.device_id }}">
{% csrf_token %}
{% if server_key %}<input type="hidden" name="server_key" value="{{ server_key }}">{% endif %}
<div class="position-relative">
<label for="dt-search-{{ libre_device.device_id }}" class="visually-hidden">
Search device types
</label>
<input type="text"
class="form-control form-control-sm"
id="dt-search-{{ libre_device.device_id }}"
placeholder="Search device types…"
autocomplete="off"
{% if preselect_device_type %}value="{% if preselect_device_type.manufacturer %}{{ preselect_device_type.manufacturer.name }} — {% endif %}{{ preselect_device_type.display }}"{% endif %}>
<input type="hidden"
name="device_type_id"
id="dt-id-{{ libre_device.device_id }}"
value="{% if preselect_device_type %}{{ preselect_device_type.pk }}{% endif %}">
<div id="dt-dropdown-{{ libre_device.device_id }}"
class="dropdown-menu w-100 shadow"
style="z-index:1050; max-height:200px; overflow-y:auto; display:none;"></div>
</div>
{# Disabled until a typeahead item is chosen (or a preselect value is present). #}
<button type="submit"
id="dt-submit-{{ libre_device.device_id }}"
class="btn btn-sm btn-primary mt-1 w-100"
{% if not preselect_device_type %}disabled{% endif %}>{{ submit_label|default:"Add Mapping" }}</button>
</form>
<script>
(function () {
var searchEl = document.getElementById("dt-search-{{ libre_device.device_id }}");
var dropdownEl = document.getElementById("dt-dropdown-{{ libre_device.device_id }}");
var hiddenEl = document.getElementById("dt-id-{{ libre_device.device_id }}");
var submitBtn = document.getElementById("dt-submit-{{ libre_device.device_id }}");
if (!searchEl) return;
var timer = null;
var requestSeq = 0;

searchEl.addEventListener("input", function () {
clearTimeout(timer);
hiddenEl.value = "";
submitBtn.disabled = true;
var q = this.value.trim();
if (q.length < 2) { dropdownEl.style.display = "none"; dropdownEl.innerHTML = ""; return; }
timer = setTimeout(function () {
var seq = ++requestSeq;
var csrfToken = (document.querySelector('[name=csrfmiddlewaretoken]') || {}).value || "";
fetch("{% url 'dcim-api:devicetype-list' %}?q=" + encodeURIComponent(q) + "&limit=20", {
headers: { "X-CSRFToken": csrfToken }
})
.then(function (r) {
if (!r.ok) { throw new Error("HTTP " + r.status); }
return r.json();
})
.then(function (data) {
if (seq !== requestSeq) return;
dropdownEl.innerHTML = "";
if (!data.results || data.results.length === 0) {
dropdownEl.innerHTML = '<a class="dropdown-item small py-1 text-muted disabled">No results</a>';
} else {
data.results.forEach(function (dt) {
var a = document.createElement("a");
a.className = "dropdown-item small py-1";
a.href = "#";
a.textContent = (dt.manufacturer ? dt.manufacturer.display + " — " : "") + dt.display;
a.addEventListener("click", function (e) {
e.preventDefault();
searchEl.value = (dt.manufacturer ? dt.manufacturer.display + " — " : "") + dt.display;
hiddenEl.value = dt.id;
submitBtn.disabled = false;
dropdownEl.style.display = "none";
});
dropdownEl.appendChild(a);
});
}
dropdownEl.style.display = "block";
})
.catch(function (error) {
console.debug("Device type lookup failed:", error.message);
dropdownEl.style.display = "none";
});
}, 300);
});

// Outside-click hides the dropdown. The listener self-removes once the
// form leaves the DOM (HTMX swap, modal teardown) so handlers don't
// accumulate on document each time this template re-renders.
function onDocClick(e) {
if (!document.body.contains(searchEl)) {
document.removeEventListener("click", onDocClick);
return;
}
if (!searchEl.contains(e.target) && !dropdownEl.contains(e.target)) {
dropdownEl.style.display = "none";
}
}
document.addEventListener("click", onDocClick);
})();
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{% comment %}
Compact pencil icon that opens the combined "manage platform" modal (map
LibreNMS OS to an existing platform, or create a new one). Keeps the platform
cell uncluttered. Requires: libre_device, server_key.
{% endcomment %}
{% if libre_device.os and libre_device.os != "-" %}
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-1 ms-1"
hx-get="{% url 'plugins:netbox_librenms_plugin:create_platform_from_import' device_id=libre_device.device_id %}?server_key={{ server_key|urlencode }}"
hx-target="#htmx-modal-content"
hx-swap="innerHTML"
title="Map or create platform for '{{ libre_device.os }}'"
aria-label="Map or create platform">
<i class="mdi mdi-pencil"></i>
</button>
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
{% comment %}
Reusable "Add Platform Mapping" form for the import validation modal.

Required context:
- libre_device (dict-like with device_id, os)
Optional:
- preselect_platform: a Platform instance to pre-fill the typeahead.
- submit_label
{% endcomment %}
<form hx-post="{% url 'plugins:netbox_librenms_plugin:add_platform_mapping' device_id=libre_device.device_id %}"
hx-swap="none"
hx-include="[name=role_{{ libre_device.device_id }}], [name=rack_{{ libre_device.device_id }}], [name=cluster_{{ libre_device.device_id }}], #use-sysname-toggle, #strip-domain-toggle"
class="mt-1"
id="plat-mapping-form-{{ libre_device.device_id }}">
{% csrf_token %}
{% if server_key %}<input type="hidden" name="server_key" value="{{ server_key }}">{% endif %}
<div class="position-relative">
<label for="plat-search-{{ libre_device.device_id }}" class="visually-hidden">
Search platforms
</label>
<input type="text"
class="form-control form-control-sm"
id="plat-search-{{ libre_device.device_id }}"
placeholder="Search platforms…"
autocomplete="off"
{% if preselect_platform %}value="{{ preselect_platform.name }}"{% endif %}>
<input type="hidden"
name="platform_id"
id="plat-id-{{ libre_device.device_id }}"
value="{% if preselect_platform %}{{ preselect_platform.pk }}{% endif %}">
<div id="plat-dropdown-{{ libre_device.device_id }}"
class="dropdown-menu w-100 shadow"
style="z-index:1050; max-height:200px; overflow-y:auto; display:none;"></div>
</div>
{# Disabled until a typeahead item is chosen (or a preselect value is present). #}
<button type="submit"
id="plat-submit-{{ libre_device.device_id }}"
class="btn btn-sm btn-primary mt-1 w-100"
{% if not preselect_platform %}disabled{% endif %}>{{ submit_label|default:"Add Mapping" }}</button>
</form>
<script>
(function () {
var searchEl = document.getElementById("plat-search-{{ libre_device.device_id }}");
var dropdownEl = document.getElementById("plat-dropdown-{{ libre_device.device_id }}");
var hiddenEl = document.getElementById("plat-id-{{ libre_device.device_id }}");
var submitBtn = document.getElementById("plat-submit-{{ libre_device.device_id }}");
if (!searchEl) return;
var timer = null;
var requestSeq = 0;

searchEl.addEventListener("input", function () {
clearTimeout(timer);
hiddenEl.value = "";
submitBtn.disabled = true;
var q = this.value.trim();
if (q.length < 2) { dropdownEl.style.display = "none"; dropdownEl.innerHTML = ""; return; }
timer = setTimeout(function () {
var seq = ++requestSeq;
var csrfToken = (document.querySelector('[name=csrfmiddlewaretoken]') || {}).value || "";
fetch("{% url 'dcim-api:platform-list' %}?q=" + encodeURIComponent(q) + "&limit=20", {
headers: { "X-CSRFToken": csrfToken }
})
.then(function (r) {
if (!r.ok) { throw new Error("HTTP " + r.status); }
return r.json();
})
.then(function (data) {
if (seq !== requestSeq) return;
dropdownEl.innerHTML = "";
if (!data.results || data.results.length === 0) {
dropdownEl.innerHTML = '<a class="dropdown-item small py-1 text-muted disabled">No results</a>';
} else {
data.results.forEach(function (p) {
var a = document.createElement("a");
a.className = "dropdown-item small py-1";
a.href = "#";
a.textContent = p.display || p.name;
a.addEventListener("click", function (e) {
e.preventDefault();
searchEl.value = p.display || p.name;
hiddenEl.value = p.id;
submitBtn.disabled = false;
dropdownEl.style.display = "none";
});
dropdownEl.appendChild(a);
});
}
dropdownEl.style.display = "block";
})
.catch(function (error) {
console.debug("Platform lookup failed:", error.message);
dropdownEl.style.display = "none";
});
}, 300);
});

// Outside-click hides the dropdown. The listener self-removes once the
// form leaves the DOM (HTMX swap, modal teardown) so handlers don't
// accumulate on document each time this template re-renders.
function onDocClick(e) {
if (!document.body.contains(searchEl)) {
document.removeEventListener("click", onDocClick);
return;
}
if (!searchEl.contains(e.target) && !dropdownEl.contains(e.target)) {
dropdownEl.style.display = "none";
}
}
document.addEventListener("click", onDocClick);
})();
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,19 @@
{# server_key - (optional) LibreNMS server key, forwarded as hidden field #}
<div class="modal-header">
<h5 class="modal-title" id="createPlatformModalLabel">
<i class="mdi mdi-alert"></i> Create New Platform
{% if libre_device %}<i class="mdi mdi-cog-outline"></i> Manage platform — <code>{{ librenms_os }}</code>{% else %}<i class="mdi mdi-alert"></i> Create New Platform{% endif %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
{% if libre_device %}
{# Map the LibreNMS OS to an EXISTING platform (separate form -> add_platform_mapping). #}
<div class="modal-body pb-2">
<h6 class="mb-2">Map <code>{{ librenms_os }}</code> to an existing platform</h6>
{% include "netbox_librenms_plugin/htmx/_platform_mapping_form.html" with preselect_platform=current_platform submit_label="Map to this platform" %}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<div class="text-center text-muted small mt-3">— or create a new platform below —</div>
</div>
<hr class="my-0">
{% endif %}
<form{% if use_htmx %} hx-post="{{ form_action }}" hx-swap="none"{% if htmx_include %} hx-include="{{ htmx_include }}"{% endif %}{% else %} method="post" action="{{ form_action }}"{% endif %}>
{% csrf_token %}
<input type="hidden" name="librenms_os" value="{{ librenms_os }}">
Expand All @@ -27,9 +36,9 @@ <h5 class="modal-title" id="createPlatformModalLabel">
<input type="hidden" name="device_pk" value="{{ device_pk }}">
{% endif %}
<div class="modal-body">
<div class="alert alert-warning">
<div class="alert alert-info py-2">
<i class="mdi mdi-information"></i>
No NetBox platform matches LibreNMS OS <strong>{{ librenms_os }}</strong>.
Create a new NetBox platform for LibreNMS OS <strong>{{ librenms_os }}</strong>.
</div>
<p>This will:</p>
<ol>
Expand Down
Loading