Initial dashboard
This commit is contained in:
parent
1c697a028b
commit
1af3f59b1a
10 changed files with 380 additions and 3 deletions
20
dashboard.go
Normal file
20
dashboard.go
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
type WidgetType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
WidgetLabel WidgetType = iota
|
||||||
|
WidgetOnOff
|
||||||
|
)
|
||||||
|
|
||||||
|
type Widget struct {
|
||||||
|
Type WidgetType
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
DatapointID int
|
||||||
|
Attributes map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Dashboard struct {
|
||||||
|
Widgets []Widget
|
||||||
|
}
|
||||||
11
main.go
11
main.go
|
|
@ -405,6 +405,17 @@ func pageIndex(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
PAGE: "index",
|
PAGE: "index",
|
||||||
CONFIG: smonConfig.Settings,
|
CONFIG: smonConfig.Settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data := make(map[string]any)
|
||||||
|
data["Dashboard"] = Dashboard{
|
||||||
|
Widgets: []Widget{
|
||||||
|
{Type: 0, X: 1, Y: 1, Attributes: map[string]string{"Label":"Services"}},
|
||||||
|
|
||||||
|
{Type: 1, X: 1, Y: 2, DatapointID: 13, Attributes: map[string]string{"Label":"Borg"}},
|
||||||
|
{Type: 1, X: 1, Y: 3, DatapointID: 13, Attributes: map[string]string{"Label":"Databasus"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
page.Data = data
|
||||||
page.Render(w, r)
|
page.Render(w, r)
|
||||||
} // }}}
|
} // }}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
body {
|
body {
|
||||||
background-image: url(/images/v0/gruvbox/background.svg);
|
/* background-image: url(/images/v0/gruvbox/background.svg); */
|
||||||
}
|
}
|
||||||
#menu {
|
#menu {
|
||||||
box-shadow: 2px 0px 5px 3px rgba(0, 0, 0, 0.25);
|
box-shadow: 2px 0px 5px 3px rgba(0, 0, 0, 0.25);
|
||||||
|
|
|
||||||
49
static/images/widget_light_green.svg
Normal file
49
static/images/widget_light_green.svg
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="31.999998"
|
||||||
|
height="31.999998"
|
||||||
|
viewBox="0 0 8.466666 8.4666661"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
||||||
|
sodipodi:docname="widget_light_on.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#ffffff"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
showborder="false"
|
||||||
|
inkscape:zoom="32"
|
||||||
|
inkscape:cx="18.171875"
|
||||||
|
inkscape:cy="16.25"
|
||||||
|
inkscape:window-width="1916"
|
||||||
|
inkscape:window-height="1161"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<circle
|
||||||
|
style="fill:#55d400;stroke:none;stroke-width:1.00012"
|
||||||
|
id="circle1"
|
||||||
|
cx="4.2333331"
|
||||||
|
cy="4.2333331"
|
||||||
|
r="3.7041667" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
49
static/images/widget_light_off.svg
Normal file
49
static/images/widget_light_off.svg
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="31.999998"
|
||||||
|
height="31.999998"
|
||||||
|
viewBox="0 0 8.466666 8.4666661"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
||||||
|
sodipodi:docname="widget_light_off.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#ffffff"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
showborder="false"
|
||||||
|
inkscape:zoom="32"
|
||||||
|
inkscape:cx="18.171875"
|
||||||
|
inkscape:cy="16.25"
|
||||||
|
inkscape:window-width="1916"
|
||||||
|
inkscape:window-height="1161"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<circle
|
||||||
|
style="fill:#666666;stroke:none;stroke-width:1.00012;fill-opacity:1"
|
||||||
|
id="circle1"
|
||||||
|
cx="4.2333331"
|
||||||
|
cy="4.2333331"
|
||||||
|
r="3.7041667" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
49
static/images/widget_light_red.svg
Normal file
49
static/images/widget_light_red.svg
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="31.999998"
|
||||||
|
height="31.999998"
|
||||||
|
viewBox="0 0 8.466666 8.4666661"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
|
||||||
|
sodipodi:docname="widget_light_off.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#ffffff"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
showborder="false"
|
||||||
|
inkscape:zoom="32"
|
||||||
|
inkscape:cx="18.171875"
|
||||||
|
inkscape:cy="16.25"
|
||||||
|
inkscape:window-width="1916"
|
||||||
|
inkscape:window-height="1161"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<circle
|
||||||
|
style="fill:#d41400;stroke:none;stroke-width:1.00012;fill-opacity:1"
|
||||||
|
id="circle1"
|
||||||
|
cx="4.2333331"
|
||||||
|
cy="4.2333331"
|
||||||
|
r="3.7041667" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
137
static/js/dashboard.mjs
Normal file
137
static/js/dashboard.mjs
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
||||||
|
|
||||||
|
export class SmonDashboard extends CustomHTMLElement {
|
||||||
|
static {
|
||||||
|
this.tmpl = document.createElement('template')
|
||||||
|
this.tmpl.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-auto-columns: min-content;
|
||||||
|
grid-gap: 8px 16px;
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div data-el="grid">
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
constructor() {
|
||||||
|
super(true)
|
||||||
|
const script = this.querySelector('script')
|
||||||
|
this.data = JSON.parse(script.textContent)
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const widgets = []
|
||||||
|
for (const wd of this.data.Widgets) {
|
||||||
|
const widget = SmonWidget.create(wd.Type, wd)
|
||||||
|
widgets.push(widget)
|
||||||
|
}
|
||||||
|
this.elGrid.replaceChildren(...widgets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SmonWidget extends CustomHTMLElement {
|
||||||
|
static create(t, data) {
|
||||||
|
switch (t) {
|
||||||
|
case 0: return new SmonWidgetLabel(data)
|
||||||
|
case 1: return new SmonWidgetOnOff(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(data) {
|
||||||
|
super(true)
|
||||||
|
this.data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.style.gridColumn = this.data.X
|
||||||
|
this.style.gridRow = this.data.Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SmonWidgetLabel extends SmonWidget {
|
||||||
|
static {
|
||||||
|
this.tmpl = document.createElement('template')
|
||||||
|
this.tmpl.innerHTML = `
|
||||||
|
<style>
|
||||||
|
.el-label {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div data-el="label"></div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(data) {
|
||||||
|
super(data)
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
super.render()
|
||||||
|
this.elLabel.innerText = this.data.Attributes?.Label || '[set Label]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SmonWidgetOnOff extends SmonWidget {
|
||||||
|
static {
|
||||||
|
this.tmpl = document.createElement('template')
|
||||||
|
this.tmpl.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content 1fr;
|
||||||
|
align-items: center;
|
||||||
|
grid-gap: 8px;
|
||||||
|
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<img data-el="status">
|
||||||
|
<div data-el="label"></div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(data) {
|
||||||
|
super(data)
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
super.render()
|
||||||
|
|
||||||
|
let img
|
||||||
|
switch (this.data.Attributes.Status) {
|
||||||
|
case 'ON':
|
||||||
|
img = 'widget_light_green.svg'
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'OFF':
|
||||||
|
img = 'widget_light_red.svg'
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
img = 'widget_light_off.svg'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.elStatus.setAttribute('src', `/images/${_VERSION}/${img}`)
|
||||||
|
this.elLabel.innerText = this.data.Attributes.Label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
customElements.define("smon-widget", SmonWidget)
|
||||||
|
customElements.define("smon-widget-label", SmonWidgetLabel)
|
||||||
|
customElements.define("smon-widget-onoff", SmonWidgetOnOff)
|
||||||
|
customElements.define("smon-dashboard", SmonDashboard)
|
||||||
51
static/js/lib/custom_html_element.mjs
Normal file
51
static/js/lib/custom_html_element.mjs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
/* 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('-', ''))
|
||||||
|
}// }}}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,9 @@
|
||||||
{{ template "fonts" }}
|
{{ template "fonts" }}
|
||||||
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/main.css">
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/main.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/{{ .CONFIG.THEME }}.css">
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/{{ .CONFIG.THEME }}.css">
|
||||||
|
<script>
|
||||||
|
globalThis._VERSION = {{ .VERSION }}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="page-error" class="{{ if ne .ERROR "" }}show{{ end }}">
|
<div id="page-error" class="{{ if ne .ERROR "" }}show{{ end }}">
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,21 @@
|
||||||
{{ define "page" }}
|
{{ define "page" }}
|
||||||
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/index.css">
|
<link rel="stylesheet" type="text/css" href="/css/{{ .VERSION }}/{{ .CONFIG.THEME }}/index.css">
|
||||||
|
|
||||||
<div style="float: left;">
|
<script type="module" defer>
|
||||||
|
import { } from '/js/{{ .VERSION }}/dashboard.mjs'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
<img src="/images/{{ .VERSION }}/{{ .CONFIG.THEME }}/logo.svg" style="width: 64px; margin-right: 32px;">
|
<img src="/images/{{ .VERSION }}/{{ .CONFIG.THEME }}/logo.svg" style="width: 64px; margin-right: 32px;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="float: left;">
|
<div style="display: grid; grid-template-columns: min-content 1fr; align-items: end; grid-gap: 8px;">
|
||||||
<h1>SMon</h1>
|
<h1>SMon</h1>
|
||||||
<h2>{{ .VERSION }}</h2>
|
<h2>{{ .VERSION }}</h2>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<smon-dashboard style="margin-top: 32px">
|
||||||
|
<script type="application/json">{{ .Data.Dashboard }}</script>
|
||||||
|
</smon-dashboard>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue