Skip to content

Commit

Permalink
Add edit grid tile dialog.
Browse files Browse the repository at this point in the history
Fix some dashboard grid layout issues.
  • Loading branch information
hspaay committed Aug 16, 2024
1 parent 152dc2b commit 42aac59
Show file tree
Hide file tree
Showing 35 changed files with 631 additions and 221 deletions.
10 changes: 5 additions & 5 deletions bindings/hiveoview/src/service/HiveovService.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ import (
"github.com/hiveot/hub/bindings/hiveoview/src/session"
"github.com/hiveot/hub/bindings/hiveoview/src/views"
"github.com/hiveot/hub/bindings/hiveoview/src/views/app"
"github.com/hiveot/hub/bindings/hiveoview/src/views/comps"
"github.com/hiveot/hub/bindings/hiveoview/src/views/dashboard"
"github.com/hiveot/hub/bindings/hiveoview/src/views/directory"
"github.com/hiveot/hub/bindings/hiveoview/src/views/history"
"github.com/hiveot/hub/bindings/hiveoview/src/views/login"
"github.com/hiveot/hub/bindings/hiveoview/src/views/status"
"github.com/hiveot/hub/bindings/hiveoview/src/views/thing"
"github.com/hiveot/hub/bindings/hiveoview/src/views/tile"
"github.com/hiveot/hub/bindings/hiveoview/src/views/value"
"github.com/hiveot/hub/lib/hubclient"
"github.com/hiveot/hub/lib/tlsserver"
"log/slog"
Expand Down Expand Up @@ -155,13 +154,14 @@ func (svc *HiveovService) createRoutes(router *chi.Mux, rootPath string) http.Ha
r.Get("/tile/{dashboardID}/{tileID}", tile.RenderTile)
r.Get("/tile/{dashboardID}/{tileID}/confirmDelete", tile.RenderConfirmDeleteTile)
r.Get("/tile/{dashboardID}/{tileID}/edit", tile.RenderEditTile)
r.Get("/tile/{dashboardID}/{tileID}/sources", tile.RenderSources)
r.Get("/tile/{dashboardID}/{tileID}/selectSources", tile.RenderSelectSources)
r.Get("/tile/{thingID}/{key}/sourceRow", tile.RenderTileSourceRow)
r.Post("/tile/{dashboardID}/{tileID}", tile.SubmitEditTile)
r.Delete("/tile/{dashboardID}/{tileID}", tile.SubmitDeleteTile)

// value history. Optional query params 'timestamp' and 'duration'
r.Get("/value/{thingID}/{key}/history", value.RenderHistoryPage)
r.Get("/value/{thingID}/{key}/latest", comps.RenderLatestValueRow)
r.Get("/value/{thingID}/{key}/history", history.RenderHistoryPage)
r.Get("/value/{thingID}/{key}/latest", history.RenderLatestValueRow)

// Status components
r.Get("/status", status.RenderStatus)
Expand Down
22 changes: 16 additions & 6 deletions bindings/hiveoview/src/session/ClientSession.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type ClientSession struct {
clientModel *ClientDataModel

// Client view model for generating re-usable data
//viewModel *ClientViewModel
viewModel *ClientViewModel

// ClientID is the login ID of the user
clientID string
Expand Down Expand Up @@ -93,6 +93,16 @@ func (cs *ClientSession) GetClientData() *ClientDataModel {
return cs.clientModel
}

// GetViewModel returns the hiveoview view model of this client
func (cs *ClientSession) GetViewModel() *ClientViewModel {
return cs.viewModel
}

// GetHubClient returns the hub client connection for use in pub/sub
func (cs *ClientSession) GetHubClient() hubclient.IHubClient {
return cs.hc
}

// GetStatus returns the status of hub connection
// This returns:
//
Expand All @@ -106,11 +116,6 @@ func (cs *ClientSession) GetStatus() hubclient.TransportStatus {
return status
}

// GetHubClient returns the hub client connection for use in pub/sub
func (cs *ClientSession) GetHubClient() hubclient.IHubClient {
return cs.hc
}

// IsActive returns whether the session has a connection to the Hub or is in the process of connecting.
func (cs *ClientSession) IsActive() bool {
status := cs.hc.GetStatus()
Expand Down Expand Up @@ -283,6 +288,10 @@ func (cs *ClientSession) SaveState() error {
// cs.clientModelChanged = true
//}

// SendNotify sends a 'notify' event for showing in a toast popup.
// To send an SSE event use SendSSE()
//
// ntype is the toast notification type: "info", "error", "warning"
func (cs *ClientSession) SendNotify(ntype NotifyType, text string) {
cs.mux.RLock()
defer cs.mux.RUnlock()
Expand Down Expand Up @@ -351,6 +360,7 @@ func NewClientSession(sessionID string, hc hubclient.IHubClient, remoteAddr stri
sseClients: make([]chan SSEEvent, 0),
lastActivity: time.Now(),
clientModel: NewClientDataModel(),
viewModel: NewClientViewModel(hc),
}
hc.SetMessageHandler(cs.onMessage)
hc.SetConnectHandler(cs.onConnectChange)
Expand Down
105 changes: 82 additions & 23 deletions bindings/hiveoview/src/session/ClientViewModel.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package session

import (
"encoding/json"
"fmt"
"github.com/hiveot/hub/api/go/digitwin"
"github.com/hiveot/hub/api/go/vocab"
"github.com/hiveot/hub/lib/hubclient"
"github.com/hiveot/hub/lib/things"
"github.com/hiveot/hub/services/history/historyclient"
"sort"
"time"
)

// ReadDirLimit is the maximum amount of TDs to read in one call
Expand All @@ -17,16 +21,70 @@ type AgentThings struct {
Things []*things.TD
}

// ClientViewModel generates view model data for rendering user interfaces.
// Some short time minimal caching may take place for performance optimization.
//type ClientViewModel struct {
// //
// mux sync.RWMutex
//}
// ClientViewModel for querying and transforming server data for presentation
type ClientViewModel struct {
hc hubclient.IHubClient
}

// ReadHistory returns historical values of a thing key
func (v *ClientViewModel) ReadHistory(
thingID string, key string, timestamp time.Time, duration int, limit int) (
[]*things.ThingMessage, bool, error) {

hist := historyclient.NewReadHistoryClient(v.hc)
values, itemsRemaining, err := hist.ReadHistory(
thingID, key, timestamp, duration, limit)
return values, itemsRemaining, err
}

// GetLatest returns a map with the latest property values of a thing or nil if failed
// TODO: The generated API doesnt know return types because WoT TD has no
// place to define them. Find a better solution.
func (v *ClientViewModel) GetLatest(thingID string) (things.ThingMessageMap, error) {
valuesMap := things.NewThingMessageMap()
tvsJson, err := digitwin.OutboxReadLatest(v.hc, nil, "", "", thingID)
if err != nil {
return valuesMap, err
}
tvs, _ := things.NewThingMessageMapFromSource(tvsJson)
for _, tv := range tvs {
valuesMap.Set(tv.Key, tv)
}
return valuesMap, nil
}

// GetTD is a simple helper to retrieve a TD.
// This can re-use a cached version if this model supports caching.
func (v *ClientViewModel) GetTD(thingID string) (*things.TD, error) {
td := &things.TD{}
tdJson, err := digitwin.DirectoryReadTD(v.hc, thingID)
if err == nil {
err = json.Unmarshal([]byte(tdJson), &td)
}
return td, err
}

// GetValue returns the latest thing message value of an thing event or property
func (v *ClientViewModel) GetValue(thingID string, key string) (*things.ThingMessage, error) {

// TODO: cache this to avoid multiple reruns
tmmapJson, err := digitwin.OutboxReadLatest(
v.hc, []string{key}, vocab.MessageTypeEvent, "", thingID)
tmmap, _ := things.NewThingMessageMapFromSource(tmmapJson)
if err != nil {
return nil, err
}
value, found := tmmap[key]
if !found {
return nil, fmt.Errorf("key '%s' not found in thing '%s'", key, thingID)
}
return value, nil
}

// GroupByAgent groups Things by agent and sorts them by Thing title
func GroupByAgent(tds map[string]*things.TD) map[string]*AgentThings {
func (v *ClientViewModel) GroupByAgent(tds map[string]*things.TD) []*AgentThings {
agentMap := make(map[string]*AgentThings)
// first split the things by their agent
for thingID, td := range tds {
agentID, _ := things.SplitDigiTwinThingID(thingID)
agentGroup, found := agentMap[agentID]
Expand All @@ -39,20 +97,26 @@ func GroupByAgent(tds map[string]*things.TD) map[string]*AgentThings {
}
agentGroup.Things = append(agentGroup.Things, td)
}
// next, sort the agent things
agentsList := make([]*AgentThings, 0, len(agentMap))
for _, grp := range agentMap {
SortThingsByTitle(grp.Things)
agentsList = append(agentsList, grp)
v.SortThingsByTitle(grp.Things)
}
return agentMap
// last sort the agents
sort.Slice(agentsList, func(i, j int) bool {
return agentsList[i].AgentID < agentsList[j].AgentID
})
return agentsList
}

// ReadDirectory loads and decodes Things from the directory.
// This currently limits the nr of things to ReadDirLimit.
// hc is the connection to use.
func ReadDirectory(hc hubclient.IHubClient) (map[string]*things.TD, error) {
func (v *ClientViewModel) ReadDirectory() (map[string]*things.TD, error) {
newThings := make(map[string]*things.TD)

// TODO: support for paging
thingsList, err := digitwin.DirectoryReadTDs(hc, ReadDirLimit, 0)
thingsList, err := digitwin.DirectoryReadTDs(v.hc, ReadDirLimit, 0)
if err != nil {
return newThings, err
}
Expand All @@ -66,21 +130,16 @@ func ReadDirectory(hc hubclient.IHubClient) (map[string]*things.TD, error) {
return newThings, nil
}

// ReadTD is a simple helper to read and unmarshal a TD
func ReadTD(hc hubclient.IHubClient, thingID string) (*things.TD, error) {
td := &things.TD{}
tdJson, err := digitwin.DirectoryReadTD(hc, thingID)
if err == nil {
err = json.Unmarshal([]byte(tdJson), &td)
}
return td, err
}

// SortThingsByTitle as the name suggests sorts the things in the given slice
func SortThingsByTitle(tds []*things.TD) {
func (v *ClientViewModel) SortThingsByTitle(tds []*things.TD) {
sort.Slice(tds, func(i, j int) bool {
tdI := tds[i]
tdJ := tds[j]
return tdI.Title < tdJ.Title
})
}

func NewClientViewModel(hc hubclient.IHubClient) *ClientViewModel {
v := &ClientViewModel{hc: hc}
return v
}
4 changes: 3 additions & 1 deletion bindings/hiveoview/src/static/gridstack-all.10.3.1.min.js

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bindings/hiveoview/src/views/base.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

{{/* dashboard */}}
<link rel="stylesheet" href="/static/gridstack-10.3.1.min.css"/>
<link rel="stylesheet" href="/static/gridstack-extra.10.3.1.min.css"/>
<script src="/static/gridstack-all.10.3.1.min.js"></script>

<!-- download htmx sse extension from: https://extensions.htmx.org/ -->
Expand Down
2 changes: 2 additions & 0 deletions bindings/hiveoview/src/views/dashboard/RenderDashboardPage.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type DashboardPageTemplateData struct {
SubmitDashboardLayoutPath string
RenderNewTilePath string
RenderConfirmDeleteTilePath string
DashboardUpdatedEvent string
}

// GetTileTemplateData returns empty rendering data for rendering a tile.
Expand Down Expand Up @@ -57,6 +58,7 @@ func RenderDashboardPage(w http.ResponseWriter, r *http.Request) {
// tile paths
data.RenderNewTilePath = getDashboardPath(RenderNewTilePath, cdc)
data.RenderConfirmDeleteTilePath = getDashboardPath(RenderConfirmDeleteTilePath, cdc)
data.DashboardUpdatedEvent = getDashboardPath(tile.DashboardUpdatedEvent, cdc)

// full render or fragment render
buff, err := app.RenderAppOrFragment(r, RenderDashboardTemplate, data)
Expand Down
58 changes: 34 additions & 24 deletions bindings/hiveoview/src/views/dashboard/RenderDashboardPage.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@
{{else}}


<main class="dashboard container-fluid" >

{{/*Show the dashboard. Reload when tiles are deleted*/}}
<main class="dashboard container-fluid"
hx-get=""
hx-trigger="sse:{{.DashboardUpdatedEvent}}"
hx-swap="outerHTML"
>
<div>
Dashboard: {{.Dashboard.Title}} :
<button hx-trigger="click"
Expand All @@ -31,22 +35,25 @@
>Delete Dashboard</button>
</div>

{{/* change event: When layout changes push the new layout to the server*/}}
<div class="grid-stack"
hx-trigger="change"
hx-post="{{.SubmitDashboardLayoutPath}}"
hx-vals='js:{layout: saveLayout()}'
hx-swap="none"
>
{{range $k,$v := .Dashboard.Tiles }}
<div class="grid-stack-item" gs-id="{{$k}}"
gs-size-to-content="false">
<div class="grid-stack-item-content" style="overflow:unset">
{{template "RenderTile.gohtml" $.GetTileTemplateData $k}}
{{/* Add a div to ensure filling the screen size*/}}
<div style="flex-grow:1">
{{/* change event: When layout changes push the new layout to the server*/}}
<div class="grid-stack"
hx-trigger="change"
hx-post="{{.SubmitDashboardLayoutPath}}"
hx-vals='js:{layout: saveLayout()}'
hx-swap="none"
>
{{range $k,$v := .Dashboard.Tiles }}
<div class="grid-stack-item" gs-id="{{$k}}" gs-min-h="4"
gs-size-to-content="false">
<div class="grid-stack-item-content" style="overflow:unset">
{{template "RenderTile.gohtml" $.GetTileTemplateData $k}}
</div>
</div>
</div>
{{end}}
{{end}}

</div>
</div>
</main>
<div id="dashboardDialog"></div>
Expand All @@ -59,10 +66,16 @@
function startGridStack() {
// debugger;
var options = { // put in gridstack options here
float: false,
// handle: ".tile-header" // only allow dragging by the header
cellHeight: 'initial', // 0, auto
staticGrid:false, // allow resize in edit mode
animate:true,
float: true, // allow cells to move anywhere
cellHeight: 30, // fix the row height to avoid scaling content
staticGrid:false, // allow resize in edit mode
minRow: 3,
columnOpts: {
layout: "moveResize",
breakpointForWindow:false,
breakpoints: [{w: 300, c: 1}, {w: 600, c: 4}, {w: 800, c: 8}, {w:1200, c:12}]
}
};
grid = GridStack.init(options)//.load(serializedData);

Expand Down Expand Up @@ -97,10 +110,7 @@
flex-direction: column;
}

.grid-stack {
/*use the full available space*/
flex-grow:1;
}

.grid-stack-item {
}
.grid-stack-item-content {
Expand Down
9 changes: 5 additions & 4 deletions bindings/hiveoview/src/views/directory/RenderDirectory.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const DirectoryTemplate = "RenderDirectory.gohtml"
//}

type DirectoryTemplateData struct {
Groups map[string]*session.AgentThings
Groups []*session.AgentThings
//PageNr int
}

Expand All @@ -33,20 +33,21 @@ func RenderDirectory(w http.ResponseWriter, r *http.Request) {
var buff *bytes.Buffer

// 1: get session
sess, hc, err := session.GetSessionFromContext(r)
sess, _, err := session.GetSessionFromContext(r)
if err != nil {
sess.WriteError(w, err, 0)
return
}
v := sess.GetViewModel()

tdMap, err := session.ReadDirectory(hc)
tdMap, err := v.ReadDirectory()
if err != nil {
err = fmt.Errorf("unable to load directory: %w", err)
slog.Error(err.Error())
sess.SendNotify(session.NotifyError, err.Error())
}

agentGroups := session.GroupByAgent(tdMap)
agentGroups := v.GroupByAgent(tdMap)
data := DirectoryTemplateData{}
data.Groups = agentGroups

Expand Down
Loading

0 comments on commit 42aac59

Please sign in to comment.