mirror of https://github.com/grafana/grafana
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.SplitStringpull/55886/head
parent
202dce66ff
commit
e31cb93ec0
@ -0,0 +1,38 @@ |
||||
package plugins |
||||
|
||||
import ( |
||||
"context" |
||||
) |
||||
|
||||
type FakePluginStore struct { |
||||
Store |
||||
|
||||
PluginList []PluginDTO |
||||
} |
||||
|
||||
func (pr FakePluginStore) Plugin(_ context.Context, pluginID string) (PluginDTO, bool) { |
||||
for _, v := range pr.PluginList { |
||||
if v.ID == pluginID { |
||||
return v, true |
||||
} |
||||
} |
||||
|
||||
return PluginDTO{}, false |
||||
} |
||||
|
||||
func (pr FakePluginStore) Plugins(_ context.Context, pluginTypes ...Type) []PluginDTO { |
||||
var result []PluginDTO |
||||
if len(pluginTypes) == 0 { |
||||
pluginTypes = PluginTypes |
||||
} |
||||
|
||||
for _, v := range pr.PluginList { |
||||
for _, t := range pluginTypes { |
||||
if v.Type == t { |
||||
result = append(result, v) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return result |
||||
} |
@ -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) |
||||
}) |
||||
} |
@ -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) |
||||
}) |
||||
} |
@ -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 |
||||
} |
Loading…
Reference in new issue