fix: Empty dashboards are hidden (#599)

This commit is contained in:
James Read 2025-06-06 16:11:02 +01:00 committed by GitHub
parent b2e7509959
commit 1357eae9b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 188 additions and 37 deletions

View File

@ -0,0 +1,25 @@
#
# Integration Test Config: emptyDashboardsAreHidden
#
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "DEBUG"
checkForUpdates: false
actions:
- title: Ping {{ server.hostname }}
shell: ping {{ server.hostname }}
icon: ping
entity: server
entities:
- file: entities/servers.yaml
name: server
dashboards:
- title: Empty Dashboard
contents:
- title: Ping {{ server.hostname }}

View File

@ -0,0 +1,42 @@
import { describe, it, before, after } from 'mocha'
import { expect } from 'chai'
import { By, until, Condition } from 'selenium-webdriver'
//import * as waitOn from 'wait-on'
import {
getRootAndWait,
takeScreenshotOnFailure,
} from '../lib/elements.js'
describe('config: empty dashboards are hidden', function () {
before(async function () {
await runner.start('emptyDashboardsAreHidden')
})
after(async () => {
await runner.stop()
})
afterEach(function () {
takeScreenshotOnFailure(this.currentTest, webdriver);
});
it('Test hidden dashboard', async function () {
await getRootAndWait()
const title = await webdriver.getTitle()
expect(title).to.be.equal("OliveTin")
await webdriver.findElement(By.id('sidebar-toggler-button')).click()
const navigationLinks = await webdriver.findElements(By.css('#navigation-links a'))
console.log('navigationLinks', navigationLinks)
expect(navigationLinks).to.not.be.empty
expect(navigationLinks.length).to.be.equal(1, 'Expected the nav to only have 1 link')
const firstLinkId = await navigationLinks[0].getAttribute('id')
expect(firstLinkId).to.be.equal('showActions', 'Expected the first link to be the actions link')
})
})

View File

@ -325,8 +325,6 @@ func (api *oliveTinAPI) GetDashboardComponents(ctx ctx.Context, req *apiv1.GetDa
log.Tracef("GetDashboardComponents: %v", res) log.Tracef("GetDashboardComponents: %v", res)
dashboardCfgToPb(res, cfg.Dashboards, cfg)
return res, nil return res, nil
} }

View File

@ -10,6 +10,12 @@ import (
"sort" "sort"
) )
type DashboardRenderRequest struct {
AuthenticatedUser *acl.AuthenticatedUser
AllowedActionTitles []string `json:"allows_action_titles"`
cfg *config.Config
}
func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl.AuthenticatedUser) *apiv1.GetDashboardComponentsResponse { func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl.AuthenticatedUser) *apiv1.GetDashboardComponentsResponse {
res := &apiv1.GetDashboardComponentsResponse{ res := &apiv1.GetDashboardComponentsResponse{
AuthenticatedUser: user.Username, AuthenticatedUser: user.Username,
@ -36,12 +42,29 @@ func buildDashboardResponse(ex *executor.Executor, cfg *config.Config, user *acl
} }
}) })
rr := &DashboardRenderRequest{
AuthenticatedUser: user,
AllowedActionTitles: getActionTitles(res.Actions),
cfg: cfg,
}
res.EffectivePolicy = buildEffectivePolicy(user.EffectivePolicy) res.EffectivePolicy = buildEffectivePolicy(user.EffectivePolicy)
res.Diagnostics = buildDiagnostics(res.EffectivePolicy.ShowDiagnostics) res.Diagnostics = buildDiagnostics(res.EffectivePolicy.ShowDiagnostics)
res.Dashboards = dashboardCfgToPb(rr)
return res return res
} }
func getActionTitles(actions []*apiv1.Action) []string {
titles := make([]string, 0, len(actions))
for _, action := range actions {
titles = append(titles, action.Title)
}
return titles
}
func buildEffectivePolicy(policy *config.ConfigurationPolicy) *apiv1.EffectivePolicy { func buildEffectivePolicy(policy *config.ConfigurationPolicy) *apiv1.EffectivePolicy {
ret := &apiv1.EffectivePolicy{ ret := &apiv1.EffectivePolicy{
ShowDiagnostics: policy.ShowDiagnostics, ShowDiagnostics: policy.ShowDiagnostics,

View File

@ -3,42 +3,78 @@ package grpcapi
import ( import (
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1" apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
config "github.com/OliveTin/OliveTin/internal/config" config "github.com/OliveTin/OliveTin/internal/config"
log "github.com/sirupsen/logrus"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
func dashboardCfgToPb(res *apiv1.GetDashboardComponentsResponse, dashboards []*config.DashboardComponent, cfg *config.Config) { func dashboardCfgToPb(rr *DashboardRenderRequest) []*apiv1.DashboardComponent {
for _, dashboard := range dashboards { ret := make([]*apiv1.DashboardComponent, 0)
res.Dashboards = append(res.Dashboards, &apiv1.DashboardComponent{
for _, dashboard := range cfg.Dashboards {
pbdb := &apiv1.DashboardComponent{
Type: "dashboard", Type: "dashboard",
Title: dashboard.Title, Title: dashboard.Title,
Contents: getDashboardComponentContents(dashboard, cfg), Contents: removeNulls(getDashboardComponentContents(dashboard, rr)),
}) }
if len(pbdb.Contents) == 0 {
log.WithFields(log.Fields{
"dashboard": dashboard.Title,
"username": rr.AuthenticatedUser.Username,
}).Debugf("Dashboard has no readable contents, so it will not be visible in the web ui")
continue
}
ret = append(ret, pbdb)
} }
return ret
} }
func getDashboardComponentContents(dashboard *config.DashboardComponent, cfg *config.Config) []*apiv1.DashboardComponent { func removeNulls(components []*apiv1.DashboardComponent) []*apiv1.DashboardComponent {
ret := make([]*apiv1.DashboardComponent, 0)
for _, component := range components {
if component == nil {
continue
}
ret = append(ret, component)
}
return ret
}
func getDashboardComponentContents(dashboard *config.DashboardComponent, rr *DashboardRenderRequest) []*apiv1.DashboardComponent {
ret := make([]*apiv1.DashboardComponent, 0) ret := make([]*apiv1.DashboardComponent, 0)
for _, subitem := range dashboard.Contents { for _, subitem := range dashboard.Contents {
if subitem.Type == "fieldset" && subitem.Entity != "" { if subitem.Type == "fieldset" && subitem.Entity != "" {
ret = append(ret, buildEntityFieldsets(subitem.Entity, &subitem)...) ret = append(ret, buildEntityFieldsets(subitem.Entity, &subitem, rr)...)
continue } else {
ret = append(ret, buildDashboardComponentSimple(&subitem, rr))
} }
newitem := &apiv1.DashboardComponent{
Title: subitem.Title,
Type: getDashboardComponentType(&subitem),
Contents: getDashboardComponentContents(&subitem, cfg),
Icon: getDashboardComponentIcon(&subitem, cfg),
CssClass: subitem.CssClass,
}
ret = append(ret, newitem)
} }
return ret return ret
} }
func buildDashboardComponentSimple(subitem *config.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
if !slices.Contains(rr.AllowedActionTitles, subitem.Title) {
return nil
}
newitem := &apiv1.DashboardComponent{
Title: subitem.Title,
Type: getDashboardComponentType(subitem),
Contents: getDashboardComponentContents(subitem, rr),
Icon: getDashboardComponentIcon(subitem, rr.cfg),
CssClass: subitem.CssClass,
}
return newitem
}
func getDashboardComponentIcon(item *config.DashboardComponent, cfg *config.Config) string { func getDashboardComponentIcon(item *config.DashboardComponent, cfg *config.Config) string {
if item.Icon == "" { if item.Icon == "" {
return cfg.DefaultIconForDirectories return cfg.DefaultIconForDirectories

View File

@ -4,48 +4,75 @@ import (
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1" apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
config "github.com/OliveTin/OliveTin/internal/config" config "github.com/OliveTin/OliveTin/internal/config"
sv "github.com/OliveTin/OliveTin/internal/stringvariables" sv "github.com/OliveTin/OliveTin/internal/stringvariables"
"golang.org/x/exp/slices"
) )
func buildEntityFieldsets(entityTitle string, tpl *config.DashboardComponent) []*apiv1.DashboardComponent { func buildEntityFieldsets(entityTitle string, tpl *config.DashboardComponent, rr *DashboardRenderRequest) []*apiv1.DashboardComponent {
ret := make([]*apiv1.DashboardComponent, 0) ret := make([]*apiv1.DashboardComponent, 0)
entityCount := sv.GetEntityCount(entityTitle) entityCount := sv.GetEntityCount(entityTitle)
for i := 0; i < entityCount; i++ { for i := range entityCount {
ret = append(ret, buildEntityFieldset(tpl, entityTitle, i)) fs := buildEntityFieldset(tpl, entityTitle, i, rr)
if len(fs.Contents) > 0 {
ret = append(ret, fs)
}
} }
return ret return ret
} }
func buildEntityFieldset(tpl *config.DashboardComponent, entityTitle string, entityIndex int) *apiv1.DashboardComponent { func buildEntityFieldset(tpl *config.DashboardComponent, entityTitle string, entityIndex int, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
prefix := sv.GetEntityPrefix(entityTitle, entityIndex) prefix := sv.GetEntityPrefix(entityTitle, entityIndex)
return &apiv1.DashboardComponent{ return &apiv1.DashboardComponent{
Title: sv.ReplaceEntityVars(prefix, tpl.Title), Title: sv.ReplaceEntityVars(prefix, tpl.Title),
Type: "fieldset", Type: "fieldset",
Contents: buildEntityFieldsetContents(tpl.Contents, prefix), Contents: removeFieldsetIfHasNoLinks(buildEntityFieldsetContents(tpl.Contents, prefix, rr)),
CssClass: sv.ReplaceEntityVars(prefix, tpl.CssClass), CssClass: sv.ReplaceEntityVars(prefix, tpl.CssClass),
} }
} }
func buildEntityFieldsetContents(contents []config.DashboardComponent, prefix string) []*apiv1.DashboardComponent { func removeFieldsetIfHasNoLinks(contents []*apiv1.DashboardComponent) []*apiv1.DashboardComponent {
for _, subitem := range contents {
if subitem.Type == "link" {
return contents
}
}
return nil
}
func buildEntityFieldsetContents(contents []config.DashboardComponent, prefix string, rr *DashboardRenderRequest) []*apiv1.DashboardComponent {
ret := make([]*apiv1.DashboardComponent, 0) ret := make([]*apiv1.DashboardComponent, 0)
for _, subitem := range contents { for _, subitem := range contents {
clone := &apiv1.DashboardComponent{} c := cloneItem(&subitem, prefix, rr)
clone.CssClass = sv.ReplaceEntityVars(prefix, subitem.CssClass)
if subitem.Type == "" || subitem.Type == "link" { if c != nil {
clone.Type = "link" ret = append(ret, c)
clone.Title = sv.ReplaceEntityVars(prefix, subitem.Title)
} else {
clone.Title = sv.ReplaceEntityVars(prefix, subitem.Title)
clone.Type = subitem.Type
} }
ret = append(ret, clone)
} }
return ret return ret
} }
func cloneItem(subitem *config.DashboardComponent, prefix string, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
clone := &apiv1.DashboardComponent{}
clone.CssClass = sv.ReplaceEntityVars(prefix, subitem.CssClass)
if subitem.Type == "" || subitem.Type == "link" {
clone.Type = "link"
clone.Title = sv.ReplaceEntityVars(prefix, subitem.Title)
if !slices.Contains(rr.AllowedActionTitles, clone.Title) {
return nil
}
} else {
clone.Title = sv.ReplaceEntityVars(prefix, subitem.Title)
clone.Type = subitem.Type
}
return clone
}

View File

@ -27,7 +27,7 @@
<h1 id = "page-title">OliveTin</h1> <h1 id = "page-title">OliveTin</h1>
<nav hidden> <nav id = "mainnav" hidden>
<ul id = "navigation-links"> <ul id = "navigation-links">
<li title = "Actions"> <li title = "Actions">
<a id = "showActions">Actions</a> <a id = "showActions">Actions</a>