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/navtree.go

575 lines
20 KiB

package navtreeimpl
import (
"sort"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/authn"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/navtree"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/services/star"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlesimpl"
"github.com/grafana/grafana/pkg/setting"
)
type ServiceImpl struct {
cfg *setting.Cfg
log log.Logger
accessControl ac.AccessControl
authnService authn.Service
pluginStore pluginstore.Store
pluginSettings pluginsettings.Service
starService star.Service
features featuremgmt.FeatureToggles
dashboardService dashboards.DashboardService
accesscontrolService ac.Service
kvStore kvstore.KVStore
apiKeyService apikey.Service
license licensing.Licensing
// Navigation
navigationAppConfig map[string]NavigationAppConfig
navigationAppPathConfig map[string]NavigationAppConfig
}
type NavigationAppConfig struct {
SectionID string
SortWeight int64
Text string
Icon string
}
func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStore pluginstore.Store, pluginSettings pluginsettings.Service, starService star.Service,
features featuremgmt.FeatureToggles, dashboardService dashboards.DashboardService, accesscontrolService ac.Service, kvStore kvstore.KVStore, apiKeyService apikey.Service,
license licensing.Licensing, authnService authn.Service) navtree.Service {
service := &ServiceImpl{
cfg: cfg,
log: log.New("navtree service"),
accessControl: accessControl,
authnService: authnService,
pluginStore: pluginStore,
pluginSettings: pluginSettings,
starService: starService,
features: features,
dashboardService: dashboardService,
accesscontrolService: accesscontrolService,
kvStore: kvStore,
apiKeyService: apiKeyService,
license: license,
}
service.readNavigationSettings()
return service
}
//nolint:gocyclo
func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Preference) (*navtree.NavTreeRoot, error) {
hasAccess := ac.HasAccess(s.accessControl, c)
treeRoot := &navtree.NavTreeRoot{}
treeRoot.AddSection(s.getHomeNode(c, prefs))
if hasAccess(ac.EvalPermission(dashboards.ActionDashboardsRead)) {
starredItemsLinks, err := s.buildStarredItemsNavLinks(c)
if err != nil {
return nil, err
}
treeRoot.AddSection(&navtree.NavLink{
Text: "Starred",
Id: "starred",
Icon: "star",
SortWeight: navtree.WeightSavedItems,
Children: starredItemsLinks,
EmptyMessageId: "starred-empty",
Url: s.cfg.AppSubURL + "/dashboards?starred",
})
}
if c.IsPublicDashboardView() || hasAccess(ac.EvalAny(
ac.EvalPermission(dashboards.ActionFoldersRead), ac.EvalPermission(dashboards.ActionFoldersCreate),
ac.EvalPermission(dashboards.ActionDashboardsRead), ac.EvalPermission(dashboards.ActionDashboardsCreate)),
) {
dashboardChildLinks := s.buildDashboardNavLinks(c)
dashboardLink := &navtree.NavLink{
Text: "Dashboards",
Id: navtree.NavIDDashboards,
SubTitle: "Create and manage dashboards to visualize your data",
Icon: "apps",
Url: s.cfg.AppSubURL + "/dashboards",
SortWeight: navtree.WeightDashboard,
Children: dashboardChildLinks,
}
treeRoot.AddSection(dashboardLink)
}
if s.cfg.ExploreEnabled && hasAccess(ac.EvalPermission(ac.ActionDatasourcesExplore)) {
exploreChildNavLinks := s.buildExploreNavLinks(c)
treeRoot.AddSection(&navtree.NavLink{
Text: "Explore",
Id: navtree.NavIDExplore,
SubTitle: "Explore your data",
Icon: "compass",
SortWeight: navtree.WeightExplore,
Url: s.cfg.AppSubURL + "/explore",
Children: exploreChildNavLinks,
})
}
if s.cfg.ProfileEnabled && c.IsSignedIn {
treeRoot.AddSection(s.getProfileNode(c))
}
_, uaIsDisabledForOrg := s.cfg.UnifiedAlerting.DisabledOrgs[c.SignedInUser.GetOrgID()]
uaVisibleForOrg := s.cfg.UnifiedAlerting.IsEnabled() && !uaIsDisabledForOrg
if uaVisibleForOrg {
if alertingSection := s.buildAlertNavLinks(c); alertingSection != nil {
treeRoot.AddSection(alertingSection)
}
}
if connectionsSection := s.buildDataConnectionsNavLink(c); connectionsSection != nil {
treeRoot.AddSection(connectionsSection)
}
orgAdminNode, err := s.getAdminNode(c)
if orgAdminNode != nil && len(orgAdminNode.Children) > 0 {
treeRoot.AddSection(orgAdminNode)
} else if err != nil {
return nil, err
}
s.addHelpLinks(treeRoot, c)
if err := s.addAppLinks(treeRoot, c); err != nil {
return nil, err
}
// remove user access if empty. Happens if grafana-auth-app is not injected
if sec := treeRoot.FindById(navtree.NavIDCfgAccess); sec != nil && len(sec.Children) == 0 {
treeRoot.RemoveSectionByID(navtree.NavIDCfgAccess)
}
// double-check and remove admin menu if empty
if sec := treeRoot.FindById(navtree.NavIDCfg); sec != nil && len(sec.Children) == 0 {
treeRoot.RemoveSectionByID(navtree.NavIDCfg)
}
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagPinNavItems) && c.IsSignedIn {
treeRoot.AddSection(&navtree.NavLink{
Text: "Bookmarks",
Id: navtree.NavIDBookmarks,
Icon: "bookmark",
SortWeight: navtree.WeightBookmarks,
Children: []*navtree.NavLink{},
EmptyMessageId: "bookmarks-empty",
Url: s.cfg.AppSubURL + "/bookmarks",
})
}
return treeRoot, nil
}
func (s *ServiceImpl) getHomeNode(c *contextmodel.ReqContext, prefs *pref.Preference) *navtree.NavLink {
homeUrl := s.cfg.AppSubURL + "/"
if !c.IsSignedIn && !s.cfg.AnonymousEnabled {
homeUrl = s.cfg.AppSubURL + "/login"
} else {
homePage := s.cfg.HomePage
if prefs.HomeDashboardID == 0 && len(homePage) > 0 {
homeUrl = homePage
}
}
homeNode := &navtree.NavLink{
Text: "Home",
Id: "home",
Url: homeUrl,
Icon: "home-alt",
SortWeight: navtree.WeightHome,
}
ctx := c.Req.Context()
if s.features.IsEnabled(ctx, featuremgmt.FlagHomeSetupGuide) {
var children []*navtree.NavLink
// setup guide (a submenu item under Home)
children = append(children, &navtree.NavLink{
Id: "home-setup-guide",
Text: "Setup guide",
Url: homeUrl + "/setup-guide",
SortWeight: navtree.WeightHome,
})
homeNode.Children = children
}
return homeNode
}
func isSupportBundlesEnabled(s *ServiceImpl) bool {
return s.cfg.SectionWithEnvOverrides("support_bundles").Key("enabled").MustBool(true)
}
func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *contextmodel.ReqContext) {
if s.cfg.HelpEnabled {
// The version subtitle is set later by NavTree.ApplyHelpVersion
helpNode := &navtree.NavLink{
Text: "Help",
Id: "help",
Url: "#",
Icon: "question-circle",
SortWeight: navtree.WeightHelp,
Children: []*navtree.NavLink{},
}
treeRoot.AddSection(helpNode)
hasAccess := ac.HasAccess(s.accessControl, c)
supportBundleAccess := ac.EvalAny(
ac.EvalPermission(supportbundlesimpl.ActionRead),
ac.EvalPermission(supportbundlesimpl.ActionCreate),
)
if isSupportBundlesEnabled(s) && hasAccess(supportBundleAccess) {
supportBundleNode := &navtree.NavLink{
Text: "Support bundles",
Id: "support-bundles",
Url: "/support-bundles",
Icon: "wrench",
SortWeight: navtree.WeightHelp,
}
helpNode.Children = append(helpNode.Children, supportBundleNode)
}
}
}
func (s *ServiceImpl) getProfileNode(c *contextmodel.ReqContext) *navtree.NavLink {
// Only set login if it's different from the name
var login string
if c.SignedInUser.GetLogin() != c.SignedInUser.GetName() {
login = c.SignedInUser.GetLogin()
}
gravatarURL := dtos.GetGravatarUrl(s.cfg, c.SignedInUser.GetEmail())
children := []*navtree.NavLink{
{
Text: "Profile", Id: "profile/settings", Url: s.cfg.AppSubURL + "/profile", Icon: "sliders-v-alt",
},
}
children = append(children, &navtree.NavLink{
Text: "Notification history", Id: "profile/notifications", Url: s.cfg.AppSubURL + "/profile/notifications", Icon: "bell",
})
if s.cfg.AddChangePasswordLink() {
children = append(children, &navtree.NavLink{
Text: "Change password", Id: "profile/password", Url: s.cfg.AppSubURL + "/profile/password",
Icon: "lock",
})
}
if !s.cfg.DisableSignoutMenu {
// add sign out first
children = append(children, &navtree.NavLink{
Text: "Sign out",
Id: "sign-out",
Url: s.cfg.AppSubURL + "/logout",
Icon: "arrow-from-right",
Target: "_self",
HideFromTabs: true,
})
}
return &navtree.NavLink{
Text: c.SignedInUser.GetName(),
SubTitle: login,
Id: "profile",
Img: gravatarURL,
Url: s.cfg.AppSubURL + "/profile",
SortWeight: navtree.WeightProfile,
Children: children,
RoundIcon: true,
}
}
func (s *ServiceImpl) buildStarredItemsNavLinks(c *contextmodel.ReqContext) ([]*navtree.NavLink, error) {
starredItemsChildNavs := []*navtree.NavLink{}
userID, _ := identity.UserIdentifier(c.SignedInUser.GetID())
query := star.GetUserStarsQuery{
UserID: userID,
}
starredDashboardResult, err := s.starService.GetByUser(c.Req.Context(), &query)
if err != nil {
return nil, err
}
if len(starredDashboardResult.UserStars) > 0 {
var uids []string
for uid := range starredDashboardResult.UserStars {
uids = append(uids, uid)
}
starredDashboards, err := s.dashboardService.GetDashboards(c.Req.Context(), &dashboards.GetDashboardsQuery{DashboardUIDs: uids, OrgID: c.SignedInUser.GetOrgID()})
if err != nil {
return nil, err
}
// Set a loose limit to the first 50 starred dashboards found
if len(starredDashboards) > 50 {
starredDashboards = starredDashboards[:50]
}
sort.Slice(starredDashboards, func(i, j int) bool {
return starredDashboards[i].Title < starredDashboards[j].Title
})
for _, starredItem := range starredDashboards {
starredItemsChildNavs = append(starredItemsChildNavs, &navtree.NavLink{
Id: "starred/" + starredItem.UID,
Text: starredItem.Title,
Url: starredItem.GetURL(),
})
}
}
return starredItemsChildNavs, nil
}
func (s *ServiceImpl) buildDashboardNavLinks(c *contextmodel.ReqContext) []*navtree.NavLink {
hasAccess := ac.HasAccess(s.accessControl, c)
dashboardChildNavs := []*navtree.NavLink{}
if c.IsSignedIn {
if c.SignedInUser.HasRole(org.RoleViewer) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Playlists", SubTitle: "Groups of dashboards that are displayed in a sequence", Id: "dashboards/playlists", Url: s.cfg.AppSubURL + "/playlists", Icon: "presentation-play",
})
}
if s.cfg.SnapshotEnabled && hasAccess(ac.EvalPermission(dashboards.ActionSnapshotsRead)) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Snapshots",
SubTitle: "Interactive, publicly available, point-in-time representations of dashboards",
Id: "dashboards/snapshots",
Url: s.cfg.AppSubURL + "/dashboard/snapshots",
Icon: "camera",
})
}
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Library panels",
SubTitle: "Reusable panels that can be added to multiple dashboards",
Id: "dashboards/library-panels",
Url: s.cfg.AppSubURL + "/library-panels",
Icon: "library-panel",
})
if s.cfg.PublicDashboardsEnabled {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Public dashboards",
Id: "dashboards/public",
Url: s.cfg.AppSubURL + "/dashboard/public",
Icon: "library-panel",
})
}
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagDashboardRestore) && (c.SignedInUser.GetOrgRole() == org.RoleAdmin || c.IsGrafanaAdmin) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Recently deleted",
SubTitle: "Any items listed here for more than 30 days will be automatically deleted.",
Id: "dashboards/recently-deleted",
Url: s.cfg.AppSubURL + "/dashboard/recently-deleted",
})
}
}
if hasAccess(ac.EvalPermission(dashboards.ActionDashboardsCreate)) {
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "New dashboard", Icon: "plus", Url: s.cfg.AppSubURL + "/dashboard/new", HideFromTabs: true, Id: "dashboards/new", IsCreateAction: true,
})
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
Text: "Import dashboard", SubTitle: "Import dashboard from file or Grafana.com", Id: "dashboards/import", Icon: "plus",
Url: s.cfg.AppSubURL + "/dashboard/import", HideFromTabs: true, IsCreateAction: true,
})
}
return dashboardChildNavs
}
func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.NavLink {
hasAccess := ac.HasAccess(s.accessControl, c)
var alertChildNavs []*navtree.NavLink
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "Alert rules", SubTitle: "Rules that determine whether an alert will fire", Id: "alert-list", Url: s.cfg.AppSubURL + "/alerting/list", Icon: "list-ul",
})
}
contactPointsPerms := []ac.Evaluator{
ac.EvalPermission(ac.ActionAlertingNotificationsRead),
ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead),
}
// With the new alerting API, we have other permissions to consider. We don't want to consider these with the old
// alerting API to maintain backwards compatibility.
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingApiServer) {
contactPointsPerms = append(contactPointsPerms,
ac.EvalPermission(ac.ActionAlertingReceiversRead),
ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets),
ac.EvalPermission(ac.ActionAlertingReceiversCreate),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesRead),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesWrite),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesDelete),
)
}
if hasAccess(ac.EvalAny(contactPointsPerms...)) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "Contact points", SubTitle: "Choose how to notify your contact points when an alert instance fires", Id: "receivers", Url: s.cfg.AppSubURL + "/alerting/notifications",
Icon: "comment-alt-share",
})
}
if hasAccess(ac.EvalAny(
ac.EvalPermission(ac.ActionAlertingNotificationsRead),
ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead),
ac.EvalPermission(ac.ActionAlertingRoutesRead),
ac.EvalPermission(ac.ActionAlertingRoutesWrite),
ac.EvalPermission(ac.ActionAlertingNotificationsTimeIntervalsRead),
ac.EvalPermission(ac.ActionAlertingNotificationsTimeIntervalsWrite),
)) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Notification policies", SubTitle: "Determine how alerts are routed to contact points", Id: "am-routes", Url: s.cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"})
}
if hasAccess(ac.EvalAny(
ac.EvalPermission(ac.ActionAlertingInstanceRead),
ac.EvalPermission(ac.ActionAlertingInstancesExternalRead),
ac.EvalPermission(ac.ActionAlertingSilencesRead),
)) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Silences", SubTitle: "Stop notifications from one or more alerting rules", Id: "silences", Url: s.cfg.AppSubURL + "/alerting/silences", Icon: "bell-slash"})
}
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingInstanceRead), ac.EvalPermission(ac.ActionAlertingInstancesExternalRead))) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Alert groups", SubTitle: "See grouped alerts with active notifications", Id: "groups", Url: s.cfg.AppSubURL + "/alerting/groups", Icon: "layer-group"})
}
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingCentralAlertHistory) {
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead))) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "History",
SubTitle: "View a history of all alert events generated by your Grafana-managed alert rules. All alert events are displayed regardless of whether silences or mute timings are set.",
Id: "alerts-history",
Url: s.cfg.AppSubURL + "/alerting/history",
Icon: "history",
})
}
}
if c.SignedInUser.GetOrgRole() == org.RoleAdmin {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "Settings", Id: "alerting-admin", Url: s.cfg.AppSubURL + "/alerting/admin",
Icon: "cog",
})
}
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleCreate), ac.EvalPermission(ac.ActionAlertingRuleExternalWrite))) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "Create alert rule", SubTitle: "Create an alert rule", Id: "alert",
Icon: "plus", Url: s.cfg.AppSubURL + "/alerting/new", HideFromTabs: true, IsCreateAction: true,
})
}
if len(alertChildNavs) > 0 {
var alertNav = navtree.NavLink{
Text: "Alerting",
SubTitle: "Learn about problems in your systems moments after they occur",
Id: navtree.NavIDAlerting,
Icon: "bell",
Children: alertChildNavs,
SortWeight: navtree.WeightAlerting,
Url: s.cfg.AppSubURL + "/alerting",
}
return &alertNav
}
return nil
}
func (s *ServiceImpl) buildDataConnectionsNavLink(c *contextmodel.ReqContext) *navtree.NavLink {
hasAccess := ac.HasAccess(s.accessControl, c)
var children []*navtree.NavLink
var navLink *navtree.NavLink
baseUrl := s.cfg.AppSubURL + "/connections"
if hasAccess(datasources.ConfigurationPageAccess) {
// Add new connection
children = append(children, &navtree.NavLink{
Id: "connections-add-new-connection",
Text: "Add new connection",
SubTitle: "Browse and create new connections",
Url: baseUrl + "/add-new-connection",
Children: []*navtree.NavLink{},
Keywords: []string{"csv", "graphite", "json", "loki", "prometheus", "sql", "tempo"},
})
// Data sources
children = append(children, &navtree.NavLink{
Id: "connections-datasources",
Text: "Data sources",
SubTitle: "View and manage your connected data source connections",
Url: baseUrl + "/datasources",
Children: []*navtree.NavLink{},
})
}
if len(children) > 0 {
// Connections (main)
navLink = &navtree.NavLink{
Text: "Connections",
Icon: "adjust-circle",
Id: "connections",
Url: baseUrl,
Children: children,
SortWeight: navtree.WeightDataConnections,
}
return navLink
}
return nil
}
func (s *ServiceImpl) buildExploreNavLinks(c *contextmodel.ReqContext) []*navtree.NavLink {
exploreChildNavs := []*navtree.NavLink{}
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetrics) {
exploreChildNavs = append(exploreChildNavs, &navtree.NavLink{
Text: "Metrics",
SubTitle: "Queryless exploration of your metrics",
Id: "explore/metrics",
Url: s.cfg.AppSubURL + "/explore/metrics",
Icon: "code-branch",
})
}
return exploreChildNavs
}