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)
dashboardCfgToPb(res, cfg.Dashboards, cfg)
return res, nil
}

View File

@ -10,6 +10,12 @@ import (
"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 {
res := &apiv1.GetDashboardComponentsResponse{
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.Diagnostics = buildDiagnostics(res.EffectivePolicy.ShowDiagnostics)
res.Dashboards = dashboardCfgToPb(rr)
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 {
ret := &apiv1.EffectivePolicy{
ShowDiagnostics: policy.ShowDiagnostics,

View File

@ -3,40 +3,76 @@ package grpcapi
import (
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
config "github.com/OliveTin/OliveTin/internal/config"
log "github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
)
func dashboardCfgToPb(res *apiv1.GetDashboardComponentsResponse, dashboards []*config.DashboardComponent, cfg *config.Config) {
for _, dashboard := range dashboards {
res.Dashboards = append(res.Dashboards, &apiv1.DashboardComponent{
func dashboardCfgToPb(rr *DashboardRenderRequest) []*apiv1.DashboardComponent {
ret := make([]*apiv1.DashboardComponent, 0)
for _, dashboard := range cfg.Dashboards {
pbdb := &apiv1.DashboardComponent{
Type: "dashboard",
Title: dashboard.Title,
Contents: getDashboardComponentContents(dashboard, cfg),
})
}
Contents: removeNulls(getDashboardComponentContents(dashboard, rr)),
}
func getDashboardComponentContents(dashboard *config.DashboardComponent, cfg *config.Config) []*apiv1.DashboardComponent {
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 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)
for _, subitem := range dashboard.Contents {
if subitem.Type == "fieldset" && subitem.Entity != "" {
ret = append(ret, buildEntityFieldsets(subitem.Entity, &subitem)...)
continue
ret = append(ret, buildEntityFieldsets(subitem.Entity, &subitem, rr)...)
} else {
ret = append(ret, buildDashboardComponentSimple(&subitem, rr))
}
}
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, cfg),
Icon: getDashboardComponentIcon(&subitem, cfg),
Type: getDashboardComponentType(subitem),
Contents: getDashboardComponentContents(subitem, rr),
Icon: getDashboardComponentIcon(subitem, rr.cfg),
CssClass: subitem.CssClass,
}
ret = append(ret, newitem)
}
return ret
return newitem
}
func getDashboardComponentIcon(item *config.DashboardComponent, cfg *config.Config) string {

View File

@ -4,48 +4,75 @@ import (
apiv1 "github.com/OliveTin/OliveTin/gen/grpc/olivetin/api/v1"
config "github.com/OliveTin/OliveTin/internal/config"
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)
entityCount := sv.GetEntityCount(entityTitle)
for i := 0; i < entityCount; i++ {
ret = append(ret, buildEntityFieldset(tpl, entityTitle, i))
for i := range entityCount {
fs := buildEntityFieldset(tpl, entityTitle, i, rr)
if len(fs.Contents) > 0 {
ret = append(ret, fs)
}
}
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)
return &apiv1.DashboardComponent{
Title: sv.ReplaceEntityVars(prefix, tpl.Title),
Type: "fieldset",
Contents: buildEntityFieldsetContents(tpl.Contents, prefix),
Contents: removeFieldsetIfHasNoLinks(buildEntityFieldsetContents(tpl.Contents, prefix, rr)),
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)
for _, subitem := range contents {
c := cloneItem(&subitem, prefix, rr)
if c != nil {
ret = append(ret, c)
}
}
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
}
ret = append(ret, clone)
}
return ret
return clone
}

View File

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