diff --git a/helper.go b/helper.go index e8fcbe0..99541ba 100644 --- a/helper.go +++ b/helper.go @@ -63,7 +63,7 @@ func applyTimeOffset(t time.Time, duration time.Duration, amountStr string) time 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 == "" { return } @@ -73,4 +73,4 @@ func presetTimeInterval(duration time.Duration, presetStr string, timeFrom, time (*timeFrom) = now.Add(duration * -1 * time.Duration(presetTime)) (*timeTo) = now return -}// }}} +} diff --git a/main.go b/main.go index cc67c04..583ff95 100644 --- a/main.go +++ b/main.go @@ -29,7 +29,7 @@ import ( "time" ) -const VERSION = "v29" +const VERSION = "v28" var ( logger *slog.Logger @@ -521,21 +521,7 @@ func pageProblems(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ CONFIG: smonConfig.Settings, } - // Manage the values from the timefilter component - var err error - var timeFrom, timeTo time.Time - timeFrom, timeTo, err = timefilterParse( - r.URL.Query().Get("time-f"), - r.URL.Query().Get("time-t"), - r.URL.Query().Get("time-offset"), - r.URL.Query().Get("time-preset"), - ) - if err != nil { - httpError(w, err) - return - } - - problems, err := ProblemsRetrieve(true, timeFrom, timeTo) + problems, err := ProblemsRetrieve() if err != nil { httpError(w, werr.Wrap(err).Log()) return @@ -560,10 +546,6 @@ func pageProblems(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ page.Data = map[string]any{ "Problems": problems, "ProblemsGrouped": problemsGrouped, - - "TimeSubmit": "/problems", - "TimeFrom": timeFrom.Format("2006-01-02T15:04:05"), - "TimeTo": timeTo.Format("2006-01-02T15:04:05"), } page.Render(w, r) return @@ -727,14 +709,31 @@ func pageDatapointValues(w http.ResponseWriter, r *http.Request, _ *session.T) { return } - // Manage the values from the timefilter component + // GET parameters. + display := r.URL.Query().Get("display") + if display == "" && datapoint.Datatype == INT { + display = "graph" + } + var timeFrom, timeTo time.Time - timeFrom, timeTo, err = timefilterParse( - r.URL.Query().Get("time-f"), - r.URL.Query().Get("time-t"), - r.URL.Query().Get("time-offset"), - r.URL.Query().Get("time-preset"), - ) + yesterday := time.Now().Add(time.Duration(-24 * time.Hour)) + timeFrom, err = parseHTMLDateTime(r.URL.Query().Get("f"), yesterday) + if err != nil { + httpError(w, werr.Wrap(err).WithData(r.URL.Query().Get("f")).Log()) + return + } + + 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. var values []DatapointValue @@ -744,12 +743,6 @@ func pageDatapointValues(w http.ResponseWriter, r *http.Request, _ *session.T) { return } - // GET parameters. - display := r.URL.Query().Get("display") - if display == "" && datapoint.Datatype == INT { - display = "graph" - } - page := Page{ LAYOUT: "main", PAGE: "datapoint_values", @@ -762,11 +755,9 @@ func pageDatapointValues(w http.ResponseWriter, r *http.Request, _ *session.T) { page.Data = map[string]any{ "Datapoint": datapoint, "Values": values, + "TimeFrom": timeFrom.Format("2006-01-02T15:04:05"), + "TimeTo": timeTo.Format("2006-01-02T15:04:05"), "Display": display, - - "TimeSubmit": "/datapoint/values/" + strconv.Itoa(datapoint.ID), - "TimeFrom": timeFrom.Format("2006-01-02T15:04:05"), - "TimeTo": timeTo.Format("2006-01-02T15:04:05"), } page.Render(w, r) return @@ -779,14 +770,14 @@ func actionDatapointJson(w http.ResponseWriter, r *http.Request, _ *session.T) { return } - fromStr := r.URL.Query().Get("time-f") + fromStr := r.URL.Query().Get("f") from, err := time.ParseInLocation("2006-01-02 15:04:05", fromStr[0:min(19, len(fromStr))], smonConfig.Timezone()) if err != nil { httpError(w, werr.Wrap(err).Log()) return } - toStr := r.URL.Query().Get("time-t") + toStr := r.URL.Query().Get("t") to, err := time.ParseInLocation("2006-01-02 15:04:05", toStr[0:min(19, len(toStr))], smonConfig.Timezone()) if err != nil { httpError(w, werr.Wrap(err).Log()) @@ -1207,14 +1198,27 @@ func actionConfigurationNotificationDelete(w http.ResponseWriter, r *http.Reques func pageNotifications(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ var err error - // Manage the values from the timefilter component + // GET parameters. var timeFrom, timeTo time.Time - timeFrom, timeTo, err = timefilterParse( - r.URL.Query().Get("time-f"), - r.URL.Query().Get("time-t"), - r.URL.Query().Get("time-offset"), - r.URL.Query().Get("time-preset"), - ) + lastWeek := time.Now().Add(time.Duration(-7 * 24 * time.Hour)) + + timeFrom, err = parseHTMLDateTime(r.URL.Query().Get("f"), lastWeek) + if err != nil { + httpError(w, werr.Wrap(err).Log()) + 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) if err != nil { @@ -1227,7 +1231,6 @@ func pageNotifications(w http.ResponseWriter, r *http.Request, _ *session.T) { / CONFIG: smonConfig.Settings, Data: map[string]any{ "Notifications": nss, - "TimeSubmit": "/notifications", "TimeFrom": timeFrom.Format("2006-01-02T15:04:05"), "TimeTo": timeTo.Format("2006-01-02T15:04:05"), }, diff --git a/problem.go b/problem.go index 24f8151..944942c 100644 --- a/problem.go +++ b/problem.go @@ -2,7 +2,7 @@ package main import ( // External - werr "git.gibonuddevalla.se/go/wrappederror" + we "git.gibonuddevalla.se/go/wrappederror" // Standard "database/sql" @@ -16,7 +16,7 @@ import ( type Problem struct { // {{{ ID int Start time.Time - End time.Time + End sql.NullTime Acknowledged bool Datapoints map[string]any DatapointValues map[string]any `json:"datapoints"` @@ -26,7 +26,7 @@ type Problem struct { // {{{ SectionName string `json:"section_name"` } // }}} -func ProblemsRetrieve(archived bool, from, to time.Time) (problems []Problem, err error) { // {{{ +func ProblemsRetrieve() (problems []Problem, err error) { // {{{ problems = []Problem{} row := service.Db.Conn.QueryRow(` SELECT @@ -35,7 +35,7 @@ func ProblemsRetrieve(archived bool, from, to time.Time) (problems []Problem, er (SELECT p.id, p.start, - TO_CHAR(p.end, 'YYYY-MM-DD"T"HH24:MI:SSTZH:TZM') AS end, + p.end, p.acknowledged, p.datapoints, t.id AS trigger_id, @@ -47,18 +47,12 @@ func ProblemsRetrieve(archived bool, from, to time.Time) (problems []Problem, er INNER JOIN section s ON t.section_id = s.id INNER JOIN area a ON s.area_id = a.id WHERE - CASE - WHEN $1 THEN true - WHEN NOT $1 THEN p.end IS NULL - END AND - p.start >= $2 AND - p.end <= $3 - + p.end IS NULL ORDER BY p.start DESC) UNION ALL - + (SELECT -1 AS id, null, @@ -77,16 +71,13 @@ func ProblemsRetrieve(archived bool, from, to time.Time) (problems []Problem, er dp.nodata_is_problem ORDER BY dp.name ASC) - ) AS problems`, - archived, - from, - to, - ) + ) AS problems + `) var jsonBody []byte err = row.Scan(&jsonBody) if err != nil { - err = werr.Wrap(err) + err = we.Wrap(err) return } @@ -96,7 +87,7 @@ func ProblemsRetrieve(archived bool, from, to time.Time) (problems []Problem, er err = json.Unmarshal(jsonBody, &problems) if err != nil { - err = werr.Wrap(err) + err = we.Wrap(err) } return } // }}} @@ -114,22 +105,17 @@ func ProblemStart(trigger Trigger) (problemID int, err error) { // {{{ var openProblems int err = row.Scan(&openProblems) if err != nil && err != sql.ErrNoRows { - err = werr.Wrap(err).WithData(trigger.ID) + err = we.Wrap(err).WithData(trigger.ID) return } // Open up a new problem if no open exists. if openProblems == 0 { datapointValuesJson, _ := json.Marshal(trigger.DatapointValues) - row = service.Db.Conn.QueryRow( - `INSERT INTO problem(trigger_id, datapoints, trigger_expression) VALUES($1, $2, $3) RETURNING id`, - trigger.ID, - datapointValuesJson, - trigger.Expression, - ) + row = service.Db.Conn.QueryRow(`INSERT INTO problem(trigger_id, datapoints) VALUES($1, $2) RETURNING id`, trigger.ID, datapointValuesJson) err = row.Scan(&problemID) if err != nil { - err = werr.Wrap(err).WithData(trigger) + err = we.Wrap(err).WithData(trigger) } } return @@ -144,7 +130,7 @@ func ProblemClose(trigger Trigger) (problemID int, err error) { // {{{ } if err != nil { - err = werr.Wrap(err).WithData(trigger) + err = we.Wrap(err).WithData(trigger) return } return @@ -152,13 +138,13 @@ func ProblemClose(trigger Trigger) (problemID int, err error) { // {{{ func ProblemAcknowledge(id int, state bool) (err error) { // {{{ _, err = service.Db.Conn.Exec(`UPDATE problem SET "acknowledged"=$2 WHERE id=$1`, id, state) if err != nil { - err = werr.Wrap(err).WithData(id) + err = we.Wrap(err).WithData(id) return } return } // }}} -func (p Problem) FormattedValues() string { // {{{ +func (p Problem) FormattedValues() string { out := []string{} for key, val := range p.DatapointValues { var keyval string @@ -188,7 +174,4 @@ func (p Problem) FormattedValues() string { // {{{ sort.Strings(out) return strings.Join(out, "\n") -} // }}} -func (p Problem) IsArchived() bool { // {{{ - return !p.End.IsZero() -} // }}} +} diff --git a/sql/00023.sql b/sql/00023.sql deleted file mode 100644 index b29ea09..0000000 --- a/sql/00023.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public.problem ADD COLUMN trigger_expression VARCHAR NOT NULL DEFAULT ''; diff --git a/static/css/default_light/datapoints.css b/static/css/default_light/datapoints.css index 14a1aa5..a534d2d 100644 --- a/static/css/default_light/datapoints.css +++ b/static/css/default_light/datapoints.css @@ -99,3 +99,25 @@ .graph #graph-values { 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; +} diff --git a/static/css/default_light/main.css b/static/css/default_light/main.css index 4bae369..10baf58 100644 --- a/static/css/default_light/main.css +++ b/static/css/default_light/main.css @@ -226,37 +226,3 @@ label { width: min-content; 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; -} diff --git a/static/css/default_light/notifications.css b/static/css/default_light/notifications.css index 9dee5c0..c747e0d 100644 --- a/static/css/default_light/notifications.css +++ b/static/css/default_light/notifications.css @@ -1,3 +1,37 @@ +#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"] { padding: 6px; } diff --git a/static/css/gruvbox/datapoints.css b/static/css/gruvbox/datapoints.css index fdc0124..c948a85 100644 --- a/static/css/gruvbox/datapoints.css +++ b/static/css/gruvbox/datapoints.css @@ -99,3 +99,25 @@ .graph #graph-values { 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; +} diff --git a/static/css/gruvbox/main.css b/static/css/gruvbox/main.css index 727b671..98f785f 100644 --- a/static/css/gruvbox/main.css +++ b/static/css/gruvbox/main.css @@ -226,37 +226,3 @@ label { width: min-content; 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; -} diff --git a/static/css/gruvbox/notifications.css b/static/css/gruvbox/notifications.css index 1cc900e..60db03b 100644 --- a/static/css/gruvbox/notifications.css +++ b/static/css/gruvbox/notifications.css @@ -1,3 +1,37 @@ +#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"] { padding: 6px; } diff --git a/static/js/datapoint_values.js b/static/js/datapoint_values.js index 82b0ff7..20d0e54 100644 --- a/static/js/datapoint_values.js +++ b/static/js/datapoint_values.js @@ -1,7 +1,13 @@ -function selectDisplay(display) { - const inputDisplay = document.getElementById('input-display') - inputDisplay.value = display - inputDisplay.form.submit() +function preset(hours) { + const inputPreset = document.querySelector('input[name="preset"]') + inputPreset.value = hours + inputPreset.form.submit() +} + +function offsetTime(seconds) { + const inputPreset = document.querySelector('input[name="offset-time"]') + inputPreset.value = seconds + inputPreset.form.submit() } class Graph { @@ -55,9 +61,7 @@ class Dataset { constructor(id, initialData) { this.datapointID = id this.values = {} - if (initialData === null) - return - initialData.forEach(v => this.values[v.ID] = v) + initialData.forEach(v=>this.values[v.ID] = v) } xValues() { @@ -72,7 +76,7 @@ class Dataset { return fetch(`/datapoint/json/${this.datapointID}?f=${from}&t=${to}`) .then(data => data.json()) .then(datapointValues => { - datapointValues.forEach(dp => { + datapointValues.forEach(dp=>{ this.values[dp.ID] = dp }) document.getElementById('num-values').innerText = Object.keys(this.values).length diff --git a/static/less/datapoints.less b/static/less/datapoints.less index 8491560..8391a00 100644 --- a/static/less/datapoints.less +++ b/static/less/datapoints.less @@ -120,3 +120,30 @@ 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; + } +} diff --git a/static/less/main.less b/static/less/main.less index 689ca8f..aa11833 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -280,46 +280,3 @@ label { width: min-content; 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; - } - } -} - diff --git a/static/less/notifications.less b/static/less/notifications.less index 48a9614..00f7db7 100644 --- a/static/less/notifications.less +++ b/static/less/notifications.less @@ -1,5 +1,47 @@ @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"] { padding: 6px; } diff --git a/timefilter.go b/timefilter.go deleted file mode 100644 index 8d221db..0000000 --- a/timefilter.go +++ /dev/null @@ -1,41 +0,0 @@ -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 -} diff --git a/views/components/timefilter.gotmpl b/views/components/timefilter.gotmpl deleted file mode 100644 index 8742015..0000000 --- a/views/components/timefilter.gotmpl +++ /dev/null @@ -1,61 +0,0 @@ -{{ define "timefilter" }} - - - -
- - - -
-
From
-
To
- - - - - -
-
Presets
-
Offsets
- - - -
-
Hour
-
- - - -
-
Day
-
- - - -
-
Week
-
- - - -
-
Month
-
-
- - -
-
-{{ end }} diff --git a/views/pages/datapoint_values.gotmpl b/views/pages/datapoint_values.gotmpl index 40895ae..c7d1bc8 100644 --- a/views/pages/datapoint_values.gotmpl +++ b/views/pages/datapoint_values.gotmpl @@ -7,20 +7,56 @@ {{ block "page_label" . }}{{end}} -
- - -
+
+ + - {{ block "timefilter" . }}{{ end }} + {{ if eq .Data.Datapoint.Datatype "INT" }} +
+ + +
+ {{ end }} - +
+
Values from
+
Values to
+ + + + +
+
Presets
+
Offsets
+ + + +
+
Hour
+
+ + + +
+
Day
+
+ + + +
+
Week
+
+ + + +
+
Month
+
+
+ +
+ +
{{ if $graph }}
@@ -36,6 +72,7 @@ {{ .Data.Datapoint.ID }}, {{ .Data.Values }}, ) + {{ else }}
diff --git a/views/pages/notifications.gotmpl b/views/pages/notifications.gotmpl index d99776a..2bf8e8a 100644 --- a/views/pages/notifications.gotmpl +++ b/views/pages/notifications.gotmpl @@ -10,9 +10,61 @@ 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() + } - {{ block "timefilter" . }}{{ end }} +
+ + + +
+
From
+
To
+ + + +
+
Presets
+
Offsets
+ + + + +
Hour
+ + + + + +
Day
+ + + + + +
Week
+ + + + + +
Month
+ +
+ +
+
Sent
diff --git a/views/pages/problems.gotmpl b/views/pages/problems.gotmpl index a258601..6076e94 100644 --- a/views/pages/problems.gotmpl +++ b/views/pages/problems.gotmpl @@ -8,15 +8,13 @@ - {{ block "page_label" . }}{{ end }} + {{ block "page_label" . }}{{end}} -
+
- {{ block "timefilter" . }}{{ end }} -