fix: Empty dashboards are hidden (#599)
This commit is contained in:
parent
b2e7509959
commit
1357eae9b8
|
|
@ -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 }}
|
||||||
|
|
||||||
|
|
@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue