Skip to content

Commit b9c6d2a

Browse files
committed
[fix] Address coderabbit comments and fix tests
1 parent d960b84 commit b9c6d2a

5 files changed

Lines changed: 111 additions & 73 deletions

File tree

openwisp_monitoring/device/static/monitoring/css/device-map.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@
192192
.table-container.is-loading-append .table-spinner {
193193
top: var(--table-spinner-top) !important;
194194
left: 50% !important;
195-
transform: translateY(-50%);
195+
transform: translate(-50%, -50%);
196196
}
197197
.no-devices {
198198
font-weight: bold;

openwisp_monitoring/device/static/monitoring/js/device-map.js

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -121,30 +121,30 @@
121121
}
122122

123123
popupContent = `
124-
<div class="map-detail">
125-
<h2>${escapeHtml(popupTitle)} (${data.count})</h2>
126-
<div class="input-container">
127-
<input id="device-search" placeholder="${gettext("Search for devices")}" />
128-
</div>
129-
<div class="label-container">
130-
${statusFilterButtons}
131-
<input id="status-filter" type="hidden" />
132-
</div>
133-
<div class="table-container">
134-
<table>
135-
<thead>
136-
<tr>
137-
<th>${gettext("name")}</th>
138-
<th class="th-status"><span class ="health-status-heading">${gettext("status")}</span></th>
139-
</tr>
140-
</thead>
141-
<tbody>${renderRows(netjsongraphInstance)}</tbody>
142-
</table>
143-
<div class="ow-loading-spinner table-spinner"></div>
144-
</div>
145-
${floorplan_btn}
146-
</div>
147-
`;
124+
<div class="map-detail">
125+
<h2>${escapeHtml(popupTitle)} (${data.count})</h2>
126+
<div class="input-container">
127+
<input id="device-search" placeholder="${gettext("Search for devices")}" />
128+
</div>
129+
<div class="label-container">
130+
${statusFilterButtons}
131+
<input id="status-filter" type="hidden" />
132+
</div>
133+
<div class="table-container">
134+
<table>
135+
<thead>
136+
<tr>
137+
<th>${gettext("name")}</th>
138+
<th class="th-status"><span class ="health-status-heading">${gettext("status")}</span></th>
139+
</tr>
140+
</thead>
141+
<tbody>${renderRows(devices)}</tbody>
142+
</table>
143+
<div class="ow-loading-spinner table-spinner"></div>
144+
</div>
145+
${floorplan_btn}
146+
</div>
147+
`;
148148
loadingOverlay.hide();
149149
return popupContent;
150150
} catch (error) {
@@ -155,9 +155,7 @@
155155
}
156156
}
157157

158-
function renderRows(netjsongraphInstance, deviceList) {
159-
deviceList = deviceList || netjsongraphInstance.leaflet._popupState.devices;
160-
const popup = $(".map-detail");
158+
function renderRows(deviceList) {
161159
if (deviceList.length === 0) {
162160
const emptyRow = `
163161
<tr>
@@ -166,7 +164,6 @@
166164
</td>
167165
</tr>
168166
`;
169-
popup.find("tbody").html(emptyRow);
170167
return emptyRow;
171168
}
172169
const rows = deviceList
@@ -183,7 +180,6 @@
183180
`,
184181
)
185182
.join("");
186-
popup.find("tbody").html(rows);
187183
return rows;
188184
}
189185

@@ -198,10 +194,15 @@
198194
netjsongraphInstance?.leaflet?._popupState;
199195
const el = $(currentPopup.getElement());
200196
let fetchDevicesTimeout;
197+
let activeRequest = null;
201198
let loading = false;
202199
function fetchDevices(url, ms = 0, append) {
203-
if (!url || loading) return;
200+
if (!url) return;
201+
if (append && loading) return;
204202
clearTimeout(fetchDevicesTimeout);
203+
if (!append && activeRequest) {
204+
activeRequest.abort();
205+
}
205206
loading = true;
206207
const container = el.find(".table-container");
207208
const spinner = el.find(".table-spinner");
@@ -239,7 +240,7 @@
239240
} else {
240241
fetchUrl = queryString ? `${url}?${queryString}` : url;
241242
}
242-
$.ajax({
243+
activeRequest = $.ajax({
243244
dataType: "json",
244245
url: fetchUrl,
245246
xhrFields: { withCredentials: true },
@@ -250,14 +251,16 @@
250251
devices = data.results;
251252
}
252253
nextUrl = data.next;
253-
renderRows(netjsongraphInstance, devices);
254+
el.find("tbody").html(renderRows(devices));
254255
if (!append) container.scrollTop(0);
255256
},
256-
error() {
257+
error(_jqXHR, textStatus) {
258+
if (textStatus === "abort") return;
257259
console.error(gettext("Could not load more devices from"), url);
258260
alert(gettext("Could not load more devices."));
259261
},
260262
complete() {
263+
activeRequest = null;
261264
loading = false;
262265
spinner.hide();
263266
container.removeClass("is-loading");
@@ -284,7 +287,7 @@
284287
btn.addClass("active");
285288
activeStatuses.push(status);
286289
}
287-
$(`#status-filter`).val(activeStatuses.join(","));
290+
el.find("#status-filter").val(activeStatuses.join(","));
288291
fetchDevices(url);
289292
});
290293
el.find(".table-container").on("scroll", function () {
@@ -297,6 +300,8 @@
297300
window.openFloorPlan(floorplanUrl, locationId);
298301
});
299302
currentPopup.on("remove", () => {
303+
clearTimeout(fetchDevicesTimeout);
304+
activeRequest?.abort();
300305
netjsongraphInstance.leaflet._popupState = null;
301306
});
302307
}
@@ -441,7 +446,6 @@
441446
map.leaflet.addControl(new L.control.scale(scale));
442447
}
443448

444-
445449
try {
446450
const features = (map.data && map.data.features) || [];
447451
if (features.length) {

openwisp_monitoring/device/static/monitoring/js/floorplan.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
selectedIndex: 0,
3232
isFullScreen: false,
3333
// Used to ignore late ajax responses from older sessions.
34-
_sessionId: `${Date.now()}_${Math.random()}`,
34+
_sessionId: window.crypto.randomUUID(),
3535
popstateHandler: null,
3636
hashchangeHandler: null,
3737
};
@@ -140,7 +140,10 @@
140140
setFloorplanState(floorplanState);
141141
}
142142

143-
async function openFloorPlan(url, id = null, floor = null) {
143+
async function openFloorPlan(url, id, floor = null) {
144+
if (id == null) {
145+
throw new Error("openFloorPlan requires a locationId");
146+
}
144147
if (document.getElementById("floorplan-overlay")) {
145148
destroyFloorplan();
146149
}
@@ -290,7 +293,27 @@
290293
floorplanState.maps[floorplanState.state.currentFloor].utils.removeUrlFragment(
291294
id,
292295
);
296+
return;
293297
}
298+
299+
// Fallback: if the overlay is closed before the indoor map instance is
300+
// initialized, ensure any indoor fragment is removed from the URL so a
301+
// reload/back-navigation doesn't reopen the overlay unexpectedly.
302+
const raw = window.location.hash.replace(/^#/, "");
303+
if (!raw) return;
304+
const fragments = decodeURIComponent(raw)
305+
.split(";")
306+
.map((f) => f.trim())
307+
.filter(Boolean);
308+
if (!fragments.length) return;
309+
const kept = fragments.filter((fragment) => {
310+
const params = new URLSearchParams(fragment);
311+
const id = params.get("id");
312+
return id === "dashboard-geo-map";
313+
});
314+
const nextHash = kept.length ? `#${encodeURIComponent(kept.join(";"))}` : "";
315+
const nextUrl = `${window.location.pathname}${window.location.search}${nextHash}`;
316+
window.history.replaceState(null, "", nextUrl);
294317
}
295318

296319
function addFloorButtons() {
@@ -476,6 +499,9 @@
476499
async onReady() {
477500
const floorplanState = getFloorplanState();
478501
if (!floorplanState?.state) return;
502+
// Guard against stale async continuation if the overlay is closed or a
503+
// newer floorplan session replaces the current one while awaiting.
504+
const sessionId = floorplanState._sessionId;
479505
const map = this.leaflet;
480506
floorplanState.maps[floor] = indoorMap;
481507
setFloorplanState(floorplanState);
@@ -491,6 +517,16 @@
491517
$(".floorplan-loading-spinner").hide();
492518
return;
493519
}
520+
521+
const latestState = getFloorplanState();
522+
if (
523+
!latestState?.state ||
524+
latestState._sessionId !== sessionId ||
525+
latestState.maps[floor] !== indoorMap
526+
) {
527+
// Don't touch map/echarts/DOM: this continuation is stale.
528+
return;
529+
}
494530
let initialZoom;
495531
const h = img.height;
496532
const w = h * (img.width / img.height);

openwisp_monitoring/device/static/monitoring/js/lib/netjsongraph.min.js

Lines changed: 1 addition & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

openwisp_monitoring/tests/test_selenium.py

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ class TestDashboardMap(
322322
floorplan_model = Floorplan
323323
object_location_model = DeviceLocation
324324
config_app_label = "config"
325+
retry_max = 1
325326

326327
def _open_popup(self, mapType, id):
327328
self.web_driver.execute_script(
@@ -330,6 +331,27 @@ def _open_popup(self, mapType, id):
330331
str(id),
331332
)
332333

334+
def _wait_for_popup_table_ready(self, timeout=5):
335+
# The popup keeps the table mounted during replace/append fetches, so
336+
# these loading-state classes are the most reliable signal that row
337+
# updates have finished.
338+
try:
339+
WebDriverWait(self.web_driver, timeout).until(
340+
lambda d: all(
341+
cls
342+
not in (
343+
d.find_element(
344+
By.CSS_SELECTOR, ".map-detail .table-container"
345+
).get_attribute("class")
346+
or ""
347+
)
348+
for cls in ("is-loading", "is-loading-append")
349+
)
350+
)
351+
except TimeoutException as e:
352+
print(self.get_browser_logs())
353+
self.fail(f"Popup table did not finish loading within {timeout}s: {e}")
354+
333355
def test_features_on_device_popup(self):
334356
org = self._get_org()
335357
d1 = self._create_device(
@@ -368,9 +390,7 @@ def test_features_on_device_popup(self):
368390
self.assertFalse(status_ok_close_btn.is_displayed())
369391
status_ok.click()
370392
self.assertTrue(status_ok_close_btn.is_displayed())
371-
self.wait_for_invisibility(
372-
By.CSS_SELECTOR, ".map-detail .ow-loading-spinner"
373-
)
393+
self._wait_for_popup_table_ready()
374394
table_entries = self.find_elements(By.CSS_SELECTOR, ".map-detail tbody tr")
375395
self.assertEqual(len(table_entries), 1)
376396
self.assertIn(d2.name, table_entries[0].text)
@@ -385,35 +405,27 @@ def test_features_on_device_popup(self):
385405
self.assertFalse(status_unknown_close_btn.is_displayed())
386406
status_unknown.click()
387407
self.assertTrue(status_unknown_close_btn.is_displayed())
388-
self.wait_for_invisibility(
389-
By.CSS_SELECTOR, ".map-detail .ow-loading-spinner"
390-
)
408+
self._wait_for_popup_table_ready()
391409
table_entries = self.find_elements(By.CSS_SELECTOR, ".map-detail tbody tr")
392410
self.assertEqual(len(table_entries), 2)
393411

394412
with self.subTest("Test removing filters by clicking close button"):
395413
status_ok_close_btn.click()
396414
self.assertFalse(status_ok_close_btn.is_displayed())
397-
self.wait_for_invisibility(
398-
By.CSS_SELECTOR, ".map-detail .ow-loading-spinner", timeout=5
399-
)
415+
self._wait_for_popup_table_ready()
400416
table_entries = self.find_elements(By.CSS_SELECTOR, ".map-detail tbody tr")
401417
self.assertEqual(len(table_entries), 1)
402418
self.assertIn(d1.name, table_entries[0].text)
403419
status_unknown.click()
404420
self.assertFalse(status_unknown_close_btn.is_displayed())
405-
self.wait_for_invisibility(
406-
By.CSS_SELECTOR, ".map-detail .ow-loading-spinner"
407-
)
421+
self._wait_for_popup_table_ready()
408422
table_entries = self.find_elements(By.CSS_SELECTOR, ".map-detail tbody tr")
409423
self.assertEqual(len(table_entries), 2)
410424

411425
with self.subTest("Test search field"):
412426
input_field = self.find_element(By.CSS_SELECTOR, "#device-search")
413427
input_field.send_keys("device1")
414-
self.wait_for_invisibility(
415-
By.CSS_SELECTOR, ".map-detail .ow-loading-spinner"
416-
)
428+
self._wait_for_popup_table_ready()
417429
table_entries = self.find_elements(By.CSS_SELECTOR, ".map-detail tbody tr")
418430
self.assertEqual(len(table_entries), 1)
419431
self.assertIn(d1.name, table_entries[0].text)
@@ -422,17 +434,13 @@ def test_features_on_device_popup(self):
422434
input_field.clear()
423435
# Just clearing the input field does not trigger the event listeners
424436
input_field.send_keys(" ")
425-
self.wait_for_invisibility(
426-
By.CSS_SELECTOR, ".map-detail .ow-loading-spinner"
427-
)
437+
self._wait_for_popup_table_ready()
428438
table_entries = self.find_elements(By.CSS_SELECTOR, ".map-detail tbody tr")
429439
self.assertEqual(len(table_entries), 2)
430440

431441
with self.subTest("Test filtering to get no results"):
432442
input_field.send_keys("Non-Existent-Device")
433-
self.wait_for_invisibility(
434-
By.CSS_SELECTOR, ".map-detail .ow-loading-spinner"
435-
)
443+
self._wait_for_popup_table_ready()
436444
table_entries = self.find_elements(By.CSS_SELECTOR, ".map-detail tbody tr")
437445
self.assertEqual(len(table_entries), 1)
438446
self.assertIn("No devices found", table_entries[0].text)
@@ -467,7 +475,9 @@ def test_infinite_scroll_on_popup(self):
467475
self.web_driver.execute_script(
468476
"arguments[0].scrollTop = arguments[0].scrollHeight", table_container
469477
)
470-
self.wait_for_invisibility(By.CSS_SELECTOR, ".map-detail .ow-loading-spinner")
478+
# Allow scroll animation to trigger infinite scroll fetch.
479+
sleep(0.3)
480+
self._wait_for_popup_table_ready()
471481
table_entries = self.find_elements(By.CSS_SELECTOR, ".map-detail tbody tr")
472482
self.assertEqual(len(table_entries), 20)
473483

0 commit comments

Comments
 (0)