Skip to content

Conversation

@Simrayz
Copy link
Contributor

@Simrayz Simrayz commented Oct 29, 2025

Scope and purpose

Resolves #3635.

This PR refactors the dashboard navlets logic from a complicated JS flow, to use HTMX where possible, and simplifies necessary javascript plugin interoperability.

See issue #3635 for a full description of changes.

This pull request

  • Implements loading and updating of navlets with HTMX
  • Replaces navlet_controller and navlets_controller with navlets_htmx_controller
  • Renders entire dashboard on initial load of the page, with individual data fetching for each navlet
  • Simplifies refresh of image- and ajax reload navlets

How to test

Loading navlets

Navlet Load

  1. Open your default dashboard
  2. Verify that navlets have a Spinner that indicates their loading state
  3. Verify that all navlets load and resolve in their "VIEW" mode

Adding and Removing Navlets

  1. Create a new dashboard. Verify that the dashboard has a "No widgets"
  2. Add a new Navlet to your dashboard.
    1. Verify that the Navlet is rendered in the leftmost column at the top, and that the "No widgets" message is removed.
    2. Verify that the navlet is highlighted, and that the highlight is removed on hover.
    3. Add another Navlet to your dashboard.
    4. Reload the page, and verify that positions have been saved.
  3. Delete a Navlet.
    1. Verify that it is removed from the dashboard.
    2. Remove all remaining navlets, and verify that a "No widgets" message is added to the left-most column.

Dashboard Layout

Re-ordering Navlets

  1. Open a dashboard with many navlets, e.g. your Default dashboard.
  2. Hold the drag handle, and move a navlet into a different column.
  3. Reload the page, and verify that the positions were saved.

Updating columns

  1. Open a dashboard with many navlets, e.g. your Default dashboard.
  2. Open dashboard settings, and change the column count.
  3. Verify that the dashboard is reloaded, and the column count is changed.

Compact Dashboard Mode

  1. Open a dashboard with many navlets, e.g. your Default dashboard.
  2. Change the column density from "Normal" to "Compact"
  3. Verify that the spacing between navlets is removed.

Editing Navlets

Fill in section about how to test editing of navlets.

Periodic Refresh of Navlets

Refresh with standard interval

This can be tested with all Navlets that have a refresh_interval defined, and don't have ajax_reload or image_reload set to True. I have selected RoomStatus as an example as the refresh interval is short.

  1. Add a RoomStatus ("Rooms with active alerts") navlet to a dashboard.
  2. Take note of the "Last update" time, and wait 30 seconds.
  3. Verify that the Navlet is reloaded. You can also view this in the Network tab in dev tools.

Refresh with ajax_reload=True

  • Create an empty dashboard.
  • Add an Alert, PDU and UPS navlet.
  • Configure the navlets to show content.
  • The default refresh interval is 30 seconds, so open the network tab and look for repeating render requests.
  • Verify that the "Last update" text in the bottom-right is updated every thirty seconds. Does not work for the Alert widget, if there is no data available in graphite.

Refresh for VlanGraphNavlet and GraphWidget

  1. Add a Vlan Graph to a dashboard.
    1. Go into edit mode, and select a Vlan. Not all of them have an URL, so try one that works, e.g. VLAN 20 for employees.
    2. Verify that an image (or placeholder with No Data) is rendered.
  2. Add a Chart (GraphWidget) to a dashboard.
    1. Go into edit mode. You need a Chart URL and a Target URL. I found the most practical way to find values for this form was to find a GraphWidget in the account_navlet table, and copy the values from preferences.
    2. Verify that an image (or placeholder with No Data) is rendered.

Contributor Checklist

Every pull request should have this checklist filled out, no matter how small it is.
More information about contributing to NAV can be found in the
Hacker's guide to NAV.

  • Added a changelog fragment for towncrier
  • Added/amended tests for new/changed code
  • Added/changed documentation
  • Linted/formatted the code with ruff, easiest by using pre-commit
  • Wrote the commit message so that the first line continues the sentence "If applied, this commit will ...", starts with a capital letter, does not end with punctuation and is 50 characters or less long. See https://cbea.ms/git-commit/
  • Based this pull request on the correct upstream branch: For a patch/bugfix affecting the latest stable version, it should be based on that version's branch (<major>.<minor>.x). For a new feature or other additions, it should be based on master.
  • If applicable: Created new issues if this PR does not fix the issue completely/there is further work to be done
  • If it's not obvious from a linked issue, described how to interact with NAV in order for a reviewer to observe the effects of this change first-hand (commands, URLs, UI interactions)
  • If this results in changes in the UI: Added screenshots of the before and after
  • If this adds a new Python source code file: Added the boilerplate header to that file

@github-actions
Copy link

github-actions bot commented Oct 29, 2025

Test results

   11 files     11 suites   12m 21s ⏱️
2 691 tests 2 689 ✅ 0 💤 2 ❌
8 570 runs  8 564 ✅ 0 💤 6 ❌

For more details on these failures, see this check.

Results for commit 143c621.

♻️ This comment has been updated with latest results.

@codecov
Copy link

codecov bot commented Oct 29, 2025

Codecov Report

❌ Patch coverage is 83.13253% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.82%. Comparing base (5eaae6b) to head (143c621).
⚠️ Report is 6 commits behind head on master.

Files with missing lines Patch % Lines
python/nav/web/navlets/vlangraph.py 0.00% 4 Missing ⚠️
python/nav/web/navlets/__init__.py 93.75% 3 Missing ⚠️
python/nav/web/navlets/graph.py 0.00% 3 Missing ⚠️
python/nav/web/navlets/status2.py 50.00% 2 Missing ⚠️
python/nav/web/navlets/feedreader.py 0.00% 1 Missing ⚠️
python/nav/web/navlets/report.py 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3634      +/-   ##
==========================================
+ Coverage   62.81%   62.82%   +0.01%     
==========================================
  Files         611      611              
  Lines       45125    45162      +37     
  Branches       43       43              
==========================================
+ Hits        28344    28374      +30     
- Misses      16771    16778       +7     
  Partials       10       10              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Simrayz Simrayz force-pushed the chore/refactor-dashboard-navlets-to-use-htmx branch 3 times, most recently from 0303f9b to 577b2c8 Compare October 30, 2025 11:36
@Simrayz Simrayz force-pushed the chore/refactor-dashboard-navlets-to-use-htmx branch from 577b2c8 to 79f0b28 Compare October 30, 2025 12:34
@Simrayz Simrayz requested a review from a team October 30, 2025 14:21
@Simrayz Simrayz force-pushed the chore/refactor-dashboard-navlets-to-use-htmx branch from 79f0b28 to c9d6286 Compare October 30, 2025 14:41
@Simrayz Simrayz force-pushed the chore/refactor-dashboard-navlets-to-use-htmx branch from c9d6286 to 20865d9 Compare October 30, 2025 14:43
Copy link
Contributor

@hmpf hmpf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add one commit that fixes the get's and post's of all the TemplateView desecendants to also have *args (or *_) and **kwargs (or **__) even if they don use them. Devs used to Django expect it.

raise NotImplementedError

def get_template_names(self):
def get_template_names(self, override_mode=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put in the class docstring what override_mode is used for.

return context

def post(self, _request, **kwargs):
def post(self, request, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change signature to def post(self, request, *_, **kwargs):, due to Liskov substitution principle: as this is now it's a TemplateView that breaks the rules of a TemplateView.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoa, many old sins in this code, Liskov-wise...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 108 to 111
@property
def mode(self):
"""Fetch the mode of this request"""
return self.request.GET.get('mode', NAVLET_MODE_VIEW)
Copy link
Contributor

@hmpf hmpf Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing viewclass.as_view() to spit out two different classes depending on mode might be better, but is out of scope. Django-style would be to have the mode as part of the url (`path("something/str:mode", ..) and two different views but that's even more out of scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. The Navlets code is quite strange, and really we should separate GET and POST at least. I opted not to dig too much into the Navlets in this round, and leave that for later.

Comment on lines 60 to 62
url = self.preferences.get('url')
timestamp = int(datetime.now().timestamp())
context['graph_url'] = f'{url}&bust={timestamp}'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cache-busting? Add a comment about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bust parameter was defined here.

if (this.navlet.image_reload) {
this.refresh = setInterval(function () {
// Find image each time because of async loading
var image = that.node.find('img[data-image-reload], [data-image-reload] img');
if (image.length) {
// Add bust parameter to url to prevent caching
var uri = new URI(image.get(0)).setSearch('bust', Math.random());
image.attr('src', uri.href());
}
}, preferences.refresh_interval);
} else if (this.navlet.ajax_reload) {

I created a new staticmethod on the Navlet class, add_bust_param_to_url

Comment on lines 50 to 51
timestamp = int(datetime.now().timestamp())
context['graph_url'] = f'{url}&bust={timestamp}'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Twice now.. should we have a cache-buster function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a method on the Navlet class

Comment on lines +8 to +10
hx-post="{% url 'add-user-navlet' dashboard_id %}"
hx-swap="beforeend"
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if you keep action and post in addition to what's here now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The native form submit event is overruled by HTMX, so adding them would make no difference. Action is only used for html form submission, or by certain javascript submit handlers across the codebase.

@Simrayz Simrayz force-pushed the chore/refactor-dashboard-navlets-to-use-htmx branch from 06a86d6 to 4418f4c Compare October 31, 2025 10:45
@Simrayz Simrayz force-pushed the chore/refactor-dashboard-navlets-to-use-htmx branch from 4418f4c to 143c621 Compare November 3, 2025 09:32
@sonarqubecloud
Copy link

sonarqubecloud bot commented Nov 3, 2025

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor dashboard navlets to use HTMX

3 participants