2022-09-23 04:04:48 +08:00
package navtreeimpl
import (
"path"
"sort"
2022-09-28 14:29:35 +08:00
"strconv"
2022-09-23 04:04:48 +08:00
"github.com/grafana/grafana/pkg/plugins"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
2023-01-27 15:50:36 +08:00
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
2022-09-23 04:04:48 +08:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/navtree"
2023-03-27 17:15:37 +08:00
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
2023-03-08 00:22:30 +08:00
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
2023-09-11 19:59:24 +08:00
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
2022-09-28 14:29:35 +08:00
"github.com/grafana/grafana/pkg/util"
2022-09-23 04:04:48 +08:00
)
2023-01-27 15:50:36 +08:00
func ( s * ServiceImpl ) addAppLinks ( treeRoot * navtree . NavTreeRoot , c * contextmodel . ReqContext ) error {
2022-09-23 04:04:48 +08:00
hasAccess := ac . HasAccess ( s . accessControl , c )
appLinks := [ ] * navtree . NavLink { }
2025-04-10 20:42:23 +08:00
pss , err := s . pluginSettings . GetPluginSettings ( c . Req . Context ( ) , & pluginsettings . GetArgs { OrgID : c . GetOrgID ( ) } )
2022-09-23 04:04:48 +08:00
if err != nil {
2022-09-28 14:29:35 +08:00
return err
2022-09-23 04:04:48 +08:00
}
2023-09-11 19:59:24 +08:00
isPluginEnabled := func ( plugin pluginstore . Plugin ) bool {
2022-09-23 04:04:48 +08:00
if plugin . AutoEnabled {
return true
}
for _ , ps := range pss {
if ps . PluginID == plugin . ID {
return ps . Enabled
}
}
return false
}
2023-06-08 18:21:19 +08:00
for _ , plugin := range s . pluginStore . Plugins ( c . Req . Context ( ) , plugins . TypeApp ) {
2022-09-23 04:04:48 +08:00
if ! isPluginEnabled ( plugin ) {
continue
}
2023-05-30 21:39:09 +08:00
if ! hasAccess ( ac . EvalPermission ( pluginaccesscontrol . ActionAppAccess , pluginaccesscontrol . ScopeProvider . GetResourceScope ( plugin . ID ) ) ) {
2022-09-23 04:04:48 +08:00
continue
}
2023-04-14 16:43:11 +08:00
if appNode := s . processAppPlugin ( plugin , c , treeRoot ) ; appNode != nil {
2022-09-28 14:29:35 +08:00
appLinks = append ( appLinks , appNode )
2022-09-23 04:04:48 +08:00
}
2022-09-28 14:29:35 +08:00
}
2022-09-23 04:04:48 +08:00
2022-09-28 14:29:35 +08:00
if len ( appLinks ) > 0 {
sort . SliceStable ( appLinks , func ( i , j int ) bool {
return appLinks [ i ] . Text < appLinks [ j ] . Text
} )
}
2022-10-05 17:46:27 +08:00
for _ , appLink := range appLinks {
treeRoot . AddSection ( appLink )
2022-09-28 14:29:35 +08:00
}
return nil
}
2023-09-11 19:59:24 +08:00
func ( s * ServiceImpl ) processAppPlugin ( plugin pluginstore . Plugin , c * contextmodel . ReqContext , treeRoot * navtree . NavTreeRoot ) * navtree . NavLink {
2022-11-16 22:54:04 +08:00
hasAccessToInclude := s . hasAccessToInclude ( c , plugin . ID )
2022-09-28 14:29:35 +08:00
appLink := & navtree . NavLink {
Text : plugin . Name ,
Id : "plugin-page-" + plugin . ID ,
Img : plugin . Info . Logos . Small ,
2023-01-23 18:02:05 +08:00
SubTitle : plugin . Info . Description ,
2022-09-28 14:29:35 +08:00
SortWeight : navtree . WeightPlugin ,
2022-10-31 18:01:34 +08:00
IsSection : true ,
2022-11-04 04:19:42 +08:00
PluginID : plugin . ID ,
2023-04-14 16:43:11 +08:00
Url : s . cfg . AppSubURL + "/a/" + plugin . ID ,
2022-09-28 14:29:35 +08:00
}
for _ , include := range plugin . Includes {
2022-11-16 22:54:04 +08:00
if ! hasAccessToInclude ( include ) {
2022-09-28 14:29:35 +08:00
continue
2022-09-23 04:04:48 +08:00
}
2022-11-07 22:19:31 +08:00
if include . Type == "page" {
2022-09-28 14:29:35 +08:00
link := & navtree . NavLink {
2022-11-04 04:19:42 +08:00
Text : include . Name ,
Icon : include . Icon ,
PluginID : plugin . ID ,
2022-09-23 04:04:48 +08:00
}
2022-09-28 14:29:35 +08:00
if len ( include . Path ) > 0 {
link . Url = s . cfg . AppSubURL + include . Path
2022-11-07 22:19:31 +08:00
if include . DefaultNav && include . AddToNav {
2022-09-28 14:29:35 +08:00
appLink . Url = link . Url
}
} else {
link . Url = s . cfg . AppSubURL + "/plugins/" + plugin . ID + "/page/" + include . Slug
}
2022-11-04 04:19:42 +08:00
// Register standalone plugin pages to certain sections using the Grafana config
2022-09-28 14:29:35 +08:00
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
2022-11-04 04:19:42 +08:00
// 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 )
}
2022-09-23 04:04:48 +08:00
}
2022-11-07 22:19:31 +08:00
// Register the page under the app
} else if include . AddToNav {
2022-09-23 04:04:48 +08:00
appLink . Children = append ( appLink . Children , link )
}
2022-09-28 14:29:35 +08:00
}
2022-09-23 04:04:48 +08:00
2022-09-28 14:29:35 +08:00
if include . Type == "dashboard" && include . AddToNav {
dboardURL := include . DashboardURLPath ( )
if dboardURL != "" {
link := & navtree . NavLink {
2022-11-04 04:19:42 +08:00
Url : path . Join ( s . cfg . AppSubURL , dboardURL ) ,
Text : include . Name ,
PluginID : plugin . ID ,
2022-09-23 04:04:48 +08:00
}
2022-09-28 14:29:35 +08:00
appLink . Children = append ( appLink . Children , link )
2022-09-23 04:04:48 +08:00
}
}
2022-09-28 14:29:35 +08:00
}
2022-10-05 17:46:27 +08:00
// 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 )
2022-09-28 14:29:35 +08:00
}
2022-10-05 17:46:27 +08:00
}
appLink . Children = childrenWithoutDefault
2022-09-28 14:29:35 +08:00
2022-11-18 17:05:45 +08:00
s . addPluginToSection ( c , treeRoot , plugin , appLink )
2025-04-22 20:20:36 +08:00
if plugin . ID == "grafana-slo-app" {
// Add Service Center as a standalone nav item under Alerts & IRM
if alertsSection := treeRoot . FindById ( navtree . NavIDAlertsAndIncidents ) ; alertsSection != nil {
serviceLink := & navtree . NavLink {
Text : "Service Center" ,
Id : "standalone-plugin-page-slo-services" ,
Url : s . cfg . AppSubURL + "/a/grafana-slo-app/services" ,
SortWeight : 1 ,
IsNew : true ,
}
alertsSection . Children = append ( alertsSection . Children , serviceLink )
}
}
2022-11-18 17:05:45 +08:00
return nil
}
2023-09-11 19:59:24 +08:00
func ( s * ServiceImpl ) addPluginToSection ( c * contextmodel . ReqContext , treeRoot * navtree . NavTreeRoot , plugin pluginstore . Plugin , appLink * navtree . NavLink ) {
2022-10-05 17:46:27 +08:00
// Handle moving apps into specific navtree sections
2024-01-06 07:19:12 +08:00
var alertingNodes [ ] * navtree . NavLink
2022-10-05 17:46:27 +08:00
alertingNode := treeRoot . FindById ( navtree . NavIDAlerting )
2024-01-06 07:19:12 +08:00
if alertingNode != nil {
alertingNodes = append ( alertingNodes , alertingNode )
}
2022-11-07 22:19:31 +08:00
sectionID := navtree . NavIDApps
2022-09-28 14:29:35 +08:00
2022-10-05 17:46:27 +08:00
if navConfig , hasOverride := s . navigationAppConfig [ plugin . ID ] ; hasOverride {
appLink . SortWeight = navConfig . SortWeight
sectionID = navConfig . SectionID
2022-11-14 17:01:23 +08:00
if len ( navConfig . Text ) > 0 {
appLink . Text = navConfig . Text
}
2023-01-10 18:29:07 +08:00
if len ( navConfig . Icon ) > 0 {
appLink . Icon = navConfig . Icon
}
2025-04-02 18:43:21 +08:00
if len ( navConfig . SubTitle ) > 0 {
appLink . SubTitle = navConfig . SubTitle
}
2025-04-09 21:32:34 +08:00
if navConfig . IsNew {
appLink . IsNew = true
}
2022-10-05 17:46:27 +08:00
}
2022-09-23 04:04:48 +08:00
2022-11-18 17:05:45 +08:00
if sectionID == navtree . NavIDRoot {
treeRoot . AddSection ( appLink )
} else if navNode := treeRoot . FindById ( sectionID ) ; navNode != nil {
2022-10-05 17:46:27 +08:00
navNode . Children = append ( navNode . Children , appLink )
} else {
switch sectionID {
case navtree . NavIDApps :
treeRoot . AddSection ( & navtree . NavLink {
2024-10-01 18:31:31 +08:00
Text : "More apps" ,
2022-11-29 20:02:58 +08:00
Icon : "layer-group" ,
2022-10-05 17:46:27 +08:00
SubTitle : "App plugins that extend the Grafana experience" ,
Id : navtree . NavIDApps ,
Children : [ ] * navtree . NavLink { appLink } ,
SortWeight : navtree . WeightApps ,
Url : s . cfg . AppSubURL + "/apps" ,
} )
2025-05-27 22:05:28 +08:00
case navtree . NavIDObservability :
2022-10-05 17:46:27 +08:00
treeRoot . AddSection ( & navtree . NavLink {
2023-04-21 15:39:49 +08:00
Text : "Observability" ,
2025-05-27 22:05:28 +08:00
Id : navtree . NavIDObservability ,
2023-04-21 15:39:49 +08:00
SubTitle : "Observability and infrastructure apps" ,
2022-10-05 17:46:27 +08:00
Icon : "heart-rate" ,
2025-05-27 22:05:28 +08:00
SortWeight : navtree . WeightObservability ,
2022-10-05 17:46:27 +08:00
Children : [ ] * navtree . NavLink { appLink } ,
2025-05-27 22:05:28 +08:00
Url : s . cfg . AppSubURL + "/observability" ,
2022-10-05 17:46:27 +08:00
} )
2023-11-30 23:18:05 +08:00
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" ,
} )
2022-10-05 17:46:27 +08:00
case navtree . NavIDAlertsAndIncidents :
2023-03-17 19:08:36 +08:00
alertsAndIncidentsChildren := [ ] * navtree . NavLink { }
2024-01-06 07:19:12 +08:00
for _ , alertingNode := range alertingNodes {
2025-04-22 20:20:36 +08:00
if alertingNode . Id == "alerting" {
alertingNode . SortWeight = 2
}
2023-03-17 19:08:36 +08:00
alertsAndIncidentsChildren = append ( alertsAndIncidentsChildren , alertingNode )
2022-10-05 17:46:27 +08:00
treeRoot . RemoveSection ( alertingNode )
2022-09-23 04:04:48 +08:00
}
2023-03-17 19:08:36 +08:00
alertsAndIncidentsChildren = append ( alertsAndIncidentsChildren , appLink )
treeRoot . AddSection ( & navtree . NavLink {
2023-03-31 16:40:06 +08:00
Text : "Alerts & IRM" ,
2023-03-17 19:08:36 +08:00
Id : navtree . NavIDAlertsAndIncidents ,
SubTitle : "Alerting and incident management apps" ,
Icon : "bell" ,
SortWeight : navtree . WeightAlertsAndIncidents ,
Children : alertsAndIncidentsChildren ,
Url : s . cfg . AppSubURL + "/alerts-and-incidents" ,
} )
2023-12-12 18:57:52 +08:00
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" ,
} )
2022-10-05 17:46:27 +08:00
default :
s . log . Error ( "Plugin app nav id not found" , "pluginId" , plugin . ID , "navId" , sectionID )
2022-09-23 04:04:48 +08:00
}
}
2022-09-28 14:29:35 +08:00
}
2023-01-27 15:50:36 +08:00
func ( s * ServiceImpl ) hasAccessToInclude ( c * contextmodel . ReqContext , pluginID string ) func ( include * plugins . Includes ) bool {
2022-11-16 22:54:04 +08:00
hasAccess := ac . HasAccess ( s . accessControl , c )
return func ( include * plugins . Includes ) bool {
2025-02-25 20:44:40 +08:00
if include . RequiresRBACAction ( ) && ! hasAccess ( pluginaccesscontrol . GetPluginRouteEvaluator ( pluginID , include . Action ) ) {
2022-11-16 22:54:04 +08:00
s . log . Debug ( "plugin include is covered by RBAC, user doesn't have access" ,
"plugin" , pluginID ,
"include" , include . Name )
return false
2025-02-25 20:44:40 +08:00
} else if ! include . RequiresRBACAction ( ) && ! c . HasUserRole ( include . Role ) {
2022-11-16 22:54:04 +08:00
return false
}
return true
}
}
2022-09-28 14:29:35 +08:00
func ( s * ServiceImpl ) readNavigationSettings ( ) {
s . navigationAppConfig = map [ string ] NavigationAppConfig {
2025-05-27 22:05:28 +08:00
"grafana-asserts-app" : { SectionID : navtree . NavIDObservability , SortWeight : 1 , Icon : "asserts" } ,
"grafana-app-observability-app" : { SectionID : navtree . NavIDObservability , SortWeight : 2 , Text : "Application" } ,
"grafana-csp-app" : { SectionID : navtree . NavIDObservability , SortWeight : 3 , Icon : "cloud-provider" } ,
"grafana-k8s-app" : { SectionID : navtree . NavIDObservability , SortWeight : 4 , Text : "Kubernetes" } ,
"grafana-dbo11y-app" : { SectionID : navtree . NavIDObservability , SortWeight : 5 , Text : "Databases" } ,
"grafana-kowalski-app" : { SectionID : navtree . NavIDObservability , SortWeight : 6 , Text : "Frontend" } ,
2025-04-12 04:45:14 +08:00
"grafana-metricsdrilldown-app" : { SectionID : navtree . NavIDDrilldown , SortWeight : 1 , Text : "Metrics" } ,
2025-02-21 01:56:55 +08:00
"grafana-lokiexplore-app" : { SectionID : navtree . NavIDDrilldown , SortWeight : 2 , Text : "Logs" } ,
"grafana-exploretraces-app" : { SectionID : navtree . NavIDDrilldown , SortWeight : 3 , Text : "Traces" } ,
"grafana-pyroscope-app" : { SectionID : navtree . NavIDDrilldown , SortWeight : 4 , Text : "Profiles" } ,
2024-02-06 21:43:11 +08:00
"grafana-synthetic-monitoring-app" : { SectionID : navtree . NavIDTestingAndSynthetics , SortWeight : 2 , Text : "Synthetics" } ,
2025-04-22 20:20:36 +08:00
"grafana-irm-app" : { SectionID : navtree . NavIDAlertsAndIncidents , SortWeight : 3 , Text : "IRM" } ,
"grafana-oncall-app" : { SectionID : navtree . NavIDAlertsAndIncidents , SortWeight : 4 , Text : "OnCall" } ,
"grafana-incident-app" : { SectionID : navtree . NavIDAlertsAndIncidents , SortWeight : 5 , Text : "Incident" } ,
2025-05-20 23:24:21 +08:00
"grafana-ml-app" : { SectionID : navtree . NavIDRoot , SortWeight : navtree . WeightAIAndML , Text : "AI & machine learning" , SubTitle : "Explore AI and machine learning features" , Icon : "gf-ml-alt" } ,
2025-04-22 20:20:36 +08:00
"grafana-slo-app" : { SectionID : navtree . NavIDAlertsAndIncidents , SortWeight : 7 } ,
2024-06-12 23:45:13 +08:00
"grafana-cloud-link-app" : { SectionID : navtree . NavIDCfgPlugins , SortWeight : 3 } ,
2023-10-18 00:15:51 +08:00
"grafana-costmanagementui-app" : { SectionID : navtree . NavIDCfg , Text : "Cost management" } ,
2024-01-08 22:25:11 +08:00
"grafana-adaptive-metrics-app" : { SectionID : navtree . NavIDCfg , Text : "Adaptive Metrics" } ,
2024-07-04 00:59:47 +08:00
"grafana-adaptivelogs-app" : { SectionID : navtree . NavIDCfg , Text : "Adaptive Logs" } ,
2024-12-18 23:05:18 +08:00
"grafana-adaptivetraces-app" : { SectionID : navtree . NavIDCfg , Text : "Adaptive Traces" } ,
2024-05-16 05:26:18 +08:00
"grafana-attributions-app" : { SectionID : navtree . NavIDCfg , Text : "Attributions" } ,
2024-01-08 22:25:11 +08:00
"grafana-logvolumeexplorer-app" : { SectionID : navtree . NavIDCfg , Text : "Log Volume Explorer" } ,
2023-02-03 21:48:06 +08:00
"grafana-easystart-app" : { SectionID : navtree . NavIDRoot , SortWeight : navtree . WeightApps + 1 , Text : "Connections" , Icon : "adjust-circle" } ,
2024-02-06 21:43:11 +08:00
"k6-app" : { SectionID : navtree . NavIDTestingAndSynthetics , SortWeight : 1 , Text : "Performance" } ,
2022-09-23 04:04:48 +08:00
}
2025-02-20 21:53:04 +08:00
if s . features . IsEnabledGlobally ( featuremgmt . FlagGrafanaAdvisor ) {
2025-04-02 18:43:21 +08:00
s . navigationAppConfig [ "grafana-advisor-app" ] = NavigationAppConfig {
SectionID : navtree . NavIDCfg ,
Text : "Advisor" ,
SubTitle : "Keep Grafana running smoothly and securely" ,
2025-04-09 21:32:34 +08:00
IsNew : true ,
2025-04-02 18:43:21 +08:00
}
2025-02-20 21:53:04 +08:00
}
2022-09-28 14:29:35 +08:00
s . navigationAppPathConfig = map [ string ] NavigationAppConfig {
2024-06-12 23:45:13 +08:00
"/a/grafana-auth-app" : { SectionID : navtree . NavIDCfgAccess , SortWeight : 2 } ,
2022-09-28 14:29:35 +08:00
}
2022-10-06 18:57:03 +08:00
appSections := s . cfg . Raw . Section ( "navigation.app_sections" )
appStandalonePages := s . cfg . Raw . Section ( "navigation.app_standalone_pages" )
2022-09-28 14:29:35 +08:00
2022-10-06 18:57:03 +08:00
for _ , key := range appSections . Keys ( ) {
2022-09-28 14:29:35 +08:00
pluginId := key . Name ( )
// Support <id> <weight> value
2022-10-06 18:57:03 +08:00
values := util . SplitString ( appSections . Key ( key . Name ( ) ) . MustString ( "" ) )
2022-09-28 14:29:35 +08:00
appCfg := & NavigationAppConfig { SectionID : values [ 0 ] }
if len ( values ) > 1 {
if weight , err := strconv . ParseInt ( values [ 1 ] , 10 , 64 ) ; err == nil {
appCfg . SortWeight = weight
}
}
2023-04-21 15:39:49 +08:00
// 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
}
2022-09-28 14:29:35 +08:00
}
2022-10-06 18:57:03 +08:00
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
}
2022-09-23 04:04:48 +08:00
}