Compare commits

..

No commits in common. "main" and "v32" have entirely different histories.
main ... v32

48 changed files with 379 additions and 1141 deletions

View file

@ -162,6 +162,8 @@ func DatapointAdd[T any](name string, value T) (err error) { // {{{
return return
} }
service.Db.Conn.Exec(`UPDATE datapoint SET last_value = NOW(), nodata_is_problem = false WHERE id=$1`, dpID)
return return
} // }}} } // }}}
@ -170,19 +172,30 @@ func DatapointsRetrieve() (dps []Datapoint, err error) { // {{{
var rows *sqlx.Rows var rows *sqlx.Rows
rows, err = service.Db.Conn.Queryx(` rows, err = service.Db.Conn.Queryx(`
SELECT SELECT
id, name, datatype, last_value, "group", comment, nodata_problem_seconds, dp.id,
last_value_id AS v_id, dp.name,
CASE dp.datatype,
WHEN last_value_id IS NULL THEN null dp.last_value,
ELSE last_value dp.group,
END AS ts, dp.comment,
last_value_int AS value_int, dp.nodata_problem_seconds,
last_value_string AS value_string,
last_value_datetime AS value_datetime dpv.id AS v_id,
FROM datapoint dpv.ts,
dpv.value_int,
dpv.value_string,
dpv.value_datetime
FROM public.datapoint dp
LEFT JOIN (
SELECT
*,
row_number() OVER (PARTITION BY "datapoint_id" ORDER BY ts DESC) AS rn
FROM datapoint_value
) dpv ON dpv.datapoint_id = dp.id AND rn = 1
ORDER BY ORDER BY
"group" ASC, dp.group ASC,
name ASC dp.name ASC
`) `)
if err != nil { if err != nil {
err = werr.Wrap(err) err = werr.Wrap(err)
@ -242,11 +255,11 @@ func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{
var query string var query string
var param any var param any
if id > 0 { if id > 0 {
query = `SELECT id, "group", name, "datatype", comment, last_value, nodata_problem_seconds, nodata_is_problem, true AS found FROM public.datapoint WHERE id = $1` query = `SELECT *, true AS found FROM datapoint WHERE id = $1`
param = id param = id
dp.ID = id dp.ID = id
} else { } else {
query = `SELECT id, "group", name, "datatype", comment, last_value, nodata_problem_seconds, nodata_is_problem, true AS found FROM public.datapoint WHERE name = $1` query = `SELECT *, true AS found FROM datapoint WHERE name = $1`
param = name param = name
} }

56
main.go
View file

@ -29,7 +29,7 @@ import (
"time" "time"
) )
const VERSION = "v41" const VERSION = "v32"
var ( var (
logger *slog.Logger logger *slog.Logger
@ -663,27 +663,6 @@ func pageDatapointEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { /
} }
} }
/* Triggers using this datapoint is provided as a list to update
* if changing the datapoint name. Parsing expr and automatically
* changing it to renamed datapoints would be nice in the future. */
var triggers []Trigger
triggers, err = TriggersRetrieveByDatapoint(datapoint.Name)
if err != nil {
httpError(w, werr.Wrap(err).Log())
return
}
slices.SortFunc(triggers, func(a, b Trigger) int {
an := strings.ToUpper(a.Name)
bn := strings.ToUpper(b.Name)
if an < bn {
return -1
}
if an > bn {
return 1
}
return 0
})
page := Page{ page := Page{
LAYOUT: "main", LAYOUT: "main",
PAGE: "datapoint_edit", PAGE: "datapoint_edit",
@ -695,7 +674,6 @@ func pageDatapointEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { /
page.Data = map[string]any{ page.Data = map[string]any{
"Datapoint": datapoint, "Datapoint": datapoint,
"Triggers": triggers,
} }
page.Render(w, r) page.Render(w, r)
return return
@ -711,15 +689,8 @@ func actionDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T)
var nodataSeconds int var nodataSeconds int
nodataSeconds, _ = strconv.Atoi(r.FormValue("nodata_seconds")) nodataSeconds, _ = strconv.Atoi(r.FormValue("nodata_seconds"))
// Datapoint needs to be retrieved from database for the name.
// If name has changed, trigger expressions needs to be updated.
var dp Datapoint var dp Datapoint
dp, err = DatapointRetrieve(id, "") dp.ID = id
if err != nil {
httpError(w, werr.Wrap(err).WithData(id).Log())
return
}
prevDatapointName := dp.Name
dp.Group = r.FormValue("group") dp.Group = r.FormValue("group")
dp.Name = r.FormValue("name") dp.Name = r.FormValue("name")
dp.Datatype = DatapointType(r.FormValue("datatype")) dp.Datatype = DatapointType(r.FormValue("datatype"))
@ -731,29 +702,6 @@ func actionDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T)
return return
} }
// Update the trigger expressions using this
// datapoint name if changed.
if prevDatapointName != dp.Name {
var triggers []Trigger
triggers, err = TriggersRetrieveByDatapoint(dp.Name)
if err != nil {
httpError(w, werr.Wrap(err).WithData(dp.Name))
return
}
for _, trigger := range triggers {
err = trigger.RenameDatapoint(prevDatapointName, dp.Name)
if err != nil {
httpError(w, werr.Wrap(err).WithData([]string{prevDatapointName, dp.Name}))
return
}
err = trigger.Update()
if err != nil {
httpError(w, werr.Wrap(err).WithData([]string{prevDatapointName, dp.Name, trigger.Name}))
return
}
}
}
w.Header().Add("Location", "/datapoints") w.Header().Add("Location", "/datapoints")
w.WriteHeader(302) w.WriteHeader(302)
} // }}} } // }}}

View file

@ -81,7 +81,7 @@ func (ntfy NTFY) Send(problemID int, msg []byte) (err error) {
ackURL := fmt.Sprintf("http, OK, %s/notification/ack?problemID=%d", ntfy.AcknowledgeURL, problemID) ackURL := fmt.Sprintf("http, OK, %s/notification/ack?problemID=%d", ntfy.AcknowledgeURL, problemID)
req.Header.Add("X-Actions", ackURL) req.Header.Add("X-Actions", ackURL)
req.Header.Add("X-Priority", "5") req.Header.Add("X-Priority", "4") // XXX: should be 5
req.Header.Add("X-Tags", "calendar") req.Header.Add("X-Tags", "calendar")
res, err = http.DefaultClient.Do(req) res, err = http.DefaultClient.Do(req)

View file

@ -2,7 +2,7 @@ package main
import ( import (
// External // External
werr "git.gibonuddevalla.se/go/wrappederror" we "git.gibonuddevalla.se/go/wrappederror"
// Standard // Standard
"fmt" "fmt"
@ -25,7 +25,7 @@ type Page struct {
func (p *Page) Render(w http.ResponseWriter, r *http.Request) { func (p *Page) Render(w http.ResponseWriter, r *http.Request) {
tmpl, err := getPage(p.LAYOUT, p.PAGE) tmpl, err := getPage(p.LAYOUT, p.PAGE)
if err != nil { if err != nil {
httpError(w, werr.Wrap(err).Log()) httpError(w, we.Wrap(err).Log())
return return
} }
@ -58,6 +58,6 @@ func (p *Page) Render(w http.ResponseWriter, r *http.Request) {
err = tmpl.Execute(w, data) err = tmpl.Execute(w, data)
if err != nil { if err != nil {
httpError(w, werr.Wrap(err).Log()) httpError(w, we.Wrap(err).Log())
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -1,62 +0,0 @@
/* Adding last values to the datapoint table since they are a regularly used value. */
ALTER TABLE public.datapoint ADD COLUMN last_value_id int4 NULL;
ALTER TABLE public.datapoint ADD COLUMN last_value_int int8 NULL;
ALTER TABLE public.datapoint ADD COLUMN last_value_string varchar NULL;
ALTER TABLE public.datapoint ADD COLUMN last_value_datetime timestamptz NULL;
/* Once-run query to update it to the latest, to avoid user having to wait for the next entry. */
UPDATE public.datapoint AS dp
SET
last_value_id = dpv.id,
last_value_int = dpv.value_int,
last_value_string = dpv.value_string,
last_value_datetime = dpv.value_datetime
FROM (
SELECT
dp.id AS datapoint_id,
dpv.id,
dpv.value_int,
dpv.value_string,
dpv.value_datetime
FROM public.datapoint dp
LEFT JOIN (
SELECT
*,
row_number() OVER (PARTITION BY "datapoint_id" ORDER BY ts DESC) AS rn
FROM datapoint_value
) dpv ON dpv.datapoint_id = dp.id AND rn = 1
) AS dpv
WHERE
dpv.datapoint_id = dp.id;
/* A trigger keeps the value current without bugs introduced in software missing the entry. */
CREATE OR REPLACE FUNCTION datapoint_entry()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS
$$
BEGIN
UPDATE public.datapoint
SET
nodata_is_problem = false,
last_value = NEW.ts,
last_value_id = NEW.id,
last_value_int = NEW.value_int,
last_value_string = NEW.value_string,
last_value_datetime = NEW.value_datetime
WHERE
id = NEW.datapoint_id;
RETURN NEW;
END;
$$;
CREATE TRIGGER datapoint_entry
AFTER INSERT
ON public.datapoint_value
FOR EACH ROW
EXECUTE PROCEDURE datapoint_entry();

View file

@ -1,22 +0,0 @@
/* Updating a datapoint name also updates the jsonb array entry */
CREATE OR REPLACE FUNCTION update_triggers_datapoint_name()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS
$$
BEGIN
UPDATE "trigger"
SET
datapoints = (datapoints - OLD.name) || jsonb_build_array(NEW.name)
WHERE
datapoints ? OLD.name;
RETURN NEW;
END;
$$;
CREATE TRIGGER datapoint_renamed
AFTER UPDATE
ON public.datapoint
FOR EACH ROW
EXECUTE PROCEDURE update_triggers_datapoint_name();

View file

@ -1,6 +1,3 @@
#datapoints-filter.invalid-regex {
background-color: #ffd5d5;
}
#datapoints { #datapoints {
display: grid; display: grid;
grid-template-columns: repeat(6, min-content); grid-template-columns: repeat(6, min-content);
@ -100,5 +97,5 @@
margin-top: 16px; margin-top: 16px;
} }
.graph #graph-values { .graph #graph-values {
height: calc(100vh - 200px); height: calc(100vh - 416px);
} }

View file

@ -49,9 +49,7 @@ dialog,
#acknowledged-list, #acknowledged-list,
#values, #values,
#services, #services,
#notifications, #notifications {
#group,
.table {
background-color: #fff !important; background-color: #fff !important;
border: 1px solid #ddd; border: 1px solid #ddd;
box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.25); box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.25);

View file

@ -20,4 +20,5 @@ input[type="datetime-local"] {
background-color: #1b4e78; background-color: #1b4e78;
color: #ccc; color: #ccc;
border: 1px solid #535353; border: 1px solid #535353;
padding: 6px;
} }

View file

@ -1,28 +1,3 @@
.table {
display: grid;
grid-gap: 6px 16px;
align-items: center;
margin-top: 32px;
margin-bottom: 32px;
background-color: #2979b8;
padding: 16px 24px;
width: min-content;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.table .row {
grid-column: 1 / -1;
}
.table > div {
white-space: nowrap;
line-height: 24px;
}
.table .header {
font-size: 0.85em;
font-weight: bold;
color: #7bb8eb;
line-height: unset !important;
}
html { html {
box-sizing: border-box; box-sizing: border-box;
} }
@ -258,34 +233,42 @@ label {
border-radius: 8px; border-radius: 8px;
} }
#time-selector { #time-selector {
position: absolute;
top: 16px;
right: 16px;
display: grid; display: grid;
grid-template-columns: 8px repeat(2, min-content) 8px min-content 8px 1px 8px repeat(3, min-content) 8px repeat(3, min-content) 8px 1px 8px repeat(2, min-content) 8px; grid-template-columns: min-content min-content;
grid-gap: 6px 8px; grid-gap: 6px 16px;
align-items: center;
width: min-content; width: min-content;
background-color: #fff; background-color: #fff;
border: 1px solid #2979b8; border: 1px solid #2979b8;
padding: 16px;
border-radius: 6px; border-radius: 6px;
} }
#time-selector.hidden { #time-selector.hidden {
display: none; display: none;
} }
#time-selector .vertical-line {
background-color: #2979b8;
}
#time-selector .header {
padding-top: 12px;
font-weight: bold;
font-size: 0.85em;
}
#time-selector button { #time-selector button {
width: 100px; width: 100px;
margin-top: 12px; margin-top: 12px;
justify-self: end; justify-self: end;
} }
#time-selector div { #time-selector #time-filter {
white-space: nowrap; 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,8 +1,11 @@
input[type="datetime-local"] {
padding: 6px;
}
#notifications { #notifications {
display: grid; display: grid;
grid-template-columns: repeat(5, min-content); grid-template-columns: repeat(5, min-content);
grid-gap: 4px 16px; grid-gap: 4px 16px;
margin-top: 96px; margin-top: 32px;
margin-bottom: 32px; margin-bottom: 32px;
background-color: #2979b8; background-color: #2979b8;
padding: 16px 24px; padding: 16px 24px;

View file

@ -1,45 +1,61 @@
#problems-list, #problems-list,
#acknowledged-list { #acknowledged-list {
grid-template-columns: repeat(7, min-content); display: grid;
grid-template-columns: repeat(6, min-content);
grid-gap: 4px 16px;
margin-top: 32px;
margin-bottom: 32px;
background-color: #2979b8;
padding: 16px 24px;
width: min-content;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
#problems-list div,
#acknowledged-list div {
white-space: nowrap;
line-height: 24px;
}
#problems-list .header,
#acknowledged-list .header {
font-weight: 800;
color: #7bb8eb;
} }
#problems-list .trigger, #problems-list .trigger,
#acknowledged-list .trigger { #acknowledged-list .trigger {
color: #1b4e78; color: #1b4e78;
font-weight: 800;
} }
#problems-list img.acknowledge, #problems-list .acknowledge img,
#acknowledged-list img.acknowledge { #acknowledged-list .acknowledge img {
height: 16px; height: 16px;
} }
#problems-list .info, #problems-list .info,
#acknowledged-list .info { #acknowledged-list .info {
margin-right: 8px; margin-right: 8px;
} }
#problems-list .icons,
#acknowledged-list .icons {
display: grid;
grid-template-columns: min-content min-content;
align-items: center;
}
#acknowledged-list.hidden { #acknowledged-list.hidden {
display: none; display: none;
} }
#area-grouped { #areas {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 24px; gap: 24px;
margin-top: 16px; margin-top: 16px;
align-items: flex-start;
} }
#area-grouped .area { #areas .area .section {
grid-template-columns: repeat(5, min-content); display: grid;
grid-template-columns: repeat(4, min-content);
grid-gap: 8px 12px;
} }
#area-grouped .area .section { #areas .area .section .name {
padding: 4px 10px; color: #000;
border-radius: 5px; grid-column: 1 / -1;
background: #2979b8; font-weight: bold !important;
color: #fff; line-height: 24px;
width: min-content; }
margin-bottom: 8px; #areas .area .section div {
white-space: nowrap;
} }
.hidden { .hidden {
display: none; display: none;

View file

@ -1,25 +0,0 @@
.table {
display: grid;
grid-gap: 6px 16px;
align-items: center;
margin-top: 32px;
margin-bottom: 32px;
background-color: #2979b8;
padding: 16px 24px;
width: min-content;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.table .row {
grid-column: 1 / -1;
}
.table > div {
white-space: nowrap;
line-height: 24px;
}
.table .header {
font-size: 0.85em;
font-weight: bold;
color: #7bb8eb;
line-height: unset !important;
}

View file

@ -13,24 +13,17 @@
.widgets .datapoints { .widgets .datapoints {
font: "Roboto Mono", monospace; font: "Roboto Mono", monospace;
display: grid; display: grid;
grid-template-columns: repeat(4, min-content); grid-template-columns: min-content min-content 1fr;
gap: 6px 8px; gap: 6px 8px;
margin-bottom: 8px; margin-bottom: 8px;
white-space: nowrap; white-space: nowrap;
} }
.widgets .datapoints div {
white-space: nowrap;
}
.widgets .datapoints .invalid { .widgets .datapoints .invalid {
color: #c83737; color: #c83737;
} }
.widgets .datapoints .delete img { .widgets .datapoints .delete img {
height: 16px; height: 16px;
} }
.widgets .datapoints .values img {
height: 16px;
width: 16px;
}
.widgets .action { .widgets .action {
display: grid; display: grid;
grid-template-columns: min-content min-content 1fr; grid-template-columns: min-content min-content 1fr;

View file

@ -1,6 +1,3 @@
#datapoints-filter.invalid-regex {
background-color: #ffd5d5;
}
#datapoints { #datapoints {
display: grid; display: grid;
grid-template-columns: repeat(6, min-content); grid-template-columns: repeat(6, min-content);
@ -100,5 +97,5 @@
margin-top: 16px; margin-top: 16px;
} }
.graph #graph-values { .graph #graph-values {
height: calc(100vh - 200px); height: calc(100vh - 416px);
} }

View file

@ -49,9 +49,7 @@ dialog,
#acknowledged-list, #acknowledged-list,
#values, #values,
#services, #services,
#notifications, #notifications {
#group,
.table {
background-color: #fff !important; background-color: #fff !important;
border: 1px solid #ddd; border: 1px solid #ddd;
box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.25); box-shadow: 5px 5px 8px 0px rgba(0, 0, 0, 0.25);

View file

@ -20,4 +20,5 @@ input[type="datetime-local"] {
background-color: #202020; background-color: #202020;
color: #ccc; color: #ccc;
border: 1px solid #535353; border: 1px solid #535353;
padding: 6px;
} }

View file

@ -1,28 +1,3 @@
.table {
display: grid;
grid-gap: 6px 16px;
align-items: center;
margin-top: 32px;
margin-bottom: 32px;
background-color: #333;
padding: 16px 24px;
width: min-content;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.table .row {
grid-column: 1 / -1;
}
.table > div {
white-space: nowrap;
line-height: 24px;
}
.table .header {
font-size: 0.85em;
font-weight: bold;
color: #777;
line-height: unset !important;
}
html { html {
box-sizing: border-box; box-sizing: border-box;
} }
@ -258,34 +233,42 @@ label {
border-radius: 8px; border-radius: 8px;
} }
#time-selector { #time-selector {
position: absolute;
top: 16px;
right: 16px;
display: grid; display: grid;
grid-template-columns: 8px repeat(2, min-content) 8px min-content 8px 1px 8px repeat(3, min-content) 8px repeat(3, min-content) 8px 1px 8px repeat(2, min-content) 8px; grid-template-columns: min-content min-content;
grid-gap: 6px 8px; grid-gap: 6px 16px;
align-items: center;
width: min-content; width: min-content;
background-color: #282828; background-color: #282828;
border: 1px solid #333; border: 1px solid #333;
padding: 16px;
border-radius: 6px; border-radius: 6px;
} }
#time-selector.hidden { #time-selector.hidden {
display: none; display: none;
} }
#time-selector .vertical-line {
background-color: #333;
}
#time-selector .header {
padding-top: 12px;
font-weight: bold;
font-size: 0.85em;
}
#time-selector button { #time-selector button {
width: 100px; width: 100px;
margin-top: 12px; margin-top: 12px;
justify-self: end; justify-self: end;
} }
#time-selector div { #time-selector #time-filter {
white-space: nowrap; 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,8 +1,11 @@
input[type="datetime-local"] {
padding: 6px;
}
#notifications { #notifications {
display: grid; display: grid;
grid-template-columns: repeat(5, min-content); grid-template-columns: repeat(5, min-content);
grid-gap: 4px 16px; grid-gap: 4px 16px;
margin-top: 96px; margin-top: 32px;
margin-bottom: 32px; margin-bottom: 32px;
background-color: #333; background-color: #333;
padding: 16px 24px; padding: 16px 24px;

View file

@ -1,45 +1,61 @@
#problems-list, #problems-list,
#acknowledged-list { #acknowledged-list {
grid-template-columns: repeat(7, min-content); display: grid;
grid-template-columns: repeat(6, min-content);
grid-gap: 4px 16px;
margin-top: 32px;
margin-bottom: 32px;
background-color: #333;
padding: 16px 24px;
width: min-content;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
#problems-list div,
#acknowledged-list div {
white-space: nowrap;
line-height: 24px;
}
#problems-list .header,
#acknowledged-list .header {
font-weight: 800;
color: #777;
} }
#problems-list .trigger, #problems-list .trigger,
#acknowledged-list .trigger { #acknowledged-list .trigger {
color: #fb4934; color: #fb4934;
font-weight: 800;
} }
#problems-list img.acknowledge, #problems-list .acknowledge img,
#acknowledged-list img.acknowledge { #acknowledged-list .acknowledge img {
height: 16px; height: 16px;
} }
#problems-list .info, #problems-list .info,
#acknowledged-list .info { #acknowledged-list .info {
margin-right: 8px; margin-right: 8px;
} }
#problems-list .icons,
#acknowledged-list .icons {
display: grid;
grid-template-columns: min-content min-content;
align-items: center;
}
#acknowledged-list.hidden { #acknowledged-list.hidden {
display: none; display: none;
} }
#area-grouped { #areas {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 24px; gap: 24px;
margin-top: 16px; margin-top: 16px;
align-items: flex-start;
} }
#area-grouped .area { #areas .area .section {
grid-template-columns: repeat(5, min-content); display: grid;
grid-template-columns: repeat(4, min-content);
grid-gap: 8px 12px;
} }
#area-grouped .area .section { #areas .area .section .name {
padding: 4px 10px; color: #f7edd7;
border-radius: 5px; grid-column: 1 / -1;
background: #333; font-weight: bold !important;
color: #fff; line-height: 24px;
width: min-content; }
margin-bottom: 8px; #areas .area .section div {
white-space: nowrap;
} }
.hidden { .hidden {
display: none; display: none;

View file

@ -1,25 +0,0 @@
.table {
display: grid;
grid-gap: 6px 16px;
align-items: center;
margin-top: 32px;
margin-bottom: 32px;
background-color: #333;
padding: 16px 24px;
width: min-content;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.table .row {
grid-column: 1 / -1;
}
.table > div {
white-space: nowrap;
line-height: 24px;
}
.table .header {
font-size: 0.85em;
font-weight: bold;
color: #777;
line-height: unset !important;
}

View file

@ -13,24 +13,17 @@
.widgets .datapoints { .widgets .datapoints {
font: "Roboto Mono", monospace; font: "Roboto Mono", monospace;
display: grid; display: grid;
grid-template-columns: repeat(4, min-content); grid-template-columns: min-content min-content 1fr;
gap: 6px 8px; gap: 6px 8px;
margin-bottom: 8px; margin-bottom: 8px;
white-space: nowrap; white-space: nowrap;
} }
.widgets .datapoints div {
white-space: nowrap;
}
.widgets .datapoints .invalid { .widgets .datapoints .invalid {
color: #c83737; color: #c83737;
} }
.widgets .datapoints .delete img { .widgets .datapoints .delete img {
height: 16px; height: 16px;
} }
.widgets .datapoints .values img {
height: 16px;
width: 16px;
}
.widgets .action { .widgets .action {
display: grid; display: grid;
grid-template-columns: min-content min-content 1fr; grid-template-columns: min-content min-content 1fr;

View file

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="19.999975"
height="19.999975"
viewBox="0 0 5.29166 5.2916603"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="forward.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="22.627417"
inkscape:cx="9.3028736"
inkscape:cy="12.772116"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2190"
inkscape:window-height="1404"
inkscape:window-x="1463"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
showborder="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-123.825,-155.04583)">
<title
id="title1">alert</title>
<title
id="title1-1">arrow-right-bold-circle</title>
<path
d="m 123.825,157.69166 a 2.6458334,2.6458334 0 0 1 2.64583,-2.64583 2.6458334,2.6458334 0 0 1 2.64583,2.64583 2.6458334,2.6458334 0 0 1 -2.64583,2.64583 2.6458334,2.6458334 0 0 1 -2.64583,-2.64583 m 3.96875,0 -1.32292,-1.32292 v 0.79375 h -1.05833 v 1.05834 h 1.05833 v 0.79375 z"
id="path1"
style="stroke-width:0.264583;fill:#abc837" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="20.000002"
height="20.000013"
viewBox="0 0 5.291667 5.2916703"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="ok.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.9208594"
inkscape:cx="9.7574023"
inkscape:cy="10.27095"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2190"
inkscape:window-height="1404"
inkscape:window-x="1463"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
showborder="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-90.222917,-148.43125)">
<title
id="title1">check-circle</title>
<path
d="m 92.86875,148.43125 c -1.455208,0 -2.645833,1.19063 -2.645833,2.64583 0,1.45521 1.190625,2.64584 2.645833,2.64584 1.455209,0 2.645834,-1.19063 2.645834,-2.64584 0,-1.4552 -1.190625,-2.64583 -2.645834,-2.64583 m -0.529166,3.96875 -1.322917,-1.32292 0.373062,-0.37306 0.949855,0.94721 2.008187,-2.00819 0.373063,0.37571 z"
id="path1"
style="fill:#aad400;stroke-width:0.264583" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -1,68 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="23.157846"
height="20.000013"
viewBox="0 0 6.1271801 5.2916704"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="warning.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="22.627417"
inkscape:cx="9.3028736"
inkscape:cy="12.772116"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2190"
inkscape:window-height="1404"
inkscape:window-x="1463"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
showborder="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-123.825,-155.04583)">
<title
id="title1">alert</title>
<path
d="m 127.21387,158.38793 h -0.65056 v -1.39253 h 0.65056 m 0,2.50657 h -0.65056 v -0.55701 h 0.65056 m -3.38887,1.39254 h 6.12718 l -3.06358,-5.29167 z"
id="path1"
style="fill:#ff9800;fill-opacity:1;stroke-width:0.278508"
sodipodi:nodetypes="cccccccccccc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="19.999975"
height="19.999975"
viewBox="0 0 5.29166 5.2916603"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="forward.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="22.627417"
inkscape:cx="9.3028736"
inkscape:cy="12.772116"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2190"
inkscape:window-height="1404"
inkscape:window-x="1463"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
showborder="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-123.825,-155.04583)">
<title
id="title1">alert</title>
<title
id="title1-1">arrow-right-bold-circle</title>
<path
d="m 123.825,157.69166 a 2.6458334,2.6458334 0 0 1 2.64583,-2.64583 2.6458334,2.6458334 0 0 1 2.64583,2.64583 2.6458334,2.6458334 0 0 1 -2.64583,2.64583 2.6458334,2.6458334 0 0 1 -2.64583,-2.64583 m 3.96875,0 -1.32292,-1.32292 v 0.79375 h -1.05833 v 1.05834 h 1.05833 v 0.79375 z"
id="path1"
style="stroke-width:0.264583;fill:#abc837" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="20.000002"
height="20.000013"
viewBox="0 0 5.291667 5.2916703"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="ok.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.9208594"
inkscape:cx="9.7574023"
inkscape:cy="10.27095"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2190"
inkscape:window-height="1404"
inkscape:window-x="1463"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
showborder="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-90.222917,-148.43125)">
<title
id="title1">check-circle</title>
<path
d="m 92.86875,148.43125 c -1.455208,0 -2.645833,1.19063 -2.645833,2.64583 0,1.45521 1.190625,2.64584 2.645833,2.64584 1.455209,0 2.645834,-1.19063 2.645834,-2.64584 0,-1.4552 -1.190625,-2.64583 -2.645834,-2.64583 m -0.529166,3.96875 -1.322917,-1.32292 0.373062,-0.37306 0.949855,0.94721 2.008187,-2.00819 0.373063,0.37571 z"
id="path1"
style="fill:#aad400;stroke-width:0.264583" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -1,68 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="23.157846"
height="20.000013"
viewBox="0 0 6.1271801 5.2916704"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"
sodipodi:docname="warning.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="22.627417"
inkscape:cx="9.3028736"
inkscape:cy="12.772116"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="2190"
inkscape:window-height="1404"
inkscape:window-x="1463"
inkscape:window-y="16"
inkscape:window-maximized="0"
inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d6d6d6"
showborder="true" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-123.825,-155.04583)">
<title
id="title1">alert</title>
<path
d="m 127.21387,158.38793 h -0.65056 v -1.39253 h 0.65056 m 0,2.50657 h -0.65056 v -0.55701 h 0.65056 m -3.38887,1.39254 h 6.12718 l -3.06358,-5.29167 z"
id="path1"
style="fill:#ff9800;fill-opacity:1;stroke-width:0.278508"
sodipodi:nodetypes="cccccccccccc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -1,6 +1,5 @@
export class UI { export class UI {
constructor(datapointData) { constructor() {
this.datapoint = datapointData
document.addEventListener('keydown', evt=>this.keyHandler(evt)) document.addEventListener('keydown', evt=>this.keyHandler(evt))
document.querySelector('input[name="group"]').focus() document.querySelector('input[name="group"]').focus()
} }

View file

@ -19,10 +19,9 @@ export class UI {
let html = Object.keys(this.trigger.datapoints).sort().map(dpName => { let html = Object.keys(this.trigger.datapoints).sort().map(dpName => {
const dp = this.trigger.datapoints[dpName] const dp = this.trigger.datapoints[dpName]
return ` return `
<div class="datapoint delete"><a href="#" onclick="_ui.deleteDatapoint('${dp.Name}')"><img src="/images/${this.version}/${this.theme}/delete.svg"></a></div>
<div class="datapoint name ${dp.Found ? 'valid' : 'invalid'}"><b>${dp.Name}</b></div> <div class="datapoint name ${dp.Found ? 'valid' : 'invalid'}"><b>${dp.Name}</b></div>
<div class="datapoint value">${dp.Found ? dp.LastDatapointValue.TemplateValue : ''}</div> <div class="datapoint value">${dp.Found ? dp.LastDatapointValue.TemplateValue : ''}</div>
<div class="daatpoint values"><a href="/datapoint/values/${dp.ID}"><img src="/images/${this.version}/${this.theme}/values.svg"></a></div>
<div class="datapoint delete"><a href="#" onclick="_ui.deleteDatapoint('${dp.Name}')"><img src="/images/${this.version}/${this.theme}/delete.svg"></a></div>
` `
}).join('') }).join('')
datapoints.innerHTML += html datapoints.innerHTML += html
@ -85,7 +84,7 @@ export class UI {
}) })
}//}}} }//}}}
deleteDatapoint(name) {//{{{ deleteDatapoint(name) {//{{{
if (!confirm(`Remove datapoint ${name} from this trigger?`)) { if (!confirm(`Delete ${name}?`)) {
return return
} }

View file

@ -1,9 +1,5 @@
@import "theme-@{THEME}.less"; @import "theme-@{THEME}.less";
#datapoints-filter.invalid-regex {
background-color: #ffd5d5;
}
#datapoints { #datapoints {
display: grid; display: grid;
grid-template-columns: repeat(6, min-content); grid-template-columns: repeat(6, min-content);
@ -121,6 +117,6 @@
margin-top: 16px; margin-top: 16px;
#graph-values { #graph-values {
height: calc(100vh - 200px); height: calc(100vh - 416px);
} }
} }

View file

@ -62,7 +62,7 @@ dialog {
border-radius: 8px; border-radius: 8px;
} }
dialog, #datapoints, #problems-list, #acknowledged-list, #values, #services, #notifications, #group, .table { dialog, #datapoints, #problems-list, #acknowledged-list, #values, #services, #notifications {
background-color: #fff !important; background-color: #fff !important;
border: 1px solid #ddd; border: 1px solid #ddd;
box-shadow: 5px 5px 8px 0px rgba(0,0,0,0.25); box-shadow: 5px 5px 8px 0px rgba(0,0,0,0.25);

View file

@ -29,4 +29,5 @@ input[type="datetime-local"] {
background-color: @bg2; background-color: @bg2;
color: #ccc; color: #ccc;
border: 1px solid #535353; border: 1px solid #535353;
padding: 6px;
} }

View file

@ -1,5 +1,4 @@
@import "theme-@{THEME}.less"; @import "theme-@{THEME}.less";
@import "table.less";
html { html {
box-sizing: border-box; box-sizing: border-box;
@ -291,42 +290,51 @@ label {
} }
#time-selector { #time-selector {
position: absolute;
top: 16px;
right: 16px;
display: grid; display: grid;
grid-template-columns: 8px repeat(2,min-content) 8px min-content 8px 1px 8px repeat(3,min-content) 8px repeat(3,min-content) 8px 1px 8px repeat(2,min-content) 8px; grid-template-columns: min-content min-content;
grid-gap: 6px 8px; grid-gap: 6px 16px;
align-items: center;
width: min-content; width: min-content;
background-color: @bg1; background-color: @bg1;
border: 1px solid @bg3; border: 1px solid @bg3;
padding: 16px;
border-radius: 6px; border-radius: 6px;
&.hidden { &.hidden {
display: none; display: none;
} }
.vertical-line {
background-color: @bg3;
}
.header {
padding-top: 12px;
font-weight: bold;
font-size: 0.85em;
}
button { button {
width: 100px; width: 100px;
margin-top: 12px; margin-top: 12px;
justify-self: end; justify-self: end;
} }
div { #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; white-space: nowrap;
justify-self: start;
padding-right: 32px;
}
} }
} }

View file

@ -1,10 +1,14 @@
@import "theme-@{THEME}.less"; @import "theme-@{THEME}.less";
input[type="datetime-local"] {
padding: 6px;
}
#notifications { #notifications {
display: grid; display: grid;
grid-template-columns: repeat(5, min-content); grid-template-columns: repeat(5, min-content);
grid-gap: 4px 16px; grid-gap: 4px 16px;
margin-top: 96px; margin-top: 32px;
margin-bottom: 32px; margin-bottom: 32px;
background-color: @bg3; background-color: @bg3;
padding: 16px 24px; padding: 16px 24px;

View file

@ -1,48 +1,69 @@
@import "theme-@{THEME}.less"; @import "theme-@{THEME}.less";
#problems-list, #acknowledged-list { #problems-list, #acknowledged-list {
grid-template-columns: repeat(7, min-content); display: grid;
grid-template-columns: repeat(6, min-content);
grid-gap: 4px 16px;
margin-top: 32px;
margin-bottom: 32px;
background-color: @bg3;
padding: 16px 24px;
width: min-content;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
div {
white-space: nowrap;
line-height: 24px;
}
.header {
font-weight: @bold;
color: @text3;
}
.trigger { .trigger {
color: @color1; color: @color1;
font-weight: @bold;
} }
img.acknowledge { .acknowledge {
img {
height: 16px; height: 16px;
} }
}
.info { .info {
margin-right: 8px; margin-right: 8px;
} }
.icons {
display: grid;
grid-template-columns: min-content min-content;
align-items: center;
}
} }
#acknowledged-list.hidden{ #acknowledged-list.hidden{
display: none; display: none;
} }
#area-grouped { #areas {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 24px; gap: 24px;
margin-top: 16px; margin-top: 16px;
align-items: flex-start;
.area { .area {
grid-template-columns: repeat(5, min-content);
.section { .section {
padding: 4px 10px; display: grid;
border-radius: 5px; grid-template-columns: repeat(4, min-content);
background: @bg3; grid-gap: 8px 12px;
color: #fff;
width: min-content; .name {
margin-bottom: 8px; color: @text2;
grid-column: ~"1 / -1";
font-weight: bold !important;
line-height: 24px;
}
div {
white-space: nowrap;
}
} }
} }
} }

View file

@ -1,31 +0,0 @@
@import "theme-@{THEME}.less";
.table {
display: grid;
grid-gap: 6px 16px;
align-items: center;
.row {
grid-column: ~"1 / -1";
}
margin-top: 32px;
margin-bottom: 32px;
background-color: @bg3;
padding: 16px 24px;
width: min-content;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
& > div {
white-space: nowrap;
line-height: 24px;
}
.header {
font-size: 0.85em;
font-weight: bold;
color: @text3;
line-height: unset !important;
}
}

View file

@ -19,15 +19,11 @@
.datapoints { .datapoints {
font: "Roboto Mono", monospace; font: "Roboto Mono", monospace;
display: grid; display: grid;
grid-template-columns: repeat(4, min-content); grid-template-columns: min-content min-content 1fr;
gap: 6px 8px; gap: 6px 8px;
margin-bottom: 8px; margin-bottom: 8px;
white-space: nowrap; white-space: nowrap;
div {
white-space: nowrap;
}
.invalid { .invalid {
color: #c83737; color: #c83737;
} }
@ -35,11 +31,6 @@
.delete img { .delete img {
height: 16px; height: 16px;
} }
.values img {
height: 16px;
width: 16px;
}
} }
.action { .action {

View file

@ -4,8 +4,6 @@ import (
// External // External
werr "git.gibonuddevalla.se/go/wrappederror" werr "git.gibonuddevalla.se/go/wrappederror"
"github.com/expr-lang/expr" "github.com/expr-lang/expr"
"github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/parser"
"github.com/lib/pq" "github.com/lib/pq"
// Standard // Standard
@ -24,17 +22,6 @@ type Trigger struct {
DatapointValues map[string]any DatapointValues map[string]any
} }
type ExprRenamePatcher struct {
OldName string
NewName string
}
func (p ExprRenamePatcher) Visit(node *ast.Node) {
if n, ok := (*node).(*ast.IdentifierNode); ok && n.Value == p.OldName {
ast.Patch(node, &ast.IdentifierNode{Value: p.NewName})
}
}
func TriggerCreate(sectionID int, name string) (t Trigger, err error) { // {{{ func TriggerCreate(sectionID int, name string) (t Trigger, err error) { // {{{
t.SectionID = sectionID t.SectionID = sectionID
t.Name = name t.Name = name
@ -140,14 +127,6 @@ func TriggerRetrieve(id int) (trigger Trigger, err error) { // {{{
err = json.Unmarshal(jsonData, &trigger) err = json.Unmarshal(jsonData, &trigger)
return return
} // }}} } // }}}
func TriggerDelete(id int) (err error) { // {{{
_, err = service.Db.Conn.Exec(`DELETE FROM public.trigger WHERE id=$1`, id)
if err != nil {
return werr.Wrap(err).WithData(id)
}
return
} // }}}
func (t *Trigger) Validate() (ok bool, err error) { // {{{ func (t *Trigger) Validate() (ok bool, err error) { // {{{
if strings.TrimSpace(t.Name) == "" { if strings.TrimSpace(t.Name) == "" {
err = fmt.Errorf("Name can't be empty") err = fmt.Errorf("Name can't be empty")
@ -233,6 +212,14 @@ func (t *Trigger) Update() (err error) { // {{{
} }
return return
} // }}} } // }}}
func TriggerDelete(id int) (err error) { // {{{
_, err = service.Db.Conn.Exec(`DELETE FROM public.trigger WHERE id=$1`, id)
if err != nil {
return werr.Wrap(err).WithData(id)
}
return
} // }}}
func (t *Trigger) Run() (output any, err error) { // {{{ func (t *Trigger) Run() (output any, err error) { // {{{
datapoints := make(map[string]Datapoint) datapoints := make(map[string]Datapoint)
for _, dpname := range t.Datapoints { for _, dpname := range t.Datapoints {
@ -261,14 +248,3 @@ func (t *Trigger) Run() (output any, err error) { // {{{
} }
return return
} // }}} } // }}}
func (t *Trigger) RenameDatapoint(from, to string) error { // {{{
tree, err := parser.Parse(t.Expression)
if err != nil {
return werr.Wrap(err).WithData(t.Expression)
}
ast.Walk(&tree.Node, ExprRenamePatcher{from, to})
t.Expression = tree.Node.String()
return nil
} // }}}

View file

@ -11,11 +11,6 @@
el.value = seconds el.value = seconds
el.form.submit() el.form.submit()
} }
function enterHandler(evt) {
if (evt.key == 'Enter')
document.getElementById('form-time-selector').submit()
}
</script> </script>
@ -24,95 +19,43 @@
<input type="hidden" name="time-offset" value=0> <input type="hidden" name="time-offset" value=0>
<div id="time-selector"> <div id="time-selector">
{{/* ====== Row 1 ====== */}}
<div></div>
<div class="header" style="grid-column: 2 / 6">Date and time</div>
<div></div>
<div class="vertical-line" style="grid-column: 7; grid-row: 1 / 5; height: 100%">&nbsp;</div>
<div></div>
<div class="header" style="grid-column: 9 / 16">Offsets</div>
<div></div>
<div class="vertical-line" style="grid-column: 17; grid-row: 1 / 5; height: 100%"></div>
<div></div>
<div class="header" style="grid-column: 19 / 21">Presets</div>
<div></div>
{{/* ====== Row 2 ====== */}}
<div></div>
<div>From</div> <div>From</div>
<input name="time-f" value="{{ .Data.TimeFrom }}" type="datetime-local" onkeydown="enterHandler(event)"> <div>To</div>
<div></div>
<div></div>
<div></div> <input name="time-f" value="{{ .Data.TimeFrom }}" type="datetime-local">
{{/* Vertical line */}} <input name="time-t" value="{{ .Data.TimeTo }}" type="datetime-local">
<div></div>
<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><a href="#" onclick="offsetTime(-3600)">◀</a></div>
<div>Hour</div> <div>Hour</div>
<div><a href="#" onclick="offsetTime(3600)">▶</a></div> <div><a href="#" onclick="offsetTime(3600)">▶</a></div>
<div></div> <div class="preset"><a href="#" onclick="preset(24)">Last 24 hours</a></div>
<div><a href="#" onclick="offsetTime(-7 * 86400)">◀</a></div>
<div>Week</div>
<div><a href="#" onclick="offsetTime(7 * 86400)">▶</a></div>
<div></div>
{{/* Vertical line */}}
<div></div>
<div class="preset">⚫︎ <a href="#" onclick="preset(1)">Last hour</a></div>
<div class="preset">⚫︎ <a href="#" onclick="preset(24 * 7)">Last 7 days</a></div>
<div></div>
{{/* ====== Row 3 ====== */}}
<div></div>
<div>To</div>
<input name="time-t" value="{{ .Data.TimeTo }}" type="datetime-local" onkeydown="enterHandler(event)">
<div><img src="/images/{{ .VERSION }}/{{ .CONFIG.THEME }}/forward.svg" onclick="document.getElementById('form-time-selector').submit()"></div>
<div></div>
<div></div>
{{/* Vertical line */}}
<div></div>
<div><a href="#" onclick="offsetTime(-86400)">◀</a></div> <div><a href="#" onclick="offsetTime(-86400)">◀</a></div>
<div>Day</div> <div>Day</div>
<div><a href="#" onclick="offsetTime(86400)">▶</a></div> <div><a href="#" onclick="offsetTime(86400)">▶</a></div>
<div></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><a href="#" onclick="offsetTime(-31 * 86400)">◀</a></div>
<div>Month</div> <div>Month</div>
<div><a href="#" onclick="offsetTime(31 * 86400)">▶</a></div> <div><a href="#" onclick="offsetTime(31 * 86400)">▶</a></div>
</div>
<div></div> <button>OK</button>
{{/* Vertical line */}}
<div></div>
<div class="preset">⚫︎ <a href="#" onclick="preset(24)">Last 24 hours</a></div>
<div class="preset">⚫︎ <a href="#" onclick="preset(24 * 31)">Last 31 days</a></div>
<div></div>
{{/* ====== Row 4 ====== */}}
<div style="grid-column: 1 / 5; height: 8px"></div>
<div style="grid-column: 8 / 17; height: 8px"></div>
<div style="grid-column: 18 / 22; height: 8px"></div>
</div> </div>
</form> </form>
{{ end }} {{ end }}

View file

@ -3,7 +3,7 @@
<script type="module" defer> <script type="module" defer>
import {UI} from "/js/{{ .VERSION }}/datapoint_edit.mjs" import {UI} from "/js/{{ .VERSION }}/datapoint_edit.mjs"
window._ui = new UI({{ .Data.Datapoint }}) window._ui = new UI()
</script> </script>
{{ block "page_label" . }}{{end}} {{ block "page_label" . }}{{end}}
@ -46,17 +46,6 @@
<button id="button-update">Update</button> <button id="button-update">Update</button>
{{ end }} {{ end }}
</div> </div>
<div></div>
<div style="margin-top: 32px">
<b>Used in the following triggers:</b>
<ul>
{{ range .Data.Triggers }}
<li><a href="/trigger/edit/{{ .ID }}">{{ .Name }}</a></li>
{{ end }}
</ul>
</div>
</div>
</form> </form>
{{ end }} {{ end }}

View file

@ -12,7 +12,6 @@
{{ if eq .Data.Datapoint.Datatype "INT" }} {{ if eq .Data.Datapoint.Datatype "INT" }}
<div style="margin-top: 16px"> <div style="margin-top: 16px">
<input onchange="selectDisplay('graph')" name="display" type="radio" id="display-graph" {{ if $graph }} checked {{ end}}> <label for="display-graph">Graph</label> <input onchange="selectDisplay('graph')" name="display" type="radio" id="display-graph" {{ if $graph }} checked {{ end}}> <label for="display-graph">Graph</label>
<br>
<input onchange="selectDisplay('list')" name="display" type="radio" id="display-list" {{ if not $graph }} checked {{ end }}> <label for="display-list">List</label> <input onchange="selectDisplay('list')" name="display" type="radio" id="display-list" {{ if not $graph }} checked {{ end }}> <label for="display-list">List</label>
</div> </div>
{{ end }} {{ end }}

View file

@ -3,46 +3,6 @@
{{ $theme := .CONFIG.THEME }} {{ $theme := .CONFIG.THEME }}
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/datapoints.css"> <link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/datapoints.css">
<script type="text/javascript" defer> <script type="text/javascript" defer>
function validateRegex(rxp) {
try {
''.match(rxp)
return true
} catch {
return false
}
}
function showDatapoints(id) {
if (!id)
document
.querySelectorAll(`[x-datapoint-id]`)
.forEach(matchedDP=>matchedDP.classList.remove('hidden'))
else
document
.querySelectorAll(`[x-datapoint-id="${id}"]`)
.forEach(matchedDP=>matchedDP.classList.remove('hidden'))
}
function hideDatapoints(id) {
if (!id)
document.querySelectorAll(`[x-datapoint-id]`).forEach(matchedDP=>
matchedDP.classList.add('hidden')
)
else
document.querySelectorAll(`[x-datapoint-id="${id}"]`).forEach(matchedDP=>
matchedDP.classList.add('hidden')
)
}
function updateRegexValidatedStatus(validated) {
const inputFilter = document.getElementById('datapoints-filter')
if (validated)
inputFilter.classList.remove('invalid-regex')
else
inputFilter.classList.add('invalid-regex')
}
function filterDatapoints(inputFilter) { function filterDatapoints(inputFilter) {
const filter = inputFilter.value.toLowerCase() const filter = inputFilter.value.toLowerCase()
const datapoints = document.querySelectorAll('#datapoints .name') const datapoints = document.querySelectorAll('#datapoints .name')
@ -50,25 +10,25 @@
// Shortcut to show everything if a filter is not given. // Shortcut to show everything if a filter is not given.
if (filter == '') { if (filter == '') {
showDatapoints() document.querySelectorAll(`[x-datapoint-id]`).forEach(matchedDP=>
matchedDP.classList.remove('hidden')
)
return return
} }
// Show nothing if the regex is invalid and can't matching anything.
if (!validateRegex(filter)) {
hideDatapoints()
updateRegexValidatedStatus(false)
return
} else
updateRegexValidatedStatus(true)
datapoints.forEach(dp=>{ datapoints.forEach(dp=>{
const dpName = dp.getAttribute('x-datapoint-name') const dpName = dp.getAttribute('x-datapoint-name')
if (dpName.toLowerCase().includes(filter)) {
datapointID = dp.getAttribute('x-datapoint-id') datapointID = dp.getAttribute('x-datapoint-id')
if (dpName.toLowerCase().match(filter)) document.querySelectorAll(`[x-datapoint-id="${datapointID}"]`).forEach(matchedDP=>
showDatapoints(datapointID) matchedDP.classList.remove('hidden')
else )
hideDatapoints(datapointID) } else {
datapointID = dp.getAttribute('x-datapoint-id')
document.querySelectorAll(`[x-datapoint-id="${datapointID}"]`).forEach(matchedDP=>
matchedDP.classList.add('hidden')
)
}
}) })
} }
@ -89,7 +49,7 @@
<a href="/datapoint/edit/0">Create</a> <a href="/datapoint/edit/0">Create</a>
<div style="margin-top: 16px"> <div style="margin-top: 16px">
<input id="datapoints-filter" type="text" placeholder="Filter (regexp)" style="width: 320px;" oninput="filterDatapoints(this)"> <input id="datapoints-filter" type="text" placeholder="Filter" style="width: 320px;" oninput="filterDatapoints(this)">
</div> </div>
<div id="datapoints"> <div id="datapoints">

View file

@ -10,133 +10,114 @@
{{ block "page_label" . }}{{ end }} {{ block "page_label" . }}{{ end }}
<div style="margin-bottom: 16px; display: grid; grid-template-columns: min-content min-content; grid-gap: 32px;"> <div style="margin-bottom: 16px">
<div style="white-space: nowrap"> <input {{ if eq .Data.Selection "CURRENT" }}checked{{ end }} type="radio" name="selection" id="selection-current" onclick="_ui.selectCurrent()"> <label for="selection-current">Current problems</label>
<b>Problem selection</b><br> <input {{ if eq .Data.Selection "ALL" }}checked{{ end }} type="radio" name="selection" id="selection-all" onclick="_ui.selectAll()"> <label for="selection-all">Time-filtered problems</label>
<input {{ if eq .Data.Selection "CURRENT" }}checked{{ end }} type="radio" name="selection" id="selection-current" onclick="_ui.selectCurrent()"> <label for="selection-current">Current</label>
<br>
<input {{ if eq .Data.Selection "ALL" }}checked{{ end }} type="radio" name="selection" id="selection-all" onclick="_ui.selectAll()"> <label for="selection-all">All</label>
</div> </div>
<div style="white-space: nowrap">
<b>Show</b><br> <hr style="margin-bottom: 16px">
{{ block "timefilter" . }}{{ end }}
<div style="margin-top: 16px">
<input type="radio" name="display" id="display-table" onclick="_ui.displayAreas()"> <label for="display-table">Areas</label> <input type="radio" name="display" id="display-table" onclick="_ui.displayAreas()"> <label for="display-table">Areas</label>
<br>
<input type="radio" name="display" id="display-list" onclick="_ui.displayList()"> <label for="display-list">List</label> <input type="radio" name="display" id="display-list" onclick="_ui.displayList()"> <label for="display-list">List</label>
<div style="margin-top: 8px"> <div style="margin-top: 8px">
<input type="checkbox" id="show-acked" onclick="_ui.toggleAcknowledged(event)"> <label for="show-acked">Show acknowledged</label> <input type="checkbox" id="show-acked" onclick="_ui.toggleAcknowledged(event)"> <label for="show-acked">Show acknowledged</label>
</div> </div>
</div> </div>
</div>
{{ block "timefilter" . }}{{ end }}
<div class="display-list hidden"> <div class="display-list hidden">
<div id="problems-list" class="table"> <div id="problems-list">
<div class="row"><h2>Unacknowledged</h2></div> <div style="grid-column: 1/-1;"><h2>Unacknowledged</h2></div>
<div class="header">OK</div>
<div class="header">Trigger</div> <div class="header">Trigger</div>
<div class="header">Area</div> <div class="header">Area</div>
<div class="header">Section</div> <div class="header">Section</div>
<div class="header">Since</div> <div class="header">Since</div>
<div class="header">Until</div>
<div class="header"></div>
{{ range .Data.Problems }} {{ range .Data.Problems }}
{{ if .Acknowledged }} {{ if .Acknowledged }}
{{ continue }} {{ continue }}
{{ end }} {{ end }}
<div class="line"></div> <div class="line"></div>
{{/* NODATA datapoints */}}
{{ if eq .TriggerID -1 }} {{ if eq .TriggerID -1 }}
<div class="{{ if .Acknowledged }}acked hidden{{ end }}">{{ if .IsArchived }}<img src="/images/{{ $version }}/{{ $theme }}/ok.svg">{{ else }}<img src="/images/{{ $version }}/{{ $theme }}/warning.svg">{{ end }}</div>
<div class="trigger">{{ .TriggerName }}</div> <div class="trigger">{{ .TriggerName }}</div>
<div class="area">{{ .AreaName }}</div> <div class="area">{{ .AreaName }}</div>
<div class="section">{{ .SectionName }}</div> <div class="section">{{ .SectionName }}</div>
<div class="start">{{ format_time .Start }}</div> <div class="start"></div>
<div class="end"></div> <div class="acknowledge">
<div class="icons">
<img class="info" src="/images/{{ $version }}/{{ $theme }}/info-outline.svg"> <img class="info" src="/images/{{ $version }}/{{ $theme }}/info-outline.svg">
<img class="acknowledge" src="/images/{{ $version }}/{{ $theme }}/acknowledge.svg"> <img src="/images/{{ $version }}/{{ $theme }}/acknowledge.svg">
</div> </div>
{{ else }} {{ else }}
<div class="{{ if .Acknowledged }}acked hidden{{ end }}">{{ if .IsArchived }}<img src="/images/{{ $version }}/{{ $theme }}/ok.svg">{{ else }}<img src="/images/{{ $version }}/{{ $theme }}/warning.svg">{{ end }}</div>
<div class="trigger"><a href="/trigger/edit/{{ .TriggerID }}">{{ .TriggerName }}</a></div> <div class="trigger"><a href="/trigger/edit/{{ .TriggerID }}">{{ .TriggerName }}</a></div>
<div class="area">{{ .AreaName }}</div> <div class="area">{{ .AreaName }}</div>
<div class="section">{{ .SectionName }}</div> <div class="section">{{ .SectionName }}</div>
<div class="start">{{ format_time .Start }}</div> <div class="start">{{ format_time .Start }}</div>
<div class="end">{{ if not .End.IsZero }}{{ format_time .End }}{{ else }}-{{ end }}</div> <div class="acknowledge">
<div class="icons">
{{ if .FormattedValues }} {{ if .FormattedValues }}
<img class="info" src="/images/{{ $version }}/{{ $theme }}/info-filled.svg" title="{{ .FormattedValues }}"> <img class="info" src="/images/{{ $version }}/{{ $theme }}/info-filled.svg" title="{{ .FormattedValues }}">
{{ else }} {{ else }}
<img class="info" src="/images/{{ $version }}/{{ $theme }}/info-outline.svg"> <img class="info" src="/images/{{ $version }}/{{ $theme }}/info-outline.svg">
{{ end }} {{ end }}
<img class="acknowledge" onclick="location.href = '/problem/acknowledge/{{ .ID }}'" src="/images/{{ $version }}/{{ $theme }}/acknowledge-filled.svg"> <a href="/problem/acknowledge/{{ .ID }}">
<img src="/images/{{ $version }}/{{ $theme }}/acknowledge-filled.svg">
</a>
</div> </div>
{{ end }} {{ end }}
{{ end }} {{ end }}
</div> </div>
<div id="acknowledged-list" class="table hidden"> <div id="acknowledged-list" class="hidden">
<div class="row"><h2>Acknowledged</h2></div> <div style="grid-column: 1/-1;"><h2>Acknowledged</h2></div>
<div class="header">OK</div>
<div class="header">Trigger</div> <div class="header">Trigger</div>
<div class="header">Area</div> <div class="header">Area</div>
<div class="header">Section</div> <div class="header">Section</div>
<div class="header">Since</div> <div class="header">Since</div>
<div class="header">Until</div>
<div class="header"></div>
{{ range .Data.Problems }} {{ range .Data.Problems }}
{{ if not .Acknowledged }} {{ if not .Acknowledged }}
{{ continue }} {{ continue }}
{{ end }} {{ end }}
<div class="line"></div> <div class="line"></div>
<div class="{{ if .Acknowledged }}acked hidden{{ end }}">{{ if .IsArchived }}<img src="/images/{{ $version }}/{{ $theme }}/ok.svg">{{ else }}<img src="/images/{{ $version }}/{{ $theme }}/warning.svg">{{ end }}</div>
<div class="trigger"><a href="/trigger/edit/{{ .TriggerID }}">{{ .TriggerName }}</a></div> <div class="trigger"><a href="/trigger/edit/{{ .TriggerID }}">{{ .TriggerName }}</a></div>
<div class="area">{{ .AreaName }}</div> <div class="area">{{ .AreaName }}</div>
<div class="section">{{ .SectionName }}</div> <div class="section">{{ .SectionName }}</div>
<div class="start">{{ format_time .Start }}</div> <div class="start">{{ format_time .Start }}</div>
<div class="end">{{ if not .End.IsZero }}{{ format_time .End }}{{ else }}-{{ end }}</div> <div class="acknowledge">
<div class="icons">
{{ if .FormattedValues }} {{ if .FormattedValues }}
<img class="info" src="/images/{{ $version }}/{{ $theme }}/info-filled.svg" title="{{ .FormattedValues }}"> <img class="info" src="/images/{{ $version }}/{{ $theme }}/info-filled.svg" title="{{ .FormattedValues }}">
{{ else }} {{ else }}
<img class="info" src="/images/{{ $version }}/{{ $theme }}/info-outline.svg"> <img class="info" src="/images/{{ $version }}/{{ $theme }}/info-outline.svg">
{{ end }} {{ end }}
<img class="acknowledge" onclick="location.href = '/problem/unacknowledge/{{ .ID }}'" src="/images/{{ $version }}/{{ $theme }}/acknowledge-outline.svg"> <a href="/problem/unacknowledge/{{ .ID }}"><img src="/images/{{ $version }}/{{ $theme }}/acknowledge-outline.svg"></a>
</div> </div>
{{ end }} {{ end }}
</div> </div>
</div> </div>
<div class="display-areas hidden"> <div class="display-areas hidden">
<div id="area-grouped"> <div id="areas">
{{ range $areaName, $sections := .Data.ProblemsGrouped }} {{ range $areaName, $sections := .Data.ProblemsGrouped }}
<div class="area table"> <div class="area">
<div class="row"><h2>{{ $areaName }}</h2></div> <div class="name">{{ $areaName }}</div>
{{ range $sectionName, $problems := $sections }} {{ range $sectionName, $problems := $sections }}
<div class="section row" style="margin-top: 16px; font-weight: bold;">{{ $sectionName }}</div> <div class="section problems">
<div class="name">{{ $sectionName }}</div>
<div class="header">OK</div>
<div class="header">Trigger</div>
<div class="header">Since</div>
<div class="header">Until</div>
<div class="header"></div>
<div class="line"></div>
{{ range $problems }} {{ range $problems }}
<div class="{{ if .Acknowledged }}acked hidden{{ end }}">{{ if .IsArchived }}<img src="/images/{{ $version }}/{{ $theme }}/ok.svg">{{ else }}<img src="/images/{{ $version }}/{{ $theme }}/warning.svg">{{ end }}</div> <div class="{{ if .Acknowledged }}acked hidden{{ end }}">{{ if .IsArchived }}<span class="ok">Archived</span>{{ else }}<span class="error">Current</span>{{ end }}</div>
<div class="{{ if .Acknowledged }}acked hidden{{ end }} trigger">{{ .TriggerName }}</div> <div class="{{ if .Acknowledged }}acked hidden{{ end }} trigger">{{ .TriggerName }}</div>
<div class="{{ if .Acknowledged }}acked hidden{{ end }} since">{{ if not .Start.IsZero }}{{ format_time .Start }}{{ else }}-{{ end }}</div> {{ if eq (.Start | html) "0001-01-01 00:00:00 +0000 UTC" }}
<div class="{{ if .Acknowledged }}acked hidden{{ end }} until">{{ if not .End.IsZero }}{{ format_time .End }}{{ else }}-{{ end }}</div> <div class="{{ if .Acknowledged }}acked hidden{{ end }} since"></div>
{{ else }}
<div class="{{ if .Acknowledged }}acked hidden{{ end }} since">{{ format_time .Start }}</div>
{{ end }}
{{ if .FormattedValues }} {{ if .FormattedValues }}
<div class="{{ if .Acknowledged }}acked hidden{{ end }}"><img src="/images/{{ $version }}/{{ $theme }}/info-filled.svg" title="{{ .FormattedValues }}"></div> <div class="{{ if .Acknowledged }}acked hidden{{ end }}"><img src="/images/{{ $version }}/{{ $theme }}/info-filled.svg" title="{{ .FormattedValues }}"></div>
@ -145,6 +126,7 @@
{{ end }} {{ end }}
{{ end }} {{ end }}
</div>
{{ end }} {{ end }}
</div> </div>
{{ end }} {{ end }}