Refactored time filter.

This commit is contained in:
Magnus Åhall 2024-07-04 13:29:39 +02:00
parent 4c908f4891
commit 9700bc9d3c
16 changed files with 252 additions and 296 deletions

View File

@ -63,7 +63,7 @@ func applyTimeOffset(t time.Time, duration time.Duration, amountStr string) time
return t.Add(duration * time.Duration(amount)) return t.Add(duration * time.Duration(amount))
} // }}} } // }}}
func presetTimeInterval(duration time.Duration, presetStr string, timeFrom, timeTo *time.Time) () { func presetTimeInterval(duration time.Duration, presetStr string, timeFrom, timeTo *time.Time) () {// {{{
if presetStr == "" { if presetStr == "" {
return return
} }
@ -73,4 +73,4 @@ func presetTimeInterval(duration time.Duration, presetStr string, timeFrom, time
(*timeFrom) = now.Add(duration * -1 * time.Duration(presetTime)) (*timeFrom) = now.Add(duration * -1 * time.Duration(presetTime))
(*timeTo) = now (*timeTo) = now
return return
} }// }}}

73
main.go
View File

@ -709,31 +709,14 @@ func pageDatapointValues(w http.ResponseWriter, r *http.Request, _ *session.T) {
return return
} }
// GET parameters. // Manage the values from the timefilter component
display := r.URL.Query().Get("display")
if display == "" && datapoint.Datatype == INT {
display = "graph"
}
var timeFrom, timeTo time.Time var timeFrom, timeTo time.Time
yesterday := time.Now().Add(time.Duration(-24 * time.Hour)) timeFrom, timeTo, err = timefilterParse(
timeFrom, err = parseHTMLDateTime(r.URL.Query().Get("f"), yesterday) r.URL.Query().Get("time-f"),
if err != nil { r.URL.Query().Get("time-t"),
httpError(w, werr.Wrap(err).WithData(r.URL.Query().Get("f")).Log()) r.URL.Query().Get("time-offset"),
return r.URL.Query().Get("time-preset"),
} )
timeTo, err = parseHTMLDateTime(r.URL.Query().Get("t"), time.Now())
if err != nil {
httpError(w, werr.Wrap(err).WithData(r.URL.Query().Get("t")).Log())
return
}
presetTimeInterval(time.Hour, r.URL.Query().Get("preset"), &timeFrom, &timeTo)
// Apply an optionally set offset (in seconds).
timeFrom = applyTimeOffset(timeFrom, time.Second, r.URL.Query().Get("offset-time"))
timeTo = applyTimeOffset(timeTo, time.Second, r.URL.Query().Get("offset-time"))
// Fetch data point values according to the times. // Fetch data point values according to the times.
var values []DatapointValue var values []DatapointValue
@ -743,6 +726,12 @@ func pageDatapointValues(w http.ResponseWriter, r *http.Request, _ *session.T) {
return return
} }
// GET parameters.
display := r.URL.Query().Get("display")
if display == "" && datapoint.Datatype == INT {
display = "graph"
}
page := Page{ page := Page{
LAYOUT: "main", LAYOUT: "main",
PAGE: "datapoint_values", PAGE: "datapoint_values",
@ -755,9 +744,11 @@ func pageDatapointValues(w http.ResponseWriter, r *http.Request, _ *session.T) {
page.Data = map[string]any{ page.Data = map[string]any{
"Datapoint": datapoint, "Datapoint": datapoint,
"Values": values, "Values": values,
"Display": display,
"TimeSubmit": "/datapoint/values/" + strconv.Itoa(datapoint.ID),
"TimeFrom": timeFrom.Format("2006-01-02T15:04:05"), "TimeFrom": timeFrom.Format("2006-01-02T15:04:05"),
"TimeTo": timeTo.Format("2006-01-02T15:04:05"), "TimeTo": timeTo.Format("2006-01-02T15:04:05"),
"Display": display,
} }
page.Render(w, r) page.Render(w, r)
return return
@ -770,14 +761,14 @@ func actionDatapointJson(w http.ResponseWriter, r *http.Request, _ *session.T) {
return return
} }
fromStr := r.URL.Query().Get("f") fromStr := r.URL.Query().Get("time-f")
from, err := time.ParseInLocation("2006-01-02 15:04:05", fromStr[0:min(19, len(fromStr))], smonConfig.Timezone()) from, err := time.ParseInLocation("2006-01-02 15:04:05", fromStr[0:min(19, len(fromStr))], smonConfig.Timezone())
if err != nil { if err != nil {
httpError(w, werr.Wrap(err).Log()) httpError(w, werr.Wrap(err).Log())
return return
} }
toStr := r.URL.Query().Get("t") toStr := r.URL.Query().Get("time-t")
to, err := time.ParseInLocation("2006-01-02 15:04:05", toStr[0:min(19, len(toStr))], smonConfig.Timezone()) to, err := time.ParseInLocation("2006-01-02 15:04:05", toStr[0:min(19, len(toStr))], smonConfig.Timezone())
if err != nil { if err != nil {
httpError(w, werr.Wrap(err).Log()) httpError(w, werr.Wrap(err).Log())
@ -1198,27 +1189,14 @@ func actionConfigurationNotificationDelete(w http.ResponseWriter, r *http.Reques
func pageNotifications(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ func pageNotifications(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
var err error var err error
// GET parameters. // Manage the values from the timefilter component
var timeFrom, timeTo time.Time var timeFrom, timeTo time.Time
lastWeek := time.Now().Add(time.Duration(-7 * 24 * time.Hour)) timeFrom, timeTo, err = timefilterParse(
r.URL.Query().Get("time-f"),
timeFrom, err = parseHTMLDateTime(r.URL.Query().Get("f"), lastWeek) r.URL.Query().Get("time-t"),
if err != nil { r.URL.Query().Get("time-offset"),
httpError(w, werr.Wrap(err).Log()) r.URL.Query().Get("time-preset"),
return )
}
timeTo, err = parseHTMLDateTime(r.URL.Query().Get("t"), time.Now())
if err != nil {
httpError(w, werr.Wrap(err).Log())
return
}
presetTimeInterval(time.Hour, r.URL.Query().Get("preset"), &timeFrom, &timeTo)
// Apply an optionally set offset (in seconds).
timeFrom = applyTimeOffset(timeFrom, time.Second, r.URL.Query().Get("offset-time"))
timeTo = applyTimeOffset(timeTo, time.Second, r.URL.Query().Get("offset-time"))
nss, err := notificationsSent(timeFrom, timeTo) nss, err := notificationsSent(timeFrom, timeTo)
if err != nil { if err != nil {
@ -1231,6 +1209,7 @@ func pageNotifications(w http.ResponseWriter, r *http.Request, _ *session.T) { /
CONFIG: smonConfig.Settings, CONFIG: smonConfig.Settings,
Data: map[string]any{ Data: map[string]any{
"Notifications": nss, "Notifications": nss,
"TimeSubmit": "/notifications",
"TimeFrom": timeFrom.Format("2006-01-02T15:04:05"), "TimeFrom": timeFrom.Format("2006-01-02T15:04:05"),
"TimeTo": timeTo.Format("2006-01-02T15:04:05"), "TimeTo": timeTo.Format("2006-01-02T15:04:05"),
}, },

1
sql/00023.sql Normal file
View File

@ -0,0 +1 @@
ALTER TABLE public.problem ADD COLUMN trigger_expression VARCHAR NOT NULL DEFAULT '';

View File

@ -99,25 +99,3 @@
.graph #graph-values { .graph #graph-values {
height: calc(100vh - 416px); height: calc(100vh - 416px);
} }
.time-offset {
display: grid;
grid-template-columns: min-content repeat(3, min-content);
grid-gap: 16px;
margin-top: 16px;
align-items: center;
justify-items: center;
}
.time-offset .header-1 {
font-weight: bold;
justify-self: start;
}
.time-offset .header-2 {
font-weight: bold;
justify-self: start;
grid-column: 2 / -1;
}
.time-offset .preset {
white-space: nowrap;
justify-self: start;
padding-right: 32px;
}

View File

@ -226,3 +226,37 @@ label {
width: min-content; width: min-content;
border-radius: 8px; border-radius: 8px;
} }
#time-selector {
display: grid;
grid-template-columns: min-content min-content;
grid-gap: 6px 16px;
width: min-content;
border-radius: 6px;
}
#time-selector button {
width: 100px;
margin-top: 12px;
justify-self: end;
}
#time-selector #time-filter {
display: grid;
grid-template-columns: min-content repeat(3, min-content);
grid-gap: 16px;
margin-top: 16px;
align-items: center;
justify-items: center;
}
#time-selector #time-filter .header-1 {
font-weight: bold;
justify-self: start;
}
#time-selector #time-filter .header-2 {
font-weight: bold;
justify-self: start;
grid-column: 2 / -1;
}
#time-selector #time-filter .preset {
white-space: nowrap;
justify-self: start;
padding-right: 32px;
}

View File

@ -1,37 +1,3 @@
#time-select {
display: grid;
grid-template-columns: min-content min-content;
grid-gap: 6px 16px;
width: min-content;
border-radius: 6px;
}
#time-select button {
width: 100px;
margin-top: 12px;
justify-self: end;
}
#time-select #time-offsets {
display: grid;
grid-template-columns: min-content repeat(3, min-content);
grid-gap: 16px;
margin-top: 16px;
align-items: center;
justify-items: center;
}
#time-select #time-offsets .header-1 {
font-weight: bold;
justify-self: start;
}
#time-select #time-offsets .header-2 {
font-weight: bold;
justify-self: start;
grid-column: 2 / -1;
}
#time-select #time-offsets .preset {
white-space: nowrap;
justify-self: start;
padding-right: 32px;
}
input[type="datetime-local"] { input[type="datetime-local"] {
padding: 6px; padding: 6px;
} }

View File

@ -99,25 +99,3 @@
.graph #graph-values { .graph #graph-values {
height: calc(100vh - 416px); height: calc(100vh - 416px);
} }
.time-offset {
display: grid;
grid-template-columns: min-content repeat(3, min-content);
grid-gap: 16px;
margin-top: 16px;
align-items: center;
justify-items: center;
}
.time-offset .header-1 {
font-weight: bold;
justify-self: start;
}
.time-offset .header-2 {
font-weight: bold;
justify-self: start;
grid-column: 2 / -1;
}
.time-offset .preset {
white-space: nowrap;
justify-self: start;
padding-right: 32px;
}

View File

@ -226,3 +226,37 @@ label {
width: min-content; width: min-content;
border-radius: 8px; border-radius: 8px;
} }
#time-selector {
display: grid;
grid-template-columns: min-content min-content;
grid-gap: 6px 16px;
width: min-content;
border-radius: 6px;
}
#time-selector button {
width: 100px;
margin-top: 12px;
justify-self: end;
}
#time-selector #time-filter {
display: grid;
grid-template-columns: min-content repeat(3, min-content);
grid-gap: 16px;
margin-top: 16px;
align-items: center;
justify-items: center;
}
#time-selector #time-filter .header-1 {
font-weight: bold;
justify-self: start;
}
#time-selector #time-filter .header-2 {
font-weight: bold;
justify-self: start;
grid-column: 2 / -1;
}
#time-selector #time-filter .preset {
white-space: nowrap;
justify-self: start;
padding-right: 32px;
}

View File

@ -1,37 +1,3 @@
#time-select {
display: grid;
grid-template-columns: min-content min-content;
grid-gap: 6px 16px;
width: min-content;
border-radius: 6px;
}
#time-select button {
width: 100px;
margin-top: 12px;
justify-self: end;
}
#time-select #time-offsets {
display: grid;
grid-template-columns: min-content repeat(3, min-content);
grid-gap: 16px;
margin-top: 16px;
align-items: center;
justify-items: center;
}
#time-select #time-offsets .header-1 {
font-weight: bold;
justify-self: start;
}
#time-select #time-offsets .header-2 {
font-weight: bold;
justify-self: start;
grid-column: 2 / -1;
}
#time-select #time-offsets .preset {
white-space: nowrap;
justify-self: start;
padding-right: 32px;
}
input[type="datetime-local"] { input[type="datetime-local"] {
padding: 6px; padding: 6px;
} }

View File

@ -1,13 +1,7 @@
function preset(hours) { function selectDisplay(display) {
const inputPreset = document.querySelector('input[name="preset"]') const inputDisplay = document.getElementById('input-display')
inputPreset.value = hours inputDisplay.value = display
inputPreset.form.submit() inputDisplay.form.submit()
}
function offsetTime(seconds) {
const inputPreset = document.querySelector('input[name="offset-time"]')
inputPreset.value = seconds
inputPreset.form.submit()
} }
class Graph { class Graph {
@ -61,7 +55,9 @@ class Dataset {
constructor(id, initialData) { constructor(id, initialData) {
this.datapointID = id this.datapointID = id
this.values = {} this.values = {}
initialData.forEach(v=>this.values[v.ID] = v) if (initialData === null)
return
initialData.forEach(v => this.values[v.ID] = v)
} }
xValues() { xValues() {
@ -76,7 +72,7 @@ class Dataset {
return fetch(`/datapoint/json/${this.datapointID}?f=${from}&t=${to}`) return fetch(`/datapoint/json/${this.datapointID}?f=${from}&t=${to}`)
.then(data => data.json()) .then(data => data.json())
.then(datapointValues => { .then(datapointValues => {
datapointValues.forEach(dp=>{ datapointValues.forEach(dp => {
this.values[dp.ID] = dp this.values[dp.ID] = dp
}) })
document.getElementById('num-values').innerText = Object.keys(this.values).length document.getElementById('num-values').innerText = Object.keys(this.values).length

View File

@ -120,30 +120,3 @@
height: calc(100vh - 416px); height: calc(100vh - 416px);
} }
} }
.time-offset {
display: grid;
grid-template-columns: min-content repeat(3, min-content);
grid-gap: 16px;
margin-top: 16px;
align-items: center;
justify-items: center;
.header-1 {
font-weight: bold;
justify-self: start;
}
.header-2 {
font-weight: bold;
justify-self: start;
grid-column: ~"2 / -1";
}
.preset {
white-space: nowrap;
justify-self: start;
padding-right: 32px;
}
}

View File

@ -280,3 +280,46 @@ label {
width: min-content; width: min-content;
border-radius: 8px; border-radius: 8px;
} }
#time-selector {
display: grid;
grid-template-columns: min-content min-content;
grid-gap: 6px 16px;
width: min-content;
border-radius: 6px;
button {
width: 100px;
margin-top: 12px;
justify-self: end;
}
#time-filter {
display: grid;
grid-template-columns: min-content repeat(3, min-content);
grid-gap: 16px;
margin-top: 16px;
align-items: center;
justify-items: center;
.header-1 {
font-weight: bold;
justify-self: start;
}
.header-2 {
font-weight: bold;
justify-self: start;
grid-column: ~"2 / -1";
}
.preset {
white-space: nowrap;
justify-self: start;
padding-right: 32px;
}
}
}

View File

@ -1,47 +1,5 @@
@import "theme-@{THEME}.less"; @import "theme-@{THEME}.less";
#time-select {
display: grid;
grid-template-columns: min-content min-content;
grid-gap: 6px 16px;
width: min-content;
border-radius: 6px;
button {
width: 100px;
margin-top: 12px;
justify-self: end;
}
#time-offsets {
display: grid;
grid-template-columns: min-content repeat(3, min-content);
grid-gap: 16px;
margin-top: 16px;
align-items: center;
justify-items: center;
.header-1 {
font-weight: bold;
justify-self: start;
}
.header-2 {
font-weight: bold;
justify-self: start;
grid-column: ~"2 / -1";
}
.preset {
white-space: nowrap;
justify-self: start;
padding-right: 32px;
}
}
}
input[type="datetime-local"] { input[type="datetime-local"] {
padding: 6px; padding: 6px;
} }

41
timefilter.go Normal file
View File

@ -0,0 +1,41 @@
package main
import (
// External
werr "git.gibonuddevalla.se/go/wrappederror"
// Standard
"time"
)
func timefilterParse(timeFromStr, timeToStr, offset, preset string) (timeFrom, timeTo time.Time, err error) {
if preset != "" {
presetTimeInterval(time.Hour, preset, &timeFrom, &timeTo)
return
}
yesterday := time.Now().Add(time.Duration(-24 * time.Hour))
timeFrom, err = parseHTMLDateTime(timeFromStr, yesterday)
if err != nil {
err = werr.Wrap(err).WithData(timeFromStr)
return
}
timeTo, err = parseHTMLDateTime(timeToStr, time.Now())
if err != nil {
err = werr.Wrap(err).WithData(timeToStr)
return
}
// Protect the user from switching from/to dates, leading to
// zero matching values from the database.
if timeFrom.After(timeTo) {
timeFrom, timeTo = timeTo, timeFrom
}
// Apply an optionally set offset (in seconds).
timeFrom = applyTimeOffset(timeFrom, time.Second, offset)
timeTo = applyTimeOffset(timeTo, time.Second, offset)
return
}

View File

@ -0,0 +1,61 @@
{{ define "timefilter" }}
<script type="text/javascript">
function preset(hours) {
const inputPreset = document.querySelector('input[name="time-preset"]')
inputPreset.value = hours
inputPreset.form.submit()
}
function offsetTime(seconds) {
const el = document.querySelector('input[name="time-offset"]')
el.value = seconds
el.form.submit()
}
</script>
<form action="{{ .Data.TimeSubmit }}" method="get" id="form-time-selector">
<input type="hidden" name="time-preset" value="">
<input type="hidden" name="time-offset" value=0>
<div id="time-selector">
<div>From</div>
<div>To</div>
<input name="time-f" value="{{ .Data.TimeFrom }}" type="datetime-local">
<input name="time-t" value="{{ .Data.TimeTo }}" type="datetime-local">
<div id="time-filter">
<div class="header-1">Presets</div>
<div class="header-2">Offsets</div>
<div class="preset"><a href="#" onclick="preset(1)">Last hour</a></div>
<div><a href="#" onclick="offsetTime(-3600)">◀</a></div>
<div>Hour</div>
<div><a href="#" onclick="offsetTime(3600)">▶</a></div>
<div class="preset"><a href="#" onclick="preset(24)">Last 24 hours</a></div>
<div><a href="#" onclick="offsetTime(-86400)">◀</a></div>
<div>Day</div>
<div><a href="#" onclick="offsetTime(86400)">▶</a></div>
<div class="preset"><a href="#" onclick="preset(24 * 7)">Last 7 days</a></div>
<div><a href="#" onclick="offsetTime(-7 * 86400)">◀</a></div>
<div>Week</div>
<div><a href="#" onclick="offsetTime(7 * 86400)">▶</a></div>
<div class="preset"><a href="#" onclick="preset(24 * 31)">Last 31 days</a></div>
<div><a href="#" onclick="offsetTime(-31 * 86400)">◀</a></div>
<div>Month</div>
<div><a href="#" onclick="offsetTime(31 * 86400)">▶</a></div>
</div>
<button>OK</button>
</div>
</form>
{{ end }}

View File

@ -10,61 +10,9 @@
evt.target.close() evt.target.close()
} }
} }
function preset(hours) {
const inputPreset = document.querySelector('input[name="preset"]')
inputPreset.value = hours
inputPreset.form.submit()
}
function offsetTime(seconds) {
const el = document.querySelector('input[name="offset-time"]')
el.value = seconds
el.form.submit()
}
</script> </script>
<form action="/notifications" method="get"> {{ block "timefilter" . }}{{ end }}
<input type="hidden" name="preset" value="">
<input type="hidden" name="offset-time" value="">
<div id="time-select">
<div>From</div>
<div>To</div>
<input name="f" value="{{ .Data.TimeFrom }}" type="datetime-local">
<input name="t" value="{{ .Data.TimeTo }}" type="datetime-local">
<div id="time-offsets">
<div class="header-1">Presets</div>
<div class="header-2">Offsets</div>
<div class="preset"><a href="#" onclick="preset(1)">Last hour</a></div>
<div><a href="#" onclick="offsetTime(-3600)">◀</a></div>
<div>Hour</div>
<div><a href="#" onclick="offsetTime(3600)">▶</a></div>
<div class="preset"><a href="#" onclick="preset(24)">Last 24 hours</a></div>
<div><a href="#" onclick="offsetTime(-86400)">◀</a></div>
<div>Day</div>
<div><a href="#" onclick="offsetTime(86400)">▶</a></div>
<div class="preset"><a href="#" onclick="preset(24 * 7)">Last 7 days</a></div>
<div><a href="#" onclick="offsetTime(-7 * 86400)">◀</a></div>
<div>Week</div>
<div><a href="#" onclick="offsetTime(7 * 86400)">▶</a></div>
<div class="preset"><a href="#" onclick="preset(24 * 31)">Last 31 days</a></div>
<div><a href="#" onclick="offsetTime(-31 * 86400)">◀</a></div>
<div>Month</div>
<div><a href="#" onclick="offsetTime(31 * 86400)">▶</a></div>
</div>
<button>OK</button>
</div>
</form>
<div id="notifications"> <div id="notifications">
<div class="header">Sent</div> <div class="header">Sent</div>