[Perl] DAQ system for the FAPG.
refactor adopt additional Bootstrap component patterns
- Probe card reading status uses placeholder-glow skeleton loading state until data arrives, then removes the placeholder classes - Download buttons wrapped in d-flex gap-2 for proper spacing - Custom date fields use Bootstrap Collapse component with animated show/hide instead of the hidden attribute - Footer uses bg-body-tertiary and border-top (standard Bootstrap) instead of custom .site-footer class - Probe card dropdown button gets dropdown-toggle class for the standard caret indicator - Range control replaced from btn-group with disabled button to input-group with input-group-text label between the +/- buttons
Changed files
- roles/dashboard/public/js/dashboard/charts.js
- roles/dashboard/public/js/downloads.js
- roles/dashboard/t/06-downloads.t
- roles/dashboard/templates/dashboard/downloads.html.ep
- roles/dashboard/templates/dashboard/graph-control.html.ep
- roles/dashboard/templates/dashboard/probe-card.html.ep
- roles/dashboard/templates/layouts/default.html.ep
roles/dashboard/public/js/dashboard/charts.js
@@ -285,6 +285,7 @@
285
285
}
286
286
287
287
if (status) {
288
Added:
status.classList.remove("placeholder-glow");
288
289
status.textContent = latest
289
290
? `Latest: ${latest.value} ${latest.unit || ""} at ${formatDateTime(latest.timestamp)} (${formatAge(ageMs(latest.timestamp))})`
290
291
: "No readings found for this probe yet.";
@@ -294,7 +295,10 @@
294
295
} catch (error) {
295
296
if (error.name === "AbortError") return;
296
297
297
Removed:
if (status) status.textContent = `Could not load ${probe} readings: ${error.message}`;
298
Added:
if (status) {
299
Added:
status.classList.remove("placeholder-glow");
300
Added:
status.textContent = `Could not load ${probe} readings: ${error.message}`;
301
Added:
}
298
302
renderMessageTable(probe, []);
299
303
}
300
304
}
roles/dashboard/public/js/downloads.js
@@ -1,5 +1,6 @@
1
1
const downloadRangeButtons = Array.from(document.querySelectorAll("[data-download-range]"));
2
2
const customDateFields = document.querySelector("[data-custom-date-fields]");
3
Added:
const customDateCollapse = customDateFields ? new bootstrap.Collapse(customDateFields, { toggle: false }) : null;
3
4
const downloadFrom = document.querySelector("#download-from");
4
5
const downloadTo = document.querySelector("#download-to");
5
6
const errorBanner = document.querySelector("[data-downloads-error]");
@@ -10,8 +11,8 @@
10
11
option.addEventListener("change", () => {
11
12
const custom = option.value === "custom";
12
13
13
Removed:
if (customDateFields) {
14
Removed:
customDateFields.hidden = !custom;
14
Added:
if (customDateCollapse) {
15
Added:
custom ? customDateCollapse.show() : customDateCollapse.hide();
15
16
}
16
17
17
18
if (custom) {
roles/dashboard/t/06-downloads.t
@@ -23,9 +23,9 @@
23
23
->element_exists('[data-downloads-error]')
24
24
->element_exists('input[name="date_range"][value="week"][checked]')
25
25
->element_exists(
26
Removed:
'[data-custom-date-fields][hidden] input[name="from"][type="date"]')
26
Added:
'[data-custom-date-fields].collapse:not(.show) input[name="from"][type="date"]')
27
27
->element_exists(
28
Removed:
'[data-custom-date-fields][hidden] input[name="to"][type="date"]')
28
Added:
'[data-custom-date-fields].collapse:not(.show) input[name="to"][type="date"]')
29
29
->text_like( 'button.downloads-button[type="submit"]', qr/Download CSV/ )
30
30
->element_exists(
31
31
'button.downloads-button[type="submit"][data-download-button] [data-download-spinner].d-none')
roles/dashboard/templates/dashboard/downloads.html.ep
@@ -45,19 +45,21 @@
45
45
<label class="btn btn-outline-secondary" for="range-custom">Custom</label>
46
46
</div>
47
47
48
Removed:
<div class="d-flex flex-wrap gap-3 mt-3" data-custom-date-fields hidden>
49
Removed:
<div>
50
Removed:
<label class="form-label fw-bold" for="download-from">From</label>
51
Removed:
<input class="form-control" id="download-from" name="from" type="date" value="<%= $from %>" required>
48
Added:
<div class="collapse mt-3" id="custom-date-fields" data-custom-date-fields>
49
Added:
<div class="d-flex flex-wrap gap-3">
50
Added:
<div>
51
Added:
<label class="form-label fw-bold" for="download-from">From</label>
52
Added:
<input class="form-control" id="download-from" name="from" type="date" value="<%= $from %>" required>
53
Added:
</div>
54
Added:
<div>
55
Added:
<label class="form-label fw-bold" for="download-to">To</label>
56
Added:
<input class="form-control" id="download-to" name="to" type="date" value="<%= $to %>" required>
57
Added:
</div>
52
58
</div>
53
Removed:
<div>
54
Removed:
<label class="form-label fw-bold" for="download-to">To</label>
55
Removed:
<input class="form-control" id="download-to" name="to" type="date" value="<%= $to %>" required>
56
Removed:
</div>
57
59
</div>
58
60
</fieldset>
59
61
60
Removed:
<div>
62
Added:
<div class="d-flex gap-2">
61
63
<button class="btn btn-success downloads-button" type="submit" data-download-button>
62
64
<span class="spinner-border spinner-border-sm d-none" aria-hidden="true" data-download-spinner></span>
63
65
Download CSV
roles/dashboard/templates/dashboard/graph-control.html.ep
@@ -2,20 +2,18 @@
2
2
3
3
<div class="graph-control-scroll overflow-x-auto">
4
4
<div class="graph-control d-inline-flex gap-2 flex-nowrap" aria-label="<%= $aria_label %>">
5
Removed:
<div class="btn-group range-control" role="group" aria-label="Adjust range">
5
Added:
<div class="input-group input-group-sm range-control" aria-label="Adjust range">
6
6
<button
7
Removed:
class="btn btn-sm btn-outline-secondary"
7
Added:
class="btn btn-outline-secondary"
8
8
type="button"
9
9
data-range-adjustment="decrease"
10
10
aria-label="Decrease range">−</button>
11
Removed:
<button
12
Removed:
class="btn btn-sm btn-outline-secondary range-label"
13
Removed:
type="button"
11
Added:
<span
12
Added:
class="input-group-text range-label"
14
13
data-range-label
15
Removed:
disabled
16
Removed:
aria-live="polite">1</button>
14
Added:
aria-live="polite">1</span>
17
15
<button
18
Removed:
class="btn btn-sm btn-outline-secondary"
16
Added:
class="btn btn-outline-secondary"
19
17
type="button"
20
18
data-range-adjustment="increase"
21
19
aria-label="Increase range">+</button>
roles/dashboard/templates/dashboard/probe-card.html.ep
@@ -13,7 +13,7 @@
13
13
</div>
14
14
<div class="dropdown flex-shrink-0">
15
15
<button
16
Removed:
class="btn btn-sm btn-outline-secondary"
16
Added:
class="btn btn-sm btn-outline-secondary dropdown-toggle"
17
17
type="button"
18
18
data-bs-toggle="dropdown"
19
19
aria-expanded="false"
@@ -39,8 +39,8 @@
39
39
<canvas id="chart-<%= $probe->{key} %>"></canvas>
40
40
</div>
41
41
% unless (stash('single_probe')) {
42
Removed:
<p class="text-muted small mt-2 mb-0 reading-status" data-status="<%= $probe->{key} %>">
43
Removed:
Waiting for data…
42
Added:
<p class="text-muted small mt-2 mb-0 reading-status placeholder-glow" data-status="<%= $probe->{key} %>">
43
Added:
<span class="placeholder col-8">Waiting for data…</span>
44
44
</p>
45
45
% }
46
46
</section>
roles/dashboard/templates/layouts/default.html.ep
@@ -78,7 +78,7 @@
78
78
<%= content %>
79
79
</main>
80
80
81
Removed:
<footer class="site-footer text-center text-muted small py-3 mt-4">
81
Added:
<footer class="text-center text-muted small py-3 mt-4 bg-body-tertiary border-top">
82
82
<p class="mb-0">
83
83
<a class="text-muted" href="https://www.lafermeaquaponique.com/">lafermeaquaponique.com</a>
84
84
</p>