diff --git a/cmd/ui/config/config.go b/cmd/ui/config/config.go
index 1a3a5fd..4d8e8da 100644
--- a/cmd/ui/config/config.go
+++ b/cmd/ui/config/config.go
@@ -10,8 +10,12 @@ import (
)
type Config struct {
- Endpoint string `yaml:"endpoint"`
- WidgetRows []map[string]*widget.Widget `yaml:"widgets"`
+ Endpoint string `yaml:"endpoint"`
+ Widgets map[string]*widget.Widget `yaml:"widgets"`
+
+ Dashboard struct {
+ Columns int `yaml:"columns"`
+ } `yaml:"dashboard"`
}
func Load(path string) (*Config, error) {
@@ -32,13 +36,5 @@ func Load(path string) (*Config, error) {
return nil, fmt.Errorf("parse yaml: %w", err)
}
- for _, row := range cfg.WidgetRows {
- for _, w := range row {
- if valid, err := w.IsValid(); !valid {
- return nil, fmt.Errorf("widget '%s': %w", w.Title, err)
- }
- }
- }
-
return &cfg, nil
}
diff --git a/cmd/ui/front/src/components/Dashboard.vue b/cmd/ui/front/src/components/Dashboard.vue
index 6a9a64e..e3bc6dd 100644
--- a/cmd/ui/front/src/components/Dashboard.vue
+++ b/cmd/ui/front/src/components/Dashboard.vue
@@ -1,8 +1,12 @@
.container
- .tile.is-ancestor(v-for="row in widgets")
- .tile.is-parent(v-for="(widget, path) in row")
- Widget(:widget="widget" :path="path")
+ transition(name="fade")
+ div(v-if="widgets")
+ .tile.is-ancestor(v-for="row in widgets")
+ .tile.is-parent(v-for="(widget, path) in row")
+ Widget(:widget="widget" :path="path")
+ template(v-else)
+ progress.progress.is-small.is-dark.mt-5
+
+
\ No newline at end of file
diff --git a/cmd/ui/front/src/components/Widget.vue b/cmd/ui/front/src/components/Widget.vue
index b8e8167..805feab 100644
--- a/cmd/ui/front/src/components/Widget.vue
+++ b/cmd/ui/front/src/components/Widget.vue
@@ -1,7 +1,8 @@
.tile.is-child.card.is-flex.is-flex-direction-column
header.card-header
- p.card-header-title {{widget.title}}
+ p.card-header-title(:class="{'has-text-danger': failed}") {{widget.title}}
+
.card-header-icon(v-if="widget.description")
.dropdown.is-hoverable.is-right
.dropdown-trigger
@@ -13,11 +14,13 @@
.card-header-icon(v-if="failed")
.dropdown.is-hoverable.is-right
.dropdown-trigger
- span.icon(style="color:red")
+ span.icon.has-text-danger
icon(icon="times")
.dropdown-menu
.dropdown-content
- .dropdown-item There was an error submitting the changes to this value
+ .dropdown-item
+ | There was an error submitting the changes to this value.
+ | Press 'r' to reload.
.card-content.p-1.pt-2(v-if="widget.type == 'group'")
.tile.is-ancestor
diff --git a/cmd/ui/front/src/icons.ts b/cmd/ui/front/src/icons.ts
index 0b64aef..a76984f 100644
--- a/cmd/ui/front/src/icons.ts
+++ b/cmd/ui/front/src/icons.ts
@@ -1,8 +1,8 @@
import { library } from '@fortawesome/fontawesome-svg-core'
-import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'
+import { faInfoCircle, faTimes } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
-library.add(faInfoCircle)
+library.add(faInfoCircle, faTimes)
import { App } from 'vue';
diff --git a/cmd/ui/front/src/utils.ts b/cmd/ui/front/src/utils.ts
new file mode 100644
index 0000000..ffcb927
--- /dev/null
+++ b/cmd/ui/front/src/utils.ts
@@ -0,0 +1,10 @@
+import { onMounted, onUnmounted } from "vue";
+
+export function onWindowKeyDown(callback: (ev: KeyboardEvent) => void) {
+ onMounted(() => {
+ window.addEventListener("keydown", callback)
+ })
+ onUnmounted(() => {
+ window.removeEventListener("keydown", callback)
+ })
+}
\ No newline at end of file
diff --git a/cmd/ui/main.go b/cmd/ui/main.go
index 3895f51..6848465 100644
--- a/cmd/ui/main.go
+++ b/cmd/ui/main.go
@@ -26,29 +26,29 @@ func main() {
app := iris.New()
app.Get("/api/data", func(ctx iris.Context) {
resp := &struct {
- Widgets []map[string]*widget.Widget `json:"widgets"`
- Data map[string]interface{} `json:"data"`
+ Widgets map[string]*widget.Widget `json:"widgets"`
+ Columns int `json:"columns"`
+ Data map[string]interface{} `json:"data"`
}{
- Widgets: cfg.WidgetRows,
+ Widgets: cfg.Widgets,
+ Columns: cfg.Dashboard.Columns,
Data: make(map[string]interface{}),
}
- for _, row := range cfg.WidgetRows {
- for path, w := range row {
- clvalue := hat.Get(client.SplitPath(path)...)
- if err := clvalue.Error(); err != nil {
- //TODO Handle error
- continue
- }
-
- value, err := w.UnmarshalValue(clvalue.Raw())
- if err != nil {
- //TODO Handle error
- continue
- }
+ for path, w := range cfg.Widgets {
+ clvalue := hat.Get(client.SplitPath(path)...)
+ if err := clvalue.Error(); err != nil {
+ //TODO Handle error
+ continue
+ }
- resp.Data[path] = value
+ value, err := w.UnmarshalValue(clvalue.Raw())
+ if err != nil {
+ //TODO Handle error
+ continue
}
+
+ resp.Data[path] = value
}
ctx.JSON(resp)
diff --git a/cmd/ui/widget/widget.go b/cmd/ui/widget/widget.go
index eadb48a..e011c5e 100644
--- a/cmd/ui/widget/widget.go
+++ b/cmd/ui/widget/widget.go
@@ -3,7 +3,10 @@ package widget
import (
"encoding/json"
"errors"
+ "fmt"
"math"
+
+ "gopkg.in/yaml.v3"
)
var (
@@ -29,6 +32,7 @@ type Widget struct {
Title string `json:"title" yaml:"title"`
Type WidgetType `json:"type" yaml:"type"`
Description string `json:"description,omitempty" yaml:"description"`
+ Colspan int `json:"colspan" yaml:"colspan"`
// Text widget
Placeholder string `json:"placeholder,omitempty" yaml:"placeholder"`
@@ -42,43 +46,62 @@ type Widget struct {
StoreIndex bool `json:"-" yaml:"storeIndex"`
}
-func (w *Widget) IsValid() (valid bool, reason error) {
+var _ yaml.Unmarshaler = (*Widget)(nil)
+
+func (w *Widget) UnmarshalYAML(value *yaml.Node) error {
+ type alias Widget
+ widget := &alias{Colspan: 1}
+
+ if err := value.Decode(widget); err != nil {
+ return err
+ }
+
+ *w = Widget(*widget)
+
+ if err := w.isValid(); err != nil {
+ return fmt.Errorf("invalid widget '%s': %w", w.Title, err)
+ }
+
+ return nil
+}
+
+func (w *Widget) isValid() (reason error) {
if w.Title == "" {
- return false, ErrMissingTitle
+ return ErrMissingTitle
}
switch w.Type {
case WidgetOnOff:
if w.Placeholder != "" || w.Big || w.Children != nil || w.Options != nil {
- return false, ErrInvalidOptions
+ return ErrInvalidOptions
}
case WidgetText:
if w.Children != nil || w.Options != nil {
- return false, ErrInvalidOptions
+ return ErrInvalidOptions
}
case WidgetGroup:
if w.Options != nil || w.Placeholder != "" || w.Big {
- return false, ErrInvalidOptions
+ return ErrInvalidOptions
}
if w.Children == nil {
- return false, ErrMissingChildren
+ return ErrMissingChildren
}
case WidgetOptions:
if w.Placeholder != "" || w.Big || w.Children != nil {
- return false, ErrInvalidOptions
+ return ErrInvalidOptions
}
if w.Options == nil {
- return false, ErrMissingOptions
+ return ErrMissingOptions
}
default:
- return false, ErrUnknownType
+ return ErrUnknownType
}
- return true, nil
+ return nil
}
func (w *Widget) UnmarshalValue(str string) (value interface{}, err error) {