The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/navtree/navtreeimpl/applinks.go

361 lines
14 KiB

package navtreeimpl
import (
"path"
"sort"
"strconv"
"github.com/grafana/grafana/pkg/plugins"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/navtree"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/util"
)
func (s *ServiceImpl) addAppLinks(treeRoot *navtree.NavTreeRoot, c *contextmodel.ReqContext) error {
hasAccess := ac.HasAccess(s.accessControl, c)
appLinks := []*navtree.NavLink{}
pss, err := s.pluginSettings.GetPluginSettings(c.Req.Context(), &pluginsettings.GetArgs{OrgID: c.SignedInUser.GetOrgID()})
if err != nil {
return err
}
isPluginEnabled := func(plugin pluginstore.Plugin) bool {
if plugin.AutoEnabled {
return true
}
for _, ps := range pss {
if ps.PluginID == plugin.ID {
return ps.Enabled
}
}
return false
}
for _, plugin := range s.pluginStore.Plugins(c.Req.Context(), plugins.TypeApp) {
if !isPluginEnabled(plugin) {
continue
}
if !hasAccess(ac.EvalPermission(pluginaccesscontrol.ActionAppAccess, pluginaccesscontrol.ScopeProvider.GetResourceScope(plugin.ID))) {
continue
}
if appNode := s.processAppPlugin(plugin, c, treeRoot); appNode != nil {
appLinks = append(appLinks, appNode)
}
}
if len(appLinks) > 0 {
sort.SliceStable(appLinks, func(i, j int) bool {
return appLinks[i].Text < appLinks[j].Text
})
}
for _, appLink := range appLinks {
treeRoot.AddSection(appLink)
}
return nil
}
func (s *ServiceImpl) processAppPlugin(plugin pluginstore.Plugin, c *contextmodel.ReqContext, treeRoot *navtree.NavTreeRoot) *navtree.NavLink {
hasAccessToInclude := s.hasAccessToInclude(c, plugin.ID)
appLink := &navtree.NavLink{
Text: plugin.Name,
Id: "plugin-page-" + plugin.ID,
Img: plugin.Info.Logos.Small,
SubTitle: plugin.Info.Description,
SortWeight: navtree.WeightPlugin,
IsSection: true,
PluginID: plugin.ID,
Url: s.cfg.AppSubURL + "/a/" + plugin.ID,
}
for _, include := range plugin.Includes {
if !hasAccessToInclude(include) {
continue
}
if include.Type == "page" {
link := &navtree.NavLink{
Text: include.Name,
Icon: include.Icon,
PluginID: plugin.ID,
}
if len(include.Path) > 0 {
link.Url = s.cfg.AppSubURL + include.Path
if include.DefaultNav && include.AddToNav {
appLink.Url = link.Url
}
} else {
link.Url = s.cfg.AppSubURL + "/plugins/" + plugin.ID + "/page/" + include.Slug
}
// Register standalone plugin pages to certain sections using the Grafana config
if pathConfig, ok := s.navigationAppPathConfig[include.Path]; ok {
if sectionForPage := treeRoot.FindById(pathConfig.SectionID); sectionForPage != nil {
link.Id = "standalone-plugin-page-" + include.Path
link.SortWeight = pathConfig.SortWeight
// Check if the section already has a page with the same URL, and in that case override it
// (This only happens if it is explicitly set by `navigation.app_standalone_pages` in the INI config)
isOverridingCorePage := false
for _, child := range sectionForPage.Children {
if child.Url == link.Url {
child.Id = link.Id
child.SortWeight = link.SortWeight
child.PluginID = link.PluginID
child.Children = []*navtree.NavLink{}
isOverridingCorePage = true
break
}
}
// Append the page to the section
if !isOverridingCorePage {
sectionForPage.Children = append(sectionForPage.Children, link)
}
}
// Register the page under the app
} else if include.AddToNav {
appLink.Children = append(appLink.Children, link)
}
}
if include.Type == "dashboard" && include.AddToNav {
dboardURL := include.DashboardURLPath()
if dboardURL != "" {
link := &navtree.NavLink{
Url: path.Join(s.cfg.AppSubURL, dboardURL),
Text: include.Name,
PluginID: plugin.ID,
}
appLink.Children = append(appLink.Children, link)
}
}
}
// Apps without any nav children are not part of navtree
if len(appLink.Children) == 0 {
return nil
}
// If we only have one child and it's the app default nav then remove it from children
if len(appLink.Children) == 1 && appLink.Children[0].Url == appLink.Url {
appLink.Children = []*navtree.NavLink{}
}
// Remove default nav child
childrenWithoutDefault := []*navtree.NavLink{}
for _, child := range appLink.Children {
if child.Url != appLink.Url {
childrenWithoutDefault = append(childrenWithoutDefault, child)
}
}
appLink.Children = childrenWithoutDefault
s.addPluginToSection(c, treeRoot, plugin, appLink)
return nil
}
func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *navtree.NavTreeRoot, plugin pluginstore.Plugin, appLink *navtree.NavLink) {
// Handle moving apps into specific navtree sections
var alertingNodes []*navtree.NavLink
alertingNode := treeRoot.FindById(navtree.NavIDAlerting)
if alertingNode != nil {
alertingNodes = append(alertingNodes, alertingNode)
}
sectionID := navtree.NavIDApps
if navConfig, hasOverride := s.navigationAppConfig[plugin.ID]; hasOverride {
appLink.SortWeight = navConfig.SortWeight
sectionID = navConfig.SectionID
if len(navConfig.Text) > 0 {
appLink.Text = navConfig.Text
}
if len(navConfig.Icon) > 0 {
appLink.Icon = navConfig.Icon
}
}
if sectionID == navtree.NavIDRoot {
treeRoot.AddSection(appLink)
} else if navNode := treeRoot.FindById(sectionID); navNode != nil {
navNode.Children = append(navNode.Children, appLink)
} else {
switch sectionID {
case navtree.NavIDApps:
treeRoot.AddSection(&navtree.NavLink{
Text: "More apps",
Icon: "layer-group",
SubTitle: "App plugins that extend the Grafana experience",
Id: navtree.NavIDApps,
Children: []*navtree.NavLink{appLink},
SortWeight: navtree.WeightApps,
Url: s.cfg.AppSubURL + "/apps",
})
case navtree.NavIDMonitoring:
treeRoot.AddSection(&navtree.NavLink{
Text: "Observability",
Id: navtree.NavIDMonitoring,
SubTitle: "Observability and infrastructure apps",
Icon: "heart-rate",
SortWeight: navtree.WeightMonitoring,
Children: []*navtree.NavLink{appLink},
Url: s.cfg.AppSubURL + "/monitoring",
})
case navtree.NavIDInfrastructure:
treeRoot.AddSection(&navtree.NavLink{
Text: "Infrastructure",
Id: navtree.NavIDInfrastructure,
SubTitle: "Understand your infrastructure's health",
Icon: "heart-rate",
SortWeight: navtree.WeightInfrastructure,
Children: []*navtree.NavLink{appLink},
Url: s.cfg.AppSubURL + "/infrastructure",
})
case navtree.NavIDFrontend:
treeRoot.AddSection(&navtree.NavLink{
Text: "Frontend",
Id: navtree.NavIDFrontend,
SubTitle: "Gain real user monitoring insights",
Icon: "frontend-observability",
SortWeight: navtree.WeightFrontend,
Children: []*navtree.NavLink{appLink},
Url: s.cfg.AppSubURL + "/frontend",
})
case navtree.NavIDAlertsAndIncidents:
alertsAndIncidentsChildren := []*navtree.NavLink{}
for _, alertingNode := range alertingNodes {
alertsAndIncidentsChildren = append(alertsAndIncidentsChildren, alertingNode)
treeRoot.RemoveSection(alertingNode)
}
alertsAndIncidentsChildren = append(alertsAndIncidentsChildren, appLink)
treeRoot.AddSection(&navtree.NavLink{
Text: "Alerts & IRM",
Id: navtree.NavIDAlertsAndIncidents,
SubTitle: "Alerting and incident management apps",
Icon: "bell",
SortWeight: navtree.WeightAlertsAndIncidents,
Children: alertsAndIncidentsChildren,
Url: s.cfg.AppSubURL + "/alerts-and-incidents",
})
case navtree.NavIDTestingAndSynthetics:
treeRoot.AddSection(&navtree.NavLink{
Text: "Testing & synthetics",
Id: navtree.NavIDTestingAndSynthetics,
SubTitle: "Optimize performance with k6 and Synthetic Monitoring insights",
Icon: "k6",
SortWeight: navtree.WeightTestingAndSynthetics,
Children: []*navtree.NavLink{appLink},
Url: s.cfg.AppSubURL + "/testing-and-synthetics",
})
default:
s.log.Error("Plugin app nav id not found", "pluginId", plugin.ID, "navId", sectionID)
}
}
}
func (s *ServiceImpl) hasAccessToInclude(c *contextmodel.ReqContext, pluginID string) func(include *plugins.Includes) bool {
hasAccess := ac.HasAccess(s.accessControl, c)
return func(include *plugins.Includes) bool {
useRBAC := s.features.IsEnabledGlobally(featuremgmt.FlagAccessControlOnCall) && include.RequiresRBACAction()
if useRBAC && !hasAccess(pluginaccesscontrol.GetPluginRouteEvaluator(pluginID, include.Action)) {
s.log.Debug("plugin include is covered by RBAC, user doesn't have access",
"plugin", pluginID,
"include", include.Name)
return false
} else if !useRBAC && !c.HasUserRole(include.Role) {
return false
}
return true
}
}
func (s *ServiceImpl) readNavigationSettings() {
s.navigationAppConfig = map[string]NavigationAppConfig{
"grafana-k8s-app": {SectionID: navtree.NavIDInfrastructure, SortWeight: 1, Text: "Kubernetes"},
"grafana-dbo11y-app": {SectionID: navtree.NavIDInfrastructure, SortWeight: 2, Text: "Databases"},
"grafana-app-observability-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightApplication, Text: "Application", Icon: "graph-bar"},
"grafana-lokiexplore-app": {SectionID: navtree.NavIDExplore, SortWeight: 2, Text: "Logs"},
"grafana-exploretraces-app": {SectionID: navtree.NavIDExplore, SortWeight: 3, Text: "Traces"},
"grafana-pyroscope-app": {SectionID: navtree.NavIDExplore, SortWeight: 4, Text: "Profiles"},
"grafana-kowalski-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightFrontend, Text: "Frontend", Icon: "frontend-observability"},
"grafana-synthetic-monitoring-app": {SectionID: navtree.NavIDTestingAndSynthetics, SortWeight: 2, Text: "Synthetics"},
"grafana-irm-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 1, Text: "IRM"},
"grafana-oncall-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 2, Text: "OnCall"},
"grafana-incident-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 3, Text: "Incident"},
"grafana-ml-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 4, Text: "Machine Learning"},
"grafana-slo-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 5},
"grafana-cloud-link-app": {SectionID: navtree.NavIDCfgPlugins, SortWeight: 3},
"grafana-costmanagementui-app": {SectionID: navtree.NavIDCfg, Text: "Cost management"},
"grafana-adaptive-metrics-app": {SectionID: navtree.NavIDCfg, Text: "Adaptive Metrics"},
"grafana-adaptivelogs-app": {SectionID: navtree.NavIDCfg, Text: "Adaptive Logs"},
"grafana-adaptivetraces-app": {SectionID: navtree.NavIDCfg, Text: "Adaptive Traces"},
"grafana-attributions-app": {SectionID: navtree.NavIDCfg, Text: "Attributions"},
"grafana-logvolumeexplorer-app": {SectionID: navtree.NavIDCfg, Text: "Log Volume Explorer"},
"grafana-easystart-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightApps + 1, Text: "Connections", Icon: "adjust-circle"},
"k6-app": {SectionID: navtree.NavIDTestingAndSynthetics, SortWeight: 1, Text: "Performance"},
"grafana-asserts-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightAsserts, Icon: "asserts"},
"grafana-csp-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightCloudServiceProviders, Icon: "cloud"},
}
if s.features.IsEnabledGlobally(featuremgmt.FlagExploreMetricsUseExternalAppPlugin) {
s.navigationAppConfig["grafana-metricsdrilldown-app"] = NavigationAppConfig{SectionID: navtree.NavIDExplore, SortWeight: 1, Text: "Metrics"}
}
s.navigationAppPathConfig = map[string]NavigationAppConfig{
"/a/grafana-auth-app": {SectionID: navtree.NavIDCfgAccess, SortWeight: 2},
}
appSections := s.cfg.Raw.Section("navigation.app_sections")
appStandalonePages := s.cfg.Raw.Section("navigation.app_standalone_pages")
for _, key := range appSections.Keys() {
pluginId := key.Name()
// Support <id> <weight> value
values := util.SplitString(appSections.Key(key.Name()).MustString(""))
appCfg := &NavigationAppConfig{SectionID: values[0]}
if len(values) > 1 {
if weight, err := strconv.ParseInt(values[1], 10, 64); err == nil {
appCfg.SortWeight = weight
}
}
// Only apply the new values, don't completely overwrite the entry if it exists
if entry, ok := s.navigationAppConfig[pluginId]; ok {
entry.SectionID = appCfg.SectionID
if appCfg.SortWeight != 0 {
entry.SortWeight = appCfg.SortWeight
}
s.navigationAppConfig[pluginId] = entry
} else {
s.navigationAppConfig[pluginId] = *appCfg
}
}
for _, key := range appStandalonePages.Keys() {
url := key.Name()
// Support <id> <weight> value
values := util.SplitString(appStandalonePages.Key(key.Name()).MustString(""))
appCfg := &NavigationAppConfig{SectionID: values[0]}
if len(values) > 1 {
if weight, err := strconv.ParseInt(values[1], 10, 64); err == nil {
appCfg.SortWeight = weight
}
}
s.navigationAppPathConfig[url] = *appCfg
}
}