Преглед изворни кода

fix: Empty dashboards are hidden (#599)

James Read пре 1 година
родитељ
комит
1357eae9b8

+ 25 - 0
integration-tests/configs/emptyDashboardsAreHidden/config.yaml

@@ -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 }}
+

+ 42 - 0
integration-tests/test/emptyDashboardsAreHidden.js

@@ -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')
+  })
+})

+ 0 - 2
service/internal/grpcapi/grpcApi.go

@@ -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
 }
 

+ 23 - 0
service/internal/grpcapi/grpcApiActions.go

@@ -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,

+ 53 - 17
service/internal/grpcapi/grpcApiDashboard.go

@@ -3,42 +3,78 @@ 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)),
+		}
+
+		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 _, subitem := range dashboard.Contents {
-		if subitem.Type == "fieldset" && subitem.Entity != "" {
-			ret = append(ret, buildEntityFieldsets(subitem.Entity, &subitem)...)
+	for _, component := range components {
+		if component == nil {
 			continue
 		}
 
-		newitem := &apiv1.DashboardComponent{
-			Title:    subitem.Title,
-			Type:     getDashboardComponentType(&subitem),
-			Contents: getDashboardComponentContents(&subitem, cfg),
-			Icon:     getDashboardComponentIcon(&subitem, cfg),
-			CssClass: subitem.CssClass,
-		}
+		ret = append(ret, component)
+	}
 
-		ret = append(ret, newitem)
+	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, 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, rr),
+		Icon:     getDashboardComponentIcon(subitem, rr.cfg),
+		CssClass: subitem.CssClass,
+	}
+
+	return newitem
+}
+
 func getDashboardComponentIcon(item *config.DashboardComponent, cfg *config.Config) string {
 	if item.Icon == "" {
 		return cfg.DefaultIconForDirectories

+ 44 - 17
service/internal/grpcapi/grpcApiDashboardEntities.go

@@ -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 {
-		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)
-		} else {
-			clone.Title = sv.ReplaceEntityVars(prefix, subitem.Title)
-			clone.Type = subitem.Type
-		}
+		c := cloneItem(&subitem, prefix, rr)
 
-		ret = append(ret, clone)
+		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
+	}
+
+	return clone
+}

+ 1 - 1
webui.dev/index.html

@@ -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>