NavTree: Make it possible to configure where in nav tree plugins live (#55484)
* NewIA: Plugin nav config * progress * Progress * Things are working * Add monitoring node * Add alerts and incidents * added experiment with standalone page * Refactoring by adding a type for navtree root * First test working * More tests * more tests * Progress on richer config and sorting * Sort weight working * Path config * Improving logic for not including admin or cfg nodes, making it the last step so that enterprise can add admin nodes without having to worry about the section not existing * fixed index routes * removed file * Fixes * Fixing tests * Fixing more tests and adding support for weight config * Updates * Remove unused fake * More fixes * Minor tweak * Minor fix * Can now control position using sortweight even when existing items have no sortweight * Added tests for frontend standalone page logic * more tests * Remove unused fake and fixed lint issue * Moving reading settings to navtree impl package * remove nav_id setting prefix * Remove old test file * Fix trailing newline * Fixed bug with adding nil node * fixing lint issue * remove some code we have to rethink * move read settings to PrivideService and switch to util.SplitString
This commit is contained in:
@@ -55,16 +55,15 @@ func ProvideService(cfg *setting.Cfg, hooksService *hooks.HooksService) *OSSLice
|
||||
HooksService: hooksService,
|
||||
}
|
||||
l.HooksService.AddIndexDataHook(func(indexData *dtos.IndexViewData, req *models.ReqContext) {
|
||||
for _, node := range indexData.NavTree {
|
||||
if node.Id == "admin" {
|
||||
node.Children = append(node.Children, &navtree.NavLink{
|
||||
Text: "Stats and license",
|
||||
Id: "upgrading",
|
||||
Url: l.LicenseURL(req.IsGrafanaAdmin),
|
||||
Icon: "unlock",
|
||||
})
|
||||
}
|
||||
if adminNode := indexData.NavTree.FindById(navtree.NavIDAdmin); adminNode != nil {
|
||||
adminNode.Children = append(adminNode.Children, &navtree.NavLink{
|
||||
Text: "Stats and license",
|
||||
Id: "upgrading",
|
||||
Url: l.LicenseURL(req.IsGrafanaAdmin),
|
||||
Icon: "unlock",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
+135
-17
@@ -1,5 +1,10 @@
|
||||
package navtree
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
)
|
||||
|
||||
const (
|
||||
// These weights may be used by an extension to reliably place
|
||||
// itself in relation to a particular item in the menu. The weights
|
||||
@@ -25,6 +30,17 @@ const (
|
||||
NavSectionConfig string = "config"
|
||||
)
|
||||
|
||||
const (
|
||||
NavIDDashboards = "dashboards"
|
||||
NavIDDashboardsBrowse = "dashboards/browse"
|
||||
NavIDCfg = "cfg" // NavIDCfg is the id for org configuration navigation node
|
||||
NavIDAdmin = "admin"
|
||||
NavIDAlertsAndIncidents = "alerts-and-incidents"
|
||||
NavIDAlerting = "alerting"
|
||||
NavIDMonitoring = "monitoring"
|
||||
NavIDReporting = "reports"
|
||||
)
|
||||
|
||||
type NavLink struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
Text string `json:"text"`
|
||||
@@ -47,24 +63,126 @@ type NavLink struct {
|
||||
EmptyMessageId string `json:"emptyMessageId,omitempty"`
|
||||
}
|
||||
|
||||
// NavIDCfg is the id for org configuration navigation node
|
||||
const NavIDCfg = "cfg"
|
||||
func (node *NavLink) Sort() {
|
||||
Sort(node.Children)
|
||||
}
|
||||
|
||||
func GetServerAdminNode(children []*NavLink) *NavLink {
|
||||
url := ""
|
||||
if len(children) > 0 {
|
||||
url = children[0].Url
|
||||
type NavTreeRoot struct {
|
||||
Children []*NavLink
|
||||
}
|
||||
|
||||
func (root *NavTreeRoot) AddSection(node *NavLink) {
|
||||
root.Children = append(root.Children, node)
|
||||
}
|
||||
|
||||
func (root *NavTreeRoot) RemoveSection(node *NavLink) {
|
||||
var result []*NavLink
|
||||
|
||||
for _, child := range root.Children {
|
||||
if child != node {
|
||||
result = append(result, child)
|
||||
}
|
||||
}
|
||||
return &NavLink{
|
||||
Text: "Server admin",
|
||||
SubTitle: "Manage all users and orgs",
|
||||
Description: "Manage server-wide settings and access to resources such as organizations, users, and licenses",
|
||||
HideFromTabs: true,
|
||||
Id: "admin",
|
||||
Icon: "shield",
|
||||
Url: url,
|
||||
SortWeight: WeightAdmin,
|
||||
Section: NavSectionConfig,
|
||||
Children: children,
|
||||
|
||||
root.Children = result
|
||||
}
|
||||
|
||||
func (root *NavTreeRoot) FindById(id string) *NavLink {
|
||||
return FindById(root.Children, id)
|
||||
}
|
||||
|
||||
func (root *NavTreeRoot) RemoveEmptySectionsAndApplyNewInformationArchitecture(topNavEnabled bool) {
|
||||
// Remove server admin node if it has no children or set the url to first child
|
||||
if node := root.FindById(NavIDAdmin); node != nil {
|
||||
if len(node.Children) == 0 {
|
||||
root.RemoveSection(node)
|
||||
} else {
|
||||
node.Url = node.Children[0].Url
|
||||
}
|
||||
}
|
||||
|
||||
if topNavEnabled {
|
||||
orgAdminNode := root.FindById(NavIDCfg)
|
||||
|
||||
if orgAdminNode != nil {
|
||||
orgAdminNode.Url = "/admin"
|
||||
orgAdminNode.Text = "Administration"
|
||||
}
|
||||
|
||||
if serverAdminNode := root.FindById(NavIDAdmin); serverAdminNode != nil {
|
||||
serverAdminNode.Url = "/admin/settings"
|
||||
serverAdminNode.Text = "Server admin"
|
||||
serverAdminNode.SortWeight = 10000
|
||||
|
||||
if orgAdminNode != nil {
|
||||
orgAdminNode.Children = append(orgAdminNode.Children, serverAdminNode)
|
||||
root.RemoveSection(serverAdminNode)
|
||||
}
|
||||
}
|
||||
|
||||
// Move reports into dashboards
|
||||
if reports := root.FindById(NavIDReporting); reports != nil {
|
||||
if dashboards := root.FindById(NavIDDashboards); dashboards != nil {
|
||||
reports.SortWeight = 0
|
||||
dashboards.Children = append(dashboards.Children, reports)
|
||||
root.RemoveSection(reports)
|
||||
}
|
||||
}
|
||||
|
||||
// Change id of dashboards
|
||||
if dashboards := root.FindById(NavIDDashboards); dashboards != nil {
|
||||
dashboards.Id = "dashboards/browse"
|
||||
}
|
||||
}
|
||||
|
||||
// Remove top level cfg / administration node if it has no children (needs to be after topnav new info archicture logic above that moves server admin into it)
|
||||
// Remove server admin node if it has no children or set the url to first child
|
||||
if node := root.FindById(NavIDCfg); node != nil {
|
||||
if len(node.Children) == 0 {
|
||||
root.RemoveSection(node)
|
||||
} else if !topNavEnabled {
|
||||
node.Url = node.Children[0].Url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (root *NavTreeRoot) Sort() {
|
||||
Sort(root.Children)
|
||||
}
|
||||
|
||||
func (root *NavTreeRoot) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(root.Children)
|
||||
}
|
||||
|
||||
func Sort(nodes []*NavLink) {
|
||||
sort.SliceStable(nodes, func(i, j int) bool {
|
||||
iw := nodes[i].SortWeight
|
||||
if iw == 0 {
|
||||
iw = int64(i) + 1
|
||||
}
|
||||
jw := nodes[j].SortWeight
|
||||
if jw == 0 {
|
||||
jw = int64(j) + 1
|
||||
}
|
||||
|
||||
return iw < jw
|
||||
})
|
||||
|
||||
for _, child := range nodes {
|
||||
child.Sort()
|
||||
}
|
||||
}
|
||||
|
||||
func FindById(nodes []*NavLink, id string) *NavLink {
|
||||
for _, child := range nodes {
|
||||
if child.Id == id {
|
||||
return child
|
||||
} else if len(child.Children) > 0 {
|
||||
if found := FindById(child.Children, id); found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package navtree
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNavTreeRoot(t *testing.T) {
|
||||
t.Run("Should remove empty admin and server admin sections", func(t *testing.T) {
|
||||
treeRoot := NavTreeRoot{
|
||||
Children: []*NavLink{
|
||||
{Id: NavIDCfg},
|
||||
{Id: NavIDAdmin},
|
||||
},
|
||||
}
|
||||
|
||||
treeRoot.RemoveEmptySectionsAndApplyNewInformationArchitecture(false)
|
||||
|
||||
require.Equal(t, 0, len(treeRoot.Children))
|
||||
})
|
||||
|
||||
t.Run("Should not remove admin sections when they have children", func(t *testing.T) {
|
||||
treeRoot := NavTreeRoot{
|
||||
Children: []*NavLink{
|
||||
{Id: NavIDCfg, Children: []*NavLink{{Id: "child"}}},
|
||||
{Id: NavIDAdmin, Children: []*NavLink{{Id: "child"}}},
|
||||
},
|
||||
}
|
||||
|
||||
treeRoot.RemoveEmptySectionsAndApplyNewInformationArchitecture(false)
|
||||
|
||||
require.Equal(t, 2, len(treeRoot.Children))
|
||||
})
|
||||
|
||||
t.Run("Should move admin section into cfg and rename when topnav is enabled", func(t *testing.T) {
|
||||
treeRoot := NavTreeRoot{
|
||||
Children: []*NavLink{
|
||||
{Id: NavIDCfg},
|
||||
{Id: NavIDAdmin, Children: []*NavLink{{Id: "child"}}},
|
||||
},
|
||||
}
|
||||
|
||||
treeRoot.RemoveEmptySectionsAndApplyNewInformationArchitecture(true)
|
||||
|
||||
require.Equal(t, "Administration", treeRoot.Children[0].Text)
|
||||
require.Equal(t, NavIDAdmin, treeRoot.Children[0].Children[0].Id)
|
||||
})
|
||||
|
||||
t.Run("Should move reports into Dashboards", func(t *testing.T) {
|
||||
treeRoot := NavTreeRoot{
|
||||
Children: []*NavLink{
|
||||
{Id: NavIDDashboards},
|
||||
{Id: NavIDReporting},
|
||||
},
|
||||
}
|
||||
|
||||
treeRoot.RemoveEmptySectionsAndApplyNewInformationArchitecture(true)
|
||||
|
||||
require.Equal(t, NavIDReporting, treeRoot.Children[0].Children[0].Id)
|
||||
})
|
||||
|
||||
t.Run("Sorting by index", func(t *testing.T) {
|
||||
treeRoot := NavTreeRoot{
|
||||
Children: []*NavLink{
|
||||
{Id: "1"},
|
||||
{Id: "2"},
|
||||
{Id: "3"},
|
||||
},
|
||||
}
|
||||
treeRoot.Sort()
|
||||
require.Equal(t, "1", treeRoot.Children[0].Id)
|
||||
require.Equal(t, "3", treeRoot.Children[2].Id)
|
||||
})
|
||||
|
||||
t.Run("Sorting by index and SortWeight", func(t *testing.T) {
|
||||
treeRoot := NavTreeRoot{
|
||||
Children: []*NavLink{
|
||||
{Id: "1"},
|
||||
{Id: "2"},
|
||||
{Id: "3"},
|
||||
{Id: "4", SortWeight: 1},
|
||||
},
|
||||
}
|
||||
treeRoot.Sort()
|
||||
require.Equal(t, "1", treeRoot.Children[0].Id)
|
||||
require.Equal(t, "4", treeRoot.Children[1].Id)
|
||||
})
|
||||
}
|
||||
@@ -6,5 +6,5 @@ import (
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *pref.Preference) ([]*NavLink, error)
|
||||
GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *pref.Preference) (*NavTreeRoot, error)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
)
|
||||
|
||||
func (s *ServiceImpl) setupConfigNodes(c *models.ReqContext) ([]*navtree.NavLink, error) {
|
||||
func (s *ServiceImpl) getOrgAdminNode(c *models.ReqContext) (*navtree.NavLink, error) {
|
||||
var configNodes []*navtree.NavLink
|
||||
|
||||
hasAccess := ac.HasAccess(s.accessControl, c)
|
||||
@@ -103,7 +103,75 @@ func (s *ServiceImpl) setupConfigNodes(c *models.ReqContext) ([]*navtree.NavLink
|
||||
Url: s.cfg.AppSubURL + "/org/serviceaccounts",
|
||||
})
|
||||
}
|
||||
return configNodes, nil
|
||||
|
||||
configNode := &navtree.NavLink{
|
||||
Id: navtree.NavIDCfg,
|
||||
Text: "Configuration",
|
||||
SubTitle: "Organization: " + c.OrgName,
|
||||
Icon: "cog",
|
||||
Section: navtree.NavSectionConfig,
|
||||
SortWeight: navtree.WeightConfig,
|
||||
Children: configNodes,
|
||||
}
|
||||
|
||||
return configNode, nil
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) getServerAdminNode(c *models.ReqContext) *navtree.NavLink {
|
||||
hasAccess := ac.HasAccess(s.accessControl, c)
|
||||
hasGlobalAccess := ac.HasGlobalAccess(s.accessControl, s.accesscontrolService, c)
|
||||
orgsAccessEvaluator := ac.EvalPermission(ac.ActionOrgsRead)
|
||||
adminNavLinks := []*navtree.NavLink{}
|
||||
|
||||
if hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)) {
|
||||
adminNavLinks = append(adminNavLinks, &navtree.NavLink{
|
||||
Text: "Users", Description: "Manage and create users across the whole Grafana server", Id: "global-users", Url: s.cfg.AppSubURL + "/admin/users", Icon: "user",
|
||||
})
|
||||
}
|
||||
|
||||
if hasGlobalAccess(ac.ReqGrafanaAdmin, orgsAccessEvaluator) {
|
||||
adminNavLinks = append(adminNavLinks, &navtree.NavLink{
|
||||
Text: "Organizations", Description: "Isolated instances of Grafana running on the same server", Id: "global-orgs", Url: s.cfg.AppSubURL + "/admin/orgs", Icon: "building",
|
||||
})
|
||||
}
|
||||
|
||||
if hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)) {
|
||||
adminNavLinks = append(adminNavLinks, &navtree.NavLink{
|
||||
Text: "Settings", Description: "View the settings defined in your Grafana config", Id: "server-settings", Url: s.cfg.AppSubURL + "/admin/settings", Icon: "sliders-v-alt",
|
||||
})
|
||||
}
|
||||
|
||||
if hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)) && s.features.IsEnabled(featuremgmt.FlagStorage) {
|
||||
adminNavLinks = append(adminNavLinks, &navtree.NavLink{
|
||||
Text: "Storage",
|
||||
Id: "storage",
|
||||
Description: "Manage file storage",
|
||||
Icon: "cube",
|
||||
Url: s.cfg.AppSubURL + "/admin/storage",
|
||||
})
|
||||
}
|
||||
|
||||
if s.cfg.LDAPEnabled && hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)) {
|
||||
adminNavLinks = append(adminNavLinks, &navtree.NavLink{
|
||||
Text: "LDAP", Id: "ldap", Url: s.cfg.AppSubURL + "/admin/ldap", Icon: "book",
|
||||
})
|
||||
}
|
||||
|
||||
adminNode := &navtree.NavLink{
|
||||
Text: "Server admin",
|
||||
Description: "Manage server-wide settings and access to resources such as organizations, users, and licenses",
|
||||
Id: navtree.NavIDAdmin,
|
||||
Icon: "shield",
|
||||
SortWeight: navtree.WeightAdmin,
|
||||
Section: navtree.NavSectionConfig,
|
||||
Children: adminNavLinks,
|
||||
}
|
||||
|
||||
if len(adminNavLinks) > 0 {
|
||||
adminNode.Url = adminNavLinks[0].Url
|
||||
}
|
||||
|
||||
return adminNode
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) ReqCanAdminTeams(c *models.ReqContext) bool {
|
||||
|
||||
@@ -3,6 +3,7 @@ package navtreeimpl
|
||||
import (
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
@@ -10,15 +11,17 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/navtree"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsettings"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func (s *ServiceImpl) getAppLinks(c *models.ReqContext) ([]*navtree.NavLink, error) {
|
||||
func (s *ServiceImpl) addAppLinks(treeRoot *navtree.NavTreeRoot, c *models.ReqContext) error {
|
||||
topNavEnabled := s.features.IsEnabled(featuremgmt.FlagTopnav)
|
||||
hasAccess := ac.HasAccess(s.accessControl, c)
|
||||
appLinks := []*navtree.NavLink{}
|
||||
|
||||
pss, err := s.pluginSettings.GetPluginSettings(c.Req.Context(), &pluginsettings.GetArgs{OrgID: c.OrgID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
isPluginEnabled := func(plugin plugins.PluginDTO) bool {
|
||||
@@ -43,63 +46,8 @@ func (s *ServiceImpl) getAppLinks(c *models.ReqContext) ([]*navtree.NavLink, err
|
||||
continue
|
||||
}
|
||||
|
||||
appLink := &navtree.NavLink{
|
||||
Text: plugin.Name,
|
||||
Id: "plugin-page-" + plugin.ID,
|
||||
Img: plugin.Info.Logos.Small,
|
||||
Section: navtree.NavSectionPlugin,
|
||||
SortWeight: navtree.WeightPlugin,
|
||||
}
|
||||
|
||||
if s.features.IsEnabled(featuremgmt.FlagTopnav) {
|
||||
appLink.Url = s.cfg.AppSubURL + "/a/" + plugin.ID
|
||||
} else {
|
||||
appLink.Url = path.Join(s.cfg.AppSubURL, plugin.DefaultNavURL)
|
||||
}
|
||||
|
||||
for _, include := range plugin.Includes {
|
||||
if !c.HasUserRole(include.Role) {
|
||||
continue
|
||||
}
|
||||
|
||||
if include.Type == "page" && include.AddToNav {
|
||||
var link *navtree.NavLink
|
||||
if len(include.Path) > 0 {
|
||||
link = &navtree.NavLink{
|
||||
Url: s.cfg.AppSubURL + include.Path,
|
||||
Text: include.Name,
|
||||
}
|
||||
if include.DefaultNav && !s.features.IsEnabled(featuremgmt.FlagTopnav) {
|
||||
appLink.Url = link.Url // Overwrite the hardcoded page logic
|
||||
}
|
||||
} else {
|
||||
link = &navtree.NavLink{
|
||||
Url: s.cfg.AppSubURL + "/plugins/" + plugin.ID + "/page/" + include.Slug,
|
||||
Text: include.Name,
|
||||
}
|
||||
}
|
||||
link.Icon = include.Icon
|
||||
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,
|
||||
}
|
||||
appLink.Children = append(appLink.Children, link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(appLink.Children) > 0 {
|
||||
// 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{}
|
||||
}
|
||||
appLinks = append(appLinks, appLink)
|
||||
if appNode := s.processAppPlugin(plugin, c, topNavEnabled, treeRoot); appNode != nil {
|
||||
appLinks = append(appLinks, appNode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,5 +57,157 @@ func (s *ServiceImpl) getAppLinks(c *models.ReqContext) ([]*navtree.NavLink, err
|
||||
})
|
||||
}
|
||||
|
||||
return appLinks, nil
|
||||
if topNavEnabled {
|
||||
treeRoot.AddSection(&navtree.NavLink{
|
||||
Text: "Apps",
|
||||
Icon: "apps",
|
||||
Description: "App plugins that extend the Grafana experience",
|
||||
Id: "apps",
|
||||
Children: appLinks,
|
||||
Section: navtree.NavSectionCore,
|
||||
Url: s.cfg.AppSubURL + "/apps",
|
||||
})
|
||||
} else {
|
||||
for _, appLink := range appLinks {
|
||||
treeRoot.AddSection(appLink)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqContext, topNavEnabled bool, treeRoot *navtree.NavTreeRoot) *navtree.NavLink {
|
||||
appLink := &navtree.NavLink{
|
||||
Text: plugin.Name,
|
||||
Id: "plugin-page-" + plugin.ID,
|
||||
Img: plugin.Info.Logos.Small,
|
||||
Section: navtree.NavSectionPlugin,
|
||||
SortWeight: navtree.WeightPlugin,
|
||||
}
|
||||
|
||||
if s.features.IsEnabled(featuremgmt.FlagTopnav) {
|
||||
appLink.Url = s.cfg.AppSubURL + "/a/" + plugin.ID
|
||||
} else {
|
||||
appLink.Url = path.Join(s.cfg.AppSubURL, plugin.DefaultNavURL)
|
||||
}
|
||||
|
||||
for _, include := range plugin.Includes {
|
||||
if !c.HasUserRole(include.Role) {
|
||||
continue
|
||||
}
|
||||
|
||||
if include.Type == "page" && include.AddToNav {
|
||||
link := &navtree.NavLink{
|
||||
Text: include.Name,
|
||||
Icon: include.Icon,
|
||||
}
|
||||
|
||||
if len(include.Path) > 0 {
|
||||
link.Url = s.cfg.AppSubURL + include.Path
|
||||
if include.DefaultNav {
|
||||
appLink.Url = link.Url
|
||||
}
|
||||
} else {
|
||||
link.Url = s.cfg.AppSubURL + "/plugins/" + plugin.ID + "/page/" + include.Slug
|
||||
}
|
||||
|
||||
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
|
||||
sectionForPage.Children = append(sectionForPage.Children, link)
|
||||
}
|
||||
} else {
|
||||
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,
|
||||
}
|
||||
appLink.Children = append(appLink.Children, link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(appLink.Children) > 0 {
|
||||
// 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{}
|
||||
}
|
||||
|
||||
alertingNode := treeRoot.FindById(navtree.NavIDAlerting)
|
||||
|
||||
if navConfig, hasOverride := s.navigationAppConfig[plugin.ID]; hasOverride && topNavEnabled {
|
||||
appLink.SortWeight = navConfig.SortWeight
|
||||
|
||||
if navNode := treeRoot.FindById(navConfig.SectionID); navNode != nil {
|
||||
navNode.Children = append(navNode.Children, appLink)
|
||||
} else {
|
||||
if navConfig.SectionID == navtree.NavIDMonitoring {
|
||||
treeRoot.AddSection(&navtree.NavLink{
|
||||
Text: "Monitoring",
|
||||
Id: navtree.NavIDMonitoring,
|
||||
Description: "Monitoring and infrastructure apps",
|
||||
Icon: "heart-rate",
|
||||
Section: navtree.NavSectionCore,
|
||||
Children: []*navtree.NavLink{appLink},
|
||||
Url: s.cfg.AppSubURL + "/monitoring",
|
||||
})
|
||||
} else if navConfig.SectionID == navtree.NavIDAlertsAndIncidents && alertingNode != nil {
|
||||
treeRoot.AddSection(&navtree.NavLink{
|
||||
Text: "Alerts & incidents",
|
||||
Id: navtree.NavIDAlertsAndIncidents,
|
||||
Description: "Alerting and incident management apps",
|
||||
Icon: "bell",
|
||||
Section: navtree.NavSectionCore,
|
||||
Children: []*navtree.NavLink{alertingNode, appLink},
|
||||
Url: s.cfg.AppSubURL + "/alerts-and-incidents",
|
||||
})
|
||||
treeRoot.RemoveSection(alertingNode)
|
||||
} else {
|
||||
s.log.Error("Plugin app nav id not found", "pluginId", plugin.ID, "navId", navConfig.SectionID)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return appLink
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) readNavigationSettings() {
|
||||
s.navigationAppConfig = map[string]NavigationAppConfig{
|
||||
"grafana-k8s-app": {SectionID: navtree.NavIDMonitoring, SortWeight: 1},
|
||||
"grafana-synthetic-monitoring-app": {SectionID: navtree.NavIDMonitoring, SortWeight: 2},
|
||||
"grafana-oncall-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 1},
|
||||
"grafana-incident-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 2},
|
||||
"grafana-ml-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 3},
|
||||
}
|
||||
|
||||
s.navigationAppPathConfig = map[string]NavigationAppConfig{
|
||||
"/a/grafana-auth-app": {SectionID: navtree.NavIDCfg, SortWeight: 7},
|
||||
}
|
||||
|
||||
sec := s.cfg.Raw.Section("navigation.apps")
|
||||
|
||||
for _, key := range sec.Keys() {
|
||||
pluginId := key.Name()
|
||||
// Support <id> <weight> value
|
||||
values := util.SplitString(sec.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.navigationAppConfig[pluginId] = *appCfg
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
package navtreeimpl
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/navtree"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsettings"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAddAppLinks(t *testing.T) {
|
||||
httpReq, _ := http.NewRequest(http.MethodGet, "", nil)
|
||||
reqCtx := &models.ReqContext{SignedInUser: &user.SignedInUser{}, Context: &web.Context{Req: httpReq}}
|
||||
permissions := []ac.Permission{
|
||||
{Action: plugins.ActionAppAccess, Scope: "*"},
|
||||
}
|
||||
|
||||
testApp1 := plugins.PluginDTO{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-app1",
|
||||
Name: "Test app1 name",
|
||||
Type: plugins.App,
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
Name: "Hello",
|
||||
Path: "/a/test-app1/catalog",
|
||||
Type: "page",
|
||||
AddToNav: true,
|
||||
DefaultNav: true,
|
||||
},
|
||||
{
|
||||
Name: "Hello",
|
||||
Path: "/a/test-app1/page2",
|
||||
Type: "page",
|
||||
AddToNav: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testApp2 := plugins.PluginDTO{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-app2",
|
||||
Name: "Test app2 name",
|
||||
Type: plugins.App,
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
Name: "Hello",
|
||||
Path: "/a/quick-app/catalog",
|
||||
Type: "page",
|
||||
AddToNav: true,
|
||||
DefaultNav: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pluginSettings := pluginsettings.FakePluginSettings{Plugins: map[string]*pluginsettings.DTO{
|
||||
testApp1.ID: {ID: 0, OrgID: 1, PluginID: testApp1.ID, PluginVersion: "1.0.0", Enabled: true},
|
||||
testApp2.ID: {ID: 0, OrgID: 1, PluginID: testApp2.ID, PluginVersion: "1.0.0", Enabled: true},
|
||||
}}
|
||||
|
||||
service := ServiceImpl{
|
||||
log: log.New("navtree"),
|
||||
cfg: setting.NewCfg(),
|
||||
accessControl: accesscontrolmock.New().WithPermissions(permissions),
|
||||
pluginSettings: &pluginSettings,
|
||||
features: featuremgmt.WithFeatures(),
|
||||
pluginStore: plugins.FakePluginStore{
|
||||
PluginList: []plugins.PluginDTO{testApp1, testApp2},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("Should add enabled apps with pages", func(t *testing.T) {
|
||||
treeRoot := navtree.NavTreeRoot{}
|
||||
err := service.addAppLinks(&treeRoot, reqCtx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Test app1 name", treeRoot.Children[0].Text)
|
||||
require.Equal(t, "/a/test-app1/catalog", treeRoot.Children[0].Url)
|
||||
require.Equal(t, "/a/test-app1/page2", treeRoot.Children[0].Children[1].Url)
|
||||
})
|
||||
|
||||
t.Run("Should move apps to Apps category when topnav is enabled", func(t *testing.T) {
|
||||
service.features = featuremgmt.WithFeatures(featuremgmt.FlagTopnav)
|
||||
treeRoot := navtree.NavTreeRoot{}
|
||||
err := service.addAppLinks(&treeRoot, reqCtx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Apps", treeRoot.Children[0].Text)
|
||||
require.Equal(t, "Test app1 name", treeRoot.Children[0].Children[0].Text)
|
||||
})
|
||||
|
||||
t.Run("Should move apps that have specific nav id configured to correct section", func(t *testing.T) {
|
||||
service.features = featuremgmt.WithFeatures(featuremgmt.FlagTopnav)
|
||||
service.navigationAppConfig = map[string]NavigationAppConfig{
|
||||
"test-app1": {SectionID: navtree.NavIDAdmin},
|
||||
}
|
||||
|
||||
treeRoot := navtree.NavTreeRoot{}
|
||||
treeRoot.AddSection(&navtree.NavLink{
|
||||
Id: navtree.NavIDAdmin,
|
||||
})
|
||||
|
||||
err := service.addAppLinks(&treeRoot, reqCtx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "plugin-page-test-app1", treeRoot.Children[0].Children[0].Id)
|
||||
})
|
||||
|
||||
t.Run("Should add monitoring section if plugin exists that wants to live there", func(t *testing.T) {
|
||||
service.features = featuremgmt.WithFeatures(featuremgmt.FlagTopnav)
|
||||
service.navigationAppConfig = map[string]NavigationAppConfig{
|
||||
"test-app1": {SectionID: navtree.NavIDMonitoring},
|
||||
}
|
||||
|
||||
treeRoot := navtree.NavTreeRoot{}
|
||||
|
||||
err := service.addAppLinks(&treeRoot, reqCtx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Monitoring", treeRoot.Children[0].Text)
|
||||
require.Equal(t, "Test app1 name", treeRoot.Children[0].Children[0].Text)
|
||||
})
|
||||
|
||||
t.Run("Should add Alerts and incidents section if plugin exists that wants to live there", func(t *testing.T) {
|
||||
service.features = featuremgmt.WithFeatures(featuremgmt.FlagTopnav)
|
||||
service.navigationAppConfig = map[string]NavigationAppConfig{
|
||||
"test-app1": {SectionID: navtree.NavIDAlertsAndIncidents},
|
||||
}
|
||||
|
||||
treeRoot := navtree.NavTreeRoot{}
|
||||
treeRoot.AddSection(&navtree.NavLink{Id: navtree.NavIDAlerting, Text: "Alerting"})
|
||||
|
||||
err := service.addAppLinks(&treeRoot, reqCtx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Alerts & incidents", treeRoot.Children[0].Text)
|
||||
require.Equal(t, "Alerting", treeRoot.Children[0].Children[0].Text)
|
||||
require.Equal(t, "Test app1 name", treeRoot.Children[0].Children[1].Text)
|
||||
})
|
||||
|
||||
t.Run("Should be able to control app sort order with SortWeight", func(t *testing.T) {
|
||||
service.features = featuremgmt.WithFeatures(featuremgmt.FlagTopnav)
|
||||
service.navigationAppConfig = map[string]NavigationAppConfig{
|
||||
"test-app2": {SectionID: navtree.NavIDMonitoring, SortWeight: 1},
|
||||
"test-app1": {SectionID: navtree.NavIDMonitoring, SortWeight: 2},
|
||||
}
|
||||
|
||||
treeRoot := navtree.NavTreeRoot{}
|
||||
|
||||
err := service.addAppLinks(&treeRoot, reqCtx)
|
||||
|
||||
treeRoot.Sort()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Test app2 name", treeRoot.Children[0].Children[0].Text)
|
||||
require.Equal(t, "Test app1 name", treeRoot.Children[0].Children[1].Text)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadingNavigationSettings(t *testing.T) {
|
||||
t.Run("Should include defaults", func(t *testing.T) {
|
||||
service := ServiceImpl{
|
||||
cfg: setting.NewCfg(),
|
||||
}
|
||||
|
||||
_, _ = service.cfg.Raw.NewSection("navigation.apps")
|
||||
service.readNavigationSettings()
|
||||
|
||||
require.Equal(t, "monitoring", service.navigationAppConfig["grafana-k8s-app"].SectionID)
|
||||
})
|
||||
|
||||
t.Run("Can add additional overrides via ini system", func(t *testing.T) {
|
||||
service := ServiceImpl{
|
||||
cfg: setting.NewCfg(),
|
||||
}
|
||||
|
||||
sec, _ := service.cfg.Raw.NewSection("navigation.apps")
|
||||
_, _ = sec.NewKey("grafana-k8s-app", "dashboards")
|
||||
_, _ = sec.NewKey("other-app", "admin 12")
|
||||
|
||||
service.readNavigationSettings()
|
||||
|
||||
require.Equal(t, "dashboards", service.navigationAppConfig["grafana-k8s-app"].SectionID)
|
||||
require.Equal(t, "admin", service.navigationAppConfig["other-app"].SectionID)
|
||||
|
||||
require.Equal(t, int64(0), service.navigationAppConfig["grafana-k8s-app"].SortWeight)
|
||||
require.Equal(t, int64(12), service.navigationAppConfig["other-app"].SortWeight)
|
||||
})
|
||||
}
|
||||
@@ -33,10 +33,19 @@ type ServiceImpl struct {
|
||||
accesscontrolService ac.Service
|
||||
kvStore kvstore.KVStore
|
||||
apiKeyService apikey.Service
|
||||
|
||||
// Navigation
|
||||
navigationAppConfig map[string]NavigationAppConfig
|
||||
navigationAppPathConfig map[string]NavigationAppConfig
|
||||
}
|
||||
|
||||
type NavigationAppConfig struct {
|
||||
SectionID string
|
||||
SortWeight int64
|
||||
}
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStore plugins.Store, pluginSettings pluginsettings.Service, starService star.Service, features *featuremgmt.FeatureManager, dashboardService dashboards.DashboardService, accesscontrolService ac.Service, kvStore kvstore.KVStore, apiKeyService apikey.Service) navtree.Service {
|
||||
return &ServiceImpl{
|
||||
service := &ServiceImpl{
|
||||
cfg: cfg,
|
||||
log: log.New("navtree service"),
|
||||
accessControl: accessControl,
|
||||
@@ -49,12 +58,16 @@ func ProvideService(cfg *setting.Cfg, accessControl ac.AccessControl, pluginStor
|
||||
kvStore: kvStore,
|
||||
apiKeyService: apiKeyService,
|
||||
}
|
||||
|
||||
service.readNavigationSettings()
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *pref.Preference) ([]*navtree.NavLink, error) {
|
||||
func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *pref.Preference) (*navtree.NavTreeRoot, error) {
|
||||
hasAccess := ac.HasAccess(s.accessControl, c)
|
||||
var navTree []*navtree.NavLink
|
||||
treeRoot := &navtree.NavTreeRoot{}
|
||||
|
||||
if hasAccess(ac.ReqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsRead)) {
|
||||
starredItemsLinks, err := s.buildStarredItemsNavLinks(c, prefs)
|
||||
@@ -62,7 +75,7 @@ func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
|
||||
return nil, err
|
||||
}
|
||||
|
||||
navTree = append(navTree, &navtree.NavLink{
|
||||
treeRoot.AddSection(&navtree.NavLink{
|
||||
Text: "Starred",
|
||||
Id: "starred",
|
||||
Icon: "star",
|
||||
@@ -74,25 +87,19 @@ func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
|
||||
|
||||
dashboardChildLinks := s.buildDashboardNavLinks(c, hasEditPerm)
|
||||
|
||||
dashboardsUrl := "/dashboards"
|
||||
|
||||
dashboardLink := &navtree.NavLink{
|
||||
Text: "Dashboards",
|
||||
Id: "dashboards",
|
||||
Id: navtree.NavIDDashboards,
|
||||
Description: "Create and manage dashboards to visualize your data",
|
||||
SubTitle: "Manage dashboards and folders",
|
||||
Icon: "apps",
|
||||
Url: s.cfg.AppSubURL + dashboardsUrl,
|
||||
Url: s.cfg.AppSubURL + "/dashboards",
|
||||
SortWeight: navtree.WeightDashboard,
|
||||
Section: navtree.NavSectionCore,
|
||||
Children: dashboardChildLinks,
|
||||
}
|
||||
|
||||
if s.features.IsEnabled(featuremgmt.FlagTopnav) {
|
||||
dashboardLink.Id = "dashboards/browse"
|
||||
}
|
||||
|
||||
navTree = append(navTree, dashboardLink)
|
||||
treeRoot.AddSection(dashboardLink)
|
||||
}
|
||||
|
||||
canExplore := func(context *models.ReqContext) bool {
|
||||
@@ -100,7 +107,7 @@ func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
|
||||
}
|
||||
|
||||
if setting.ExploreEnabled && hasAccess(canExplore, ac.EvalPermission(ac.ActionDatasourcesExplore)) {
|
||||
navTree = append(navTree, &navtree.NavLink{
|
||||
treeRoot.AddSection(&navtree.NavLink{
|
||||
Text: "Explore",
|
||||
Id: "explore",
|
||||
SubTitle: "Explore your data",
|
||||
@@ -111,44 +118,25 @@ func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
|
||||
})
|
||||
}
|
||||
|
||||
navTree = s.addProfile(navTree, c)
|
||||
if setting.ProfileEnabled && c.IsSignedIn {
|
||||
treeRoot.AddSection(s.getProfileNode(c))
|
||||
}
|
||||
|
||||
_, uaIsDisabledForOrg := s.cfg.UnifiedAlerting.DisabledOrgs[c.OrgID]
|
||||
uaVisibleForOrg := s.cfg.UnifiedAlerting.IsEnabled() && !uaIsDisabledForOrg
|
||||
|
||||
if setting.AlertingEnabled != nil && *setting.AlertingEnabled {
|
||||
navTree = append(navTree, s.buildLegacyAlertNavLinks(c)...)
|
||||
if legacyAlertSection := s.buildLegacyAlertNavLinks(c); legacyAlertSection != nil {
|
||||
treeRoot.AddSection(legacyAlertSection)
|
||||
}
|
||||
} else if uaVisibleForOrg {
|
||||
navTree = append(navTree, s.buildAlertNavLinks(c, hasEditPerm)...)
|
||||
if alertingSection := s.buildAlertNavLinks(c, hasEditPerm); alertingSection != nil {
|
||||
treeRoot.AddSection(alertingSection)
|
||||
}
|
||||
}
|
||||
|
||||
if s.features.IsEnabled(featuremgmt.FlagDataConnectionsConsole) {
|
||||
navTree = append(navTree, s.buildDataConnectionsNavLink(c))
|
||||
}
|
||||
|
||||
appLinks, err := s.getAppLinks(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// When topnav is enabled we can test new information architecture where plugins live in Apps category
|
||||
if s.features.IsEnabled(featuremgmt.FlagTopnav) {
|
||||
navTree = append(navTree, &navtree.NavLink{
|
||||
Text: "Apps",
|
||||
Icon: "apps",
|
||||
Description: "App plugins that extend the Grafana experience",
|
||||
Id: "apps",
|
||||
Children: appLinks,
|
||||
Section: navtree.NavSectionCore,
|
||||
Url: s.cfg.AppSubURL + "/apps",
|
||||
})
|
||||
} else {
|
||||
navTree = append(navTree, appLinks...)
|
||||
}
|
||||
|
||||
configNodes, err := s.setupConfigNodes(c)
|
||||
if err != nil {
|
||||
return navTree, err
|
||||
treeRoot.AddSection(s.buildDataConnectionsNavLink(c))
|
||||
}
|
||||
|
||||
if s.features.IsEnabled(featuremgmt.FlagLivePipeline) {
|
||||
@@ -163,7 +151,8 @@ func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
|
||||
liveNavLinks = append(liveNavLinks, &navtree.NavLink{
|
||||
Text: "Cloud", Id: "live-cloud", Url: s.cfg.AppSubURL + "/live/cloud", Icon: "cloud-upload",
|
||||
})
|
||||
navTree = append(navTree, &navtree.NavLink{
|
||||
|
||||
treeRoot.AddSection(&navtree.NavLink{
|
||||
Id: "live",
|
||||
Text: "Live",
|
||||
SubTitle: "Event streaming",
|
||||
@@ -175,60 +164,37 @@ func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
|
||||
})
|
||||
}
|
||||
|
||||
var configNode *navtree.NavLink
|
||||
var serverAdminNode *navtree.NavLink
|
||||
orgAdminNode, err := s.getOrgAdminNode(c)
|
||||
|
||||
if len(configNodes) > 0 {
|
||||
configNode = &navtree.NavLink{
|
||||
Id: navtree.NavIDCfg,
|
||||
Text: "Configuration",
|
||||
SubTitle: "Organization: " + c.OrgName,
|
||||
Icon: "cog",
|
||||
Url: configNodes[0].Url,
|
||||
Section: navtree.NavSectionConfig,
|
||||
SortWeight: navtree.WeightConfig,
|
||||
Children: configNodes,
|
||||
}
|
||||
if s.features.IsEnabled(featuremgmt.FlagTopnav) {
|
||||
configNode.Url = "/admin"
|
||||
} else {
|
||||
configNode.Url = configNodes[0].Url
|
||||
}
|
||||
navTree = append(navTree, configNode)
|
||||
if orgAdminNode != nil {
|
||||
treeRoot.AddSection(orgAdminNode)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
adminNavLinks := s.buildAdminNavLinks(c)
|
||||
serverAdminNode := s.getServerAdminNode(c)
|
||||
|
||||
if len(adminNavLinks) > 0 {
|
||||
serverAdminNode = navtree.GetServerAdminNode(adminNavLinks)
|
||||
navTree = append(navTree, serverAdminNode)
|
||||
if serverAdminNode != nil {
|
||||
treeRoot.AddSection(serverAdminNode)
|
||||
}
|
||||
|
||||
if s.features.IsEnabled(featuremgmt.FlagTopnav) {
|
||||
// Move server admin into Configuration and rename to administration
|
||||
if configNode != nil && serverAdminNode != nil {
|
||||
configNode.Text = "Administration"
|
||||
serverAdminNode.Url = "/admin/server"
|
||||
serverAdminNode.HideFromTabs = false
|
||||
configNode.Children = append(configNode.Children, serverAdminNode)
|
||||
adminNodeIndex := len(navTree) - 1
|
||||
navTree = navTree[:adminNodeIndex]
|
||||
}
|
||||
s.addHelpLinks(treeRoot, c)
|
||||
|
||||
if err := s.addAppLinks(treeRoot, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
navTree = s.addHelpLinks(navTree, c)
|
||||
|
||||
return navTree, nil
|
||||
return treeRoot, nil
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) addHelpLinks(navTree []*navtree.NavLink, c *models.ReqContext) []*navtree.NavLink {
|
||||
func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *models.ReqContext) {
|
||||
if setting.HelpEnabled {
|
||||
helpVersion := fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit)
|
||||
if s.cfg.AnonymousHideVersion && !c.IsSignedIn {
|
||||
helpVersion = setting.ApplicationName
|
||||
}
|
||||
|
||||
navTree = append(navTree, &navtree.NavLink{
|
||||
treeRoot.AddSection(&navtree.NavLink{
|
||||
Text: "Help",
|
||||
SubTitle: helpVersion,
|
||||
Id: "help",
|
||||
@@ -239,14 +205,6 @@ func (s *ServiceImpl) addHelpLinks(navTree []*navtree.NavLink, c *models.ReqCont
|
||||
Children: []*navtree.NavLink{},
|
||||
})
|
||||
}
|
||||
return navTree
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) addProfile(navTree []*navtree.NavLink, c *models.ReqContext) []*navtree.NavLink {
|
||||
if setting.ProfileEnabled && c.IsSignedIn {
|
||||
navTree = append(navTree, s.getProfileNode(c))
|
||||
}
|
||||
return navTree
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) getProfileNode(c *models.ReqContext) *navtree.NavLink {
|
||||
@@ -352,11 +310,13 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b
|
||||
}
|
||||
|
||||
dashboardChildNavs := []*navtree.NavLink{}
|
||||
|
||||
if !s.features.IsEnabled(featuremgmt.FlagTopnav) {
|
||||
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
|
||||
Text: "Browse", Id: "dashboards/browse", Url: s.cfg.AppSubURL + "/dashboards", Icon: "sitemap",
|
||||
Text: "Browse", Id: navtree.NavIDDashboardsBrowse, Url: s.cfg.AppSubURL + "/dashboards", Icon: "sitemap",
|
||||
})
|
||||
}
|
||||
|
||||
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
|
||||
Text: "Playlists", Description: "Groups of dashboards that are displayed in a sequence", Id: "dashboards/playlists", Url: s.cfg.AppSubURL + "/playlists", Icon: "presentation-play",
|
||||
})
|
||||
@@ -388,7 +348,7 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b
|
||||
})
|
||||
}
|
||||
|
||||
if hasEditPerm {
|
||||
if hasEditPerm && !s.features.IsEnabled(featuremgmt.FlagTopnav) {
|
||||
dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{
|
||||
Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true,
|
||||
})
|
||||
@@ -413,10 +373,11 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return dashboardChildNavs
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) buildLegacyAlertNavLinks(c *models.ReqContext) []*navtree.NavLink {
|
||||
func (s *ServiceImpl) buildLegacyAlertNavLinks(c *models.ReqContext) *navtree.NavLink {
|
||||
var alertChildNavs []*navtree.NavLink
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Alert rules", Id: "alert-list", Url: s.cfg.AppSubURL + "/alerting/list", Icon: "list-ul",
|
||||
@@ -446,10 +407,10 @@ func (s *ServiceImpl) buildLegacyAlertNavLinks(c *models.ReqContext) []*navtree.
|
||||
alertNav.Url = s.cfg.AppSubURL + "/alerting/list"
|
||||
}
|
||||
|
||||
return []*navtree.NavLink{&alertNav}
|
||||
return &alertNav
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) buildAlertNavLinks(c *models.ReqContext, hasEditPerm bool) []*navtree.NavLink {
|
||||
func (s *ServiceImpl) buildAlertNavLinks(c *models.ReqContext, hasEditPerm bool) *navtree.NavLink {
|
||||
hasAccess := ac.HasAccess(s.accessControl, c)
|
||||
var alertChildNavs []*navtree.NavLink
|
||||
|
||||
@@ -497,7 +458,7 @@ func (s *ServiceImpl) buildAlertNavLinks(c *models.ReqContext, hasEditPerm bool)
|
||||
Text: "Alerting",
|
||||
Description: "Learn about problems in your systems moments after they occur",
|
||||
SubTitle: "Alert rules and notifications",
|
||||
Id: "alerting",
|
||||
Id: navtree.NavIDAlerting,
|
||||
Icon: "bell",
|
||||
Children: alertChildNavs,
|
||||
Section: navtree.NavSectionCore,
|
||||
@@ -510,8 +471,9 @@ func (s *ServiceImpl) buildAlertNavLinks(c *models.ReqContext, hasEditPerm bool)
|
||||
alertNav.Url = s.cfg.AppSubURL + "/alerting/list"
|
||||
}
|
||||
|
||||
return []*navtree.NavLink{&alertNav}
|
||||
return &alertNav
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -558,46 +520,3 @@ func (s *ServiceImpl) buildDataConnectionsNavLink(c *models.ReqContext) *navtree
|
||||
|
||||
return navLink
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) buildAdminNavLinks(c *models.ReqContext) []*navtree.NavLink {
|
||||
hasAccess := ac.HasAccess(s.accessControl, c)
|
||||
hasGlobalAccess := ac.HasGlobalAccess(s.accessControl, s.accesscontrolService, c)
|
||||
orgsAccessEvaluator := ac.EvalPermission(ac.ActionOrgsRead)
|
||||
adminNavLinks := []*navtree.NavLink{}
|
||||
|
||||
if hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)) {
|
||||
adminNavLinks = append(adminNavLinks, &navtree.NavLink{
|
||||
Text: "Users", Description: "Manage and create users across the whole Grafana server", Id: "global-users", Url: s.cfg.AppSubURL + "/admin/users", Icon: "user",
|
||||
})
|
||||
}
|
||||
|
||||
if hasGlobalAccess(ac.ReqGrafanaAdmin, orgsAccessEvaluator) {
|
||||
adminNavLinks = append(adminNavLinks, &navtree.NavLink{
|
||||
Text: "Organizations", Description: "Isolated instances of Grafana running on the same server", Id: "global-orgs", Url: s.cfg.AppSubURL + "/admin/orgs", Icon: "building",
|
||||
})
|
||||
}
|
||||
|
||||
if hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)) {
|
||||
adminNavLinks = append(adminNavLinks, &navtree.NavLink{
|
||||
Text: "Settings", Description: "View the settings defined in your Grafana config", Id: "server-settings", Url: s.cfg.AppSubURL + "/admin/settings", Icon: "sliders-v-alt",
|
||||
})
|
||||
}
|
||||
|
||||
if hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionSettingsRead)) && s.features.IsEnabled(featuremgmt.FlagStorage) {
|
||||
adminNavLinks = append(adminNavLinks, &navtree.NavLink{
|
||||
Text: "Storage",
|
||||
Id: "storage",
|
||||
Description: "Manage file storage",
|
||||
Icon: "cube",
|
||||
Url: s.cfg.AppSubURL + "/admin/storage",
|
||||
})
|
||||
}
|
||||
|
||||
if s.cfg.LDAPEnabled && hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)) {
|
||||
adminNavLinks = append(adminNavLinks, &navtree.NavLink{
|
||||
Text: "LDAP", Id: "ldap", Url: s.cfg.AppSubURL + "/admin/ldap", Icon: "book",
|
||||
})
|
||||
}
|
||||
|
||||
return adminNavLinks
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package pluginsettings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type FakePluginSettings struct {
|
||||
Service
|
||||
|
||||
Plugins map[string]*DTO
|
||||
}
|
||||
|
||||
// GetPluginSettings returns all Plugin Settings for the provided Org
|
||||
func (ps *FakePluginSettings) GetPluginSettings(_ context.Context, _ *GetArgs) ([]*InfoDTO, error) {
|
||||
res := []*InfoDTO{}
|
||||
for _, dto := range ps.Plugins {
|
||||
res = append(res, &InfoDTO{
|
||||
PluginID: dto.PluginID,
|
||||
OrgID: dto.OrgID,
|
||||
Enabled: dto.Enabled,
|
||||
Pinned: dto.Pinned,
|
||||
PluginVersion: dto.PluginVersion,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetPluginSettingByPluginID returns a Plugin Settings by Plugin ID
|
||||
func (ps *FakePluginSettings) GetPluginSettingByPluginID(ctx context.Context, args *GetByPluginIDArgs) (*DTO, error) {
|
||||
if res, ok := ps.Plugins[args.PluginID]; ok {
|
||||
return res, nil
|
||||
}
|
||||
return nil, models.ErrPluginSettingNotFound
|
||||
}
|
||||
|
||||
// UpdatePluginSetting updates a Plugin Setting
|
||||
func (ps *FakePluginSettings) UpdatePluginSetting(ctx context.Context, args *UpdateArgs) error {
|
||||
var secureData map[string][]byte
|
||||
if args.SecureJSONData != nil {
|
||||
secureData := map[string][]byte{}
|
||||
for k, v := range args.SecureJSONData {
|
||||
secureData[k] = ([]byte)(v)
|
||||
}
|
||||
}
|
||||
// save
|
||||
ps.Plugins[args.PluginID] = &DTO{
|
||||
ID: int64(len(ps.Plugins)),
|
||||
OrgID: args.OrgID,
|
||||
PluginID: args.PluginID,
|
||||
PluginVersion: args.PluginVersion,
|
||||
JSONData: args.JSONData,
|
||||
SecureJSONData: secureData,
|
||||
Enabled: args.Enabled,
|
||||
Pinned: args.Pinned,
|
||||
Updated: time.Now(),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePluginSettingPluginVersion updates a Plugin Setting's plugin version
|
||||
func (ps *FakePluginSettings) UpdatePluginSettingPluginVersion(ctx context.Context, args *UpdatePluginVersionArgs) error {
|
||||
if res, ok := ps.Plugins[args.PluginID]; ok {
|
||||
res.PluginVersion = args.PluginVersion
|
||||
return nil
|
||||
}
|
||||
return models.ErrPluginSettingNotFound
|
||||
}
|
||||
|
||||
// DecryptedValues decrypts the encrypted secureJSONData of the provided plugin setting and
|
||||
// returns the decrypted values.
|
||||
func (ps *FakePluginSettings) DecryptedValues(dto *DTO) map[string]string {
|
||||
// TODO: Implement
|
||||
return nil
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func TestConfigReader(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Unknown app plugin should return error", func(t *testing.T) {
|
||||
cfgProvider := newConfigReader(log.New("test logger"), fakePluginStore{})
|
||||
cfgProvider := newConfigReader(log.New("test logger"), plugins.FakePluginStore{})
|
||||
_, err := cfgProvider.readConfig(context.Background(), unknownApp)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "plugin not installed: \"nonexisting\"", err.Error())
|
||||
@@ -47,10 +47,10 @@ func TestConfigReader(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Can read correct properties", func(t *testing.T) {
|
||||
pm := fakePluginStore{
|
||||
apps: map[string]plugins.PluginDTO{
|
||||
"test-plugin": {},
|
||||
"test-plugin-2": {},
|
||||
pm := plugins.FakePluginStore{
|
||||
PluginList: []plugins.PluginDTO{
|
||||
{JSONData: plugins.JSONData{ID: "test-plugin"}},
|
||||
{JSONData: plugins.JSONData{ID: "test-plugin-2"}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -87,15 +87,3 @@ func TestConfigReader(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type fakePluginStore struct {
|
||||
plugins.Store
|
||||
|
||||
apps map[string]plugins.PluginDTO
|
||||
}
|
||||
|
||||
func (pr fakePluginStore) Plugin(_ context.Context, pluginID string) (plugins.PluginDTO, bool) {
|
||||
p, exists := pr.apps[pluginID]
|
||||
|
||||
return p, exists
|
||||
}
|
||||
|
||||
@@ -19,10 +19,11 @@ func TestPluginUpdateChecker_HasUpdate(t *testing.T) {
|
||||
availableUpdates: map[string]string{
|
||||
"test-ds": "1.0.0",
|
||||
},
|
||||
pluginStore: fakePluginStore{
|
||||
plugins: map[string]plugins.PluginDTO{
|
||||
"test-ds": {
|
||||
pluginStore: plugins.FakePluginStore{
|
||||
PluginList: []plugins.PluginDTO{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-ds",
|
||||
Info: plugins.Info{Version: "0.9.0"},
|
||||
},
|
||||
},
|
||||
@@ -41,20 +42,23 @@ func TestPluginUpdateChecker_HasUpdate(t *testing.T) {
|
||||
"test-panel": "0.9.0",
|
||||
"test-app": "0.0.1",
|
||||
},
|
||||
pluginStore: fakePluginStore{
|
||||
plugins: map[string]plugins.PluginDTO{
|
||||
"test-ds": {
|
||||
pluginStore: plugins.FakePluginStore{
|
||||
PluginList: []plugins.PluginDTO{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-ds",
|
||||
Info: plugins.Info{Version: "0.9.0"},
|
||||
},
|
||||
},
|
||||
"test-panel": {
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-panel",
|
||||
Info: plugins.Info{Version: "0.9.0"},
|
||||
},
|
||||
},
|
||||
"test-app": {
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-app",
|
||||
Info: plugins.Info{Version: "0.9.0"},
|
||||
},
|
||||
},
|
||||
@@ -80,10 +84,11 @@ func TestPluginUpdateChecker_HasUpdate(t *testing.T) {
|
||||
availableUpdates: map[string]string{
|
||||
"test-panel": "0.9.0",
|
||||
},
|
||||
pluginStore: fakePluginStore{
|
||||
plugins: map[string]plugins.PluginDTO{
|
||||
"test-ds": {
|
||||
pluginStore: plugins.FakePluginStore{
|
||||
PluginList: []plugins.PluginDTO{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-ds",
|
||||
Info: plugins.Info{Version: "1.0.0"},
|
||||
},
|
||||
},
|
||||
@@ -122,31 +127,35 @@ func TestPluginUpdateChecker_checkForUpdates(t *testing.T) {
|
||||
availableUpdates: map[string]string{
|
||||
"test-app": "1.0.0",
|
||||
},
|
||||
pluginStore: fakePluginStore{
|
||||
plugins: map[string]plugins.PluginDTO{
|
||||
"test-ds": {
|
||||
pluginStore: plugins.FakePluginStore{
|
||||
PluginList: []plugins.PluginDTO{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-ds",
|
||||
Info: plugins.Info{Version: "0.9.0"},
|
||||
Type: plugins.DataSource,
|
||||
},
|
||||
},
|
||||
"test-app": {
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-app",
|
||||
Info: plugins.Info{Version: "0.5.0"},
|
||||
Type: plugins.App,
|
||||
},
|
||||
},
|
||||
"test-panel": {
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-panel",
|
||||
Info: plugins.Info{Version: "2.5.7"},
|
||||
Type: plugins.Panel,
|
||||
},
|
||||
},
|
||||
"test-core-panel": {
|
||||
{
|
||||
Class: plugins.Core,
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-core-panel",
|
||||
Info: plugins.Info{Version: "0.0.1"},
|
||||
Type: plugins.Panel,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -195,23 +204,3 @@ func (c *fakeHTTPClient) Get(url string) (*http.Response, error) {
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type fakePluginStore struct {
|
||||
plugins.Store
|
||||
|
||||
plugins map[string]plugins.PluginDTO
|
||||
}
|
||||
|
||||
func (pr fakePluginStore) Plugin(_ context.Context, pluginID string) (plugins.PluginDTO, bool) {
|
||||
p, exists := pr.plugins[pluginID]
|
||||
|
||||
return p, exists
|
||||
}
|
||||
|
||||
func (pr fakePluginStore) Plugins(_ context.Context, _ ...plugins.Type) []plugins.PluginDTO {
|
||||
var result []plugins.PluginDTO
|
||||
for _, p := range pr.plugins {
|
||||
result = append(result, p)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user