diff --git a/pkg/api/index.go b/pkg/api/index.go index 7868ddce08f..23ce1e540e0 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -158,7 +158,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV hs.HooksService.RunIndexDataHooks(&data, c) - data.NavTree.ApplyAdminIA() + data.NavTree.ApplyCostManagementIA() data.NavTree.ApplyHelpVersion(data.Settings.BuildInfo.VersionString) // RunIndexDataHooks can modify the version string data.NavTree.Sort() diff --git a/pkg/services/licensing/oss.go b/pkg/services/licensing/oss.go index c01caac6bab..d14b8798363 100644 --- a/pkg/services/licensing/oss.go +++ b/pkg/services/licensing/oss.go @@ -59,12 +59,13 @@ func ProvideService(cfg *setting.Cfg, hooksService *hooks.HooksService) *OSSLice return } - if adminNode := indexData.NavTree.FindById(navtree.NavIDCfg); adminNode != nil { + if adminNode := indexData.NavTree.FindById(navtree.NavIDCfgGeneral); adminNode != nil { adminNode.Children = append(adminNode.Children, &navtree.NavLink{ - Text: "Stats and license", - Id: "upgrading", - Url: l.LicenseURL(req.IsGrafanaAdmin), - Icon: "unlock", + Text: "Stats and license", + Id: "upgrading", + Url: l.LicenseURL(req.IsGrafanaAdmin), + Icon: "unlock", + SortWeight: -1, }) } }) diff --git a/pkg/services/navtree/models.go b/pkg/services/navtree/models.go index c9de55bc095..b3c22e7fdb5 100644 --- a/pkg/services/navtree/models.go +++ b/pkg/services/navtree/models.go @@ -136,113 +136,50 @@ func (root *NavTreeRoot) ApplyHelpVersion(version string) { } } -func (root *NavTreeRoot) ApplyAdminIA() { +func (root *NavTreeRoot) ApplyCostManagementIA() { orgAdminNode := root.FindById(NavIDCfg) + var costManagementApp *NavLink + var adaptiveMetricsApp *NavLink + var attributionsApp *NavLink + var logVolumeExplorerApp *NavLink if orgAdminNode != nil { adminNodeLinks := []*NavLink{} - - generalNodeLinks := []*NavLink{} - generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("upgrading")) // TODO does this even exist - generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("licensing")) - generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("org-settings")) - generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("server-settings")) - generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("global-orgs")) - generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("feature-toggles")) - generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("storage")) - generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("migrate-to-cloud")) - generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("banner-settings")) - - generalNode := &NavLink{ - Text: "General", - SubTitle: "Manage default preferences and settings across Grafana", - Id: NavIDCfgGeneral, - Url: "/admin/general", - Icon: "shield", - Children: generalNodeLinks, + for _, element := range orgAdminNode.Children { + switch navId := element.Id; navId { + case "plugin-page-grafana-costmanagementui-app": + costManagementApp = element + case "plugin-page-grafana-adaptive-metrics-app": + adaptiveMetricsApp = element + case "plugin-page-grafana-attributions-app": + attributionsApp = element + case "plugin-page-grafana-logvolumeexplorer-app": + logVolumeExplorerApp = element + default: + adminNodeLinks = append(adminNodeLinks, element) + } } - pluginsNodeLinks := []*NavLink{} - pluginsNodeLinks = AppendIfNotNil(pluginsNodeLinks, root.FindById("plugins")) - pluginsNodeLinks = AppendIfNotNil(pluginsNodeLinks, root.FindById("datasources")) - pluginsNodeLinks = AppendIfNotNil(pluginsNodeLinks, root.FindById("recordedQueries")) - pluginsNodeLinks = AppendIfNotNil(pluginsNodeLinks, root.FindById("correlations")) - pluginsNodeLinks = AppendIfNotNil(pluginsNodeLinks, root.FindById("plugin-page-grafana-cloud-link-app")) + if costManagementApp != nil { + costManagementMetricsNode := FindByURL(costManagementApp.Children, "/a/grafana-costmanagementui-app/metrics") + if costManagementMetricsNode != nil { + if adaptiveMetricsApp != nil { + costManagementMetricsNode.Children = append(costManagementMetricsNode.Children, adaptiveMetricsApp) + } + if attributionsApp != nil { + costManagementMetricsNode.Children = append(costManagementMetricsNode.Children, attributionsApp) + } + } - pluginsNode := &NavLink{ - Text: "Plugins and data", - SubTitle: "Install plugins and define the relationships between data", - Id: NavIDCfgPlugins, - Url: "/admin/plugins", - Icon: "shield", - Children: pluginsNodeLinks, - } - - accessNodeLinks := []*NavLink{} - accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("global-users")) - accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("teams")) - accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("standalone-plugin-page-/a/grafana-auth-app")) - accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("serviceaccounts")) - accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("apikeys")) - - usersNode := &NavLink{ - Text: "Users and access", - SubTitle: "Configure access for individual users, teams, and service accounts", - Id: NavIDCfgAccess, - Url: "/admin/access", - Icon: "shield", - Children: accessNodeLinks, - } - - if len(generalNode.Children) > 0 { - adminNodeLinks = append(adminNodeLinks, generalNode) - } - - if len(pluginsNode.Children) > 0 { - adminNodeLinks = append(adminNodeLinks, pluginsNode) - } - - if len(usersNode.Children) > 0 { - adminNodeLinks = append(adminNodeLinks, usersNode) - } - - authenticationNode := root.FindById("authentication") - if authenticationNode != nil { - authenticationNode.IsSection = true - adminNodeLinks = append(adminNodeLinks, authenticationNode) - } - - costManagementNode := root.FindById("plugin-page-grafana-costmanagementui-app") - - if costManagementNode != nil { - adminNodeLinks = append(adminNodeLinks, costManagementNode) - } - - costManagementMetricsNode := root.FindByURL("/a/grafana-costmanagementui-app/metrics") - adaptiveMetricsNode := root.FindById("plugin-page-grafana-adaptive-metrics-app") - - if costManagementMetricsNode != nil && adaptiveMetricsNode != nil { - costManagementMetricsNode.Children = append(costManagementMetricsNode.Children, adaptiveMetricsNode) - } - - attributionsNode := root.FindById("plugin-page-grafana-attributions-app") - - if costManagementMetricsNode != nil && attributionsNode != nil { - costManagementMetricsNode.Children = append(costManagementMetricsNode.Children, attributionsNode) - } - - costManagementLogsNode := root.FindByURL("/a/grafana-costmanagementui-app/logs") - logVolumeExplorerNode := root.FindById("plugin-page-grafana-logvolumeexplorer-app") - - if costManagementLogsNode != nil && logVolumeExplorerNode != nil { - costManagementLogsNode.Children = append(costManagementLogsNode.Children, logVolumeExplorerNode) - } - - if len(adminNodeLinks) > 0 { - orgAdminNode.Children = adminNodeLinks - } else { - root.RemoveSection(orgAdminNode) + costManagementLogsNode := FindByURL(costManagementApp.Children, "/a/grafana-costmanagementui-app/logs") + if costManagementLogsNode != nil { + if logVolumeExplorerApp != nil { + costManagementLogsNode.Children = append(costManagementLogsNode.Children, logVolumeExplorerApp) + } + } + adminNodeLinks = append(adminNodeLinks, costManagementApp) } + orgAdminNode.Children = adminNodeLinks } } diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index 5dad84f681a..a81542fbb18 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -21,60 +21,9 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink orgsAccessEvaluator := ac.EvalPermission(ac.ActionOrgsRead) authConfigUIAvailable := s.license.FeatureEnabled(social.SAMLProviderName) || s.cfg.LDAPAuthEnabled - // FIXME: If plugin admin is disabled or externally managed, server admins still need to access the page, this is why - // while we don't have a permissions for listing plugins the legacy check has to stay as a default - if pluginaccesscontrol.ReqCanAdminPlugins(s.cfg)(c) || hasAccess(pluginaccesscontrol.AdminAccessEvaluator) { - configNodes = append(configNodes, &navtree.NavLink{ - Text: "Plugins", - Id: "plugins", - SubTitle: "Extend the Grafana experience with plugins", - Icon: "plug", - Url: s.cfg.AppSubURL + "/plugins", - }) - } - - if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionOrgUsersRead), ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll))) { - configNodes = append(configNodes, &navtree.NavLink{ - Text: "Users", SubTitle: "Manage users in Grafana", Id: "global-users", Url: s.cfg.AppSubURL + "/admin/users", Icon: "user", - }) - } - - if hasAccess(ac.TeamsAccessEvaluator) { - configNodes = append(configNodes, &navtree.NavLink{ - Text: "Teams", - Id: "teams", - SubTitle: "Groups of users that have common dashboard and permission needs", - Icon: "users-alt", - Url: s.cfg.AppSubURL + "/org/teams", - }) - } - - if enableServiceAccount(s, c) { - configNodes = append(configNodes, &navtree.NavLink{ - Text: "Service accounts", - Id: "serviceaccounts", - SubTitle: "Use service accounts to run automated workloads in Grafana", - Icon: "gf-service-account", - Url: s.cfg.AppSubURL + "/org/serviceaccounts", - }) - } - - disabled, err := s.apiKeyService.IsDisabled(ctx, c.SignedInUser.GetOrgID()) - if err != nil { - return nil, err - } - if hasAccess(ac.ApiKeyAccessEvaluator) && !disabled { - configNodes = append(configNodes, &navtree.NavLink{ - Text: "API keys", - Id: "apikeys", - SubTitle: "Manage and create API keys that are used to interact with Grafana HTTP APIs", - Icon: "key-skeleton-alt", - Url: s.cfg.AppSubURL + "/org/apikeys", - }) - } - + generalNodeLinks := []*navtree.NavLink{} if hasAccess(ac.OrgPreferencesAccessEvaluator) { - configNodes = append(configNodes, &navtree.NavLink{ + generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{ Text: "Default preferences", Id: "org-settings", SubTitle: "Manage preferences across an organization", @@ -82,32 +31,18 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink Url: s.cfg.AppSubURL + "/org", }) } - - if authConfigUIAvailable && hasAccess(ssoutils.EvalAuthenticationSettings(s.cfg)) || - (hasAccess(ssoutils.OauthSettingsEvaluator(s.cfg)) && s.features.IsEnabled(ctx, featuremgmt.FlagSsoSettingsApi)) { - configNodes = append(configNodes, &navtree.NavLink{ - Text: "Authentication", - Id: "authentication", - SubTitle: "Manage your auth settings and configure single sign-on", - Icon: "signin", - Url: s.cfg.AppSubURL + "/admin/authentication", - }) - } - if hasAccess(ac.EvalPermission(ac.ActionSettingsRead, ac.ScopeSettingsAll)) { - configNodes = append(configNodes, &navtree.NavLink{ + generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{ Text: "Settings", SubTitle: "View the settings defined in your Grafana config", Id: "server-settings", Url: s.cfg.AppSubURL + "/admin/settings", Icon: "sliders-v-alt", }) } - if hasGlobalAccess(orgsAccessEvaluator) { - configNodes = append(configNodes, &navtree.NavLink{ + generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{ Text: "Organizations", SubTitle: "Isolated instances of Grafana running on the same server", Id: "global-orgs", Url: s.cfg.AppSubURL + "/admin/orgs", Icon: "building", }) } - if s.features.IsEnabled(ctx, featuremgmt.FlagFeatureToggleAdminPage) && hasAccess(ac.EvalPermission(ac.ActionFeatureManagementRead)) { - configNodes = append(configNodes, &navtree.NavLink{ + generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{ Text: "Feature Toggles", SubTitle: "View and edit feature toggles", Id: "feature-toggles", @@ -115,9 +50,51 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink Icon: "toggle-on", }) } + if hasAccess(ac.EvalPermission(ac.ActionSettingsRead, ac.ScopeSettingsAll)) && s.features.IsEnabled(ctx, featuremgmt.FlagStorage) { + generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{ + Text: "Storage", + Id: "storage", + SubTitle: "Manage file storage", + Icon: "cube", + Url: s.cfg.AppSubURL + "/admin/storage", + }) + } + if s.features.IsEnabled(ctx, featuremgmt.FlagOnPremToCloudMigrations) && c.SignedInUser.HasRole(org.RoleAdmin) { + generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{ + Text: "Migrate to Grafana Cloud", + Id: "migrate-to-cloud", + SubTitle: "Copy configuration from your self-managed installation to a cloud stack", + Url: s.cfg.AppSubURL + "/admin/migrate-to-cloud", + }) + } + generalNode := &navtree.NavLink{ + Text: "General", + SubTitle: "Manage default preferences and settings across Grafana", + Id: navtree.NavIDCfgGeneral, + Url: "/admin/general", + Icon: "shield", + Children: generalNodeLinks, + } + + if len(generalNode.Children) > 0 { + configNodes = append(configNodes, generalNode) + } + + pluginsNodeLinks := []*navtree.NavLink{} + // FIXME: If plugin admin is disabled or externally managed, server admins still need to access the page, this is why + // while we don't have a permissions for listing plugins the legacy check has to stay as a default + if pluginaccesscontrol.ReqCanAdminPlugins(s.cfg)(c) || hasAccess(pluginaccesscontrol.AdminAccessEvaluator) { + pluginsNodeLinks = append(pluginsNodeLinks, &navtree.NavLink{ + Text: "Plugins", + Id: "plugins", + SubTitle: "Extend the Grafana experience with plugins", + Icon: "plug", + Url: s.cfg.AppSubURL + "/plugins", + }) + } if s.features.IsEnabled(ctx, featuremgmt.FlagCorrelations) && hasAccess(correlations.ConfigurationPageAccess) { - configNodes = append(configNodes, &navtree.NavLink{ + pluginsNodeLinks = append(pluginsNodeLinks, &navtree.NavLink{ Text: "Correlations", Icon: "gf-glue", SubTitle: "Add and configure correlations", @@ -126,25 +103,80 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink }) } - if hasAccess(ac.EvalPermission(ac.ActionSettingsRead, ac.ScopeSettingsAll)) && s.features.IsEnabled(ctx, featuremgmt.FlagStorage) { - storage := &navtree.NavLink{ - Text: "Storage", - Id: "storage", - SubTitle: "Manage file storage", - Icon: "cube", - Url: s.cfg.AppSubURL + "/admin/storage", - } - configNodes = append(configNodes, storage) + pluginsNode := &navtree.NavLink{ + Text: "Plugins and data", + SubTitle: "Install plugins and define the relationships between data", + Id: navtree.NavIDCfgPlugins, + Url: "/admin/plugins", + Icon: "shield", + Children: pluginsNodeLinks, } - if s.features.IsEnabled(ctx, featuremgmt.FlagOnPremToCloudMigrations) && c.SignedInUser.HasRole(org.RoleAdmin) { - migrateToCloud := &navtree.NavLink{ - Text: "Migrate to Grafana Cloud", - Id: "migrate-to-cloud", - SubTitle: "Copy configuration from your self-managed installation to a cloud stack", - Url: s.cfg.AppSubURL + "/admin/migrate-to-cloud", - } - configNodes = append(configNodes, migrateToCloud) + if len(pluginsNode.Children) > 0 { + configNodes = append(configNodes, pluginsNode) + } + + accessNodeLinks := []*navtree.NavLink{} + if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionOrgUsersRead), ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll))) { + accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{ + Text: "Users", SubTitle: "Manage users in Grafana", Id: "global-users", Url: s.cfg.AppSubURL + "/admin/users", Icon: "user", + }) + } + if hasAccess(ac.TeamsAccessEvaluator) { + accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{ + Text: "Teams", + Id: "teams", + SubTitle: "Groups of users that have common dashboard and permission needs", + Icon: "users-alt", + Url: s.cfg.AppSubURL + "/org/teams", + }) + } + if enableServiceAccount(s, c) { + accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{ + Text: "Service accounts", + Id: "serviceaccounts", + SubTitle: "Use service accounts to run automated workloads in Grafana", + Icon: "gf-service-account", + Url: s.cfg.AppSubURL + "/org/serviceaccounts", + }) + } + disabled, err := s.apiKeyService.IsDisabled(ctx, c.SignedInUser.GetOrgID()) + if err != nil { + return nil, err + } + if hasAccess(ac.ApiKeyAccessEvaluator) && !disabled { + accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{ + Text: "API keys", + Id: "apikeys", + SubTitle: "Manage and create API keys that are used to interact with Grafana HTTP APIs", + Icon: "key-skeleton-alt", + Url: s.cfg.AppSubURL + "/org/apikeys", + }) + } + + usersNode := &navtree.NavLink{ + Text: "Users and access", + SubTitle: "Configure access for individual users, teams, and service accounts", + Id: navtree.NavIDCfgAccess, + Url: "/admin/access", + Icon: "shield", + Children: accessNodeLinks, + } + + if len(usersNode.Children) > 0 { + configNodes = append(configNodes, usersNode) + } + + if authConfigUIAvailable && hasAccess(ssoutils.EvalAuthenticationSettings(s.cfg)) || + (hasAccess(ssoutils.OauthSettingsEvaluator(s.cfg)) && s.features.IsEnabled(ctx, featuremgmt.FlagSsoSettingsApi)) { + configNodes = append(configNodes, &navtree.NavLink{ + Text: "Authentication", + Id: "authentication", + SubTitle: "Manage your auth settings and configure single sign-on", + Icon: "signin", + IsSection: true, + Url: s.cfg.AppSubURL + "/admin/authentication", + }) } configNode := &navtree.NavLink{ diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index 5030d033380..06ab60eb520 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -295,7 +295,7 @@ func (s *ServiceImpl) readNavigationSettings() { "grafana-incident-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 2, Text: "Incidents"}, "grafana-ml-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 3, Text: "Machine Learning"}, "grafana-slo-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 4}, - "grafana-cloud-link-app": {SectionID: navtree.NavIDCfg}, + "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-attributions-app": {SectionID: navtree.NavIDCfg, Text: "Attributions"}, @@ -307,7 +307,7 @@ func (s *ServiceImpl) readNavigationSettings() { } s.navigationAppPathConfig = map[string]NavigationAppConfig{ - "/a/grafana-auth-app": {SectionID: navtree.NavIDCfg, SortWeight: 7}, + "/a/grafana-auth-app": {SectionID: navtree.NavIDCfgAccess, SortWeight: 2}, } appSections := s.cfg.Raw.Section("navigation.app_sections") diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 62d0a07e8c4..4526465de90 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -151,7 +151,7 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere orgAdminNode, err := s.getAdminNode(c) - if orgAdminNode != nil { + if orgAdminNode != nil && len(orgAdminNode.Children) > 0 { treeRoot.AddSection(orgAdminNode) } else if err != nil { return nil, err