Skip to content

Commit

Permalink
Reload widgets
Browse files Browse the repository at this point in the history
  • Loading branch information
pipe01 committed Jan 2, 2021
1 parent cbb2c8e commit 23a0ee8
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 49 deletions.
16 changes: 6 additions & 10 deletions cmd/ui/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
}
67 changes: 60 additions & 7 deletions cmd/ui/front/src/components/Dashboard.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
<template lang="pug">
.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
</template>

<script lang="ts">
import { computed, defineComponent, provide, reactive, ref } from "vue";
import axios from "axios";
import WidgetComponent from "./Widget.vue"
import { Widget } from "../types/widget";
import { onWindowKeyDown } from "../utils";
type WidgetRow = { [path: string]: Widget };
Expand All @@ -21,14 +26,62 @@ export default defineComponent({
const widgets = ref<WidgetRow[]>()
const data = ref<Record<string, any>>()
axios.get("/api/data").then(resp => {
widgets.value = resp.data.widgets;
data.value = reactive(resp.data.data);
function refresh() {
widgets.value = null;
data.value = null;
axios.get<{
widgets: WidgetRow
columns: number
data: any
}>("/api/data").then(resp => {
var rows: WidgetRow[] = []
var currentRow: WidgetRow = {}
var i = 0;
for (const path in resp.data.widgets) {
const widget = resp.data.widgets[path];
if (i++ == resp.data.columns) {
rows.push(currentRow);
currentRow = {};
}
currentRow[path] = widget;
}
rows.push(currentRow);
widgets.value = rows;
data.value = reactive(resp.data.data);
})
}
refresh();
onWindowKeyDown(ev => {
if (ev.key == "F5") {
ev.preventDefault();
refresh();
}
})
provide("data", data)
return { widgets }
}
})
</script>

<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
position: absolute;
width: 100%;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
9 changes: 6 additions & 3 deletions cmd/ui/front/src/components/Widget.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<template lang="pug">
.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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions cmd/ui/front/src/icons.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
10 changes: 10 additions & 0 deletions cmd/ui/front/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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)
})
}
34 changes: 17 additions & 17 deletions cmd/ui/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
43 changes: 33 additions & 10 deletions cmd/ui/widget/widget.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package widget
import (
"encoding/json"
"errors"
"fmt"
"math"

"gopkg.in/yaml.v3"
)

var (
Expand All @@ -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"`
Expand All @@ -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) {
Expand Down

0 comments on commit 23a0ee8

Please sign in to comment.