package api import ( "sort" "strconv" apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1" acl "github.com/OliveTin/OliveTin/internal/acl" config "github.com/OliveTin/OliveTin/internal/config" entities "github.com/OliveTin/OliveTin/internal/entities" "github.com/OliveTin/OliveTin/internal/tpl" log "github.com/sirupsen/logrus" "golang.org/x/exp/slices" ) func renderDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.Dashboard { if dashboardTitle == "default" { return buildDefaultDashboard(rr) } return findAndRenderDashboard(rr, dashboardTitle) } func getEntityFromRequest(rr *DashboardRenderRequest) *entities.Entity { if rr.EntityType == "" || rr.EntityKey == "" { return nil } entityInstances := entities.GetEntityInstances(rr.EntityType) if entity, ok := entityInstances[rr.EntityKey]; ok { return entity } return nil } func findAndRenderDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.Dashboard { if dashboard := findDashboardByTitle(rr, dashboardTitle); dashboard != nil { return renderDashboardIfValid(dashboard, rr) } return renderDirectoryDashboard(rr, dashboardTitle) } func findDashboardByTitle(rr *DashboardRenderRequest, dashboardTitle string) *config.DashboardComponent { for _, dashboard := range rr.cfg.Dashboards { if dashboard.Title == dashboardTitle { return dashboard } } return nil } func renderDashboardIfValid(dashboard *config.DashboardComponent, rr *DashboardRenderRequest) *apiv1.Dashboard { if len(dashboard.Contents) == 0 { logEmptyDashboard(dashboard.Title, rr.AuthenticatedUser.Username) return nil } return buildDashboardFromConfig(dashboard, rr) } func renderDirectoryDashboard(rr *DashboardRenderRequest, dashboardTitle string) *apiv1.Dashboard { directoryComponent := findDirectoryComponent(rr, dashboardTitle) if directoryComponent == nil { return nil } entity := getEntityFromRequest(rr) return buildDashboardFromConfigWithEntity(directoryComponent, rr, entity) } func findDirectoryComponent(rr *DashboardRenderRequest, title string) *config.DashboardComponent { for _, dashboard := range rr.cfg.Dashboards { if component := searchDirectoryInComponent(dashboard, title); component != nil { return component } } return nil } func searchDirectoryInComponent(component *config.DashboardComponent, title string) *config.DashboardComponent { if isMatchingDirectory(component, title) { return component } return searchDirectoryInSubcomponents(component.Contents, title) } func isMatchingDirectory(component *config.DashboardComponent, title string) bool { return component.Title == title && len(component.Contents) > 0 && component.Type != "fieldset" } func searchDirectoryInSubcomponents(contents []*config.DashboardComponent, title string) *config.DashboardComponent { for _, subitem := range contents { if found := searchDirectoryInComponent(subitem, title); found != nil { return found } } return nil } func logEmptyDashboard(dashboardTitle, username string) { log.WithFields(log.Fields{ "dashboard": dashboardTitle, "username": username, }).Debugf("Dashboard has no readable contents, so it will not be visible in the web ui") } func buildDashboardFromConfig(dashboard *config.DashboardComponent, rr *DashboardRenderRequest) *apiv1.Dashboard { return buildDashboardFromConfigWithEntity(dashboard, rr, nil) } func buildDashboardFromConfigWithEntity(dashboard *config.DashboardComponent, rr *DashboardRenderRequest, entity *entities.Entity) *apiv1.Dashboard { contents, root := getDashboardComponentContentsWithEntity(dashboard, rr, entity) return &apiv1.Dashboard{ Title: dashboard.Title, Contents: orderTopLevelDashboardComponents(removeNulls(contents), root), } } //gocyclo:ignore func buildDefaultDashboard(rr *DashboardRenderRequest) *apiv1.Dashboard { db := &apiv1.Dashboard{ Title: "Actions", Contents: make([]*apiv1.DashboardComponent, 0), } fieldset := &apiv1.DashboardComponent{ Type: "fieldset", Title: "Actions", Contents: make([]*apiv1.DashboardComponent, 0), } for _, binding := range rr.ex.MapActionBindings { if binding == nil || binding.Action == nil || binding.Action.Hidden { continue } if binding.IsOnConfiguredDashboard() { continue } if !acl.IsAllowedView(rr.cfg, rr.AuthenticatedUser, binding.Action) { continue } action := buildAction(binding, rr) if action == nil { continue } comp := &apiv1.DashboardComponent{ Type: "link", Title: action.Title, Icon: action.Icon, Action: action, } if binding.Entity != nil { comp.EntityKey = binding.Entity.UniqueKey } fieldset.Contents = append(fieldset.Contents, comp) } if len(fieldset.Contents) > 0 { fieldset.Contents = sortDashboardComponents(fieldset.Contents) db.Contents = append(db.Contents, fieldset) } return db } func entityKeyLess(a, b string) bool { ai, errA := strconv.ParseInt(a, 10, 64) bi, errB := strconv.ParseInt(b, 10, 64) if errA == nil && errB == nil { return ai < bi } return a < b } //gocyclo:ignore func sortDashboardComponents(components []*apiv1.DashboardComponent) []*apiv1.DashboardComponent { sort.Slice(components, func(i, j int) bool { if components[i].Action == nil || components[j].Action == nil { if components[i].EntityKey != "" && components[j].EntityKey != "" && components[i].EntityKey != components[j].EntityKey { return entityKeyLess(components[i].EntityKey, components[j].EntityKey) } return components[i].Title < components[j].Title } if components[i].Action.Order != components[j].Action.Order { return components[i].Action.Order < components[j].Action.Order } if components[i].EntityKey != components[j].EntityKey { return entityKeyLess(components[i].EntityKey, components[j].EntityKey) } return components[i].Action.Title < components[j].Action.Title }) return components } 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 isNonEntityFieldset(component *apiv1.DashboardComponent) bool { return component != nil && component.Type == "fieldset" && component.EntityType == "" } func isRegularFieldset(component *apiv1.DashboardComponent, root *apiv1.DashboardComponent) bool { if !isNonEntityFieldset(component) { return false } return root == nil || component != root } func partitionTopLevelComponents(components []*apiv1.DashboardComponent, root *apiv1.DashboardComponent) (regular, sortables []*apiv1.DashboardComponent, isRegular []bool) { regular = make([]*apiv1.DashboardComponent, 0) sortables = make([]*apiv1.DashboardComponent, 0) isRegular = make([]bool, len(components)) for i, c := range components { anchor := isRegularFieldset(c, root) isRegular[i] = anchor if anchor { regular = append(regular, c) } else { sortables = append(sortables, c) } } return regular, sortables, isRegular } func mergeOrderedTopLevelComponents(regular, sortables []*apiv1.DashboardComponent, isRegular []bool) []*apiv1.DashboardComponent { out := make([]*apiv1.DashboardComponent, 0, len(isRegular)) regIdx, sortIdx := 0, 0 for _, anchor := range isRegular { if anchor { out = append(out, regular[regIdx]) regIdx++ } else { out = append(out, sortables[sortIdx]) sortIdx++ } } return out } func orderTopLevelDashboardComponents(components []*apiv1.DashboardComponent, root *apiv1.DashboardComponent) []*apiv1.DashboardComponent { if len(components) == 0 { return components } regular, sortables, isRegular := partitionTopLevelComponents(components, root) sortDashboardComponents(sortables) return mergeOrderedTopLevelComponents(regular, sortables, isRegular) } func getDashboardComponentContentsWithEntity(dashboard *config.DashboardComponent, rr *DashboardRenderRequest, entity *entities.Entity) ([]*apiv1.DashboardComponent, *apiv1.DashboardComponent) { ret := make([]*apiv1.DashboardComponent, 0) rootFieldset := createRootFieldset() for _, subitem := range dashboard.Contents { processDashboardSubitemWithEntity(subitem, rr, &ret, rootFieldset, entity) } if len(rootFieldset.Contents) > 0 { ret = append(ret, rootFieldset) return ret, rootFieldset } return ret, nil } func createRootFieldset() *apiv1.DashboardComponent { return &apiv1.DashboardComponent{ Type: "fieldset", Title: "Actions", Contents: make([]*apiv1.DashboardComponent, 0), } } func appendComponentIfNotNil(components *[]*apiv1.DashboardComponent, comp *apiv1.DashboardComponent) { if comp != nil { *components = append(*components, comp) } } func getDashboardComponentOrNil(subitem *config.DashboardComponent, rr *DashboardRenderRequest, entity *entities.Entity) *apiv1.DashboardComponent { if len(subitem.Contents) == 0 && rr.findActionForEntity(subitem.Title, entity) == nil { if !isAllowedType(subitem.Type) { return nil } } return buildDashboardComponentSimpleWithEntity(subitem, rr, entity) } func processDashboardSubitemWithEntity(subitem *config.DashboardComponent, rr *DashboardRenderRequest, ret *[]*apiv1.DashboardComponent, rootFieldset *apiv1.DashboardComponent, entity *entities.Entity) { if subitem.Type != "fieldset" { appendComponentIfNotNil(&rootFieldset.Contents, getDashboardComponentOrNil(subitem, rr, entity)) return } if subitem.Entity != "" { *ret = append(*ret, buildEntityFieldsets(subitem.Entity, subitem, rr)...) } else { appendComponentIfNotNil(ret, getDashboardComponentOrNil(subitem, rr, entity)) } } func buildDashboardComponentSimpleWithEntity(subitem *config.DashboardComponent, rr *DashboardRenderRequest, entity *entities.Entity) *apiv1.DashboardComponent { var contents []*apiv1.DashboardComponent if len(subitem.Contents) > 0 { contents, _ = getDashboardComponentContentsWithEntity(subitem, rr, entity) } action := rr.findActionForEntity(subitem.Title, entity) componentType := getDashboardComponentType(subitem, action) title := subitem.Title if entity != nil { title = tpl.ParseTemplateOfActionBeforeExec(subitem.Title, entity) } newitem := &apiv1.DashboardComponent{ Title: title, Type: componentType, Contents: contents, Icon: getDashboardComponentIcon(subitem, rr.cfg), CssClass: subitem.CssClass, Action: action, } return newitem } func getDashboardComponentIcon(item *config.DashboardComponent, cfg *config.Config) string { if item.Icon == "" { return cfg.DefaultIconForDirectories } return item.Icon } func getDashboardComponentType(item *config.DashboardComponent, action *apiv1.Action) string { if hasContents(item) { return getTypeForComponentWithContents(item) } if isAllowedType(item.Type) { return item.Type } return getDefaultType(action) } func hasContents(item *config.DashboardComponent) bool { return len(item.Contents) > 0 } func getTypeForComponentWithContents(item *config.DashboardComponent) string { if item.Type != "fieldset" { return "directory" } return "fieldset" } func isAllowedType(itemType string) bool { allowedTypes := []string{ "stdout-most-recent-execution", "display", } return slices.Contains(allowedTypes, itemType) } func getDefaultType(action *apiv1.Action) string { if action == nil { return "display" } return "link" }