diff --git a/dashboard.go b/dashboard.go
deleted file mode 100644
index dfb29a9..0000000
--- a/dashboard.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package main
-
-import (
- // External
- werr "git.ahall.se/go/wrappederror"
- "github.com/jackc/pgx/v5"
-
- // Standard
- "context"
- "encoding/json"
-)
-
-type WidgetType int
-
-const (
- WIDGET_SPACE WidgetType = iota
- WIDGET_LABEL
- WIDGET_ONOFF
-)
-
-type Widget struct {
- Type WidgetType
- X int
- Y int
- SpanX int
- SpanY int
- DatapointID int
- Attributes map[string]string
-}
-
-type Dashboard struct {
- ID int
- Name string
- Widgets []Widget
-}
-
-func DashboardGet(name string) (dashboard Dashboard, err error) {
- var rows pgx.Rows
- rows, err = db.Query(context.Background(), `SELECT * FROM dashboard WHERE name = $1`, name)
- if err != nil {
- err = werr.Wrap(err)
- return
- }
-
- dashboard, err = pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Dashboard])
- if err != nil {
- err = werr.Wrap(err)
- return
- }
-
- return
-}
-
-func DashboardUpdate(dashboard Dashboard) error {
- widgets, _ := json.Marshal(dashboard.Widgets)
-
- _, err := db.Exec(
- context.Background(),
- `UPDATE dashboard SET name=$2, widgets=$3 WHERE id=$1`,
- dashboard.ID,
- dashboard.Name,
- widgets,
- )
- if err != nil {
- return werr.Wrap(err)
- }
-
- return nil
-}
diff --git a/datapoint.go b/datapoint.go
index f540db8..b47ccc9 100644
--- a/datapoint.go
+++ b/datapoint.go
@@ -28,35 +28,13 @@ type Datapoint struct {
Datatype DatapointType
Comment string
LastValue time.Time `db:"last_value"`
- DataPointID int `db:"datapoint_id"`
DatapointValueJSON []byte `db:"datapoint_value_json"`
LastDatapointValue DatapointValue
- LastDatapointID *int `db:"last_value_id"`
- LastValueInt *int `db:"last_value_int"`
- LastValueString *string `db:"last_value_string"`
- LastValueDateTime *time.Time `db:"last_value_datetime"`
Found bool
NodataProblemSeconds int `db:"nodata_problem_seconds"`
NodataIsProblem bool `db:"nodata_is_problem"`
}
-type DatapointBrief struct {
- ID int
- Name string
- Comment string
- Datatype DatapointType
- LastValueInt *int `db:"last_value_int"`
- LastValueString *string `db:"last_value_string"`
- LastValueDateTime *time.Time `db:"last_value_datetime"`
- NodataIsProblem bool `db:"nodata_is_problem"`
-}
-
-type DatapointTiny struct {
- ID int
- Value any
- Valid bool
-}
-
type DatapointValue struct {
ID int
DatapointID int `db:"datapoint_id"`
@@ -284,7 +262,7 @@ func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{
}
rows, _ := db.Query(context.Background(), query, param)
- dp, err = pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Datapoint])
+ dp, err = pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Datapoint])
if err != nil {
err = werr.Wrap(err)
return
@@ -314,7 +292,7 @@ func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{
`,
dp.ID,
)
- dp.LastDatapointValue, err = pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[DatapointValue])
+ dp, err = pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Datapoint])
if err == sql.ErrNoRows {
err = nil
return
@@ -392,53 +370,3 @@ func DatapointValues(id int, from, to time.Time) (values []DatapointValue, err e
return
} // }}}
-
-// DatapointsValue only returns ID and type and is used primarily for the dashboard.
-func DatapointsValue(ids []int) (values []DatapointTiny, err error) { // {{{
- values = []DatapointTiny{}
-
- rows, _ := db.Query(
- context.Background(),
- `
- SELECT
- id,
- name,
- comment,
- datatype,
- last_value_int,
- last_value_string,
- last_value_datetime,
- nodata_is_problem
- FROM datapoint
- WHERE
- id = ANY($1)
- `,
- ids,
- )
-
- var briefs []DatapointBrief
- briefs, err = pgx.CollectRows(rows, pgx.RowToStructByNameLax[DatapointBrief])
- if err != nil {
- err = werr.Wrap(err)
- return
- }
-
- for _, brief := range briefs {
- tiny := DatapointTiny{}
- tiny.ID = brief.ID
- tiny.Valid = !brief.NodataIsProblem
-
- switch brief.Datatype {
- case "INT":
- tiny.Value = brief.LastValueInt
- case "STRING":
- tiny.Value = brief.LastValueString
- case "DATETIME":
- tiny.Value = brief.LastValueDateTime
- }
-
- values = append(values, tiny)
- }
-
- return
-} // }}}
diff --git a/go.mod b/go.mod
index 7156421..6880382 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module smon
-go 1.26.0
+go 1.25.0
require (
git.ahall.se/go/html_template v0.1.1
diff --git a/main.go b/main.go
index 4094aec..d6f81cd 100644
--- a/main.go
+++ b/main.go
@@ -29,7 +29,7 @@ import (
"time"
)
-const VERSION = "v42"
+const VERSION = "v41"
var (
logger *slog.Logger
@@ -160,9 +160,6 @@ func main() { // {{{
http.HandleFunc("/configuration/notification/delete/{prio}", actionConfigurationNotificationDelete)
http.HandleFunc("/entry/{datapoint}", actionEntryDatapoint)
- http.HandleFunc("GET /dashboard/values", actionDashboardValues)
- http.HandleFunc("POST /dashboard/update", actionDashboardUpdate)
-
go nodataLoop()
smonConfig, err = SmonConfigInit()
@@ -408,52 +405,8 @@ func pageIndex(w http.ResponseWriter, r *http.Request) { // {{{
PAGE: "index",
CONFIG: smonConfig.Settings,
}
-
- dashboard, err := DashboardGet("start")
- if err != nil {
- logger.Error("dashboard", "error", err)
- }
-
- data := make(map[string]any)
- data["Dashboard"] = dashboard
- page.Data = data
page.Render(w, r)
} // }}}
-func actionDashboardValues(w http.ResponseWriter, r *http.Request) { // {{{
- values, err := DatapointsValue([]int{13, 15})
- if err != nil {
- logger.Error("dashboard", "error", err)
- httpError(w, err)
- return
- }
-
- j, _ := json.Marshal(struct {
- OK bool
- Values []DatapointTiny
- }{
- true,
- values,
- })
- w.Write(j)
-} // }}}
-func actionDashboardUpdate(w http.ResponseWriter, r *http.Request) { // {{{
- var dashboard Dashboard
- body, _ := io.ReadAll(r.Body)
- err := json.Unmarshal(body, &dashboard)
- if err != nil {
- httpError(w, werr.Wrap(err))
- return
- }
-
- err = DashboardUpdate(dashboard)
- if err != nil {
- httpError(w, werr.Wrap(err))
- return
- }
-
- j, _ := json.Marshal(struct{ OK bool }{true})
- w.Write(j)
-} // }}}
func actionAreaNew(w http.ResponseWriter, r *http.Request) { // {{{
name := r.PathValue("name")
@@ -465,6 +418,7 @@ func actionAreaNew(w http.ResponseWriter, r *http.Request) { // {{{
w.Header().Add("Location", "/configuration")
w.WriteHeader(302)
+ return
} // }}}
func actionAreaRename(w http.ResponseWriter, r *http.Request) { // {{{
idStr := r.PathValue("id")
@@ -483,6 +437,7 @@ func actionAreaRename(w http.ResponseWriter, r *http.Request) { // {{{
w.Header().Add("Location", "/configuration")
w.WriteHeader(302)
+ return
} // }}}
func actionAreaDelete(w http.ResponseWriter, r *http.Request) { // {{{
idStr := r.PathValue("id")
@@ -500,6 +455,7 @@ func actionAreaDelete(w http.ResponseWriter, r *http.Request) { // {{{
w.Header().Add("Location", "/configuration")
w.WriteHeader(302)
+ return
} // }}}
func actionSectionNew(w http.ResponseWriter, r *http.Request) { // {{{
@@ -519,6 +475,7 @@ func actionSectionNew(w http.ResponseWriter, r *http.Request) { // {{{
w.Header().Add("Location", "/configuration")
w.WriteHeader(302)
+ return
} // }}}
func actionSectionRename(w http.ResponseWriter, r *http.Request) { // {{{
idStr := r.PathValue("id")
@@ -537,6 +494,7 @@ func actionSectionRename(w http.ResponseWriter, r *http.Request) { // {{{
w.Header().Add("Location", "/configuration")
w.WriteHeader(302)
+ return
} // }}}
func actionSectionDelete(w http.ResponseWriter, r *http.Request) { // {{{
idStr := r.PathValue("id")
@@ -554,6 +512,7 @@ func actionSectionDelete(w http.ResponseWriter, r *http.Request) { // {{{
w.Header().Add("Location", "/configuration")
w.WriteHeader(302)
+ return
} // }}}
func pageProblems(w http.ResponseWriter, r *http.Request) { // {{{
@@ -618,6 +577,7 @@ func pageProblems(w http.ResponseWriter, r *http.Request) { // {{{
"TimeTo": timeTo.Format("2006-01-02T15:04:05"),
}
page.Render(w, r)
+ return
} // }}}
func actionProblemAcknowledge(w http.ResponseWriter, r *http.Request) { // {{{
idStr := r.PathValue("id")
@@ -635,6 +595,8 @@ func actionProblemAcknowledge(w http.ResponseWriter, r *http.Request) { // {{{
w.Header().Add("Location", "/problems")
w.WriteHeader(302)
+
+ return
} // }}}
func actionProblemUnacknowledge(w http.ResponseWriter, r *http.Request) { // {{{
idStr := r.PathValue("id")
@@ -652,6 +614,8 @@ func actionProblemUnacknowledge(w http.ResponseWriter, r *http.Request) { // {{{
w.Header().Add("Location", "/problems")
w.WriteHeader(302)
+
+ return
} // }}}
func pageDatapoints(w http.ResponseWriter, r *http.Request) { // {{{
@@ -678,6 +642,7 @@ func pageDatapoints(w http.ResponseWriter, r *http.Request) { // {{{
"Datapoints": datapoints,
}
page.Render(w, r)
+ return
} // }}}
func pageDatapointEdit(w http.ResponseWriter, r *http.Request) { // {{{
idStr := r.PathValue("id")
@@ -734,6 +699,7 @@ func pageDatapointEdit(w http.ResponseWriter, r *http.Request) { // {{{
"Triggers": triggers,
}
page.Render(w, r)
+ return
} // }}}
func actionDatapointUpdate(w http.ResponseWriter, r *http.Request) { // {{{
idStr := r.PathValue("id")
@@ -866,6 +832,7 @@ func pageDatapointValues(w http.ResponseWriter, r *http.Request) { // {{{
"TimeTo": timeTo.Format("2006-01-02T15:04:05"),
}
page.Render(w, r)
+ return
} // }}}
func actionDatapointJson(w http.ResponseWriter, r *http.Request) { // {{{
idStr := r.PathValue("id")
diff --git a/problem.go b/problem.go
index d5513dd..1d4390d 100644
--- a/problem.go
+++ b/problem.go
@@ -3,13 +3,11 @@ package main
import (
// External
werr "git.ahall.se/go/wrappederror"
- "github.com/jackc/pgx/v5"
// Standard
"context"
"database/sql"
"encoding/json"
- "errors"
"fmt"
"sort"
"strings"
@@ -154,7 +152,7 @@ func ProblemClose(trigger Trigger) (problemID int, err error) { // {{{
)
err = row.Scan(&problemID)
- if errors.Is(err, pgx.ErrNoRows) {
+ if err == sql.ErrNoRows {
err = nil
return
}
diff --git a/sql/00028.sql b/sql/00028.sql
deleted file mode 100644
index 3350ded..0000000
--- a/sql/00028.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-CREATE TABLE public.dashboard (
- id serial NOT NULL,
- "name" varchar NOT NULL,
- widgets jsonb DEFAULT '{}' NOT NULL,
- CONSTRAINT dashboard_pk PRIMARY KEY (id),
- CONSTRAINT dashboard_name_unique UNIQUE ("name")
-);
diff --git a/static/css/gruvbox/gruvbox.css b/static/css/gruvbox/gruvbox.css
index e041022..6acdbc0 100644
--- a/static/css/gruvbox/gruvbox.css
+++ b/static/css/gruvbox/gruvbox.css
@@ -1,5 +1,5 @@
body {
- /* background-image: url(/images/v0/gruvbox/background.svg); */
+ background-image: url(/images/v0/gruvbox/background.svg);
}
#menu {
box-shadow: 2px 0px 5px 3px rgba(0, 0, 0, 0.25);
diff --git a/static/images/grid_management.svg b/static/images/grid_management.svg
deleted file mode 100644
index be8a424..0000000
--- a/static/images/grid_management.svg
+++ /dev/null
@@ -1,1123 +0,0 @@
-
-
-
-
diff --git a/static/images/widget_light_green.svg b/static/images/widget_light_green.svg
deleted file mode 100644
index a0bc5a5..0000000
--- a/static/images/widget_light_green.svg
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
diff --git a/static/images/widget_light_off.svg b/static/images/widget_light_off.svg
deleted file mode 100644
index bcaed9f..0000000
--- a/static/images/widget_light_off.svg
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
diff --git a/static/images/widget_light_red.svg b/static/images/widget_light_red.svg
deleted file mode 100644
index 063f076..0000000
--- a/static/images/widget_light_red.svg
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
diff --git a/static/js/dashboard.mjs b/static/js/dashboard.mjs
deleted file mode 100644
index cd00746..0000000
--- a/static/js/dashboard.mjs
+++ /dev/null
@@ -1,805 +0,0 @@
-import { CustomHTMLElement } from './lib/custom_html_element.mjs'
-
-const WIDGET_SPACE = 0
-const WIDGET_LABEL = 1
-const WIDGET_ONOFF = 2
-
-export class SmonDashboard extends CustomHTMLElement {// {{{
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `
- }// }}}
- constructor() {// {{{
- super(true)
- const script = this.querySelector('script')
- this.data = JSON.parse(script.textContent)
- this.widgets = new Map() // keyed by datapoint ID, value is an array of widgets.
- this.addColumns = 0
- this.addRows = 0
- this.editedWidget = null
- this.widgetsEdited = false
-
- this.elEditDashboard.addEventListener('click', () => this.classList.toggle('edit'))
- this.elAddColumns.addEventListener('click', () => { this.addColumns += 5; this.render() })
- this.elAddRows.addEventListener('click', () => { this.addRows += 5; this.render() })
- this.elUpdateDashboard.addEventListener('click', () => this.updateDashboard())
- this.elEditUpdate.addEventListener('click', () => this.editUpdate())
-
- this.elGridAddLeft.addEventListener('click', () => this.gridAddLeft())
- this.elGridAddRight.addEventListener('click', () => this.gridAddRight())
- this.elGridAddAbove.addEventListener('click', () => this.gridAddAbove())
- this.elGridAddBelow.addEventListener('click', () => this.gridAddBelow())
-
- this.elGridRemove.addEventListener('click', () => this.gridRemove())
- this.elGridRemoveMoveLeft.addEventListener('click', () => this.gridRemoveMoveLeft())
- this.elGridRemoveMoveUp.addEventListener('click', () => this.gridRemoveMoveUp())
-
- this.elGridAddColumnLeft.addEventListener('click', () => this.gridAddColumnLeft())
- this.elGridAddColumnAbove.addEventListener('click', () => this.gridAddColumnAbove())
- this.elGridRemoveColumn.addEventListener('click', () => this.gridRemoveColumn())
- this.elGridRemoveRow.addEventListener('click', () => this.gridRemoveRow())
-
- this.render()
- this.fetchData() // Retrieve one as fast as possible since setInterval doesn't run before an interval.
- setInterval(() => this.fetchData(), 1000)
- }// }}}
-
- render() {// {{{
- const widgets = []
- this.widgets = new Map()
- this.occupiedCells = new Map()
-
- let maxX = 0
- let maxY = 0
-
- for (const wd of this.data.Widgets) {
- if (!this.widgets.has(wd.DatapointID))
- this.widgets.set(wd.DatapointID, [])
- // widgetRefs modifies the array in the map directly.
- const widgetRefs = this.widgets.get(wd.DatapointID)
- const widget = SmonWidget.create(wd.Type, wd)
- widget.addEventListener('click', () => this.editCell(widget))
- widgetRefs.push(widget)
- widgets.push(widget)
-
- maxX = Math.max(maxX, wd.X)
- maxY = Math.max(maxY, wd.Y)
-
- this.occupiedCells.set(`${wd.X}x${wd.Y}`, true)
- for (let x = 0; x <= wd.SpanX; x++)
- this.occupiedCells.set(`${wd.X + x}x${wd.Y}`, true)
- for (let y = 0; y <= wd.SpanY; y++)
- this.occupiedCells.set(`${wd.X}x${wd.Y + y}`, true)
- }
-
- maxX += this.addColumns
- maxY += this.addRows
-
- // Empty elements are created to fill up the empty cells in the grid.
- // This is needed as placeholders for grid and dragdrop targets when editing.
- for (let x = 1; x <= maxX; x++)
- for (let y = 1; y <= maxY; y++) {
- if (this.occupiedCells.has(`${x}x${y}`))
- continue
-
- const empty = new SmonWidgetEmpty({ X: x, Y: y })
- empty.addEventListener('click', () => this.editCell(empty))
- widgets.push(empty)
- }
-
- this.elUpdateDashboard.disabled = !this.widgetsEdited
-
- this.elGrid.replaceChildren(...widgets)
- }// }}}
- error(msg) {// {{{
- this.elError.innerText = msg
- if (msg === '')
- this.elError.classList.remove('show')
- else
- this.elError.classList.add('show')
- }// }}}
- async editUpdate() {// {{{
- const changes = await this.editedWidget.editUpdate()
- this.widgetsEdited = true
-
- let replacedWidget = false
- if (changes.add) {
- const coords = `${changes.add.data.X}x${changes.add.data.Y}`
-
- for (const w of this.data.Widgets) {
- if (w.X === changes.add.data.X && w.Y === changes.add.data.Y) {
- alert(`A widget already exist at ${coords}`)
- return
- }
- }
-
- if (!replacedWidget)
- this.data.Widgets.push(changes.add.data)
- }
-
- this.elEdit.close()
- this.editedWidget = null
- this.render()
- }// }}}
- editCell(el) {// {{{
- if (!this.classList.contains('edit'))
- return
-
- this.editedWidget = el
- const components = el.edit()
- this.elEditComponents.replaceChildren(...components)
- this.elEdit.showModal()
- }// }}}
- async fetchData() {// {{{
- try {
- const res = await fetch(`/dashboard/values`)
- const json = await res.json()
-
- if (!json.OK)
- throw new Error(json.Error)
-
- for (const dp of json.Values) {
- const widgets = this.widgets.get(dp.ID) || []
- for (const w of widgets) {
- w.setValue(dp.Value, dp.Valid)
- w.render()
- }
- }
-
- this.error('')
- } catch (e) {
- console.error(e)
- this.error(e.message)
-
- // All widgets are set in a NULL state to visualize the error.
- const allDpIDs = this.widgets.keys()
- for (const id of allDpIDs) {
- const widgets = this.widgets.get(id) || []
- for (const w of widgets) {
- w.setValue(null, false)
- w.render()
- }
- }
- }
- }// }}}
- async updateDashboard() {// {{{
- try {
- const res = await fetch('/dashboard/update', {
- method: 'POST',
- body: JSON.stringify(this.data),
- })
- const json = await res.json()
- if (!json.OK)
- throw new Error(json.Error)
-
- this.widgetsEdited = false
- this.render()
- } catch (e) {
- console.error(e)
- alert(e.message)
- }
- }// }}}
-
- gridEdit() {// {{{
- this.widgetsEdited = true
- this.elEdit.close()
- this.editedWidget = null
- this.render()
- }// }}}
- gridAddLeft() {// {{{
- const fromX = this.editedWidget.data.X
- const fromY = this.editedWidget.data.Y
- for (const w of this.data.Widgets)
- if (w.X >= fromX && w.Y == fromY)
- w.X++
-
- this.gridEdit()
- }// }}}
- gridAddRight() {// {{{
- const fromX = this.editedWidget.data.X
- const fromY = this.editedWidget.data.Y
- for (const w of this.data.Widgets)
- if (w.X > fromX && w.Y == fromY)
- w.X++
-
- this.gridEdit()
- }// }}}
- gridAddAbove() {// {{{
- const fromX = this.editedWidget.data.X
- const fromY = this.editedWidget.data.Y
- for (const w of this.data.Widgets)
- if (w.X == fromX && w.Y >= fromY)
- w.Y++
-
- this.gridEdit()
- }// }}}
- gridAddBelow() {// {{{
- const fromX = this.editedWidget.data.X
- const fromY = this.editedWidget.data.Y
- for (const w of this.data.Widgets)
- if (w.X == fromX && w.Y > fromY)
- w.Y++
-
- this.gridEdit()
- }// }}}
- gridAddColumnLeft() {// {{{
- const fromX = this.editedWidget.data.X
- for (const w of this.data.Widgets)
- if (w.X >= fromX)
- w.X++
-
- this.gridEdit()
- }// }}}
- gridAddColumnAbove() {// {{{
- const fromY = this.editedWidget.data.Y
- for (const w of this.data.Widgets)
- if (w.Y >= fromY)
- w.Y++
-
- this.gridEdit()
- }// }}}
- gridRemoveColumn() {// {{{
- // Keep all widgets that's not on the edited widget's X coordinate.
- this.data.Widgets = this.data.Widgets.filter(w =>
- w.X != this.editedWidget.data.X
- )
-
- for (const w of this.data.Widgets)
- if (w.X >= this.editedWidget.data.X)
- w.X--
- this.gridEdit()
- }// }}}
- gridRemoveRow() {// {{{
- // Keep all widgets that's not on the edited widget's X coordinate.
- this.data.Widgets = this.data.Widgets.filter(w =>
- w.Y != this.editedWidget.data.Y
- )
-
- for (const w of this.data.Widgets)
- if (w.Y >= this.editedWidget.data.Y)
- w.Y--
- this.gridEdit()
- }// }}}
-
- gridRemove() {// {{{
- const fromX = this.editedWidget.data.X
- const fromY = this.editedWidget.data.Y
-
- this.data.Widgets = this.data.Widgets.filter(w =>
- w.X != fromX || w.Y != fromY
- )
- this.gridEdit()
- }// }}}
- gridRemoveMoveLeft() {// {{{
- const fromX = this.editedWidget.data.X
- const fromY = this.editedWidget.data.Y
-
- this.data.Widgets = this.data.Widgets.filter(w =>
- w.X != fromX || w.Y != fromY
- )
-
- for (const w of this.data.Widgets)
- if (w.X > fromX && w.Y == fromY)
- w.X--
-
- this.gridEdit()
- }// }}}
- gridRemoveMoveUp() {// {{{
- const fromX = this.editedWidget.data.X
- const fromY = this.editedWidget.data.Y
-
- this.data.Widgets = this.data.Widgets.filter(w =>
- w.X != fromX || w.Y != fromY
- )
-
- for (const w of this.data.Widgets)
- if (w.X == fromX && w.Y > fromY)
- w.Y--
-
- this.gridEdit()
- }// }}}
-}// }}}
-
-class SmonWidget extends CustomHTMLElement {// {{{
- static create(t, data) {// {{{
- data.Type = t
- switch (t) {
- case WIDGET_SPACE: return new SmonWidgetSpace(data)
- case WIDGET_LABEL: return new SmonWidgetLabel(data)
- case WIDGET_ONOFF: return new SmonWidgetOnOff(data)
- default:
- alert(`Unknown widget type: ${t} (${typeof t})`)
- }
- }// }}}
- constructor(data) {// {{{
- super(true)
-
- this.data = data
- this.value = null
- this.editDialog = null
-
- if (!this.data.Attributes)
- this.data.Attributes = {}
- }// }}}
-
- render() {// {{{
- this.style.gridColumn = this.data.X
- this.style.gridRow = this.data.Y
-
- if (this.data.SpanX > 0)
- this.style.gridColumn = `${this.data.X} / ${this.data.X + this.data.SpanX + 1}`
-
- if (this.data.SpanY > 0)
- this.style.gridRow = `${this.data.Y} / ${this.data.Y + this.data.SpanY + 1}`
-
- if (this.data.Attributes.BorderTop) {
- if (this.data.Attributes.Color)
- this.style.borderTop = `1px solid ${this.data.Attributes.Color}`
- else
- this.style.borderTop = `1px solid currentColor`
- }
-
- if (this.data.Attributes.BorderBottom) {
- if (this.data.Attributes.Color)
- this.style.borderBottom = `1px solid ${this.data.Attributes.Color}`
- else
- this.style.borderBottom = `1px solid currentColor`
- }
-
- if (this.data.Attributes.BorderLeft) {
- if (this.data.Attributes.Color)
- this.style.borderLeft = `1px solid ${this.data.Attributes.Color}`
- else
- this.style.borderLeft = `1px solid currentColor`
- }
-
- if (this.data.Attributes.BorderRight) {
- if (this.data.Attributes.Color)
- this.style.borderRight = `1px solid ${this.data.Attributes.Color}`
- else
- this.style.borderRight = `1px solid currentColor`
- }
- }// }}}
- setValue(v, valid) {// {{{
- this.value = valid ? v : null
- }// }}}
- edit() {// {{{
- if (this.editDialog !== null)
- return
-
- const components = this.commonEditWidgets()
- components.push(...this.editWidget())
- return components
- }// }}}
- commonEditWidgets() {// {{{
- this.editDiv = document.createElement('div')
- this.editDiv.innerHTML = `
-
- `
- this.editDiv.style.marginBottom = '16px'
-
- this.elBorderTop = this.editDiv.querySelector('input[name="top"]')
- this.elBorderBottom = this.editDiv.querySelector('input[name="bottom"]')
- this.elBorderLeft = this.editDiv.querySelector('input[name="left"]')
- this.elBorderRight = this.editDiv.querySelector('input[name="right"]')
- this.elSpanX = this.editDiv.querySelector('input[name="span-x"]')
- this.elSpanY = this.editDiv.querySelector('input[name="span-y"]')
- this.elFontSize = this.editDiv.querySelector('input[name="font-size"]')
- this.elUseColor = this.editDiv.querySelector('input[name="use-color"]')
- this.elColor = this.editDiv.querySelector('input[name="color"]')
- this.elColor.addEventListener('input', () => this.elUseColor.checked = true)
-
- this.elBorderTop.checked = this.data.Attributes.BorderTop
- this.elBorderBottom.checked = this.data.Attributes.BorderBottom
- this.elBorderLeft.checked = this.data.Attributes.BorderLeft
- this.elBorderRight.checked = this.data.Attributes.BorderRight
-
- this.elSpanX.value = this.data.SpanX
- this.elSpanY.value = this.data.SpanY
-
- this.elFontSize.value = this.data.Attributes.Size || ''
- this.elUseColor.checked = this.data.Attributes.Color ? true : false
- this.elColor.value = this.data.Attributes.Color || ''
-
- return [this.editDiv]
- }// }}}
-
- // edit dialog wants to update the widget.
- async editUpdate() {// {{{
- this.data.Attributes.BorderTop = this.elBorderTop.checked ? 'true' : ''
- this.data.Attributes.BorderBottom = this.elBorderBottom.checked ? 'true' : ''
- this.data.Attributes.BorderLeft = this.elBorderLeft.checked ? 'true' : ''
- this.data.Attributes.BorderRight = this.elBorderRight.checked ? 'true' : ''
-
- this.data.SpanX = parseInt(this.elSpanX.value)
- this.data.SpanY = parseInt(this.elSpanY.value)
-
- this.data.Attributes.Size = this.elFontSize.value
- this.data.Attributes.Color = this.elUseColor.checked ? this.elColor.value : ''
-
- return await this.updateWidget()
- }// }}}
-
- // Many widgets can have labels.
- // This function applies common attributes like size and color.
- applyLabelAttributes(el) {// {{{
- if (this.data.Attributes?.Size)
- el.style.fontSize = this.data.Attributes.Size
-
- if (this.data.Attributes?.Color)
- el.style.color = this.data.Attributes.Color
-
- if (this.data.Attributes?.Underline)
- if (this.data.Attributes?.Color)
- el.style.borderBottom = `1px solid ${this.data.Attributes.Color}`
- else
- el.style.borderBottom = `1px solid currentColor`
- }// }}}
-
-}// }}}
-class SmonWidgetEmpty extends SmonWidget {// {{{
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
- `
- }// }}}
- constructor(data) {// {{{
- super(data)
- this.render()
- }// }}}
- render() {// {{{
- super.render()
- }// }}}
- editWidget() {// {{{
- this.editDiv = document.createElement('div')
- this.editDiv.innerHTML = `
- Add widget
-
- `
- return [this.editDiv]
- }// }}}
- async updateWidget() {// {{{
- const t = parseInt(this.editDiv.querySelector('select').value)
- const widget = SmonWidget.create(t, JSON.parse(JSON.stringify(this.data)))
- return { add: widget }
- }// }}}
-}// }}}
-class SmonWidgetSpace extends SmonWidget {// {{{
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
- `
- }// }}}
- constructor(data) {// {{{
- super(data)
- this.render()
- }// }}}
- render() {// {{{
- super.render()
-
- this.elSpacer.style.minWidth = this.data.Attributes.Width || '16px'
- this.elSpacer.style.minHeight = this.data.Attributes.Height || '16px'
- }// }}}
- editWidget() {// {{{
- this.editDiv = document.createElement('div')
- this.editDiv.innerHTML = `
- Spacing
-
- `
-
- this.elWidth = this.editDiv.querySelector('[name="width"]')
- this.elHeight = this.editDiv.querySelector('[name="height"]')
-
- this.elWidth.value = this.data.Attributes?.Width || ''
- this.elHeight.value = this.data.Attributes?.Height || ''
-
- return [this.editDiv]
- }// }}}
- async updateWidget() {// {{{
- this.data.Attributes.Width = this.elWidth.value
- this.data.Attributes.Height = this.elHeight.value
- return {}
- }// }}}
-}// }}}
-class SmonWidgetLabel extends SmonWidget {// {{{
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
-
- `
- }// }}}
- constructor(data) {// {{{
- super(data)
- this.render()
- }// }}}
- render() {// {{{
- super.render()
- this.elLabel.innerText = this.data.Attributes?.Label || '[set label]'
- this.applyLabelAttributes(this.elLabel)
- }// }}}
- editWidget() {// {{{
- this.editDiv = document.createElement('div')
- this.editDiv.innerHTML = `
- Label
-
- `
- this.editDiv.querySelector('input').value = this.data.Attributes.Label || '[set label]'
- return [this.editDiv]
- }// }}}
- async updateWidget() {// {{{
- this.data.Attributes.Label = this.editDiv.querySelector('input').value
- return {}
- }// }}}
-}// }}}
-class SmonWidgetOnOff extends SmonWidget {// {{{
- static {// {{{
- this.tmpl = document.createElement('template')
- this.tmpl.innerHTML = `
-
-
-
- `
- }// }}}
- constructor(data) {// {{{
- super(data)
- this.render()
- }// }}}
- render() {// {{{
- super.render()
-
- let img
- switch (this.value) {
- case 0:
- case 'OFF':
- img = 'widget_light_red.svg'
- break
-
- case 1:
- case 'ON':
- img = 'widget_light_green.svg'
- break
-
- default:
- img = 'widget_light_off.svg'
- }
-
- this.elStatus.setAttribute('src', `/images/${_VERSION}/${img}`)
- this.elLabel.innerText = this.data.Attributes.Label || '[set label]'
- this.applyLabelAttributes(this.elLabel)
- }// }}}
- editWidget() {// {{{
- this.editDiv = document.createElement('div')
- this.editDiv.innerHTML = `
- Label
-
- `
- this.editDiv.querySelector('input').value = this.data.Attributes.Label || '[set label]'
- return [this.editDiv]
- }// }}}
- async updateWidget() {// {{{
- this.data.Attributes.Label = this.editDiv.querySelector('input').value
- return {}
- }// }}}
-}// }}}
-
-customElements.define("smon-widget", SmonWidget)
-customElements.define("smon-widget-empty", SmonWidgetEmpty)
-customElements.define("smon-widget-space", SmonWidgetSpace)
-customElements.define("smon-widget-label", SmonWidgetLabel)
-customElements.define("smon-widget-onoff", SmonWidgetOnOff)
-customElements.define("smon-dashboard", SmonDashboard)
diff --git a/static/js/lib/custom_html_element.mjs b/static/js/lib/custom_html_element.mjs
deleted file mode 100644
index d1fb7ae..0000000
--- a/static/js/lib/custom_html_element.mjs
+++ /dev/null
@@ -1,51 +0,0 @@
-/* Use data-el or data-field attribute.
- * Element with data-el="hum-ding" is accessible as this.elHumDing and fields with
- * data-field="long-dong" as this.fieldLongDong.
- *
- * All field values can be retrieved with fieldValues() and uses the data-field attribute
- * as LongDong as key.
- */
-
-export class CustomHTMLElement extends HTMLElement {
- constructor(useShadow) {// {{{
- super()
-
- this._fields = new Map()
-
- const workOn = useShadow ? this.attachShadow({ mode: 'open' }) : this
- workOn.appendChild(this.constructor.tmpl.content.cloneNode(true))
- workOn.querySelectorAll('*').forEach(el => {
- const field = el.dataset.field
- if (field !== undefined) {
- const fieldName = this.toElementName('field', field)
- this[fieldName] = el
- this._fields.set(this.toElementName('', field), el)
- }
-
- const name = el.dataset.el
- if (name !== undefined) {
- const elName = this.toElementName('el', name)
- this[elName] = el
- el.classList.add('el-' + name)
- }
- })
- }// }}}
- allFields() {// {{{
- return this._fields
- }// }}}
- fieldValues() {// {{{
- const state = {}
- for (const [name, field] of this._fields) {
- if (field.tagName.toLowerCase() == 'input' && field.getAttribute('type').toLowerCase() == 'checkbox')
- state[name] = field.checked
- else
- state[name] = field.value
-
- }
- return state
- }// }}}
- toElementName(prefix, str) {// {{{
- str = prefix + '-' + str
- return str.replace(/-(id|[a-z])/g, match => match.toUpperCase().replace('-', ''))
- }// }}}
-}
diff --git a/views/layouts/main.gotmpl b/views/layouts/main.gotmpl
index 263d61f..b82b49f 100644
--- a/views/layouts/main.gotmpl
+++ b/views/layouts/main.gotmpl
@@ -6,9 +6,6 @@
{{ template "fonts" }}
-
diff --git a/views/pages/index.gotmpl b/views/pages/index.gotmpl
index 8d1ead5..f5d7056 100644
--- a/views/pages/index.gotmpl
+++ b/views/pages/index.gotmpl
@@ -1,19 +1,13 @@
{{ define "page" }}
-
-
-
-

-
-
Smon
- {{ .VERSION }}
-
+
+
-
-
-
+
+
SMon
+ {{ .VERSION }}
+
+
{{ end }}