Compare commits
No commits in common. "main" and "v34" have entirely different histories.
41
datapoint.go
|
@ -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
|
@ -29,7 +29,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const VERSION = "v41"
|
const VERSION = "v34"
|
||||||
|
|
||||||
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)
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 13 KiB |
|
@ -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();
|
|
|
@ -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();
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
40
trigger.go
|
@ -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
|
|
||||||
} // }}}
|
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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">
|
||||||
|
|