Dashboard editing

This commit is contained in:
Magnus Åhall 2026-08-21 11:03:14 +02:00
parent c3fe5951d4
commit dc0ad136e6
6 changed files with 1596 additions and 65 deletions

View file

@ -7,12 +7,14 @@ import (
// Standard // Standard
"context" "context"
"encoding/json"
) )
type WidgetType int type WidgetType int
const ( const (
WIDGET_LABEL WidgetType = iota WIDGET_SPACE WidgetType = iota
WIDGET_LABEL
WIDGET_ONOFF WIDGET_ONOFF
) )
@ -48,3 +50,20 @@ func DashboardGet(name string) (dashboard Dashboard, err error) {
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
}

View file

@ -28,6 +28,7 @@ type Datapoint struct {
Datatype DatapointType Datatype DatapointType
Comment string Comment string
LastValue time.Time `db:"last_value"` LastValue time.Time `db:"last_value"`
DataPointID int `db:"datapoint_id"`
DatapointValueJSON []byte `db:"datapoint_value_json"` DatapointValueJSON []byte `db:"datapoint_value_json"`
LastDatapointValue DatapointValue LastDatapointValue DatapointValue
LastDatapointID *int `db:"last_value_id"` LastDatapointID *int `db:"last_value_id"`
@ -283,7 +284,7 @@ func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{
} }
rows, _ := db.Query(context.Background(), query, param) rows, _ := db.Query(context.Background(), query, param)
dp, err = pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Datapoint]) dp, err = pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Datapoint])
if err != nil { if err != nil {
err = werr.Wrap(err) err = werr.Wrap(err)
return return
@ -313,7 +314,7 @@ func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{
`, `,
dp.ID, dp.ID,
) )
dp, err = pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Datapoint]) dp.LastDatapointValue, err = pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[DatapointValue])
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
err = nil err = nil
return return

22
main.go
View file

@ -160,7 +160,8 @@ func main() { // {{{
http.HandleFunc("/configuration/notification/delete/{prio}", actionConfigurationNotificationDelete) http.HandleFunc("/configuration/notification/delete/{prio}", actionConfigurationNotificationDelete)
http.HandleFunc("/entry/{datapoint}", actionEntryDatapoint) http.HandleFunc("/entry/{datapoint}", actionEntryDatapoint)
http.HandleFunc("/dashboard/values", actionDashboardValues) http.HandleFunc("GET /dashboard/values", actionDashboardValues)
http.HandleFunc("POST /dashboard/update", actionDashboardUpdate)
go nodataLoop() go nodataLoop()
@ -408,7 +409,6 @@ func pageIndex(w http.ResponseWriter, r *http.Request) { // {{{
CONFIG: smonConfig.Settings, CONFIG: smonConfig.Settings,
} }
dashboard, err := DashboardGet("start") dashboard, err := DashboardGet("start")
if err != nil { if err != nil {
logger.Error("dashboard", "error", err) logger.Error("dashboard", "error", err)
@ -436,6 +436,24 @@ func actionDashboardValues(w http.ResponseWriter, r *http.Request) { // {{{
}) })
w.Write(j) 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) { // {{{ func actionAreaNew(w http.ResponseWriter, r *http.Request) { // {{{
name := r.PathValue("name") name := r.PathValue("name")

View file

@ -3,11 +3,13 @@ package main
import ( import (
// External // External
werr "git.ahall.se/go/wrappederror" werr "git.ahall.se/go/wrappederror"
"github.com/jackc/pgx/v5"
// Standard // Standard
"context" "context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
@ -152,7 +154,7 @@ func ProblemClose(trigger Trigger) (problemID int, err error) { // {{{
) )
err = row.Scan(&problemID) err = row.Scan(&problemID)
if err == sql.ErrNoRows { if errors.Is(err, pgx.ErrNoRows) {
err = nil err = nil
return return
} }

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -1,5 +1,9 @@
import { CustomHTMLElement } from './lib/custom_html_element.mjs' 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 {// {{{ export class SmonDashboard extends CustomHTMLElement {// {{{
static {// {{{ static {// {{{
this.tmpl = document.createElement('template') this.tmpl = document.createElement('template')
@ -13,7 +17,6 @@ export class SmonDashboard extends CustomHTMLElement {// {{{
:host(.edit) { :host(.edit) {
.el-grid { .el-grid {
grid-gap: unset;
border-top: var(--edit-border); border-top: var(--edit-border);
border-left: var(--edit-border); border-left: var(--edit-border);
@ -24,12 +27,17 @@ export class SmonDashboard extends CustomHTMLElement {// {{{
} }
& > *:hover { & > *:hover {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1) !important;
cursor: pointer; cursor: pointer;
} }
& > .empty { smon-widget-space {
background: rgba(128, 255, 128, 0.10);
}
& > smon-widget-empty {
display: block; display: block;
background: unset;
} }
} }
@ -59,12 +67,15 @@ export class SmonDashboard extends CustomHTMLElement {// {{{
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: column;
grid-auto-columns: min-content; grid-auto-columns: min-content;
grid-gap: 8px 16px;
width: min-content; width: min-content;
.empty { smon-widget-empty {
display: none; display: none;
} }
& > * {
padding: 4px 0px;
}
} }
.only-on-edit { .only-on-edit {
@ -79,6 +90,10 @@ export class SmonDashboard extends CustomHTMLElement {// {{{
} }
dialog { dialog {
svg {
cursor: pointer;
}
input[type="text"] { input[type="text"] {
padding: 4px; padding: 4px;
margin-top: 4px; margin-top: 4px;
@ -107,11 +122,34 @@ export class SmonDashboard extends CustomHTMLElement {// {{{
</div> </div>
<dialog data-el="edit"> <dialog data-el="edit">
<div style="font-weight: bold;">Cell operations</div>
<div style="margin-top: 8px; margin-bottom: 16px;">
<div>
<svg data-el="grid-add-left" style="width: 36px; height: 36px;"><use href="/images/${_VERSION}/grid_management.svg#add-left" /></svg>
<svg data-el="grid-add-right" style="width: 36px; height: 36px;"><use href="/images/${_VERSION}/grid_management.svg#add-right" /></svg>
<svg data-el="grid-add-above" style="width: 36px; height: 36px;"><use href="/images/${_VERSION}/grid_management.svg#add-above" /></svg>
<svg data-el="grid-add-below" style="width: 36px; height: 36px;"><use href="/images/${_VERSION}/grid_management.svg#add-below" /></svg>
</div>
<div>
<svg data-el="grid-add-column-left" style="width: 36px; height: 36px;"><use href="/images/${_VERSION}/grid_management.svg#add-column-left" /></svg>
<svg data-el="grid-add-column-above" style="width: 36px; height: 36px;"><use href="/images/${_VERSION}/grid_management.svg#add-column-above" /></svg>
<svg data-el="grid-remove-column" style="width: 36px; height: 36px;"><use href="/images/${_VERSION}/grid_management.svg#remove-column" /></svg>
<svg data-el="grid-remove-row" style="width: 36px; height: 36px;"><use href="/images/${_VERSION}/grid_management.svg#remove-row" /></svg>
</div>
<div>
<svg data-el="grid-remove" style="width: 36px; height: 36px;"><use href="/images/${_VERSION}/grid_management.svg#remove" /></svg>
<svg data-el="grid-remove-move-left" style="width: 36px; height: 36px;"><use href="/images/${_VERSION}/grid_management.svg#remove-move-left" /></svg>
<svg data-el="grid-remove-move-up" style="width: 36px; height: 36px;"><use href="/images/${_VERSION}/grid_management.svg#remove-move-up" /></svg>
</div>
</div>
<div data-el="edit-components"></div> <div data-el="edit-components"></div>
<div style="display: flex; gap: 8px; margin-top: 16px;"> <div style="display: flex; gap: 8px; margin-top: 16px;">
<button data-el="edit-update">Update</button> <button data-el="edit-update">Update</button>
<button data-el="edit-remove">Remove</button>
</div> </div>
</dialog> </dialog>
` `
}// }}} }// }}}
@ -119,18 +157,32 @@ export class SmonDashboard extends CustomHTMLElement {// {{{
super(true) super(true)
const script = this.querySelector('script') const script = this.querySelector('script')
this.data = JSON.parse(script.textContent) this.data = JSON.parse(script.textContent)
globalThis.foo = this.data
this.widgets = new Map() // keyed by datapoint ID, value is an array of widgets. this.widgets = new Map() // keyed by datapoint ID, value is an array of widgets.
this.addColumns = 0 this.addColumns = 0
this.addRows = 0 this.addRows = 0
this.editedWidget = null this.editedWidget = null
this.widgetsEdited = false
this.elEditDashboard.addEventListener('click', () => this.classList.toggle('edit')) this.elEditDashboard.addEventListener('click', () => this.classList.toggle('edit'))
this.elAddColumns.addEventListener('click', () => { this.addColumns += 5; this.render() }) this.elAddColumns.addEventListener('click', () => { this.addColumns += 5; this.render() })
this.elAddRows.addEventListener('click', () => { this.addRows += 5; this.render() }) this.elAddRows.addEventListener('click', () => { this.addRows += 5; this.render() })
this.elUpdateDashboard.addEventListener('click', () => this.saveDashboard()) this.elUpdateDashboard.addEventListener('click', () => this.updateDashboard())
this.elEditUpdate.addEventListener('click', () => this.editUpdate()) 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.render()
this.fetchData() // Retrieve one as fast as possible since setInterval doesn't run before an interval. this.fetchData() // Retrieve one as fast as possible since setInterval doesn't run before an interval.
setInterval(() => this.fetchData(), 1000) setInterval(() => this.fetchData(), 1000)
@ -139,10 +191,10 @@ export class SmonDashboard extends CustomHTMLElement {// {{{
render() {// {{{ render() {// {{{
const widgets = [] const widgets = []
this.widgets = new Map() this.widgets = new Map()
this.occupiedCells = new Map()
let maxX = 0 let maxX = 0
let maxY = 0 let maxY = 0
let occupiedCells = new Map()
for (const wd of this.data.Widgets) { for (const wd of this.data.Widgets) {
if (!this.widgets.has(wd.DatapointID)) if (!this.widgets.has(wd.DatapointID))
@ -150,17 +202,18 @@ export class SmonDashboard extends CustomHTMLElement {// {{{
// widgetRefs modifies the array in the map directly. // widgetRefs modifies the array in the map directly.
const widgetRefs = this.widgets.get(wd.DatapointID) const widgetRefs = this.widgets.get(wd.DatapointID)
const widget = SmonWidget.create(wd.Type, wd) const widget = SmonWidget.create(wd.Type, wd)
widget.addEventListener('click', () => this.selectCell(widget)) widget.addEventListener('click', () => this.editCell(widget))
widgetRefs.push(widget) widgetRefs.push(widget)
widgets.push(widget) widgets.push(widget)
maxX = Math.max(maxX, wd.X) maxX = Math.max(maxX, wd.X)
maxY = Math.max(maxY, wd.Y) maxY = Math.max(maxY, wd.Y)
occupiedCells.set(`${wd.X}x${wd.Y}`, true) this.occupiedCells.set(`${wd.X}x${wd.Y}`, true)
for (let x = 0; x <= wd.SpanX; x++) { for (let x = 0; x <= wd.SpanX; x++)
occupiedCells.set(`${wd.X + x}x${wd.Y}`, true) 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 maxX += this.addColumns
@ -170,14 +223,16 @@ export class SmonDashboard extends CustomHTMLElement {// {{{
// This is needed as placeholders for grid and dragdrop targets when editing. // This is needed as placeholders for grid and dragdrop targets when editing.
for (let x = 1; x <= maxX; x++) for (let x = 1; x <= maxX; x++)
for (let y = 1; y <= maxY; y++) { for (let y = 1; y <= maxY; y++) {
if (occupiedCells.has(`${x}x${y}`)) if (this.occupiedCells.has(`${x}x${y}`))
continue continue
const empty = new SmonWidgetEmpty(x, y) const empty = new SmonWidgetEmpty({ X: x, Y: y })
empty.addEventListener('click', () => this.selectCell(empty)) empty.addEventListener('click', () => this.editCell(empty))
widgets.push(empty) widgets.push(empty)
} }
this.elUpdateDashboard.disabled = !this.widgetsEdited
this.elGrid.replaceChildren(...widgets) this.elGrid.replaceChildren(...widgets)
}// }}} }// }}}
error(msg) {// {{{ error(msg) {// {{{
@ -187,15 +242,30 @@ export class SmonDashboard extends CustomHTMLElement {// {{{
else else
this.elError.classList.add('show') this.elError.classList.add('show')
}// }}} }// }}}
saveDashboard() {// {{{
}// }}}
async editUpdate() {// {{{ async editUpdate() {// {{{
await this.editedWidget.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.elEdit.close()
this.editedWidget = null this.editedWidget = null
this.render() this.render()
}// }}} }// }}}
selectCell(el) {// {{{ editCell(el) {// {{{
if (!this.classList.contains('edit')) if (!this.classList.contains('edit'))
return return
@ -215,7 +285,7 @@ export class SmonDashboard extends CustomHTMLElement {// {{{
for (const dp of json.Values) { for (const dp of json.Values) {
const widgets = this.widgets.get(dp.ID) || [] const widgets = this.widgets.get(dp.ID) || []
for (const w of widgets) { for (const w of widgets) {
w.setValue(dp.Value) w.setValue(dp.Value, dp.Valid)
w.render() w.render()
} }
} }
@ -230,26 +300,170 @@ export class SmonDashboard extends CustomHTMLElement {// {{{
for (const id of allDpIDs) { for (const id of allDpIDs) {
const widgets = this.widgets.get(id) || [] const widgets = this.widgets.get(id) || []
for (const w of widgets) { for (const w of widgets) {
w.setValue(null) w.setValue(null, false)
w.render() 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 {// {{{ class SmonWidget extends CustomHTMLElement {// {{{
static create(t, data) {// {{{ static create(t, data) {// {{{
data.Type = t
switch (t) { switch (t) {
case 0: return new SmonWidgetLabel(data) case WIDGET_SPACE: return new SmonWidgetSpace(data)
case 1: return new SmonWidgetOnOff(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) {// {{{ constructor(data) {// {{{
super(true) super(true)
this.data = data this.data = data
this.value = null this.value = null
this.editDialog = null this.editDialog = null
if (!this.data.Attributes)
this.data.Attributes = {}
}// }}} }// }}}
render() {// {{{ render() {// {{{
@ -259,23 +473,131 @@ class SmonWidget extends CustomHTMLElement {// {{{
if (this.data.SpanX > 0) if (this.data.SpanX > 0)
this.style.gridColumn = `${this.data.X} / ${this.data.X + this.data.SpanX + 1}` 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.BorderBottom) {
if (this.data.Attributes.Color) if (this.data.Attributes.Color)
this.style.borderBottom = `1px solid ${this.data.Attributes.Color}` this.style.borderBottom = `1px solid ${this.data.Attributes.Color}`
else else
this.style.borderBottom = `1px solid currentColor` 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) {// {{{ setValue(v, valid) {// {{{
this.value = v this.value = valid ? v : null
}// }}} }// }}}
edit() {// {{{ edit() {// {{{
if (this.editDialog !== null) if (this.editDialog !== null)
return return
const components = this.editWidget() const components = this.commonEditWidgets()
components.push(...this.editWidget())
return components return components
}// }}} }// }}}
commonEditWidgets() {// {{{
this.editDiv = document.createElement('div')
this.editDiv.innerHTML = `
<div style="display: flex; gap: 48px;">
<div>
<div style="font-weight: bold;">Borders</div>
<div style="margin-top: 8px; user-select: none; display: grid; width: min-content;">
<label style="grid-column: 2; grid-row: 1;"><input type="checkbox" name="top"></label>
<label style="grid-column: 3; grid-row: 2;"><input type="checkbox" name="right"></label>
<label style="grid-column: 2; grid-row: 3;"><input type="checkbox" name="bottom"></label>
<label style="grid-column: 1; grid-row: 2;"><input type="checkbox" name="left"></label>
</div>
</div>
<div>
<div style="font-weight: bold;">Spanning</div>
<div style="margin-top: 8px; display: grid; grid-template-columns: min-content 48px; grid-gap: 4px 8px; align-items: center;">
<div>X</div>
<input type="number" name="span-x">
<div>Y</div>
<input type="number" name="span-y">
</div>
</div>
<div>
<div style="font-weight: bold;">Font</div>
<div style="margin-top: 8px; display: grid; grid-template-columns: min-content 96px; grid-gap: 4px 8px; align-items: center;">
<div>Size</div>
<input type="text" name="font-size">
<div>Color</div>
<div style="display: flex; gap: 8px">
<input type="color" name="color">
<input type="checkbox" name="use-color">
</div>
</div>
</div>
</div>
`
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. // Many widgets can have labels.
// This function applies common attributes like size and color. // This function applies common attributes like size and color.
@ -292,6 +614,7 @@ class SmonWidget extends CustomHTMLElement {// {{{
else else
el.style.borderBottom = `1px solid currentColor` el.style.borderBottom = `1px solid currentColor`
}// }}} }// }}}
}// }}} }// }}}
class SmonWidgetEmpty extends SmonWidget {// {{{ class SmonWidgetEmpty extends SmonWidget {// {{{
static {// {{{ static {// {{{
@ -305,29 +628,73 @@ class SmonWidgetEmpty extends SmonWidget {// {{{
</style> </style>
` `
}// }}} }// }}}
constructor(x, y) {// {{{ constructor(data) {// {{{
super({ super(data)
Attributes: {},
X: x,
Y: y,
})
this.classList.add('empty')
this.render() this.render()
}// }}} }// }}}
render() {// {{{ render() {// {{{
super.render() super.render()
}// }}} }// }}}
editWidget() {// {{{ editWidget() {// {{{
const div = document.createElement('div') this.editDiv = document.createElement('div')
div.innerHTML = ` this.editDiv.innerHTML = `
<div>Add widget:</div> <div style="font-weight: bold;">Add widget</div>
<select style="width: 100%; padding: 4px 4px"> <select style="width: 100%; padding: 4px 4px">
<option value="0">Label</option> <option value="0">Space</option>
<option value="1">On / off</option> <option value="1">Label</option>
<option value="2">On / off</option>
</select> </select>
` `
return [div] 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 = `
<div data-el="spacer"></div>
`
}// }}}
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 = `
<div style="margin-top: 16px; font-weight: bold;">Spacing</div>
<div style="margin-top: 8px; display: grid; grid-template-columns: min-content 128px; align-items: center; grid-gap: 4px 8px;">
<div>Width</div>
<input type="text" name="width">
<div>Height</div>
<input type="text" name="height">
</div>
`
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 {// {{{ class SmonWidgetLabel extends SmonWidget {// {{{
@ -337,7 +704,7 @@ class SmonWidgetLabel extends SmonWidget {// {{{
<style> <style>
.el-label { .el-label {
font-weight: bold; font-weight: bold;
margin-bottom: 8px; white-space: nowrap;
} }
</style> </style>
<div data-el="label"></div> <div data-el="label"></div>
@ -349,21 +716,21 @@ class SmonWidgetLabel extends SmonWidget {// {{{
}// }}} }// }}}
render() {// {{{ render() {// {{{
super.render() super.render()
this.elLabel.innerText = this.data.Attributes?.Label || '[set Label]' this.elLabel.innerText = this.data.Attributes?.Label || '[set label]'
this.applyLabelAttributes(this.elLabel) this.applyLabelAttributes(this.elLabel)
}// }}} }// }}}
editWidget() {// {{{ editWidget() {// {{{
this.editDiv = document.createElement('div') this.editDiv = document.createElement('div')
this.editDiv.innerHTML = ` this.editDiv.innerHTML = `
<div>Label:</div> <div style="font-weight: bold">Label</div>
<input type="text"> <input type="text">
` `
this.editDiv.querySelector('input').value = this.data.Attributes.Label this.editDiv.querySelector('input').value = this.data.Attributes.Label || '[set label]'
return [this.editDiv] return [this.editDiv]
}// }}} }// }}}
async editUpdate() {// {{{ async updateWidget() {// {{{
console.log('hum')
this.data.Attributes.Label = this.editDiv.querySelector('input').value this.data.Attributes.Label = this.editDiv.querySelector('input').value
return {}
}// }}} }// }}}
}// }}} }// }}}
class SmonWidgetOnOff extends SmonWidget {// {{{ class SmonWidgetOnOff extends SmonWidget {// {{{
@ -397,41 +764,42 @@ class SmonWidgetOnOff extends SmonWidget {// {{{
let img let img
switch (this.value) { switch (this.value) {
case 1:
case 'ON':
img = 'widget_light_green.svg'
break
case 0: case 0:
case 'OFF': case 'OFF':
img = 'widget_light_red.svg' img = 'widget_light_red.svg'
break break
case 1:
case 'ON':
img = 'widget_light_green.svg'
break
default: default:
img = 'widget_light_off.svg' img = 'widget_light_off.svg'
} }
this.elStatus.setAttribute('src', `/images/${_VERSION}/${img}`) this.elStatus.setAttribute('src', `/images/${_VERSION}/${img}`)
this.elLabel.innerText = this.data.Attributes.Label this.elLabel.innerText = this.data.Attributes.Label || '[set label]'
this.applyLabelAttributes(this.elLabel) this.applyLabelAttributes(this.elLabel)
}// }}} }// }}}
editWidget() {// {{{ editWidget() {// {{{
this.editDiv = document.createElement('div') this.editDiv = document.createElement('div')
this.editDiv.innerHTML = ` this.editDiv.innerHTML = `
<div>Label:</div> <div style="font-weight: bold;">Label</div>
<input type="text"> <input type="text">
` `
this.editDiv.querySelector('input').value = this.data.Attributes.Label this.editDiv.querySelector('input').value = this.data.Attributes.Label || '[set label]'
return [this.editDiv] return [this.editDiv]
}// }}} }// }}}
async editUpdate() {// {{{ async updateWidget() {// {{{
this.data.Attributes.Label = this.editDiv.querySelector('input').value this.data.Attributes.Label = this.editDiv.querySelector('input').value
return {}
}// }}} }// }}}
}// }}} }// }}}
customElements.define("smon-widget", SmonWidget) customElements.define("smon-widget", SmonWidget)
customElements.define("smon-widget-empty", SmonWidgetEmpty) customElements.define("smon-widget-empty", SmonWidgetEmpty)
customElements.define("smon-widget-space", SmonWidgetSpace)
customElements.define("smon-widget-label", SmonWidgetLabel) customElements.define("smon-widget-label", SmonWidgetLabel)
customElements.define("smon-widget-onoff", SmonWidgetOnOff) customElements.define("smon-widget-onoff", SmonWidgetOnOff)
customElements.define("smon-dashboard", SmonDashboard) customElements.define("smon-dashboard", SmonDashboard)