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:
Torkel Ödegaard
2022-09-28 08:29:35 +02:00
committed by GitHub
parent 202dce66ff
commit e31cb93ec0
33 changed files with 1064 additions and 586 deletions
+4
View File
@@ -156,6 +156,10 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/alerting/", reqSignedIn, hs.Index)
r.Get("/alerting/*", reqSignedIn, hs.Index)
r.Get("/library-panels/", reqSignedIn, hs.Index)
r.Get("/monitoring/", reqSignedIn, hs.Index)
r.Get("/monitoring/*", reqSignedIn, hs.Index)
r.Get("/alerts-and-incidents", reqSignedIn, hs.Index)
r.Get("/alerts-and-incidents/*", reqSignedIn, hs.Index)
// sign up
r.Get("/verify", hs.Index)
+4 -3
View File
@@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/framework/coremodel/registry"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
@@ -58,7 +59,7 @@ func TestGetHomeDashboard(t *testing.T) {
hs := &HTTPServer{
Cfg: cfg,
pluginStore: &fakePluginStore{},
pluginStore: &plugins.FakePluginStore{},
SQLStore: mockstore.NewSQLStoreMock(),
preferenceService: prefService,
dashboardVersionService: dashboardVersionService,
@@ -141,7 +142,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
hs := &HTTPServer{
Cfg: setting.NewCfg(),
pluginStore: &fakePluginStore{},
pluginStore: &plugins.FakePluginStore{},
SQLStore: mockSQLStore,
AccessControl: accesscontrolmock.New(),
Features: featuremgmt.WithFeatures(),
@@ -1027,7 +1028,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
QuotaService: &quotaimpl.Service{
Cfg: cfg,
},
pluginStore: &fakePluginStore{},
pluginStore: &plugins.FakePluginStore{},
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
DashboardService: dashboardService,
+3 -2
View File
@@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/datasources/permissions"
@@ -47,7 +48,7 @@ func TestDataSourcesProxy_userLoggedIn(t *testing.T) {
// handler func being tested
hs := &HTTPServer{
Cfg: setting.NewCfg(),
pluginStore: &fakePluginStore{},
pluginStore: &plugins.FakePluginStore{},
DataSourcesService: &dataSourcesServiceMock{
expectedDatasources: ds,
},
@@ -71,7 +72,7 @@ func TestDataSourcesProxy_userLoggedIn(t *testing.T) {
// handler func being tested
hs := &HTTPServer{
Cfg: setting.NewCfg(),
pluginStore: &fakePluginStore{},
pluginStore: &plugins.FakePluginStore{},
}
sc.handlerFunc = hs.DeleteDataSourceByName
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
+1 -1
View File
@@ -15,7 +15,7 @@ type IndexViewData struct {
GoogleAnalyticsId string
GoogleAnalytics4Id string
GoogleTagManagerId string
NavTree []*navtree.NavLink
NavTree *navtree.NavTreeRoot
BuildVersion string
BuildCommit string
Theme string
-100
View File
@@ -2,11 +2,8 @@ package api
import (
"context"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/pluginsettings"
)
type fakePluginInstaller struct {
@@ -37,34 +34,6 @@ func (pm *fakePluginInstaller) Remove(_ context.Context, pluginID string) error
return 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, pluginTypes ...plugins.Type) []plugins.PluginDTO {
var result []plugins.PluginDTO
if len(pluginTypes) == 0 {
pluginTypes = plugins.PluginTypes
}
for _, v := range pr.plugins {
for _, t := range pluginTypes {
if v.Type == t {
result = append(result, v)
}
}
}
return result
}
type fakeRendererManager struct {
plugins.RendererManager
}
@@ -82,72 +51,3 @@ type fakePluginStaticRouteResolver struct {
func (psrr *fakePluginStaticRouteResolver) Routes() []*plugins.StaticRoute {
return psrr.routes
}
type fakePluginSettings struct {
pluginsettings.Service
plugins map[string]*pluginsettings.DTO
}
// GetPluginSettings returns all Plugin Settings for the provided Org
func (ps *fakePluginSettings) GetPluginSettings(_ context.Context, _ *pluginsettings.GetArgs) ([]*pluginsettings.InfoDTO, error) {
res := []*pluginsettings.InfoDTO{}
for _, dto := range ps.plugins {
res = append(res, &pluginsettings.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 *pluginsettings.GetByPluginIDArgs) (*pluginsettings.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 *pluginsettings.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] = &pluginsettings.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 *pluginsettings.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 *pluginsettings.DTO) map[string]string {
// TODO: Implement
return nil
}
+2 -1
View File
@@ -7,6 +7,7 @@ import (
"path/filepath"
"testing"
"github.com/grafana/grafana/pkg/plugins"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing"
@@ -51,7 +52,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.
},
SQLStore: sqlStore,
SettingsProvider: setting.ProvideProvider(cfg),
pluginStore: &fakePluginStore{},
pluginStore: &plugins.FakePluginStore{},
grafanaUpdateChecker: &updatechecker.GrafanaService{},
AccessControl: accesscontrolmock.New().WithDisabled(),
PluginSettings: pluginSettings.ProvideService(sqlStore, secretsService),
+3 -4
View File
@@ -3,7 +3,6 @@ package api
import (
"fmt"
"net/http"
"sort"
"strings"
"github.com/grafana/grafana/pkg/api/dtos"
@@ -150,9 +149,9 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
hs.HooksService.RunIndexDataHooks(&data, c)
sort.SliceStable(data.NavTree, func(i, j int) bool {
return data.NavTree[i].SortWeight < data.NavTree[j].SortWeight
})
// This will remove empty cfg or admin sections and move sections around if topnav is enabled
data.NavTree.RemoveEmptySectionsAndApplyNewInformationArchitecture(hs.Features.IsEnabled(featuremgmt.FlagTopnav))
data.NavTree.Sort()
return &data, nil
}
+1 -1
View File
@@ -44,7 +44,7 @@ func fakeSetIndexViewData(t *testing.T) {
data := &dtos.IndexViewData{
User: &dtos.CurrentUser{},
Settings: map[string]interface{}{},
NavTree: []*navtree.NavLink{},
NavTree: &navtree.NavTreeRoot{},
}
return data, nil
}
+20 -26
View File
@@ -175,10 +175,8 @@ func Test_GetPluginAssets(t *testing.T) {
requestedFile: {},
},
}
service := &fakePluginStore{
plugins: map[string]plugins.PluginDTO{
pluginID: p,
},
service := &plugins.FakePluginStore{
PluginList: []plugins.PluginDTO{p},
}
l := &logtest.Fake{}
@@ -200,10 +198,8 @@ func Test_GetPluginAssets(t *testing.T) {
},
PluginDir: pluginDir,
}
service := &fakePluginStore{
plugins: map[string]plugins.PluginDTO{
pluginID: p,
},
service := &plugins.FakePluginStore{
PluginList: []plugins.PluginDTO{p},
}
l := &logtest.Fake{}
@@ -223,10 +219,8 @@ func Test_GetPluginAssets(t *testing.T) {
},
PluginDir: pluginDir,
}
service := &fakePluginStore{
plugins: map[string]plugins.PluginDTO{
pluginID: p,
},
service := &plugins.FakePluginStore{
PluginList: []plugins.PluginDTO{p},
}
l := &logtest.Fake{}
@@ -248,10 +242,8 @@ func Test_GetPluginAssets(t *testing.T) {
},
PluginDir: pluginDir,
}
service := &fakePluginStore{
plugins: map[string]plugins.PluginDTO{
pluginID: p,
},
service := &plugins.FakePluginStore{
PluginList: []plugins.PluginDTO{p},
}
l := &logtest.Fake{}
@@ -271,8 +263,8 @@ func Test_GetPluginAssets(t *testing.T) {
})
t.Run("Given a request for an non-existing plugin", func(t *testing.T) {
service := &fakePluginStore{
plugins: map[string]plugins.PluginDTO{},
service := &plugins.FakePluginStore{
PluginList: []plugins.PluginDTO{},
}
l := &logtest.Fake{}
@@ -292,10 +284,11 @@ func Test_GetPluginAssets(t *testing.T) {
})
t.Run("Given a request for a core plugin's file", func(t *testing.T) {
service := &fakePluginStore{
plugins: map[string]plugins.PluginDTO{
pluginID: {
Class: plugins.Core,
service := &plugins.FakePluginStore{
PluginList: []plugins.PluginDTO{
{
JSONData: plugins.JSONData{ID: pluginID},
Class: plugins.Core,
},
},
}
@@ -392,8 +385,8 @@ func (c *fakePluginClient) QueryData(ctx context.Context, req *backend.QueryData
}
func Test_PluginsList_AccessControl(t *testing.T) {
pluginStore := fakePluginStore{plugins: map[string]plugins.PluginDTO{
"test-app": {
pluginStore := plugins.FakePluginStore{PluginList: []plugins.PluginDTO{
{
PluginDir: "/grafana/plugins/test-app/dist",
Class: "external",
DefaultNavURL: "/plugins/test-app/page/test",
@@ -410,7 +403,7 @@ func Test_PluginsList_AccessControl(t *testing.T) {
},
},
},
"mysql": {
{
PluginDir: "/grafana/public/app/plugins/datasource/mysql",
Class: "core",
Pinned: false,
@@ -428,7 +421,8 @@ func Test_PluginsList_AccessControl(t *testing.T) {
},
},
}}
pluginSettings := fakePluginSettings{plugins: map[string]*pluginsettings.DTO{
pluginSettings := pluginsettings.FakePluginSettings{Plugins: map[string]*pluginsettings.DTO{
"test-app": {ID: 0, OrgID: 1, PluginID: "test-app", PluginVersion: "1.0.0", Enabled: true},
"mysql": {ID: 0, OrgID: 1, PluginID: "mysql", PluginVersion: "", Enabled: true}},
}