Compare commits
No commits in common. "ff402b8fcb1a9a757f4362c2297075142cb75edb" and "047f39cfa70979802950b699ef46ccdcc1e43f47" have entirely different histories.
ff402b8fcb
...
047f39cfa7
@ -1,6 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
type FileConfiguration struct {
|
type SmonConfiguration struct {
|
||||||
LogFile string
|
LogFile string
|
||||||
NodataInterval int `json:"nodata_interval"` // in seconds
|
NodataInterval int `json:"nodata_interval"` // in seconds
|
||||||
}
|
}
|
||||||
|
@ -6,18 +6,16 @@ import (
|
|||||||
|
|
||||||
// Standard
|
// Standard
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
timezoneLocation *time.Location
|
|
||||||
Settings map[string]string
|
Settings map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
var smonConfig Configuration
|
var smonConfig Configuration
|
||||||
|
|
||||||
func SmonConfigInit() (cfg Configuration, err error) {
|
func SmonConfigInit() (err error) {
|
||||||
cfg.Settings = make(map[string]string, 8)
|
smonConfig.Settings = make(map[string]string, 8)
|
||||||
|
|
||||||
var rows *sql.Rows
|
var rows *sql.Rows
|
||||||
rows, err = service.Db.Conn.Query(`SELECT * FROM public.configuration`)
|
rows, err = service.Db.Conn.Query(`SELECT * FROM public.configuration`)
|
||||||
@ -35,49 +33,14 @@ func SmonConfigInit() (cfg Configuration, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Settings[setting] = value
|
smonConfig.Settings[setting] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cfg.LoadTimezone()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Configuration) Validate() (err error) {
|
|
||||||
mandatorySettings := []string{"THEME", "TIMEZONE"}
|
|
||||||
for _, settingsKey := range mandatorySettings {
|
|
||||||
if _, found := cfg.Settings[settingsKey]; !found {
|
|
||||||
return werr.New("Configuration missing setting '%s' in database", settingsKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Configuration) LoadTimezone() (err error) {
|
|
||||||
cfg.timezoneLocation, err = time.LoadLocation(cfg.Settings["TIMEZONE"])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Configuration) Timezone() *time.Location {
|
|
||||||
return cfg.timezoneLocation
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Configuration) SetTheme(theme string) (err error) {
|
func (cfg *Configuration) SetTheme(theme string) (err error) {
|
||||||
cfg.Settings["THEME"] = theme
|
cfg.Settings["THEME"] = theme
|
||||||
_, err = service.Db.Conn.Exec(`UPDATE public.configuration SET value=$1 WHERE setting='THEME'`, theme)
|
_, err = service.Db.Conn.Exec(`UPDATE public.configuration SET value=$1 WHERE setting='THEME'`, theme)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Configuration) SetTimezone(tz string) (err error) {
|
|
||||||
cfg.Settings["TIMEZONE"] = tz
|
|
||||||
err = cfg.LoadTimezone()
|
|
||||||
if err != nil {
|
|
||||||
return werr.Wrap(err).WithData(tz)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = service.Db.Conn.Exec(`UPDATE public.configuration SET value=$1 WHERE setting='TIMEZONE'`, tz)
|
|
||||||
if err != nil {
|
|
||||||
return werr.Wrap(err).WithData(tz)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
54
datapoint.go
54
datapoint.go
@ -120,12 +120,8 @@ func (dp Datapoint) Update() (err error) { // {{{
|
|||||||
} // }}}
|
} // }}}
|
||||||
|
|
||||||
func DatapointAdd[T any](name string, value T) (err error) { // {{{
|
func DatapointAdd[T any](name string, value T) (err error) { // {{{
|
||||||
type dpRequest = struct {
|
|
||||||
ID int
|
|
||||||
value any
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
var dpType DatapointType
|
var dpType DatapointType
|
||||||
|
|
||||||
@ -144,21 +140,13 @@ func DatapointAdd[T any](name string, value T) (err error) { // {{{
|
|||||||
case STRING:
|
case STRING:
|
||||||
_, err = service.Db.Conn.Exec(`INSERT INTO datapoint_value(datapoint_id, value_string) VALUES($1, $2)`, dpID, value)
|
_, err = service.Db.Conn.Exec(`INSERT INTO datapoint_value(datapoint_id, value_string) VALUES($1, $2)`, dpID, value)
|
||||||
case DATETIME:
|
case DATETIME:
|
||||||
// Time value is required to be a RFC 3339 formatted time string
|
_, err = service.Db.Conn.Exec(`INSERT INTO datapoint_value(datapoint_id, value_datetime) VALUES($1, $2)`, dpID, value)
|
||||||
var t time.Time
|
|
||||||
valueStr, ok := any(value).([]byte)
|
|
||||||
if !ok {
|
|
||||||
return werr.New("DATETIME value not a string").WithData(dpRequest{dpID, value})
|
|
||||||
}
|
|
||||||
t, err = stringToTime(string(valueStr))
|
|
||||||
if err != nil {
|
|
||||||
return werr.Wrap(err).WithData(dpRequest{dpID, value}).Log()
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = service.Db.Conn.Exec(`INSERT INTO datapoint_value(datapoint_id, value_datetime) VALUES($1, $2)`, dpID, t)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = werr.Wrap(err).WithData(dpRequest{dpID, value})
|
err = werr.Wrap(err).WithData(struct {
|
||||||
|
ID int
|
||||||
|
value any
|
||||||
|
}{dpID, value})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,34 +327,8 @@ func DatapointDelete(id int) (err error) { // {{{
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
func DatapointValues(id int, from, to time.Time) (values []DatapointValue, err error) { // {{{
|
func DatapointValues(id int) (values []DatapointValue, err error) { // {{{
|
||||||
_, err = service.Db.Conn.Exec(`SELECT set_config('timezone', $1, false)`, smonConfig.Timezone().String())
|
rows, err := service.Db.Conn.Queryx(`SELECT * FROM datapoint_value WHERE datapoint_id=$1 ORDER BY ts DESC LIMIT 500`, id)
|
||||||
if err != nil {
|
|
||||||
err = werr.Wrap(err).WithData(smonConfig.Timezone().String())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := service.Db.Conn.Queryx(
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
datapoint_id,
|
|
||||||
ts,
|
|
||||||
value_int,
|
|
||||||
value_string,
|
|
||||||
value_datetime
|
|
||||||
FROM datapoint_value
|
|
||||||
WHERE
|
|
||||||
datapoint_id=$1 AND
|
|
||||||
ts >= $2 AND
|
|
||||||
ts <= $3
|
|
||||||
ORDER BY
|
|
||||||
ts DESC
|
|
||||||
`,
|
|
||||||
id,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = werr.Wrap(err).WithData(id)
|
err = werr.Wrap(err).WithData(id)
|
||||||
return
|
return
|
||||||
|
29
helper.go
29
helper.go
@ -1,29 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
// External
|
|
||||||
werr "git.gibonuddevalla.se/go/wrappederror"
|
|
||||||
|
|
||||||
// Standard
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func stringToTime(strTime string) (t time.Time, err error) {// {{{
|
|
||||||
t, err = time.Parse(time.RFC3339, strTime)
|
|
||||||
return
|
|
||||||
}// }}}
|
|
||||||
func parseHTMLDateTime(str string, dflt time.Time) (t time.Time, err error) {
|
|
||||||
// Browser sending 2024-06-27T10:43 (16 characters) when seconds is 00.
|
|
||||||
if len(str) == 16 {
|
|
||||||
str += ":00"
|
|
||||||
}
|
|
||||||
if str == "" {
|
|
||||||
return dflt, nil
|
|
||||||
} else {
|
|
||||||
t, err = time.ParseInLocation("2006-01-02T15:04:05", str, smonConfig.Timezone())
|
|
||||||
if err != nil {
|
|
||||||
err = werr.Wrap(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
144
main.go
144
main.go
@ -19,7 +19,6 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
@ -28,7 +27,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const VERSION = "v22"
|
const VERSION = "v21"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
@ -40,7 +39,7 @@ var (
|
|||||||
parsedTemplates map[string]*template.Template
|
parsedTemplates map[string]*template.Template
|
||||||
componentFilenames []string
|
componentFilenames []string
|
||||||
notificationManager notification.Manager
|
notificationManager notification.Manager
|
||||||
fileConf FileConfiguration
|
smonConf SmonConfiguration
|
||||||
|
|
||||||
//go:embed sql
|
//go:embed sql
|
||||||
sqlFS embed.FS
|
sqlFS embed.FS
|
||||||
@ -92,13 +91,13 @@ func main() { // {{{
|
|||||||
}
|
}
|
||||||
|
|
||||||
j, _ := json.Marshal(service.Config.Application)
|
j, _ := json.Marshal(service.Config.Application)
|
||||||
json.Unmarshal(j, &fileConf)
|
json.Unmarshal(j, &smonConf)
|
||||||
logFile, err = os.OpenFile(fileConf.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
logFile, err = os.OpenFile(smonConf.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("application", "error", err)
|
logger.Error("application", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if fileConf.NodataInterval < 10 {
|
if smonConf.NodataInterval < 10 {
|
||||||
logger.Error("application → nodata_interval has to be larger or equal to 10.")
|
logger.Error("application → nodata_interval has to be larger or equal to 10.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -138,7 +137,6 @@ func main() { // {{{
|
|||||||
service.Register("/datapoint/update/{id}", false, false, actionDatapointUpdate)
|
service.Register("/datapoint/update/{id}", false, false, actionDatapointUpdate)
|
||||||
service.Register("/datapoint/delete/{id}", false, false, actionDatapointDelete)
|
service.Register("/datapoint/delete/{id}", false, false, actionDatapointDelete)
|
||||||
service.Register("/datapoint/values/{id}", false, false, pageDatapointValues)
|
service.Register("/datapoint/values/{id}", false, false, pageDatapointValues)
|
||||||
service.Register("/datapoint/json/{id}", false, false, actionDatapointJson)
|
|
||||||
|
|
||||||
service.Register("/triggers", false, false, pageTriggers)
|
service.Register("/triggers", false, false, pageTriggers)
|
||||||
service.Register("/trigger/create/{sectionID}/{name}", false, false, actionTriggerCreate)
|
service.Register("/trigger/create/{sectionID}/{name}", false, false, actionTriggerCreate)
|
||||||
@ -151,18 +149,11 @@ func main() { // {{{
|
|||||||
|
|
||||||
service.Register("/configuration", false, false, pageConfiguration)
|
service.Register("/configuration", false, false, pageConfiguration)
|
||||||
service.Register("/configuration/theme", false, false, actionConfigurationTheme)
|
service.Register("/configuration/theme", false, false, actionConfigurationTheme)
|
||||||
service.Register("/configuration/timezone", false, false, actionConfigurationTimezone)
|
|
||||||
service.Register("/entry/{datapoint}", false, false, actionEntryDatapoint)
|
service.Register("/entry/{datapoint}", false, false, actionEntryDatapoint)
|
||||||
|
|
||||||
go nodataLoop()
|
go nodataLoop()
|
||||||
|
|
||||||
smonConfig, err = SmonConfigInit()
|
err = SmonConfigInit()
|
||||||
if err != nil {
|
|
||||||
logger.Error("configuration", "error", werr.Wrap(err))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = smonConfig.Validate()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("configuration", "error", werr.Wrap(err))
|
logger.Error("configuration", "error", werr.Wrap(err))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -202,20 +193,6 @@ func httpError(w http.ResponseWriter, err error) { // {{{
|
|||||||
j, _ := json.Marshal(resp)
|
j, _ := json.Marshal(resp)
|
||||||
w.Write(j)
|
w.Write(j)
|
||||||
} // }}}
|
} // }}}
|
||||||
func pageError(w http.ResponseWriter, redirectURL string, pageErr error) { // {{{
|
|
||||||
u, err := url.Parse(redirectURL)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
values := u.Query()
|
|
||||||
values.Add("_err", pageErr.Error())
|
|
||||||
u.RawQuery = values.Encode()
|
|
||||||
|
|
||||||
w.Header().Add("Location", u.String())
|
|
||||||
w.WriteHeader(302)
|
|
||||||
} // }}}
|
|
||||||
|
|
||||||
func staticHandler(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
func staticHandler(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
||||||
if flagDev && !reloadTemplates(w) {
|
if flagDev && !reloadTemplates(w) {
|
||||||
@ -370,7 +347,7 @@ func getPage(layout, page string) (tmpl *template.Template, err error) { // {{{
|
|||||||
funcMap := template.FuncMap{
|
funcMap := template.FuncMap{
|
||||||
"format_time": func(t time.Time) template.HTML {
|
"format_time": func(t time.Time) template.HTML {
|
||||||
return template.HTML(
|
return template.HTML(
|
||||||
t.In(smonConfig.Timezone()).Format(`<span class="date">2006-01-02</span> <span class="time">15:04:05<span class="seconds">:05</span></span>`),
|
t.Local().Format(`<span class="date">2006-01-02</span> <span class="time">15:04<span class="seconds">:05</span></span>`),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -392,13 +369,13 @@ func getPage(layout, page string) (tmpl *template.Template, err error) { // {{{
|
|||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
||||||
func pageIndex(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
func pageIndex(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
|
||||||
page := Page{
|
page := Page{
|
||||||
LAYOUT: "main",
|
LAYOUT: "main",
|
||||||
PAGE: "index",
|
PAGE: "index",
|
||||||
CONFIG: smonConfig.Settings,
|
CONFIG: smonConfig.Settings,
|
||||||
}
|
}
|
||||||
page.Render(w, r)
|
page.Render(w)
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
||||||
func actionAreaNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
func actionAreaNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
||||||
@ -508,7 +485,7 @@ func actionSectionDelete(w http.ResponseWriter, r *http.Request, _ *session.T) {
|
|||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
||||||
func pageProblems(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
func pageProblems(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
|
||||||
page := Page{
|
page := Page{
|
||||||
LAYOUT: "main",
|
LAYOUT: "main",
|
||||||
PAGE: "problems",
|
PAGE: "problems",
|
||||||
@ -541,7 +518,7 @@ func pageProblems(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|||||||
"Problems": problems,
|
"Problems": problems,
|
||||||
"ProblemsGrouped": problemsGrouped,
|
"ProblemsGrouped": problemsGrouped,
|
||||||
}
|
}
|
||||||
page.Render(w, r)
|
page.Render(w)
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
func actionProblemAcknowledge(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
func actionProblemAcknowledge(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
||||||
@ -606,7 +583,7 @@ func pageDatapoints(w http.ResponseWriter, r *http.Request, _ *session.T) { // {
|
|||||||
page.Data = map[string]any{
|
page.Data = map[string]any{
|
||||||
"Datapoints": datapoints,
|
"Datapoints": datapoints,
|
||||||
}
|
}
|
||||||
page.Render(w, r)
|
page.Render(w)
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
func pageDatapointEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
func pageDatapointEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
||||||
@ -641,7 +618,7 @@ func pageDatapointEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { /
|
|||||||
page.Data = map[string]any{
|
page.Data = map[string]any{
|
||||||
"Datapoint": datapoint,
|
"Datapoint": datapoint,
|
||||||
}
|
}
|
||||||
page.Render(w, r)
|
page.Render(w)
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
func actionDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
func actionDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
||||||
@ -703,37 +680,8 @@ func pageDatapointValues(w http.ResponseWriter, r *http.Request, _ *session.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET parameters.
|
|
||||||
display := r.URL.Query().Get("display")
|
|
||||||
if display == "" && datapoint.Datatype == INT {
|
|
||||||
display = "graph"
|
|
||||||
}
|
|
||||||
|
|
||||||
var timeFrom, timeTo time.Time
|
|
||||||
yesterday := time.Now().Add(time.Duration(-24 * time.Hour))
|
|
||||||
|
|
||||||
timeFrom, err = parseHTMLDateTime(r.URL.Query().Get("f"), yesterday)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, werr.Wrap(err).Log())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
timeTo, err = parseHTMLDateTime(r.URL.Query().Get("t"), time.Now())
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, werr.Wrap(err).Log())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply an optionally set offset (in seconds).
|
|
||||||
var offsetTime int
|
|
||||||
offsetTimeStr := r.URL.Query().Get("offset-time")
|
|
||||||
offsetTime, err = strconv.Atoi(offsetTimeStr)
|
|
||||||
timeFrom = timeFrom.Add(time.Second * time.Duration(offsetTime))
|
|
||||||
timeTo = timeTo.Add(time.Second * time.Duration(offsetTime))
|
|
||||||
|
|
||||||
// Fetch data point values according to the times.
|
|
||||||
var values []DatapointValue
|
var values []DatapointValue
|
||||||
values, err = DatapointValues(id, timeFrom, timeTo)
|
values, err = DatapointValues(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpError(w, werr.Wrap(err).Log())
|
httpError(w, werr.Wrap(err).Log())
|
||||||
return
|
return
|
||||||
@ -751,46 +699,12 @@ func pageDatapointValues(w http.ResponseWriter, r *http.Request, _ *session.T) {
|
|||||||
page.Data = map[string]any{
|
page.Data = map[string]any{
|
||||||
"Datapoint": datapoint,
|
"Datapoint": datapoint,
|
||||||
"Values": values,
|
"Values": values,
|
||||||
"TimeFrom": timeFrom.Format("2006-01-02T15:04:05"),
|
|
||||||
"TimeTo": timeTo.Format("2006-01-02T15:04:05"),
|
|
||||||
"Display": display,
|
|
||||||
}
|
}
|
||||||
page.Render(w, r)
|
page.Render(w)
|
||||||
return
|
return
|
||||||
} // }}}
|
} // }}}
|
||||||
func actionDatapointJson(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
||||||
idStr := r.PathValue("id")
|
|
||||||
id, err := strconv.Atoi(idStr)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, werr.Wrap(err).Log())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fromStr := r.URL.Query().Get("f")
|
func pageTriggers(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
|
||||||
from, err := time.ParseInLocation("2006-01-02 15:04:05", fromStr[0:min(19, len(fromStr))], smonConfig.Timezone())
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, werr.Wrap(err).Log())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
toStr := r.URL.Query().Get("t")
|
|
||||||
to, err := time.ParseInLocation("2006-01-02 15:04:05", toStr[0:min(19, len(toStr))], smonConfig.Timezone())
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, werr.Wrap(err).Log())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
values, err := DatapointValues(id, from, to)
|
|
||||||
if err != nil {
|
|
||||||
httpError(w, werr.Wrap(err).Log())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
j, _ := json.Marshal(values)
|
|
||||||
w.Write(j)
|
|
||||||
} // }}}
|
|
||||||
|
|
||||||
func pageTriggers(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
||||||
areas, err := TriggersRetrieve()
|
areas, err := TriggersRetrieve()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpError(w, werr.Wrap(err).Log())
|
httpError(w, werr.Wrap(err).Log())
|
||||||
@ -810,7 +724,7 @@ func pageTriggers(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
page.Render(w, r)
|
page.Render(w)
|
||||||
} // }}}
|
} // }}}
|
||||||
func actionTriggerCreate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
func actionTriggerCreate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
||||||
name := r.PathValue("name")
|
name := r.PathValue("name")
|
||||||
@ -898,7 +812,7 @@ func pageTriggerEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { //
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
page.Render(w, r)
|
page.Render(w)
|
||||||
} // }}}
|
} // }}}
|
||||||
func actionTriggerDatapointAdd(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
func actionTriggerDatapointAdd(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
||||||
triggerID := r.PathValue("id")
|
triggerID := r.PathValue("id")
|
||||||
@ -1020,7 +934,7 @@ func actionTriggerDelete(w http.ResponseWriter, r *http.Request, _ *session.T) {
|
|||||||
w.WriteHeader(302)
|
w.WriteHeader(302)
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
||||||
func pageConfiguration(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
func pageConfiguration(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
|
||||||
areas, err := AreaRetrieve()
|
areas, err := AreaRetrieve()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpError(w, werr.Wrap(err).Log())
|
httpError(w, werr.Wrap(err).Log())
|
||||||
@ -1040,7 +954,7 @@ func pageConfiguration(w http.ResponseWriter, r *http.Request, _ *session.T) { /
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
page.Render(w, r)
|
page.Render(w)
|
||||||
} // }}}
|
} // }}}
|
||||||
func actionConfigurationTheme(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
func actionConfigurationTheme(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
||||||
theme := r.FormValue("theme")
|
theme := r.FormValue("theme")
|
||||||
@ -1053,21 +967,3 @@ func actionConfigurationTheme(w http.ResponseWriter, r *http.Request, _ *session
|
|||||||
w.Header().Add("Location", "/configuration")
|
w.Header().Add("Location", "/configuration")
|
||||||
w.WriteHeader(302)
|
w.WriteHeader(302)
|
||||||
} // }}}
|
} // }}}
|
||||||
func actionConfigurationTimezone(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
|
|
||||||
timezone := r.FormValue("timezone")
|
|
||||||
|
|
||||||
_, err := time.LoadLocation(timezone)
|
|
||||||
if err != nil {
|
|
||||||
pageError(w, "/configuration", werr.Wrap(err).Log())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = smonConfig.SetTimezone(timezone)
|
|
||||||
if err != nil {
|
|
||||||
pageError(w, "/configuration", werr.Wrap(err).Log())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("Location", "/configuration")
|
|
||||||
w.WriteHeader(302)
|
|
||||||
} // }}}
|
|
||||||
|
@ -24,7 +24,8 @@ func nodataLoop() {
|
|||||||
var datapoints []Datapoint
|
var datapoints []Datapoint
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
ticker := time.NewTicker(time.Second * time.Duration(fileConf.NodataInterval))
|
// TODO - should be configurable
|
||||||
|
ticker := time.NewTicker(time.Second * time.Duration(smonConf.NodataInterval))
|
||||||
for {
|
for {
|
||||||
<-ticker.C
|
<-ticker.C
|
||||||
datapoints, err = nodataDatapoints()
|
datapoints, err = nodataDatapoints()
|
||||||
|
3
page.go
3
page.go
@ -22,7 +22,7 @@ type Page struct {
|
|||||||
Data any
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Page) Render(w http.ResponseWriter, r *http.Request) {
|
func (p *Page) Render(w http.ResponseWriter) {
|
||||||
tmpl, err := getPage(p.LAYOUT, p.PAGE)
|
tmpl, err := getPage(p.LAYOUT, p.PAGE)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpError(w, we.Wrap(err).Log())
|
httpError(w, we.Wrap(err).Log())
|
||||||
@ -49,7 +49,6 @@ func (p *Page) Render(w http.ResponseWriter, r *http.Request) {
|
|||||||
"PAGE": p.PAGE,
|
"PAGE": p.PAGE,
|
||||||
"MENU": p.MENU,
|
"MENU": p.MENU,
|
||||||
"CONFIG": smonConfig.Settings,
|
"CONFIG": smonConfig.Settings,
|
||||||
"ERROR": r.URL.Query().Get("_err"),
|
|
||||||
|
|
||||||
"Label": p.Label,
|
"Label": p.Label,
|
||||||
"Icon": p.Icon,
|
"Icon": p.Icon,
|
||||||
|
@ -1 +0,0 @@
|
|||||||
INSERT INTO public.configuration(setting, value) VALUES('TIMEZONE', 'Europe/Stockholm');
|
|
@ -1 +0,0 @@
|
|||||||
CREATE INDEX datapoint_value_ts_idx ON public.datapoint_value (ts);
|
|
@ -41,16 +41,6 @@
|
|||||||
grid-template-columns: repeat(2, min-content);
|
grid-template-columns: repeat(2, min-content);
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
background-color: #2979b8;
|
|
||||||
padding: 16px 24px;
|
|
||||||
width: min-content;
|
|
||||||
border-top-left-radius: 8px;
|
|
||||||
border-top-right-radius: 8px;
|
|
||||||
margin-top: 32px;
|
|
||||||
}
|
|
||||||
#values .header {
|
|
||||||
color: #000;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
.widgets {
|
.widgets {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -77,30 +67,3 @@
|
|||||||
grid-template-columns: min-content min-content;
|
grid-template-columns: min-content min-content;
|
||||||
grid-gap: 8px;
|
grid-gap: 8px;
|
||||||
}
|
}
|
||||||
.value-selector {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, min-content);
|
|
||||||
grid-gap: 4px 16px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
.value-selector button {
|
|
||||||
width: 100px;
|
|
||||||
align-self: end;
|
|
||||||
justify-self: end;
|
|
||||||
}
|
|
||||||
.graph {
|
|
||||||
width: 99%;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
.graph #graph-values {
|
|
||||||
height: calc(100vh - 308px);
|
|
||||||
}
|
|
||||||
.time-offset {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, min-content);
|
|
||||||
gap: 6px 12px;
|
|
||||||
align-items: center;
|
|
||||||
justify-items: center;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
@ -42,8 +42,7 @@ button:focus {
|
|||||||
}
|
}
|
||||||
#datapoints,
|
#datapoints,
|
||||||
#problems-list,
|
#problems-list,
|
||||||
#acknowledged-list,
|
#acknowledged-list {
|
||||||
#values {
|
|
||||||
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);
|
||||||
|
@ -12,30 +12,6 @@ html {
|
|||||||
[onClick] {
|
[onClick] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
#page-error {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 8192;
|
|
||||||
width: 500px;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
padding: 48px;
|
|
||||||
border: 2px solid #a00;
|
|
||||||
font-weight: bold;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 10px 10px 15px 0px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
#page-error.show {
|
|
||||||
display: block;
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
#page-error .close {
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
right: 16px;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
#layout {
|
#layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "menu content";
|
grid-template-areas: "menu content";
|
||||||
@ -59,7 +35,9 @@ html {
|
|||||||
#menu .entry > a {
|
#menu .entry > a {
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
grid-template-rows: 38px 16px;
|
grid-template-rows: 38px
|
||||||
|
16px
|
||||||
|
;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
color: #7bb8eb;
|
color: #7bb8eb;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -41,16 +41,6 @@
|
|||||||
grid-template-columns: repeat(2, min-content);
|
grid-template-columns: repeat(2, min-content);
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
background-color: #333;
|
|
||||||
padding: 16px 24px;
|
|
||||||
width: min-content;
|
|
||||||
border-top-left-radius: 8px;
|
|
||||||
border-top-right-radius: 8px;
|
|
||||||
margin-top: 32px;
|
|
||||||
}
|
|
||||||
#values .header {
|
|
||||||
color: #f7edd7;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
.widgets {
|
.widgets {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -77,30 +67,3 @@
|
|||||||
grid-template-columns: min-content min-content;
|
grid-template-columns: min-content min-content;
|
||||||
grid-gap: 8px;
|
grid-gap: 8px;
|
||||||
}
|
}
|
||||||
.value-selector {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, min-content);
|
|
||||||
grid-gap: 4px 16px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
.value-selector button {
|
|
||||||
width: 100px;
|
|
||||||
align-self: end;
|
|
||||||
justify-self: end;
|
|
||||||
}
|
|
||||||
.graph {
|
|
||||||
width: 99%;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
.graph #graph-values {
|
|
||||||
height: calc(100vh - 308px);
|
|
||||||
}
|
|
||||||
.time-offset {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, min-content);
|
|
||||||
gap: 6px 12px;
|
|
||||||
align-items: center;
|
|
||||||
justify-items: center;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
@ -42,8 +42,7 @@ button:focus {
|
|||||||
}
|
}
|
||||||
#datapoints,
|
#datapoints,
|
||||||
#problems-list,
|
#problems-list,
|
||||||
#acknowledged-list,
|
#acknowledged-list {
|
||||||
#values {
|
|
||||||
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);
|
||||||
|
@ -12,30 +12,6 @@ html {
|
|||||||
[onClick] {
|
[onClick] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
#page-error {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 8192;
|
|
||||||
width: 500px;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
padding: 48px;
|
|
||||||
border: 2px solid #a00;
|
|
||||||
font-weight: bold;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 10px 10px 15px 0px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
#page-error.show {
|
|
||||||
display: block;
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
#page-error .close {
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
right: 16px;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
#layout {
|
#layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "menu content";
|
grid-template-areas: "menu content";
|
||||||
@ -59,7 +35,9 @@ html {
|
|||||||
#menu .entry > a {
|
#menu .entry > a {
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
grid-template-rows: 38px 16px;
|
grid-template-rows: 38px
|
||||||
|
16px
|
||||||
|
;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
color: #777;
|
color: #777;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -1,73 +0,0 @@
|
|||||||
function offsetTime(seconds) {
|
|
||||||
const el = document.getElementById('offset-time')
|
|
||||||
el.value = seconds
|
|
||||||
el.form.submit()
|
|
||||||
}
|
|
||||||
|
|
||||||
class Graph {
|
|
||||||
constructor(datapointID, initialData) {
|
|
||||||
this.dataset = new Dataset(datapointID, initialData)
|
|
||||||
|
|
||||||
this.createGraph()
|
|
||||||
}
|
|
||||||
|
|
||||||
async createGraph() {
|
|
||||||
this.graphValues = document.getElementById('graph-values');
|
|
||||||
|
|
||||||
const values = [{
|
|
||||||
x: this.dataset.xValues(),
|
|
||||||
y: this.dataset.yValues(),
|
|
||||||
}]
|
|
||||||
|
|
||||||
this.layout = {
|
|
||||||
margin: {
|
|
||||||
t: 24,
|
|
||||||
r: 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
Plotly.react(this.graphValues, values, this.layout);
|
|
||||||
this.graphValues.on('plotly_relayout', attr => this.relayoutHandler(attr))
|
|
||||||
}
|
|
||||||
|
|
||||||
async relayoutHandler(attr) {
|
|
||||||
if (!attr.hasOwnProperty('xaxis.range[0]') || !attr.hasOwnProperty('xaxis.range[1]'))
|
|
||||||
return
|
|
||||||
|
|
||||||
this.dataset.extend(attr['xaxis.range[0]'], attr['xaxis.range[1]'])
|
|
||||||
.then(() => {
|
|
||||||
const values = [{
|
|
||||||
x: this.dataset.xValues(),
|
|
||||||
y: this.dataset.yValues(),
|
|
||||||
}]
|
|
||||||
Plotly.react(this.graphValues, values, this.layout)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Dataset {
|
|
||||||
constructor(id, initialData) {
|
|
||||||
this.datapointID = id
|
|
||||||
this.values = {}
|
|
||||||
initialData.forEach(v=>this.values[v.ID] = v)
|
|
||||||
}
|
|
||||||
|
|
||||||
xValues() {
|
|
||||||
return Object.keys(this.values).map(dpID => this.values[dpID].Ts)
|
|
||||||
}
|
|
||||||
|
|
||||||
yValues() {
|
|
||||||
return Object.keys(this.values).map(dpID => this.values[dpID].ValueInt.Int64)
|
|
||||||
}
|
|
||||||
|
|
||||||
async extend(from, to) {
|
|
||||||
return fetch(`/datapoint/json/${this.datapointID}?f=${from}&t=${to}`)
|
|
||||||
.then(data => data.json())
|
|
||||||
.then(datapointValues => {
|
|
||||||
datapointValues.forEach(dp=>{
|
|
||||||
this.values[dp.ID] = dp
|
|
||||||
})
|
|
||||||
document.getElementById('num-values').innerText = Object.keys(this.values).length
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
8
static/js/lib/plotly-2.32.0.min.js
vendored
8
static/js/lib/plotly-2.32.0.min.js
vendored
File diff suppressed because one or more lines are too long
@ -51,17 +51,6 @@
|
|||||||
grid-template-columns: repeat(2, min-content);
|
grid-template-columns: repeat(2, min-content);
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
background-color: @bg3;
|
|
||||||
padding: 16px 24px;
|
|
||||||
width: min-content;
|
|
||||||
border-top-left-radius: 8px;
|
|
||||||
border-top-right-radius: 8px;
|
|
||||||
margin-top: 32px;
|
|
||||||
|
|
||||||
.header {
|
|
||||||
color: @text2;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.widgets {
|
.widgets {
|
||||||
@ -92,36 +81,3 @@
|
|||||||
grid-gap: 8px;
|
grid-gap: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.value-selector {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, min-content);
|
|
||||||
grid-gap: 4px 16px;
|
|
||||||
|
|
||||||
margin-top: 16px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 100px;
|
|
||||||
align-self: end;
|
|
||||||
justify-self: end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.graph {
|
|
||||||
width: 99%;
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
margin-top: 16px;
|
|
||||||
|
|
||||||
#graph-values {
|
|
||||||
height: calc(100vh - 308px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-offset {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, min-content);
|
|
||||||
gap: 6px 12px;
|
|
||||||
align-items: center;
|
|
||||||
justify-items: center;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
@ -58,7 +58,7 @@ button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#datapoints, #problems-list, #acknowledged-list, #values {
|
#datapoints, #problems-list, #acknowledged-list {
|
||||||
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);
|
||||||
|
@ -18,37 +18,6 @@ html {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
#page-error {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 8192;
|
|
||||||
|
|
||||||
width: 500px;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
|
|
||||||
padding: 48px;
|
|
||||||
border: 2px solid #a00;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 10px 10px 15px 0px rgba(0, 0, 0, 0.25);
|
|
||||||
|
|
||||||
&.show {
|
|
||||||
display: block;
|
|
||||||
position: fixed;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
right: 16px;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#layout {
|
#layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "menu content";
|
grid-template-areas: "menu content";
|
||||||
@ -68,17 +37,16 @@ html {
|
|||||||
.entry {
|
.entry {
|
||||||
&.selected {
|
&.selected {
|
||||||
background: @bg3;
|
background: @bg3;
|
||||||
|
a { color: @text2 !important; }
|
||||||
a {
|
|
||||||
color: @text2 !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&>a {
|
&>a {
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
grid-template-rows:
|
grid-template-rows:
|
||||||
38px 16px;
|
38px
|
||||||
|
16px
|
||||||
|
;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
color: @text3;
|
color: @text3;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -190,7 +158,6 @@ body {
|
|||||||
h1,
|
h1,
|
||||||
h2 {
|
h2 {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
}
|
}
|
||||||
@ -280,3 +247,4 @@ label {
|
|||||||
width: min-content;
|
width: min-content;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,11 +101,6 @@ func TriggersRetrieveByDatapoint(datapointName string) (triggers []Trigger, err
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// no triggers found for this datapoint.
|
|
||||||
if data == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(data, &triggers)
|
err = json.Unmarshal(data, &triggers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = werr.Wrap(err).WithData(datapointName)
|
err = werr.Wrap(err).WithData(datapointName)
|
||||||
|
@ -20,10 +20,6 @@
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="page-error" class="{{ if ne .ERROR "" }}show{{ end }}">
|
|
||||||
<div class="close" onclick="console.log(this.parentElement.classList.remove('show'))">✖</div>
|
|
||||||
{{ .ERROR }}
|
|
||||||
</div>
|
|
||||||
<div id="layout">
|
<div id="layout">
|
||||||
{{ block "menu" . }}{{ end }}
|
{{ block "menu" . }}{{ end }}
|
||||||
<div id="page">
|
<div id="page">
|
||||||
|
@ -95,17 +95,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1>Theme</h1>
|
<h1>Theme</h1>
|
||||||
<form action="/configuration/theme">
|
<form action="/configuration/theme" id="theme-set">
|
||||||
<select name="theme" onchange="console.log(this.form.submit())">
|
<select name="theme" onchange="console.log(this.form.submit())">
|
||||||
<option value="default_light" {{ if eq "default_light" .CONFIG.THEME }}selected{{ end }}>Default light</option>
|
<option value="default_light" {{ if eq "default_light" .CONFIG.THEME }}selected{{ end }}>Default light</option>
|
||||||
<option value="gruvbox" {{ if eq "gruvbox" .CONFIG.THEME }}selected{{ end }}>Gruvbox</option>
|
<option value="gruvbox" {{ if eq "gruvbox" .CONFIG.THEME }}selected{{ end }}>Gruvbox</option>
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h1>Timezone</h1>
|
|
||||||
<form action="/configuration/timezone" method="post">
|
|
||||||
<input name="timezone" type="text" value="{{ .CONFIG.TIMEZONE }}">
|
|
||||||
<button style="margin-left: 8px;">Update</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@ -1,72 +1,13 @@
|
|||||||
{{ define "page" }}
|
{{ define "page" }}
|
||||||
{{ $version := .VERSION }}
|
{{ $version := .VERSION }}
|
||||||
{{ $graph := and (eq .Data.Display "graph") (eq .Data.Datapoint.Datatype "INT") }}
|
|
||||||
<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 src="/js/{{ .VERSION }}/datapoint_values.js"></script>
|
|
||||||
<script src="/js/{{ .VERSION }}/lib/plotly-2.32.0.min.js" charset="utf-8"></script>
|
|
||||||
|
|
||||||
{{ block "page_label" . }}{{end}}
|
{{ block "page_label" . }}{{end}}
|
||||||
|
|
||||||
<form action="/datapoint/values/{{ .Data.Datapoint.ID }}" method="get" style="margin-top: -16px">
|
|
||||||
<input type="hidden" name="offset-time" id="offset-time" value=0>
|
|
||||||
|
|
||||||
{{ if eq .Data.Datapoint.Datatype "INT" }}
|
|
||||||
<div>
|
|
||||||
<input name="display" value="graph" type="radio" id="display-graph" {{ if $graph }} checked {{ end}}> <label for="display-graph">Graph</label>
|
|
||||||
<input name="display" value="list" type="radio" id="display-list" {{ if not $graph }} checked {{ end }}> <label for="display-list">List</label>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
<div class="value-selector">
|
|
||||||
<div>Values from</div>
|
|
||||||
<div>Values to</div>
|
|
||||||
|
|
||||||
<input name="f" type="datetime-local" step="1" value="{{ .Data.TimeFrom }}">
|
|
||||||
<input name="t" type="datetime-local" step="1" value="{{ .Data.TimeTo }}">
|
|
||||||
|
|
||||||
<div class="time-offset">
|
|
||||||
<div><a href="#" onclick="offsetTime(-3600)">◀</a></div>
|
|
||||||
<div>Hour</div>
|
|
||||||
<div><a href="#" onclick="offsetTime(3600)">▶</a></div>
|
|
||||||
|
|
||||||
<div><a href="#" onclick="offsetTime(-86400)">◀</a></div>
|
|
||||||
<div>Day</div>
|
|
||||||
<div><a href="#" onclick="offsetTime(86400)">▶</a></div>
|
|
||||||
|
|
||||||
<div><a href="#" onclick="offsetTime(-604800)">◀</a></div>
|
|
||||||
<div>Week</div>
|
|
||||||
<div><a href="#" onclick="offsetTime(604800)">▶</a></div>
|
|
||||||
</div>
|
|
||||||
<button>OK</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{{ if $graph }}
|
|
||||||
<div class="graph">
|
|
||||||
<div id="graph-values"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top: 8px;">
|
|
||||||
<b>Number of values:</b>
|
|
||||||
<span id="num-values">{{ len .Data.Values }}</span>
|
|
||||||
</div>
|
|
||||||
<script type="text/javascript">
|
|
||||||
new Graph(
|
|
||||||
{{ .Data.Datapoint.ID }},
|
|
||||||
{{ .Data.Values }},
|
|
||||||
)
|
|
||||||
|
|
||||||
</script>
|
|
||||||
{{ else }}
|
|
||||||
<div id="values">
|
<div id="values">
|
||||||
<div class="header">Value added</div>
|
|
||||||
<div class="header">Value</div>
|
|
||||||
<div class="line"></div>
|
|
||||||
{{ range .Data.Values }}
|
{{ range .Data.Values }}
|
||||||
<div class="value">{{ format_time .Ts }}</div>
|
<div class="value">{{ format_time .Ts }}</div>
|
||||||
<div class="value">{{ .Value }}</div>
|
<div class="value">{{ .Value }}</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user