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

529 lines
18 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)
}
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagPinNavItems) {
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,
}
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.GetDisplayName() {
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.GetDisplayName(),
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 ids []int64
for id := range starredDashboardResult.UserStars {
ids = append(ids, id)
}
starredDashboards, err := s.dashboardService.GetDashboards(c.Req.Context(), &dashboards.GetDashboardsQuery{DashboardIDs: ids, 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{}
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 c.IsSignedIn {
if s.cfg.SnapshotEnabled {
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.features.IsEnabled(c.Req.Context(), featuremgmt.FlagPublicDashboards) && 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.FlagDashboardRestoreUI) && c.SignedInUser.GetOrgRole() == org.RoleAdmin {
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",
})
}
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingNotificationsRead), ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead))) {
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",
})
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) {
Alerting: Central alert history part4 (#90088) * Implement EventDetails for expanded rows and pagination on the events list * Add test for getPanelDataForRule function * prettier * refactor EventState component * create interfaces for props * Add missing translations * Update some comments * Add plus button in alertrulename , to add it into the filter * Add plus button to add filters from the list labels and alert name * Add clear labels filter button * run prettier * fix RBAC checks * Update AlertLabels onLabelClick functionality * add limit=0 in useCombinedRule call * Add filter by state * remove plus button in labels * Fix state filter * Add filter by previous state * fix some errors after solving conflicts * Add comments and remove some type assertions * Update the number of transitions calculation to be for each instance * Add tests for state filters * remove type assertion * Address review comments * Update returnTo prop in alert list view url * Update translations * address review comments * prettier * update cursor to pointer * Address Deyan review comments * address review pr comments from Deyan * fix label styles * Visualize expanded row as a state graph and address some pr review comments * Add warning when limit of events is reached and rename onClickLabel * Update texts * Fix translations * Update some Labels in the expanded states visualization * move getPanelDataForRule to a separate file * Add header to the list of events * Move HistoryErrorMessage to a separate file * remove getPanelDataForRule function and test * add comment * fitler by instance label results shown inthe state chart * remove defaults.ini changes * fix having single event on time state chart --------- Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
12 months ago
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
}