diff --git a/integration-tests/configs/emptyDashboardsAreHidden/config.yaml b/integration-tests/configs/emptyDashboardsAreHidden/config.yaml new file mode 100644 index 0000000..b0aec26 --- /dev/null +++ b/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 }} + diff --git a/integration-tests/test/emptyDashboardsAreHidden.js b/integration-tests/test/emptyDashboardsAreHidden.js new file mode 100644 index 0000000..fa6f01d --- /dev/null +++ b/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') + }) +}) diff --git a/service/internal/grpcapi/grpcApi.go b/service/internal/grpcapi/grpcApi.go index 007f64f..daa5cbe 100644 --- a/service/internal/grpcapi/grpcApi.go +++ b/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 } diff --git a/service/internal/grpcapi/grpcApiActions.go b/service/internal/grpcapi/grpcApiActions.go index 6f4898c..4009900 100644 --- a/service/internal/grpcapi/grpcApiActions.go +++ b/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, diff --git a/service/internal/grpcapi/grpcApiDashboard.go b/service/internal/grpcapi/grpcApiDashboard.go index b4d3ae7..009ff2a 100644 --- a/service/internal/grpcapi/grpcApiDashboard.go +++ b/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 _, 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)) } - - 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 } +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 diff --git a/service/internal/grpcapi/grpcApiDashboardEntities.go b/service/internal/grpcapi/grpcApiDashboardEntities.go index 1a8eacc..a453ed5 100644 --- a/service/internal/grpcapi/grpcApiDashboardEntities.go +++ b/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) + c := cloneItem(&subitem, prefix, rr) - 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 + if c != nil { + ret = append(ret, c) } - - ret = append(ret, clone) } 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 +} diff --git a/webui.dev/index.html b/webui.dev/index.html index b9a4647..e9db0a6 100644 --- a/webui.dev/index.html +++ b/webui.dev/index.html @@ -27,7 +27,7 @@

OliveTin

-