diff --git a/dashboard.go b/dashboard.go index fcb24f3..dfb29a9 100644 --- a/dashboard.go +++ b/dashboard.go @@ -7,12 +7,14 @@ import ( // Standard "context" + "encoding/json" ) type WidgetType int const ( - WIDGET_LABEL WidgetType = iota + WIDGET_SPACE WidgetType = iota + WIDGET_LABEL WIDGET_ONOFF ) @@ -48,3 +50,20 @@ func DashboardGet(name string) (dashboard Dashboard, err error) { 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 1471db9..f540db8 100644 --- a/datapoint.go +++ b/datapoint.go @@ -28,6 +28,7 @@ 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"` @@ -283,7 +284,7 @@ func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{ } 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 { err = werr.Wrap(err) return @@ -313,7 +314,7 @@ func DatapointRetrieve(id int, name string) (dp Datapoint, err error) { // {{{ `, dp.ID, ) - dp, err = pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Datapoint]) + dp.LastDatapointValue, err = pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[DatapointValue]) if err == sql.ErrNoRows { err = nil return diff --git a/main.go b/main.go index e57d907..0cb8574 100644 --- a/main.go +++ b/main.go @@ -160,7 +160,8 @@ func main() { // {{{ http.HandleFunc("/configuration/notification/delete/{prio}", actionConfigurationNotificationDelete) http.HandleFunc("/entry/{datapoint}", actionEntryDatapoint) - http.HandleFunc("/dashboard/values", actionDashboardValues) + http.HandleFunc("GET /dashboard/values", actionDashboardValues) + http.HandleFunc("POST /dashboard/update", actionDashboardUpdate) go nodataLoop() @@ -408,7 +409,6 @@ func pageIndex(w http.ResponseWriter, r *http.Request) { // {{{ CONFIG: smonConfig.Settings, } - dashboard, err := DashboardGet("start") if err != nil { logger.Error("dashboard", "error", err) @@ -436,6 +436,24 @@ func actionDashboardValues(w http.ResponseWriter, r *http.Request) { // {{{ }) 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") diff --git a/problem.go b/problem.go index 1d4390d..d5513dd 100644 --- a/problem.go +++ b/problem.go @@ -3,11 +3,13 @@ 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" @@ -152,7 +154,7 @@ func ProblemClose(trigger Trigger) (problemID int, err error) { // {{{ ) err = row.Scan(&problemID) - if err == sql.ErrNoRows { + if errors.Is(err, pgx.ErrNoRows) { err = nil return } diff --git a/static/images/grid_management.svg b/static/images/grid_management.svg new file mode 100644 index 0000000..be8a424 --- /dev/null +++ b/static/images/grid_management.svg @@ -0,0 +1,1123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/js/dashboard.mjs b/static/js/dashboard.mjs index a8c8f5f..cd00746 100644 --- a/static/js/dashboard.mjs +++ b/static/js/dashboard.mjs @@ -1,5 +1,9 @@ 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') @@ -13,7 +17,6 @@ export class SmonDashboard extends CustomHTMLElement {// {{{ :host(.edit) { .el-grid { - grid-gap: unset; border-top: var(--edit-border); border-left: var(--edit-border); @@ -24,12 +27,17 @@ export class SmonDashboard extends CustomHTMLElement {// {{{ } & > *:hover { - background-color: rgba(255, 255, 255, 0.1); + background-color: rgba(255, 255, 255, 0.1) !important; cursor: pointer; } - & > .empty { + smon-widget-space { + background: rgba(128, 255, 128, 0.10); + } + + & > smon-widget-empty { display: block; + background: unset; } } @@ -59,12 +67,15 @@ export class SmonDashboard extends CustomHTMLElement {// {{{ display: grid; grid-auto-flow: column; grid-auto-columns: min-content; - grid-gap: 8px 16px; width: min-content; - - .empty { + + smon-widget-empty { display: none; } + + & > * { + padding: 4px 0px; + } } .only-on-edit { @@ -79,6 +90,10 @@ export class SmonDashboard extends CustomHTMLElement {// {{{ } dialog { + svg { + cursor: pointer; + } + input[type="text"] { padding: 4px; margin-top: 4px; @@ -107,11 +122,34 @@ export class SmonDashboard extends CustomHTMLElement {// {{{ +
Cell operations
+
+
+ + + + +
+ +
+ + + + +
+ +
+ + + +
+ +
-
+
` }// }}} @@ -119,18 +157,32 @@ export class SmonDashboard extends CustomHTMLElement {// {{{ super(true) const script = this.querySelector('script') 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.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.saveDashboard()) + 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) @@ -139,10 +191,10 @@ export class SmonDashboard extends CustomHTMLElement {// {{{ render() {// {{{ const widgets = [] this.widgets = new Map() + this.occupiedCells = new Map() let maxX = 0 let maxY = 0 - let occupiedCells = new Map() for (const wd of this.data.Widgets) { if (!this.widgets.has(wd.DatapointID)) @@ -150,17 +202,18 @@ export class SmonDashboard extends CustomHTMLElement {// {{{ // 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.selectCell(widget)) + widget.addEventListener('click', () => this.editCell(widget)) widgetRefs.push(widget) widgets.push(widget) maxX = Math.max(maxX, wd.X) maxY = Math.max(maxY, wd.Y) - occupiedCells.set(`${wd.X}x${wd.Y}`, true) - for (let x = 0; x <= wd.SpanX; x++) { - occupiedCells.set(`${wd.X + x}x${wd.Y}`, true) - } + 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 @@ -170,14 +223,16 @@ export class SmonDashboard extends CustomHTMLElement {// {{{ // 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 (occupiedCells.has(`${x}x${y}`)) + if (this.occupiedCells.has(`${x}x${y}`)) continue - const empty = new SmonWidgetEmpty(x, y) - empty.addEventListener('click', () => this.selectCell(empty)) + 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) {// {{{ @@ -187,15 +242,30 @@ export class SmonDashboard extends CustomHTMLElement {// {{{ else this.elError.classList.add('show') }// }}} - saveDashboard() {// {{{ - }// }}} 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.editedWidget = null this.render() }// }}} - selectCell(el) {// {{{ + editCell(el) {// {{{ if (!this.classList.contains('edit')) return @@ -215,7 +285,7 @@ export class SmonDashboard extends CustomHTMLElement {// {{{ for (const dp of json.Values) { const widgets = this.widgets.get(dp.ID) || [] for (const w of widgets) { - w.setValue(dp.Value) + w.setValue(dp.Value, dp.Valid) w.render() } } @@ -230,26 +300,170 @@ export class SmonDashboard extends CustomHTMLElement {// {{{ for (const id of allDpIDs) { const widgets = this.widgets.get(id) || [] for (const w of widgets) { - w.setValue(null) + 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 0: return new SmonWidgetLabel(data) - case 1: return new SmonWidgetOnOff(data) + 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() {// {{{ @@ -259,23 +473,131 @@ class SmonWidget extends CustomHTMLElement {// {{{ 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) {// {{{ - this.value = v + setValue(v, valid) {// {{{ + this.value = valid ? v : null }// }}} edit() {// {{{ if (this.editDialog !== null) return - const components = this.editWidget() + const components = this.commonEditWidgets() + components.push(...this.editWidget()) return components }// }}} + commonEditWidgets() {// {{{ + this.editDiv = document.createElement('div') + this.editDiv.innerHTML = ` +
+
+
Borders
+
+ + + + +
+
+ +
+
Spanning
+
+
X
+ + +
Y
+ +
+
+ +
+
Font
+
+
Size
+ + +
Color
+
+ + +
+
+
+
+ ` + 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. @@ -292,6 +614,7 @@ class SmonWidget extends CustomHTMLElement {// {{{ else el.style.borderBottom = `1px solid currentColor` }// }}} + }// }}} class SmonWidgetEmpty extends SmonWidget {// {{{ static {// {{{ @@ -305,29 +628,73 @@ class SmonWidgetEmpty extends SmonWidget {// {{{ ` }// }}} - constructor(x, y) {// {{{ - super({ - Attributes: {}, - X: x, - Y: y, - }) - - this.classList.add('empty') + constructor(data) {// {{{ + super(data) this.render() }// }}} render() {// {{{ super.render() }// }}} editWidget() {// {{{ - const div = document.createElement('div') - div.innerHTML = ` -
Add widget:
+ this.editDiv = document.createElement('div') + this.editDiv.innerHTML = ` +
Add widget
` - 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 = ` +
+ ` + }// }}} + 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
+
+
Width
+ + +
Height
+ +
+ ` + + 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 {// {{{ @@ -337,7 +704,7 @@ class SmonWidgetLabel extends SmonWidget {// {{{
@@ -349,21 +716,21 @@ class SmonWidgetLabel extends SmonWidget {// {{{ }// }}} 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) }// }}} editWidget() {// {{{ this.editDiv = document.createElement('div') this.editDiv.innerHTML = ` -
Label:
+
Label
` - this.editDiv.querySelector('input').value = this.data.Attributes.Label + this.editDiv.querySelector('input').value = this.data.Attributes.Label || '[set label]' return [this.editDiv] }// }}} - async editUpdate() {// {{{ - console.log('hum') + async updateWidget() {// {{{ this.data.Attributes.Label = this.editDiv.querySelector('input').value + return {} }// }}} }// }}} class SmonWidgetOnOff extends SmonWidget {// {{{ @@ -397,41 +764,42 @@ class SmonWidgetOnOff extends SmonWidget {// {{{ let img switch (this.value) { - case 1: - case 'ON': - img = 'widget_light_green.svg' - break - 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 + this.elLabel.innerText = this.data.Attributes.Label || '[set label]' this.applyLabelAttributes(this.elLabel) }// }}} editWidget() {// {{{ this.editDiv = document.createElement('div') this.editDiv.innerHTML = ` -
Label:
+
Label
` - this.editDiv.querySelector('input').value = this.data.Attributes.Label + this.editDiv.querySelector('input').value = this.data.Attributes.Label || '[set label]' return [this.editDiv] }// }}} - async editUpdate() {// {{{ + 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)