Merge pull request 'feat(alerts): live refresh table with toggle + severity colour cues' (#11) from alerts-live-refresh into main
Reviewed-on: #11
This commit is contained in:
@@ -18,6 +18,7 @@ type alertsPage struct {
|
|||||||
Alerts []store.Alert
|
Alerts []store.Alert
|
||||||
Counts alertCounts
|
Counts alertCounts
|
||||||
HostNames map[string]string // host_id → name for table rendering
|
HostNames map[string]string // host_id → name for table rendering
|
||||||
|
RefreshURL string // self-URL for the live-refresh poll
|
||||||
}
|
}
|
||||||
|
|
||||||
type alertCounts struct {
|
type alertCounts struct {
|
||||||
@@ -51,7 +52,12 @@ func (s *Server) handleUIAlerts(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
page := alertsPage{Filter: f, Alerts: alerts, HostNames: map[string]string{}}
|
page := alertsPage{
|
||||||
|
Filter: f,
|
||||||
|
Alerts: alerts,
|
||||||
|
HostNames: map[string]string{},
|
||||||
|
RefreshURL: r.URL.RequestURI(),
|
||||||
|
}
|
||||||
if hosts, err := s.deps.Store.ListHosts(r.Context()); err == nil {
|
if hosts, err := s.deps.Store.ListHosts(r.Context()); err == nil {
|
||||||
for _, h := range hosts {
|
for _, h := range hosts {
|
||||||
page.HostNames[h.ID] = h.Name
|
page.HostNames[h.ID] = h.Name
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -56,14 +56,17 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{/* severity dropdown */}}
|
{{/* severity dropdown — option text tinted to match the colour
|
||||||
|
already used in the row (dot, left border, kind chip). The
|
||||||
|
severity word is otherwise invisible to operators because the
|
||||||
|
table column shows kind only; the colour bridges the two. */}}
|
||||||
<div>
|
<div>
|
||||||
<select class="field" style="padding: 6px 10px; font-size: 11.5px; min-width: 130px;"
|
<select class="field" style="padding: 6px 10px; font-size: 11.5px; min-width: 130px;"
|
||||||
onchange="window.location='/alerts?status={{$filter.Status}}&severity='+this.value+'{{if $filter.HostID}}&host_id={{$filter.HostID}}{{end}}{{if $filter.Search}}&q={{$filter.Search}}{{end}}'">
|
onchange="window.location='/alerts?status={{$filter.Status}}&severity='+this.value+'{{if $filter.HostID}}&host_id={{$filter.HostID}}{{end}}{{if $filter.Search}}&q={{$filter.Search}}{{end}}'">
|
||||||
<option value="" {{if eq $filter.Severity ""}}selected{{end}}>Severity · any</option>
|
<option value="" {{if eq $filter.Severity ""}}selected{{end}}>Severity · any</option>
|
||||||
<option value="info" {{if eq $filter.Severity "info"}}selected{{end}}>info</option>
|
<option value="info" style="color: oklch(0.78 0.005 250);" {{if eq $filter.Severity "info"}}selected{{end}}>● info</option>
|
||||||
<option value="warning" {{if eq $filter.Severity "warning"}}selected{{end}}>warning</option>
|
<option value="warning" style="color: oklch(0.82 0.13 80);" {{if eq $filter.Severity "warning"}}selected{{end}}>● warning</option>
|
||||||
<option value="critical" {{if eq $filter.Severity "critical"}}selected{{end}}>critical</option>
|
<option value="critical" style="color: oklch(0.70 0.20 25);" {{if eq $filter.Severity "critical"}}selected{{end}}>● critical</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -90,8 +93,18 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{/* alerts table */}}
|
{{/* alerts table — polled every 5s when the tab is visible AND the
|
||||||
<div class="panel mt-3.5 rounded-[7px] overflow-hidden">
|
live toggle is on. The localStorage check is part of the htmx
|
||||||
|
trigger predicate, so flipping the toggle just sets the flag and
|
||||||
|
the next tick (or the absence of one) honours it. No need to
|
||||||
|
re-process the element when the toggle changes.
|
||||||
|
The polling lives on this div (not the page root) so the filter
|
||||||
|
strip and header don't flash on each tick. */}}
|
||||||
|
<div id="alerts-table" class="panel mt-3.5 rounded-[7px] overflow-hidden"
|
||||||
|
hx-get="{{$page.RefreshURL}}"
|
||||||
|
hx-trigger="every 5s [document.visibilityState==='visible' && localStorage.getItem('rm-alerts-live')!=='off']"
|
||||||
|
hx-select="#alerts-table"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
|
||||||
{{/* header row */}}
|
{{/* header row */}}
|
||||||
<div class="alert-row head">
|
<div class="alert-row head">
|
||||||
@@ -101,7 +114,16 @@
|
|||||||
<div>Message</div>
|
<div>Message</div>
|
||||||
<div>Raised</div>
|
<div>Raised</div>
|
||||||
<div>Last seen</div>
|
<div>Last seen</div>
|
||||||
<div></div>
|
<div style="display: flex; align-items: center; gap: 6px; justify-content: flex-end;">
|
||||||
|
<label style="display: inline-flex; align-items: center; gap: 5px; cursor: pointer; font-size: 10px;"
|
||||||
|
class="text-ink-fade" title="auto-refresh every 5s">
|
||||||
|
<input type="checkbox" id="alerts-live-toggle" checked
|
||||||
|
onchange="localStorage.setItem('rm-alerts-live', this.checked ? 'on' : 'off'); document.getElementById('alerts-live-dot').style.opacity = this.checked ? '1' : '0.3';"
|
||||||
|
style="width: 11px; height: 11px; cursor: pointer; margin: 0;" />
|
||||||
|
<span>live</span>
|
||||||
|
<span id="alerts-live-dot" class="text-accent">●</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if eq (len $page.Alerts) 0}}
|
{{if eq (len $page.Alerts) 0}}
|
||||||
@@ -126,4 +148,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Restore the live-refresh toggle from localStorage so the operator's
|
||||||
|
// last choice survives full-page navigations. Re-runs after every htmx
|
||||||
|
// swap so the freshly-rendered checkbox + dot stay in sync.
|
||||||
|
(function syncLiveToggle() {
|
||||||
|
var on = localStorage.getItem('rm-alerts-live') !== 'off';
|
||||||
|
var cb = document.getElementById('alerts-live-toggle');
|
||||||
|
var dot = document.getElementById('alerts-live-dot');
|
||||||
|
if (cb) cb.checked = on;
|
||||||
|
if (dot) dot.style.opacity = on ? '1' : '0.3';
|
||||||
|
})();
|
||||||
|
document.body.addEventListener('htmx:afterSwap', function(e) {
|
||||||
|
if (e.detail.target && e.detail.target.id === 'alerts-table') {
|
||||||
|
var on = localStorage.getItem('rm-alerts-live') !== 'off';
|
||||||
|
var cb = document.getElementById('alerts-live-toggle');
|
||||||
|
var dot = document.getElementById('alerts-live-dot');
|
||||||
|
if (cb) cb.checked = on;
|
||||||
|
if (dot) dot.style.opacity = on ? '1' : '0.3';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user