Skip to content
Open
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
1 change: 1 addition & 0 deletions changes/1136.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed the "Execute" dropdown on the Golden Config list view not carrying the selected devices over to the Job run form.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Carries all the selected devices PKs in a list view over to a Job run form.
*
* On page load it binds every anchor tagged with the `execute-job-link` class
* (i.e. the entries in the "Execute" dropdown) so that, when clicked, the Device
* PKs of the currently-checked table rows are appended to the Job run URL as
* `?device=<pk>` query parameters. As a consequence Nautobot's Job run view
* populates the matching `MultiObjectVar(model=Device)` form field from those
* parameters.
*
* Each row-select checkbox must expose its Device PK via a `data-device-pk`
* attribute. The checkbox `value` itself is left alone (it stays the row PK used
* by bulk edit/delete). Rows without a Device PK are skipped.
*
* This is how the checkbox should look like:
* <input type="checkbox" name="pk" value="<GoldenConfig-pk>"
* class="form-check-input nb-form-check-input-sm mt-2"
* data-device-pk="<Device-pk>">
*
* If no rows are selected the link is left at its pristine URL so the Job form
* simply opens unfiltered.
*
* Implemented in plain JavaScript: jQuery is deprecated as of Nautobot 3.0.
*/
function bindExecuteWithSelection() {
document.querySelectorAll("a.execute-job-link").forEach(function (link) {
// Store the base job /run URL (without query string) so repeated clicks (and
// back-forward cache restores) rebuild from a clean URL instead of stacking params.
const baseHref = link.getAttribute("href");
link.addEventListener("click", function () {
const params = new URLSearchParams();
document.querySelectorAll('input[name="pk"]:checked').forEach(function (checkbox) {
const devicePk = checkbox.getAttribute("data-device-pk");
if (devicePk) {
params.append("device", devicePk);
}
});
const query = params.toString();
link.setAttribute("href", query ? `${baseHref}?${query}` : baseHref);
});
});
}

// Self-initialize once the DOM is ready (no jQuery).
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bindExecuteWithSelection);
} else {
bindExecuteWithSelection();
}
15 changes: 14 additions & 1 deletion nautobot_golden_config/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,20 @@ class Meta(BaseTable.Meta):
class GoldenConfigTable(BaseTable):
"""Table to display Config Management Status."""

pk = ToggleColumn()
# Carry the Device PK on each row-select checkbox as data-device-pk so the "Execute"
# dropdown can pre-populate the Job form's device field (see execute_with_selected.js).
# The checkbox value stays the GoldenConfig PK, which bulk delete/edit relies on.
pk = ToggleColumn(
attrs={
"input": {
# passing attrs to ToggleColumn overrides the defaults,
# so we need to re-supply ToggleColumn default classes
"class": "form-check-input nb-form-check-input-sm mt-2",
"data-device-pk": lambda record: str(record.device_id or ""),
},
"td": {"class": "nb-w-0"},
}
)
name = LinkColumn(
"plugins:nautobot_golden_config:goldenconfig",
args=[A("pk")],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,21 @@ <h1>{% block title %}Configuration Overview{% endblock title %}</h1>
<ul class="dropdown-menu">
{% if compliance %}
<li>
<a class="dropdown-item" href="{% url 'extras:job_run_by_class_path' class_path='nautobot_golden_config.jobs.ComplianceJob' %}">
<a class="dropdown-item execute-job-link" href="{% url 'extras:job_run_by_class_path' class_path='nautobot_golden_config.jobs.ComplianceJob' %}">
<span class="mdi mdi-play-circle text-secondary" aria-hidden="true"></span> Compliance
</a>
</li>
{% endif %}
{% if intended %}
<li>
<a class="dropdown-item" href="{% url 'extras:job_run_by_class_path' class_path='nautobot_golden_config.jobs.IntendedJob' %}">
<a class="dropdown-item execute-job-link" href="{% url 'extras:job_run_by_class_path' class_path='nautobot_golden_config.jobs.IntendedJob' %}">
<span class="mdi mdi-play-circle text-secondary" aria-hidden="true"></span> Intended
</a>
</li>
{% endif %}
{% if backup %}
<li>
<a class="dropdown-item" href="{% url 'extras:job_run_by_class_path' class_path='nautobot_golden_config.jobs.BackupJob' %}">
<a class="dropdown-item execute-job-link" href="{% url 'extras:job_run_by_class_path' class_path='nautobot_golden_config.jobs.BackupJob' %}">
<span class="mdi mdi-play-circle text-secondary" aria-hidden="true"></span> Backup
</a>
</li>
Expand Down Expand Up @@ -86,6 +86,7 @@ <h1 class="modal-title">Golden Configuration</h1>
{% block javascript %}
{{ block.super }}
<script src="{% static 'js/tableconfig.js' %}"></script>
<script src="{% static 'nautobot_golden_config/execute_with_selected.js' %}"></script>
<script>
$( document ).ready(function(){
$('.openBtn').on('click',function(){
Expand Down
39 changes: 39 additions & 0 deletions nautobot_golden_config/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,45 @@ def test_page_ok(self):
response = self.client.get(f"{self._url}")
self.assertEqual(response.status_code, 200)

def test_row_checkbox_exposes_device_pk(self):
"""
Server-side render assertion for the config overview row checkboxes.

execute_with_selected.js needs the Device PK on each row, but the checkbox value has to stay
the GoldenConfig PK for the usual bulk edit/delete actions. So the two must be distinct and
the Device PK is exposed on a separate data-device-pk attribute.
"""
# we do the test only on the first row
golden_config = models.GoldenConfig.objects.first()
# The GoldenConfig PK and its Device PK must differ, so this distinguishes the two.
self.assertNotEqual(golden_config.pk, golden_config.device.pk)
# The table body is loaded via an HTMX request (Nautobot >= 3.1), so the row (and its
# checkbox) only render when the HX-Request header is set; a plain GET returns the empty
# list shell ("No golden configs found").
response = self.client.get(f"{self._url}", headers={"HX-Request": "true"})
self.assertEqual(response.status_code, 200)
html_parsed = html.fromstring(response.content.decode())
checkbox = html_parsed.xpath('//input[@name="pk" and @type="checkbox" and @data-device-pk]')[0]
# The checkbox value must remain the GoldenConfig PK, which bulk edit/delete relies on.
self.assertEqual(checkbox.get("value"), str(golden_config.pk))
# The Job form's device MultiObjectVar (the device field) is pre-filled from ?device=<pk>, so the row must
# carry the Device PK on data-device-pk for the Execute dropdown JS to read.
self.assertEqual(checkbox.get("data-device-pk"), str(golden_config.device.pk))

def test_execute_dropdown_loads_selection_script(self):
"""
Server-side render assertion for the config overview Execute dropdown.

The three Execute dropdown links (backup, intended and compliance) must be tagged with the
`execute-job-link` class, and the page must load execute_with_selected.js, or the selection
would never be carried over to the Job run form.
"""
response = self.client.get(f"{self._url}")
self.assertEqual(response.status_code, 200)
content = response.content.decode()
self.assertIn("execute-job-link", content)
self.assertIn("execute_with_selected.js", content)

# TODO: 3.0.0 Followup on whether these tests are required in Nautobot 3.0.0
# def test_headers_in_table(self):
# table_header = self._get_golden_config_table_header()
Expand Down
Loading