Implemented basic functions

This commit is contained in:
Magnus Åhall 2024-04-30 08:04:16 +02:00
parent 89f483171a
commit 965e2daeb3
22 changed files with 711 additions and 58 deletions

View File

@ -3,9 +3,12 @@ package main
import ( import (
// External // External
we "git.gibonuddevalla.se/go/wrappederror" we "git.gibonuddevalla.se/go/wrappederror"
"github.com/jmoiron/sqlx"
// Standard // Standard
"database/sql" "database/sql"
"errors"
"strings"
"time" "time"
) )
@ -20,7 +23,6 @@ const (
type Datapoint struct { type Datapoint struct {
ID int ID int
Name string Name string
SectionID int `db:"section_id"`
Datatype DatapointType Datatype DatapointType
LastValue time.Time `db:"last_value"` LastValue time.Time `db:"last_value"`
DatapointValueJSON []byte `db:"datapoint_value_json"` DatapointValueJSON []byte `db:"datapoint_value_json"`
@ -36,20 +38,53 @@ type DatapointValue struct {
ValueDateTime sql.NullTime `db:"value_datetime"` ValueDateTime sql.NullTime `db:"value_datetime"`
} }
func (dp DatapointValue) Value() any { func (dp DatapointValue) Value() any { // {{{
if dp.ValueInt.Valid { if dp.ValueInt.Valid {
return dp.ValueInt.Int64 return dp.ValueInt.Int64
} }
if dp.ValueString.Valid { if dp.ValueString.Valid {
return dp.ValueString.String return dp.ValueString.String
} }
if dp.ValueDateTime.Valid { if dp.ValueDateTime.Valid {
return dp.ValueDateTime.Time return dp.ValueDateTime.Time
} }
return nil return nil
} // }}}
func (dp DatapointValue) FormattedTime() string {
if dp.ValueDateTime.Valid {
return dp.ValueDateTime.Time.Format("2006-01-02 15:04:05")
}
return "invalid time"
}
func (dp Datapoint) Update() (err error) {
name := strings.TrimSpace(dp.Name)
if name == "" {
err = errors.New("Name can't be empty")
return
}
if dp.ID == 0 {
_, err = service.Db.Conn.Exec(
`INSERT INTO datapoint(name, datatype) VALUES($1, $2)`,
name,
dp.Datatype,
)
} else {
_, err = service.Db.Conn.Exec(
`UPDATE datapoint SET name=$2, datatype=$3 WHERE id=$1`,
dp.ID,
name,
dp.Datatype,
)
}
return
} }
func DatapointAdd[T any](name string, value T) (err error) { func DatapointAdd[T any](name string, value T) (err error) { // {{{
row := service.Db.Conn.QueryRow(`SELECT id, datatype FROM datapoint WHERE name=$1`, name) row := service.Db.Conn.QueryRow(`SELECT id, datatype FROM datapoint WHERE name=$1`, name)
var dpID int var dpID int
@ -81,13 +116,94 @@ func DatapointAdd[T any](name string, value T) (err error) {
} }
return return
} } // }}}
func DatapointRetrieve(name string) (dp Datapoint, err error) { func DatapointsRetrieve() (dps []Datapoint, err error) { // {{{
row := service.Db.Conn.QueryRowx( dps = []Datapoint{}
`SELECT * FROM datapoint dp WHERE dp.name = $1`, var rows *sqlx.Rows
name, rows, err = service.Db.Conn.Queryx(`
) SELECT
dp.id,
dp.name,
dp.datatype,
dp.last_value,
dpv.id AS v_id,
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
dp.name ASC
`)
if err != nil {
err = we.Wrap(err)
}
defer rows.Close()
type DbRes struct {
ID int
Name string
Datatype DatapointType
LastValue time.Time `db:"last_value"`
VID sql.NullInt64 `db:"v_id"`
Ts sql.NullTime
ValueInt sql.NullInt64 `db:"value_int"`
ValueString sql.NullString `db:"value_string"`
ValueDateTime sql.NullTime `db:"value_datetime"`
}
for rows.Next() {
dp := Datapoint{}
dpv := DatapointValue{}
res := DbRes{}
err = rows.StructScan(&res)
if err != nil {
err = we.Wrap(err)
return
}
dp.ID = res.ID
dp.Name = res.Name
dp.Datatype = res.Datatype
dp.LastValue = res.LastValue
if res.VID.Valid {
dpv.ID = int(res.VID.Int64)
dpv.Ts = res.Ts.Time
dpv.ValueInt = res.ValueInt
dpv.ValueString = res.ValueString
dpv.ValueDateTime = res.ValueDateTime
dp.LastDatapointValue = dpv
}
dps = append(dps, dp)
}
return
} // }}}
func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{
var query string
var param any
if id > 0 {
query = `SELECT * FROM datapoint WHERE id = $1`
param = id
dp.ID = id
} else {
query = `SELECT * FROM datapoint WHERE name = $1`
param = name
}
row := service.Db.Conn.QueryRowx(query, param)
err = row.StructScan(&dp) err = row.StructScan(&dp)
if err != nil { if err != nil {
err = we.Wrap(err).WithData(name) err = we.Wrap(err).WithData(name)
@ -104,9 +220,15 @@ func DatapointRetrieve(name string) (dp Datapoint, err error) {
dp.ID, dp.ID,
) )
err = row.StructScan(&dp.LastDatapointValue) err = row.StructScan(&dp.LastDatapointValue)
if err == sql.ErrNoRows {
err = nil
return
}
if err != nil { if err != nil {
err = we.Wrap(err).WithData(dp.ID) err = we.Wrap(err).WithData(dp.ID)
return return
} }
return return
} } // }}}

109
main.go
View File

@ -20,6 +20,7 @@ import (
"path" "path"
"sort" "sort"
"strconv" "strconv"
"time"
) )
const VERSION = "v1" const VERSION = "v1"
@ -88,8 +89,23 @@ func main() { // {{{
service.SetDatabase(sqlProvider) service.SetDatabase(sqlProvider)
service.SetStaticFS(staticFS, "static") service.SetStaticFS(staticFS, "static")
service.SetStaticDirectory("static", flagDev) service.SetStaticDirectory("static", flagDev)
service.InitDatabaseConnection()
err = service.Db.Connect()
if err != nil {
logger.Error("application", "error", err)
return
}
_, err = service.Db.Conn.Exec(`SET TIMEZONE TO 'Europe/Stockholm'`)
if err != nil {
logger.Error("application", "error", err)
return
}
service.Register("/", false, false, staticHandler) service.Register("/", false, false, staticHandler)
service.Register("/problems", false, false, pageProblems) service.Register("/problems", false, false, pageProblems)
service.Register("/datapoints", false, false, pageDatapoints)
service.Register("/datapoint/edit/{id}", false, false, pageDatapointEdit)
service.Register("/datapoint/update/{id}", false, false, pageDatapointUpdate)
service.Register("/triggers", false, false, pageTriggers) service.Register("/triggers", false, false, pageTriggers)
service.Register("/trigger/edit/{id}", false, false, pageTriggerEdit) service.Register("/trigger/edit/{id}", false, false, pageTriggerEdit)
service.Register("/trigger/update/{id}", false, false, pageTriggerUpdate) service.Register("/trigger/update/{id}", false, false, pageTriggerUpdate)
@ -187,21 +203,26 @@ func getPage(layout, page string) (tmpl *template.Template, err error) { // {{{
return tmpl, nil return tmpl, nil
} }
funcMap := template.FuncMap{
"format_time": func(t time.Time) string {
return t.Local().Format("2006-01-02 15:04:05")
},
}
filenames := []string{layoutFilename, pageFilename} filenames := []string{layoutFilename, pageFilename}
filenames = append(filenames, componentFilenames...) filenames = append(filenames, componentFilenames...)
logger.Info("template", "op", "parse", "layout", layout, "page", page, "filenames", filenames) logger.Info("template", "op", "parse", "layout", layout, "page", page, "filenames", filenames)
if flagDev { if flagDev {
parsedTemplates[page], err = template.ParseFS(os.DirFS("."), filenames...) tmpl, err = template.New("main.gotmpl").Funcs(funcMap).ParseFS(os.DirFS("."), filenames...)
} else { } else {
parsedTemplates[page], err = template.ParseFS(viewFS, filenames...) tmpl, err = template.New("main.gotmpl").ParseFS(viewFS, filenames...)
} }
tmpl = parsedTemplates[page]
if err != nil { if err != nil {
err = we.Wrap(err).Log() err = we.Wrap(err).Log()
return return
} }
parsedTemplates[page] = tmpl
return return
} // }}} } // }}}
@ -237,6 +258,80 @@ func pageProblems(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
page.Render(w) page.Render(w)
return return
} // }}} } // }}}
func pageDatapoints(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
page := Page{
LAYOUT: "main",
PAGE: "datapoints",
}
datapoints, err := DatapointsRetrieve()
if err != nil {
httpError(w, we.Wrap(err).Log())
return
}
logger.Info("FOO", "dps", datapoints)
page.Data = map[string]any{
"Datapoints": datapoints,
}
page.Render(w)
return
} // }}}
func pageDatapointEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
httpError(w, we.Wrap(err).Log())
return
}
var datapoint Datapoint
if id == 0 {
datapoint.Name = "new_datapoint"
datapoint.Datatype = "INT"
} else {
datapoint, err = DatapointRetrieve(id, "")
if err != nil {
httpError(w, we.Wrap(err).Log())
return
}
}
page := Page{
LAYOUT: "main",
PAGE: "datapoint_edit",
MENU: "datapoints",
Icon: "datapoints",
Label: "Datapoint",
}
page.Data = map[string]any{
"Datapoint": datapoint,
}
page.Render(w)
return
} // }}}
func pageDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
httpError(w, we.Wrap(err).Log())
return
}
var dp Datapoint
dp.ID = id
dp.Name = r.FormValue("name")
dp.Datatype = DatapointType(r.FormValue("datatype"))
err = dp.Update()
if err != nil {
httpError(w, we.Wrap(err).Log())
return
}
w.Header().Add("Location", "/datapoints")
w.WriteHeader(302)
} // }}}
func pageTriggers(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{ func pageTriggers(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
areas, err := TriggersRetrieve() areas, err := TriggersRetrieve()
if err != nil { if err != nil {
@ -275,7 +370,7 @@ func pageTriggerEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { //
datapoints := make(map[string]Datapoint) datapoints := make(map[string]Datapoint)
for _, dpname := range trigger.Datapoints { for _, dpname := range trigger.Datapoints {
dp, err := DatapointRetrieve(dpname) dp, err := DatapointRetrieve(0, dpname)
if err != nil { if err != nil {
httpError(w, we.Wrap(err).Log()) httpError(w, we.Wrap(err).Log())
return return
@ -320,7 +415,7 @@ func pageTriggerUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { /
return return
} }
w.Header().Add("Location", fmt.Sprintf("/trigger/edit/%d", id)) w.Header().Add("Location", "/triggers")
w.WriteHeader(302) w.WriteHeader(302)
} // }}} } // }}}
func pageTriggerRun(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{ func pageTriggerRun(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
@ -343,7 +438,7 @@ func pageTriggerRun(w http.ResponseWriter, r *http.Request, _ *session.T) { // {
datapoints := make(map[string]Datapoint) datapoints := make(map[string]Datapoint)
for _, dpname := range trigger.Datapoints { for _, dpname := range trigger.Datapoints {
dp, err := DatapointRetrieve(dpname) dp, err := DatapointRetrieve(0, dpname)
if err != nil { if err != nil {
httpError(w, we.Wrap(err).Log()) httpError(w, we.Wrap(err).Log())
return return

View File

@ -52,6 +52,7 @@ func (p *Page) Render(w http.ResponseWriter) {
"Data": p.Data, "Data": p.Data,
} }
logger.Info("foo", "tmpl", tmpl)
err = tmpl.Execute(w, data) err = tmpl.Execute(w, data)
if err != nil { if err != nil {
httpError(w, we.Wrap(err).Log()) httpError(w, we.Wrap(err).Log())

9
sql/00006.sql Normal file
View File

@ -0,0 +1,9 @@
CREATE TABLE public.problem (
id serial8 NOT NULL,
trigger_id int4 NOT NULL,
"start" timestamptz DEFAULT now() NOT NULL,
"end" timestamptz NULL,
acknowledged bool DEFAULT false NOT NULL,
CONSTRAINT problem_pk PRIMARY KEY (id),
CONSTRAINT problem_trigger_fk FOREIGN KEY (trigger_id) REFERENCES public."trigger"(id) ON DELETE CASCADE ON UPDATE CASCADE
);

1
sql/00007.sql Normal file
View File

@ -0,0 +1 @@
ALTER TABLE datapoint DROP COLUMN section_id;

115
static/css/datapoints.css Normal file
View File

@ -0,0 +1,115 @@
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
*:focus {
outline: none;
}
[onClick] {
cursor: pointer;
}
html,
body {
margin: 0;
padding: 0;
}
body {
background: #282828;
font-family: "Roboto", sans-serif;
font-weight: 300;
color: #d5c4a1;
font-size: 11pt;
}
h1,
h2 {
margin-top: 0px;
margin-bottom: 4px;
}
h1 {
font-size: 1.5em;
color: #fb4934;
}
h2 {
font-size: 1.25em;
}
a {
color: #3f9da1;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
b {
font-weight: 500;
}
.roboto-light {
font-family: "Roboto", sans-serif;
font-weight: 300;
font-style: normal;
}
.roboto-medium {
font-family: "Roboto", sans-serif;
font-weight: 500;
font-style: normal;
}
input[type="text"],
textarea,
select {
font-family: "Roboto Mono", monospace;
background: #202020;
color: #d5c4a1;
padding: 4px 8px;
border: none;
font-size: 1em;
}
button {
background: #202020;
color: #d5c4a1;
padding: 8px 32px;
border: 1px solid #3a3a3a;
font-size: 1em;
height: 3em;
}
button:focus {
background: #333;
}
#datapoints {
display: grid;
grid-template-columns: repeat(4, min-content);
grid-gap: 8px 16px;
margin-top: 16px;
}
#datapoints .header {
font-weight: 500;
}
#datapoints div {
white-space: nowrap;
}
.widgets {
display: grid;
grid-template-columns: min-content 1fr;
gap: 8px 16px;
}
.widgets .label {
margin-top: 4px;
}
.widgets input[type="text"],
.widgets textarea {
width: 100%;
}
.widgets .datapoints {
font: "Roboto Mono", monospace;
display: grid;
grid-template-columns: min-content 1fr;
gap: 6px 8px;
margin-bottom: 8px;
}
.widgets .action {
display: grid;
grid-template-columns: min-content min-content;
grid-gap: 8px;
}

View File

@ -20,6 +20,7 @@ body {
body { body {
background: #282828; background: #282828;
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
font-weight: 300;
color: #d5c4a1; color: #d5c4a1;
font-size: 11pt; font-size: 11pt;
} }
@ -35,6 +36,16 @@ h1 {
h2 { h2 {
font-size: 1.25em; font-size: 1.25em;
} }
a {
color: #3f9da1;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
b {
font-weight: 500;
}
.roboto-light { .roboto-light {
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
font-weight: 300; font-weight: 300;
@ -46,7 +57,8 @@ h2 {
font-style: normal; font-style: normal;
} }
input[type="text"], input[type="text"],
textarea { textarea,
select {
font-family: "Roboto Mono", monospace; font-family: "Roboto Mono", monospace;
background: #202020; background: #202020;
color: #d5c4a1; color: #d5c4a1;
@ -62,6 +74,9 @@ button {
font-size: 1em; font-size: 1em;
height: 3em; height: 3em;
} }
button:focus {
background: #333;
}
#layout { #layout {
display: grid; display: grid;
grid-template-areas: "menu content"; grid-template-areas: "menu content";
@ -94,6 +109,7 @@ button {
margin-bottom: 32px; margin-bottom: 32px;
} }
#page .page-label div { #page .page-label div {
font-weight: 500;
font-size: 1.5em; font-size: 1.5em;
color: #fb4934; color: #fb4934;
} }
@ -113,7 +129,7 @@ button {
#areas .area > .name { #areas .area > .name {
background: #fb4934; background: #fb4934;
color: #fff; color: #fff;
font-weight: bold; font-weight: 500;
padding: 4px 16px; padding: 4px 16px;
border-top-left-radius: 4px; border-top-left-radius: 4px;
border-top-right-radius: 4px; border-top-right-radius: 4px;
@ -122,7 +138,7 @@ button {
margin: 8px 16px; margin: 8px 16px;
} }
#areas .area .section > .name { #areas .area .section > .name {
font-weight: bold; font-weight: 500;
} }
#areas .area .section .triggers a { #areas .area .section .triggers a {
color: inherit; color: inherit;
@ -139,5 +155,5 @@ button {
height: 16px; height: 16px;
} }
#areas .area .section .triggers .trigger .label { #areas .area .section .triggers .trigger .label {
color: #f7edd7; color: #3f9da1;
} }

View File

@ -20,6 +20,7 @@ body {
body { body {
background: #282828; background: #282828;
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
font-weight: 300;
color: #d5c4a1; color: #d5c4a1;
font-size: 11pt; font-size: 11pt;
} }
@ -35,6 +36,16 @@ h1 {
h2 { h2 {
font-size: 1.25em; font-size: 1.25em;
} }
a {
color: #3f9da1;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
b {
font-weight: 500;
}
.roboto-light { .roboto-light {
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
font-weight: 300; font-weight: 300;
@ -46,7 +57,8 @@ h2 {
font-style: normal; font-style: normal;
} }
input[type="text"], input[type="text"],
textarea { textarea,
select {
font-family: "Roboto Mono", monospace; font-family: "Roboto Mono", monospace;
background: #202020; background: #202020;
color: #d5c4a1; color: #d5c4a1;
@ -62,3 +74,6 @@ button {
font-size: 1em; font-size: 1em;
height: 3em; height: 3em;
} }
button:focus {
background: #333;
}

View File

@ -20,6 +20,7 @@ body {
body { body {
background: #282828; background: #282828;
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
font-weight: 300;
color: #d5c4a1; color: #d5c4a1;
font-size: 11pt; font-size: 11pt;
} }
@ -35,6 +36,16 @@ h1 {
h2 { h2 {
font-size: 1.25em; font-size: 1.25em;
} }
a {
color: #3f9da1;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
b {
font-weight: 500;
}
.roboto-light { .roboto-light {
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
font-weight: 300; font-weight: 300;
@ -46,7 +57,8 @@ h2 {
font-style: normal; font-style: normal;
} }
input[type="text"], input[type="text"],
textarea { textarea,
select {
font-family: "Roboto Mono", monospace; font-family: "Roboto Mono", monospace;
background: #202020; background: #202020;
color: #d5c4a1; color: #d5c4a1;
@ -62,6 +74,9 @@ button {
font-size: 1em; font-size: 1em;
height: 3em; height: 3em;
} }
button:focus {
background: #333;
}
.widgets { .widgets {
display: grid; display: grid;
grid-template-columns: min-content 1fr; grid-template-columns: min-content 1fr;

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32.000126"
height="17.454464"
viewBox="0 0 8.4667 4.6181601"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="datapoints.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">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="8.516629"
inkscape:cx="7.9843798"
inkscape:cy="-12.622365"
inkscape:window-width="1916"
inkscape:window-height="1041"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-102.9229,-148.76801)">
<title
id="title1">file-chart</title>
<title
id="title1-6">chart-timeline-variant</title>
<path
d="m 103.69267,151.8468 0.19242,0.0269 1.75874,-1.75874 c -0.0692,-0.25015 -0.004,-0.53493 0.20012,-0.73505 0.30018,-0.30402 0.78508,-0.30402 1.08527,0 0.20396,0.20012 0.26937,0.4849 0.20011,0.73505 l 0.98905,0.98905 0.19242,-0.0269 c 0.0692,0 0.13469,0 0.19242,0.0269 l 1.37389,-1.37389 c -0.0269,-0.0577 -0.0269,-0.1232 -0.0269,-0.19242 a 0.76968815,0.76968815 0 0 1 0.76969,-0.76969 0.76968815,0.76968815 0 0 1 0.7697,0.76969 0.76968815,0.76968815 0 0 1 -0.7697,0.76968 c -0.0692,0 -0.13469,0 -0.19242,-0.0269 l -1.37389,1.3739 c 0.0269,0.0577 0.0269,0.1232 0.0269,0.19242 a 0.76968815,0.76968815 0 0 1 -0.76969,0.76968 0.76968815,0.76968815 0 0 1 -0.76969,-0.76968 l 0.0269,-0.19242 -0.98904,-0.98905 c -0.1232,0.0269 -0.2617,0.0269 -0.38485,0 l -1.75873,1.75873 0.0269,0.19242 a 0.76968815,0.76968815 0 0 1 -0.7697,0.76969 0.76968815,0.76968815 0 0 1 -0.76969,-0.76969 0.76968815,0.76968815 0 0 1 0.76969,-0.76968 z"
id="path1-2"
style="stroke-width:0.384844;fill:#777777;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="8.4666996mm"
height="4.6181598mm"
viewBox="0 0 8.4666996 4.6181597"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="datapoints_selected.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">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="3.1691345"
inkscape:cx="-0.9466307"
inkscape:cy="-2.3665768"
inkscape:window-width="1916"
inkscape:window-height="1041"
inkscape:window-x="0"
inkscape:window-y="18"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-100.80625,-145.78542)">
<title
id="title1">file-chart</title>
<path
d="m 101.57602,148.86421 0.19242,0.0269 1.75874,-1.75874 c -0.0692,-0.25015 -0.004,-0.53493 0.20012,-0.73505 0.30018,-0.30402 0.78508,-0.30402 1.08527,0 0.20396,0.20012 0.26937,0.4849 0.20011,0.73505 l 0.98905,0.98905 0.19242,-0.0269 c 0.0692,0 0.13469,0 0.19242,0.0269 l 1.37389,-1.37389 c -0.0269,-0.0577 -0.0269,-0.1232 -0.0269,-0.19242 a 0.76968815,0.76968815 0 0 1 0.76969,-0.76969 0.76968815,0.76968815 0 0 1 0.7697,0.76969 0.76968815,0.76968815 0 0 1 -0.7697,0.76968 c -0.0692,0 -0.13469,0 -0.19242,-0.0269 l -1.37389,1.3739 c 0.0269,0.0577 0.0269,0.1232 0.0269,0.19242 a 0.76968815,0.76968815 0 0 1 -0.76969,0.76968 0.76968815,0.76968815 0 0 1 -0.76969,-0.76968 l 0.0269,-0.19242 -0.98904,-0.98905 c -0.1232,0.0269 -0.2617,0.0269 -0.38485,0 l -1.75873,1.75873 0.0269,0.19242 a 0.76968815,0.76968815 0 0 1 -0.7697,0.76969 0.76968815,0.76968815 0 0 1 -0.76969,-0.76969 0.76968815,0.76968815 0 0 1 0.76969,-0.76968 z"
id="path1-2"
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#fb4934;fill-opacity:1;stroke-width:0.529166;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -2,12 +2,12 @@
<!-- Created with Inkscape (http://www.inkscape.org/) --> <!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
width="32" width="31.99999"
height="32.000011" height="32.000011"
viewBox="0 0 8.4666665 8.4666699" viewBox="0 0 8.466664 8.4666699"
version="1.1" version="1.1"
id="svg8" id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)" inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="triggers.svg" sodipodi:docname="triggers.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
@ -26,16 +26,16 @@
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="1" inkscape:zoom="1"
inkscape:cx="7.5" inkscape:cx="7"
inkscape:cy="4" inkscape:cy="3.5"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer1" inkscape:current-layer="layer1"
showgrid="false" showgrid="false"
units="px" units="px"
inkscape:window-width="2190" inkscape:window-width="1916"
inkscape:window-height="1404" inkscape:window-height="1041"
inkscape:window-x="1463" inkscape:window-x="0"
inkscape:window-y="16" inkscape:window-y="18"
inkscape:window-maximized="0" inkscape:window-maximized="0"
inkscape:showpageshadow="true" inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0" inkscape:pagecheckerboard="0"
@ -61,9 +61,11 @@
id="title1">script-text</title> id="title1">script-text</title>
<title <title
id="title1-6">script-text-outline</title> id="title1-6">script-text-outline</title>
<title
id="title1-9">calculator-variant-outline</title>
<path <path
d="m 92.551251,156.05126 a 0.42333338,0.42333371 0 0 0 0.423332,-0.42334 v -6.35001 h -3.386666 a 0.42333338,0.42333371 0 0 0 -0.423334,0.42334 v 4.65666 h -0.846666 v -4.65666 a 1.2700002,1.2700012 0 0 1 1.27,-1.27 h 4.656667 a 1.2700002,1.2700012 0 0 1 1.27,1.27 v 0.42333 h -0.846666 v -0.42333 a 0.42333338,0.42333371 0 0 0 -0.423334,-0.42334 0.42333338,0.42333371 0 0 0 -0.423333,0.42334 v 1.69333 4.23334 a 1.2700002,1.2700012 0 0 1 -1.27,1.27 h -4.233334 a 1.2700002,1.2700012 0 0 1 -1.27,-1.27 v -0.42333 h 4.656666 a 0.84666678,0.84666745 0 0 0 0.846668,0.84667 m -2.540001,-5.92668 h 2.116668 v 0.84667 H 90.01125 v -0.84667 m 0,1.69333 h 2.116668 v 0.84667 H 90.01125 v -0.84667 m 0,1.69334 h 2.116668 v 0.84666 H 90.01125 Z" d="m 94.573842,148.43125 h -6.585184 c -0.517408,0 -0.940741,0.42334 -0.940741,0.94075 v 6.58517 c 0,0.51742 0.423333,0.94075 0.940741,0.94075 h 6.585184 c 0.517409,0 0.940739,-0.42333 0.940739,-0.94075 V 149.372 c 0,-0.51741 -0.42333,-0.94075 -0.940739,-0.94075 m 0,7.52592 H 87.988658 V 149.372 h 6.585184 v 6.58517 m -6.020739,-5.31518 h 2.35185 v 0.70556 h -2.35185 v -0.70556 m 3.198517,3.81001 h 2.351853 v 0.70554 H 91.75162 V 154.452 m 0,-1.22297 h 2.351853 v 0.70556 H 91.75162 v -0.70556 m -2.351851,2.25778 h 0.705556 v -0.94075 h 0.94074 v -0.70555 h -0.94074 v -0.94074 h -0.705556 v 0.94074 h -0.940741 v 0.70555 h 0.940741 v 0.94075 m 2.869259,-3.33963 0.658519,-0.65853 0.658518,0.65853 0.517408,-0.47037 -0.65852,-0.65852 0.65852,-0.65852 -0.517408,-0.51741 -0.658518,0.65853 -0.658519,-0.65853 -0.517408,0.51741 0.658519,0.65852 -0.658519,0.65852 z"
id="path1" id="path1-1"
style="stroke-width:0.423333;fill:#777777;fill-opacity:1" /> style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#777777;fill-opacity:1;stroke-width:0.423333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -2,12 +2,12 @@
<!-- Created with Inkscape (http://www.inkscape.org/) --> <!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
width="32" width="31.99999"
height="32.000011" height="32.000011"
viewBox="0 0 8.4666665 8.4666699" viewBox="0 0 8.466664 8.4666699"
version="1.1" version="1.1"
id="svg8" id="svg8"
inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)" inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="triggers_selected.svg" sodipodi:docname="triggers_selected.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
@ -26,16 +26,16 @@
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="1" inkscape:zoom="1"
inkscape:cx="7.5" inkscape:cx="-4"
inkscape:cy="4" inkscape:cy="64.5"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer1" inkscape:current-layer="layer1"
showgrid="false" showgrid="false"
units="px" units="px"
inkscape:window-width="2190" inkscape:window-width="1916"
inkscape:window-height="1404" inkscape:window-height="1041"
inkscape:window-x="1463" inkscape:window-x="0"
inkscape:window-y="16" inkscape:window-y="18"
inkscape:window-maximized="0" inkscape:window-maximized="0"
inkscape:showpageshadow="true" inkscape:showpageshadow="true"
inkscape:pagecheckerboard="0" inkscape:pagecheckerboard="0"
@ -56,12 +56,14 @@
inkscape:label="Layer 1" inkscape:label="Layer 1"
inkscape:groupmode="layer" inkscape:groupmode="layer"
id="layer1" id="layer1"
transform="translate(-87.047917,-148.43125)"> transform="translate(-89.918898,-132.29942)">
<title <title
id="title1">script-text</title> id="title1">script-text</title>
<title
id="title1-9">calculator-variant</title>
<path <path
d="m 93.736584,156.05125 c -0.169334,0.508 -0.635,0.84667 -1.185334,0.84667 h -4.233333 c -0.719667,0 -1.27,-0.55033 -1.27,-1.27 v -0.42334 h 1.27 3.894667 c 0.169333,0.508 0.635,0.84667 1.185333,0.84667 h 0.338667 m 0.508,-7.62 c 0.719666,0 1.27,0.55034 1.27,1.27 v 0.42333 h -0.846667 v -0.42333 c 0,-0.254 -0.169333,-0.42333 -0.423333,-0.42333 -0.254,0 -0.423334,0.16933 -0.423334,0.42333 v 5.50333 h -0.423333 c -0.254,0 -0.423333,-0.16933 -0.423333,-0.42333 v -0.42333 h -4.656667 v -4.65667 c 0,-0.71966 0.550333,-1.27 1.27,-1.27 h 4.656667 m -4.656667,1.69333 v 0.84667 h 2.963333 v -0.84667 h -2.963333 m 0,1.69334 v 0.84666 h 2.54 v -0.84666 z" d="m 97.444823,132.29942 h -6.585184 c -0.517408,0 -0.940741,0.42334 -0.940741,0.94075 v 6.58517 c 0,0.51742 0.423333,0.94075 0.940741,0.94075 h 6.585184 c 0.517408,0 0.940742,-0.42333 0.940742,-0.94075 v -6.58517 c 0,-0.51741 -0.423334,-0.94075 -0.940742,-0.94075 m -2.822222,1.92852 0.517408,-0.51741 0.658519,0.65853 0.658518,-0.65853 0.517408,0.51741 -0.65852,0.65852 0.65852,0.65852 -0.517408,0.51741 -0.658518,-0.65853 -0.658519,0.65853 -0.517408,-0.51741 0.658519,-0.65852 -0.658519,-0.65852 m -3.198517,0.28222 h 2.35185 v 0.70556 h -2.35185 v -0.70556 m 2.492962,3.90407 h -0.94074 v 0.94075 H 92.27075 v -0.94075 h -0.940741 v -0.70555 h 0.940741 v -0.94074 h 0.705556 v 0.94074 h 0.94074 v 0.70555 m 3.057408,0.56444 h -2.351853 v -0.70554 h 2.351853 v 0.70554 m 0,-1.12889 h -2.351853 v -0.70554 h 2.351853 z"
id="path1" id="path1"
style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#fb4934;fill-opacity:1;stroke-width:0.691155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" /> style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#fb4934;fill-opacity:1;stroke-width:1.22872;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,19 @@
export class UI {
constructor() {
document.addEventListener('keydown', evt=>this.keyHandler(evt))
document.querySelector('input[name="name"]').focus()
}
keyHandler(evt) {
if (!(evt.altKey && evt.shiftKey))
return
evt.preventDefault()
evt.stopPropagation()
switch (evt.key) {
case 'S':
document.getElementById('form-trigger').submit()
break
}
}
}

View File

@ -12,10 +12,20 @@ export class UI {
this.trigger.run() this.trigger.run()
} }
keyHandler(evt) { keyHandler(evt) {
if (evt.altKey && evt.shiftKey && evt.key == 'R') { if (!(evt.altKey && evt.shiftKey))
evt.preventDefault() return
evt.stopPropagation()
this.run() evt.preventDefault()
evt.stopPropagation()
switch (evt.key) {
case 'T':
this.run()
break
case 'S':
document.getElementById('form-trigger').submit()
break
} }
} }
} }

View File

@ -0,0 +1,44 @@
@import "theme.less";
#datapoints {
display: grid;
grid-template-columns: repeat(4, min-content);
grid-gap: 8px 16px;
margin-top: 16px;
.header {
font-weight: @bold;
}
div {
white-space: nowrap;
}
}
.widgets {
display: grid;
grid-template-columns: min-content 1fr;
gap: 8px 16px;
.label {
margin-top: 4px;
}
input[type="text"], textarea {
width: 100%;
}
.datapoints {
font: "Roboto Mono", monospace;
display: grid;
grid-template-columns: min-content 1fr;
gap: 6px 8px;
margin-bottom: 8px;
}
.action {
display: grid;
grid-template-columns: min-content min-content;
grid-gap: 8px;
}
}

View File

@ -37,6 +37,7 @@
margin-bottom: 32px; margin-bottom: 32px;
div { div {
font-weight: 500;
font-size: 1.5em; font-size: 1.5em;
color: @color1; color: @color1;
} }
@ -61,7 +62,7 @@
&>.name { &>.name {
background: @color1; background: @color1;
color: #fff; color: #fff;
font-weight: bold; font-weight: 500;
padding: 4px 16px; padding: 4px 16px;
border-top-left-radius: 4px; border-top-left-radius: 4px;
border-top-right-radius: 4px; border-top-right-radius: 4px;
@ -71,7 +72,7 @@
margin: 8px 16px; margin: 8px 16px;
&>.name { &>.name {
font-weight: bold; font-weight: 500;
} }
.triggers { .triggers {
@ -93,7 +94,7 @@
} }
.label { .label {
color: @text2; color: @color4;
} }
} }
} }

View File

@ -7,6 +7,12 @@
@error: #fb4934; @error: #fb4934;
@color1: #fb4934; @color1: #fb4934;
@color2: #fabd2f;
@color3: #b8bb26;
@color4: #3f9da1;
@color5: #fe8019;
@bold: 500;
html { html {
box-sizing: border-box; box-sizing: border-box;
@ -33,6 +39,7 @@ body {
body { body {
background: @bg1; background: @bg1;
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
font-weight: 300;
color: @text1; color: @text1;
font-size: 11pt; font-size: 11pt;
} }
@ -51,6 +58,19 @@ h2 {
font-size: 1.25em; font-size: 1.25em;
} }
a {
color: @color4;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
b {
font-weight: @bold;
}
.roboto-light { .roboto-light {
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
font-weight: 300; font-weight: 300;
@ -63,7 +83,7 @@ h2 {
font-style: normal; font-style: normal;
} }
input[type="text"], textarea { input[type="text"], textarea, select {
font-family: "Roboto Mono", monospace; font-family: "Roboto Mono", monospace;
background: @bg2; background: @bg2;
color: @text1; color: @text1;
@ -79,4 +99,8 @@ button {
border: 1px solid lighten(@bg2, 10%); border: 1px solid lighten(@bg2, 10%);
font-size: 1em; font-size: 1em;
height: 3em; height: 3em;
&:focus {
background: @bg3;
}
} }

View File

@ -2,6 +2,7 @@
<div id="menu"> <div id="menu">
<a href="/"><img src="/images/{{ .VERSION }}/logo{{ if eq .MENU "index" }}_selected{{ end }}.svg"></a> <a href="/"><img src="/images/{{ .VERSION }}/logo{{ if eq .MENU "index" }}_selected{{ end }}.svg"></a>
<a href="/problems"><img src="/images/{{ .VERSION }}/problems{{ if eq .MENU "problems" }}_selected{{ end }}.svg"></a> <a href="/problems"><img src="/images/{{ .VERSION }}/problems{{ if eq .MENU "problems" }}_selected{{ end }}.svg"></a>
<a href="/datapoints"><img src="/images/{{ .VERSION }}/datapoints{{ if eq .MENU "datapoints" }}_selected{{ end }}.svg"></a>
<a href="/triggers"><img src="/images/{{ .VERSION }}/triggers{{ if eq .MENU "triggers" }}_selected{{ end }}.svg"></a> <a href="/triggers"><img src="/images/{{ .VERSION }}/triggers{{ if eq .MENU "triggers" }}_selected{{ end }}.svg"></a>
<a href="/configuration"><img src="/images/{{ .VERSION }}/configuration{{ if eq .MENU "configuration" }}_selected{{ end }}.svg"></a> <a href="/configuration"><img src="/images/{{ .VERSION }}/configuration{{ if eq .MENU "configuration" }}_selected{{ end }}.svg"></a>
</div> </div>

View File

@ -0,0 +1,35 @@
{{ define "page" }}
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/datapoints.css">
<script type="module" defer>
import {UI} from "/js/{{ .VERSION }}/datapoint_edit.mjs"
window._ui = new UI()
</script>
{{ block "page_label" . }}{{end}}
<form id="form-trigger" action="/datapoint/update/{{ .Data.Datapoint.ID }}" method="post">
<div id="widgets" class="widgets">
<div class="label">Name</div>
<div><input type="text" name="name" value="{{ .Data.Datapoint.Name }}"></div>
<div class="label">Datatype</div>
<div>
<select name="datatype">
<option {{ if eq .Data.Datapoint.Datatype "INT" }}selected{{end}}>INT</option>
<option {{ if eq .Data.Datapoint.Datatype "STRING" }}selected{{end}}>STRING</option>
<option {{ if eq .Data.Datapoint.Datatype "DATETIME" }}selected{{end}}>DATETIME</option>
</select>
</div>
<div></div>
<div class="action">
{{ if eq .Data.Datapoint.ID 0 }}
<button id="button-update">Create</button>
{{ else }}
<button id="button-update">Update</button>
{{ end }}
</div>
</form>
{{ end }}

View File

@ -0,0 +1,26 @@
{{ define "page" }}
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/datapoints.css">
{{ block "page_label" . }}{{end}}
<a href="/datapoint/edit/0">Create</a>
<div id="datapoints">
<div class="header">Name</div>
<div class="header">Datatype</div>
<div class="header">Last value</div>
<div class="header">Value</div>
{{ range .Data.Datapoints }}
<div class="name"><a href="/datapoint/edit/{{ .ID }}">{{ .Name }}</a></div>
<div class="datatype">{{ .Datatype }}</div>
<div class="last-value">{{ format_time .LastValue }}</div>
{{ if eq .Datatype "DATETIME" }}
<div class="value">{{ if .LastDatapointValue.ValueDateTime.Valid }}{{ format_time .LastDatapointValue.Value }}{{ end }}</div>
{{ else }}
<div class="value">{{ .LastDatapointValue.Value }}</div>
{{ end }}
{{ end }}
</div>
{{ end }}

View File

@ -13,7 +13,7 @@
{{ block "page_label" . }}{{end}} {{ block "page_label" . }}{{end}}
<form action="/trigger/update/{{ .Data.Trigger.ID }}" method="post"> <form id="form-trigger" action="/trigger/update/{{ .Data.Trigger.ID }}" method="post">
<div id="widgets" class="widgets"> <div id="widgets" class="widgets">
<div class="label">Name</div> <div class="label">Name</div>
<div><input type="text" name="name" value="{{ .Data.Trigger.Name }}"></div> <div><input type="text" name="name" value="{{ .Data.Trigger.Name }}"></div>
@ -21,7 +21,7 @@
<div class="label">Datapoints</div> <div class="label">Datapoints</div>
<div class="datapoints" style="margin-top: 4px"> <div class="datapoints" style="margin-top: 4px">
{{ range .Data.Datapoints }} {{ range .Data.Datapoints }}
<div class="datapoint name">{{ .Name }}</div> <div class="datapoint name"><b>{{ .Name }}</b></div>
<div class="datapoint value">{{ .LastDatapointValue.Value }}</div> <div class="datapoint value">{{ .LastDatapointValue.Value }}</div>
{{ end }} {{ end }}
</div> </div>
@ -31,7 +31,7 @@
<div></div> <div></div>
<div class="action"> <div class="action">
<button>Update</button> <button id="button-update">Update</button>
<button id="button-run" onclick="window._ui.run(); return false">Test</button> <button id="button-run" onclick="window._ui.run(); return false">Test</button>
<div id="run-result"></div> <div id="run-result"></div>
</div> </div>