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