mirror of
https://github.com/grafana/grafana.git
synced 2026-08-09 12:48:18 -05:00
Provisioning: Nested folders for Dashboards (#119852)
* Backend: Provisioning Nested folders for Dashboards Add support for nested folder hierarchies when using `foldersFromFilesStructure`. The folder structure on disk (e.g. `level1/level2/dashboard.json`) is now correctly reflected in **Grafana** with proper parent-child relationships. Changes: - Refactor `getOrCreateFolder` into `getOrCreateFolderInternal` with unified logic - Add `parentUID` parameter to `getOrCreateFolderByTitle` for nested folders - `getOrCreateFolderFullpath` creates folders level by level, passing parent UID - Use `identity.WithServiceIdentity` for provisioning operations Backward compatibility: - Single folder and flat structures unchanged (`parentUID = nil`) - Explicit FolderUID lookup unchanged - All existing tests pass Tests: - Strengthen "Get nested folders from files structure" to assert `ParentUID` is correctly set when creating child folders (`level2` under `level1`) # Conflicts: # pkg/services/provisioning/dashboards/file_reader_test.go * Feedbacks from AI-driven review * Fixed access to `MaxNestedFolderDepth` # Conflicts: # pkg/services/provisioning/dashboards/file_reader_test.go * Reuse nested folders by title/parent, fix docs and tests March work: nested dashboard provisioning from filesystem layout, folder reuse by title/parent, docs and foldertest helpers. * Provisioning: address PR review (usage tracker, nested save assertions) - Track provisioned usage only after a successful save in storeDashboardsInFoldersFromFileStructure. - In Get nested folders from files structure, assert FolderUID/FolderID on SaveProvisionedDashboard (per ExternalID).
This commit is contained in:
@@ -514,7 +514,11 @@ To use `foldersFromFilesStructure`, you must unset the `folder` and `folderUid`
|
||||
To provision dashboards to the root level, store them in the root of your `path`.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
This feature doesn't let you create nested folder structures, where you have folders within folders.
|
||||
Nested folder structures are supported: the folder hierarchy on disk is recreated in Grafana.
|
||||
|
||||
For example, `folderTwo/folderThree/dashboard3.json` creates a folder `folderTwo` containing a folder `folderThree` that contains the dashboard.
|
||||
|
||||
The folder depth is limited to `4` levels.
|
||||
{{< /admonition >}}
|
||||
|
||||
## Alerting
|
||||
|
||||
@@ -66,11 +66,46 @@ func (s *FakeService) Create(ctx context.Context, cmd *folder.CreateFolderComman
|
||||
}
|
||||
|
||||
func (s *FakeService) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
|
||||
if q.UID != nil && s.foldersByUID != nil {
|
||||
if q.UID != nil && *q.UID != "" && s.foldersByUID != nil {
|
||||
if f, exists := s.foldersByUID[*q.UID]; exists {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
|
||||
if q.Title != nil && *q.Title != "" {
|
||||
wantParent := ""
|
||||
if q.ParentUID != nil {
|
||||
wantParent = *q.ParentUID
|
||||
}
|
||||
if f := s.findFolderByTitleAndParent(q.OrgID, *q.Title, wantParent); f != nil {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
|
||||
return s.ExpectedFolder, s.ExpectedError
|
||||
}
|
||||
|
||||
// findFolderByTitleAndParent returns a folder matching org, title, and parent UID (empty string means root).
|
||||
func (s *FakeService) findFolderByTitleAndParent(orgID int64, title, parentUID string) *folder.Folder {
|
||||
byUID := make(map[string]*folder.Folder)
|
||||
for _, f := range s.ExpectedFolders {
|
||||
if f != nil {
|
||||
byUID[f.UID] = f
|
||||
}
|
||||
}
|
||||
for _, f := range s.foldersByUID {
|
||||
if f != nil {
|
||||
byUID[f.UID] = f
|
||||
}
|
||||
}
|
||||
for _, f := range byUID {
|
||||
if f.OrgID == orgID && f.Title == title && f.ParentUID == parentUID {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s *FakeService) GetLegacy(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
|
||||
return s.ExpectedFolder, s.ExpectedError
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ func New(ctx context.Context, configDirectory string, provisioner dashboards.Das
|
||||
return nil, fmt.Errorf("%v: %w", "Failed to read dashboards config", err)
|
||||
}
|
||||
|
||||
fileReaders, err := getFileReaders(configs, logger, provisioner, dashboardStore, folderService)
|
||||
fileReaders, err := getFileReaders(configs, logger, provisioner, dashboardStore, folderService, cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v: %w", "Failed to initialize file readers", err)
|
||||
}
|
||||
@@ -204,6 +204,7 @@ func getFileReaders(
|
||||
service dashboards.DashboardProvisioningService,
|
||||
store utils.DashboardStore,
|
||||
folderService folder.Service,
|
||||
cfg *setting.Cfg,
|
||||
) ([]*FileReader, error) {
|
||||
var readers []*FileReader
|
||||
|
||||
@@ -216,6 +217,7 @@ func getFileReaders(
|
||||
service,
|
||||
store,
|
||||
folderService,
|
||||
cfg,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create file reader for config %v: %w", config.Name, err)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/utils"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
@@ -30,6 +31,27 @@ var (
|
||||
ErrGetOrCreateFolder = errors.New("failed to get or create provisioning folder")
|
||||
)
|
||||
|
||||
// folderPathCacheEntry holds id and uid for a folder path.
|
||||
// Used by getOrCreateFolderFullpath to avoid redundant Get/Create calls for the same path during a single walkDisk.
|
||||
// The cache is not thread-safe and is scoped to one provisioning cycle (walkDisk).
|
||||
type folderPathCacheEntry struct {
|
||||
id int64
|
||||
uid string
|
||||
}
|
||||
|
||||
// splitFolderFullpath splits folderFullpath by "/" and returns non-empty segments.
|
||||
// The path comes from the filesystem (filepath.Rel + ReplaceAll), so no escape handling is needed.
|
||||
func splitFolderFullpath(folderFullpath string) []string {
|
||||
parts := strings.Split(folderFullpath, "/")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if part != "" {
|
||||
out = append(out, part)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// FileReader is responsible for reading dashboards from disk and
|
||||
// insert/update dashboards to the Grafana database using
|
||||
// `dashboards.DashboardProvisioningService`.
|
||||
@@ -42,6 +64,7 @@ type FileReader struct {
|
||||
FoldersFromFilesStructure bool
|
||||
folderService folder.Service
|
||||
foldersInUnified bool
|
||||
settingCfg *setting.Cfg
|
||||
|
||||
mux sync.RWMutex
|
||||
usageTracker *usageTracker
|
||||
@@ -50,7 +73,7 @@ type FileReader struct {
|
||||
|
||||
// NewDashboardFileReader returns a new filereader based on `config`
|
||||
func NewDashboardFileReader(cfg *config, log log.Logger, service dashboards.DashboardProvisioningService,
|
||||
dashboardStore utils.DashboardStore, folderService folder.Service) (*FileReader, error) {
|
||||
dashboardStore utils.DashboardStore, folderService folder.Service, settingCfg *setting.Cfg) (*FileReader, error) {
|
||||
var path string
|
||||
path, ok := cfg.Options["path"].(string)
|
||||
if !ok {
|
||||
@@ -75,6 +98,7 @@ func NewDashboardFileReader(cfg *config, log log.Logger, service dashboards.Dash
|
||||
dashboardStore: dashboardStore,
|
||||
folderService: folderService,
|
||||
FoldersFromFilesStructure: foldersFromFilesStructure,
|
||||
settingCfg: settingCfg,
|
||||
usageTracker: newUsageTracker(),
|
||||
}, nil
|
||||
}
|
||||
@@ -197,7 +221,7 @@ func (fr *FileReader) storeDashboardsInFolder(ctx context.Context, filesFoundOnD
|
||||
dashboardRefs map[string]*dashboards.DashboardProvisioning, usageTracker *usageTracker) error {
|
||||
ctx, _ = identity.WithServiceIdentity(ctx, fr.Cfg.OrgID)
|
||||
|
||||
folderID, folderUID, err := fr.getOrCreateFolder(ctx, fr.Cfg, fr.dashboardProvisioningService, fr.Cfg.Folder)
|
||||
folderID, folderUID, err := fr.getOrCreateFolder(ctx, fr.Cfg, fr.Cfg.Folder)
|
||||
if err != nil && !errors.Is(err, ErrFolderNameMissing) {
|
||||
return fmt.Errorf("%w with name %q: %w", ErrGetOrCreateFolder, fr.Cfg.Folder, err)
|
||||
}
|
||||
@@ -217,27 +241,45 @@ func (fr *FileReader) storeDashboardsInFolder(ctx context.Context, filesFoundOnD
|
||||
|
||||
// storeDashboardsInFoldersFromFilesystemStructure saves dashboards from the filesystem on disk to the same folder
|
||||
// in Grafana as they are in on the filesystem.
|
||||
// folderPathCache is created per walk and passed to getOrCreateFolderFullpath to avoid redundant Get/Create
|
||||
// for shared ancestor paths. It is not thread-safe and must not be shared across provisioning cycles.
|
||||
func (fr *FileReader) storeDashboardsInFoldersFromFileStructure(ctx context.Context, filesFoundOnDisk map[string]os.FileInfo,
|
||||
dashboardRefs map[string]*dashboards.DashboardProvisioning, resolvedPath string, usageTracker *usageTracker) error {
|
||||
for path, fileInfo := range filesFoundOnDisk {
|
||||
folderName := ""
|
||||
ctx, _ = identity.WithServiceIdentity(ctx, fr.Cfg.OrgID)
|
||||
folderPathCache := make(map[string]folderPathCacheEntry)
|
||||
|
||||
for path, fileInfo := range filesFoundOnDisk {
|
||||
dashboardsFolder := filepath.Dir(path)
|
||||
if dashboardsFolder != resolvedPath {
|
||||
folderName = filepath.Base(dashboardsFolder)
|
||||
relPath, err := filepath.Rel(resolvedPath, dashboardsFolder)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to calculate relative path from %q to %q: %w", resolvedPath, dashboardsFolder, err)
|
||||
}
|
||||
|
||||
ctx, _ = identity.WithServiceIdentity(ctx, fr.Cfg.OrgID)
|
||||
folderID, folderUID, err := fr.getOrCreateFolder(ctx, fr.Cfg, fr.dashboardProvisioningService, folderName)
|
||||
if err != nil && !errors.Is(err, ErrFolderNameMissing) {
|
||||
return fmt.Errorf("%w with name %q from file system structure: %w", ErrGetOrCreateFolder, folderName, err)
|
||||
// Replace the OS separator with a forward slash to get the full path of the folder
|
||||
folderFullpath := strings.ReplaceAll(relPath, string(filepath.Separator), "/")
|
||||
if folderFullpath == "." || folderFullpath == "" {
|
||||
folderFullpath = ""
|
||||
}
|
||||
|
||||
var folderID int64
|
||||
var folderUID string
|
||||
if folderFullpath == "" {
|
||||
folderID = 0
|
||||
folderUID = ""
|
||||
} else {
|
||||
folderID, folderUID, err = fr.getOrCreateFolderFullpath(ctx, folderFullpath, fr.Cfg.OrgID, folderPathCache)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w with full path %q from file system structure: %w", ErrGetOrCreateFolder, folderFullpath, err)
|
||||
}
|
||||
}
|
||||
|
||||
provisioningMetadata, err := fr.saveDashboard(ctx, path, folderID, folderUID, fileInfo, dashboardRefs)
|
||||
usageTracker.track(provisioningMetadata)
|
||||
if err != nil {
|
||||
fr.log.Error("failed to save dashboard", "file", path, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
usageTracker.track(provisioningMetadata)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -369,28 +411,30 @@ func (fr *FileReader) getProvisionedDashboardsByPath(ctx context.Context, servic
|
||||
return byPath, nil
|
||||
}
|
||||
|
||||
func (fr *FileReader) getOrCreateFolder(ctx context.Context, cfg *config, service dashboards.DashboardProvisioningService, folderName string) (int64, string, error) {
|
||||
// getOrCreateFolderInternal is the shared logic for getting or creating a folder.
|
||||
// - explicitUID: if non-nil and non-empty, used for lookup and creation; otherwise lookup by Title+ParentUID and generate UID on create
|
||||
// - parentUID: optional parent for nested folders
|
||||
func (fr *FileReader) getOrCreateFolderInternal(ctx context.Context, orgID int64, folderName string, parentUID *string, explicitUID *string) (int64, string, error) {
|
||||
if folderName == "" {
|
||||
return 0, "", ErrFolderNameMissing
|
||||
}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
user, reqErr := identity.GetRequester(ctx)
|
||||
if reqErr != nil {
|
||||
return 0, "", reqErr
|
||||
}
|
||||
|
||||
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Provisioning).Inc()
|
||||
cmd := &folder.GetFolderQuery{
|
||||
OrgID: cfg.OrgID,
|
||||
OrgID: orgID,
|
||||
SignedInUser: user,
|
||||
}
|
||||
|
||||
if cfg.FolderUID != "" {
|
||||
cmd.UID = &cfg.FolderUID
|
||||
if explicitUID != nil && *explicitUID != "" {
|
||||
cmd.UID = explicitUID
|
||||
} else {
|
||||
// provisioning depends on unique names
|
||||
//nolint:staticcheck
|
||||
cmd.Title = &folderName
|
||||
cmd.ParentUID = parentUID
|
||||
}
|
||||
|
||||
result, err := fr.folderService.Get(ctx, cmd)
|
||||
@@ -406,7 +450,7 @@ func (fr *FileReader) getOrCreateFolder(ctx context.Context, cfg *config, servic
|
||||
// When we expect folders in unified storage, they should have a manager indicated.
|
||||
// NOTE: when everything has been running in mode5 for a while, this check can be removed.
|
||||
if err == nil && result != nil && result.ManagedBy == "" && fr.foldersInUnified {
|
||||
result, err = service.UpdateFolderWithManagedByAnnotation(ctx, result, fr.Cfg.Name)
|
||||
result, err = fr.dashboardProvisioningService.UpdateFolderWithManagedByAnnotation(ctx, result, fr.Cfg.Name)
|
||||
if err != nil {
|
||||
return 0, "", fmt.Errorf("unable to update provisioned folder")
|
||||
}
|
||||
@@ -414,14 +458,25 @@ func (fr *FileReader) getOrCreateFolder(ctx context.Context, cfg *config, servic
|
||||
|
||||
// dashboard folder not found. create one.
|
||||
if errors.Is(err, dashboards.ErrFolderNotFound) {
|
||||
// Generate a new UID for the folder if not provided
|
||||
uid := util.GenerateShortUID()
|
||||
if explicitUID != nil {
|
||||
uid = *explicitUID
|
||||
}
|
||||
|
||||
createCmd := &folder.CreateFolderCommand{
|
||||
OrgID: cfg.OrgID,
|
||||
UID: cfg.FolderUID,
|
||||
OrgID: orgID,
|
||||
UID: uid,
|
||||
Title: folderName,
|
||||
SignedInUser: user,
|
||||
}
|
||||
|
||||
f, err := service.SaveFolderForProvisionedDashboards(ctx, createCmd, fr.Cfg.Name)
|
||||
// If a parent UID is provided, set it as the parent of the new folder
|
||||
if parentUID != nil {
|
||||
createCmd.ParentUID = *parentUID
|
||||
}
|
||||
|
||||
f, err := fr.dashboardProvisioningService.SaveFolderForProvisionedDashboards(ctx, createCmd, fr.Cfg.Name)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
@@ -430,10 +485,66 @@ func (fr *FileReader) getOrCreateFolder(ctx context.Context, cfg *config, servic
|
||||
return f.ID, f.UID, nil
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
// nolint:staticcheck
|
||||
return result.ID, result.UID, nil
|
||||
}
|
||||
|
||||
func (fr *FileReader) getOrCreateFolder(ctx context.Context, cfg *config, folderName string) (int64, string, error) {
|
||||
// Ensures Requester for getOrCreateFolderInternal when callers bypass storeDashboardsInFolder (e.g. tests); redundant if parent already wrapped ctx.
|
||||
ctx, _ = identity.WithServiceIdentity(ctx, cfg.OrgID)
|
||||
var explicitUID *string
|
||||
if cfg.FolderUID != "" {
|
||||
explicitUID = &cfg.FolderUID
|
||||
}
|
||||
return fr.getOrCreateFolderInternal(ctx, cfg.OrgID, folderName, nil, explicitUID)
|
||||
}
|
||||
|
||||
// getOrCreateFolderFullpath creates the nested folder hierarchy for folderFullpath (e.g. "level1/level2"),
|
||||
// reusing cached entries when cache is provided to avoid redundant Get/Create for shared ancestors.
|
||||
func (fr *FileReader) getOrCreateFolderFullpath(ctx context.Context, folderFullpath string, orgID int64, cache map[string]folderPathCacheEntry) (int64, string, error) {
|
||||
// Same contract as getOrCreateFolder: direct callers/tests need identity; harmless duplicate when storeDashboardsInFoldersFromFileStructure already wrapped ctx.
|
||||
ctx, _ = identity.WithServiceIdentity(ctx, orgID)
|
||||
folderTitles := splitFolderFullpath(folderFullpath)
|
||||
if len(folderTitles) == 0 {
|
||||
return 0, "", fmt.Errorf("invalid folder full path: %s", folderFullpath)
|
||||
}
|
||||
|
||||
maxDepth := fr.settingCfg.MaxNestedFolderDepth
|
||||
if len(folderTitles) > maxDepth {
|
||||
return 0, "", fmt.Errorf("nested folder depth %d exceeds maximum %d", len(folderTitles), maxDepth)
|
||||
}
|
||||
|
||||
// folderUID: UID of the current folder in the chain (becomes the parent for the next).
|
||||
// parentForNext: pointer to folderUID, passed to getOrCreateFolderInternal. nil for the first level.
|
||||
var folderUID string
|
||||
var parentForNext *string
|
||||
var folderID int64 // deprecated but still required for compatibility
|
||||
for i := range folderTitles {
|
||||
cumulativePath := strings.Join(folderTitles[:i+1], "/")
|
||||
if cache != nil {
|
||||
if entry, ok := cache[cumulativePath]; ok {
|
||||
// Cache hit: reuse folder from a previous file in the same walk.
|
||||
folderID = entry.id
|
||||
folderUID = entry.uid
|
||||
parentForNext = &folderUID
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
id, uid, err := fr.getOrCreateFolderInternal(ctx, orgID, folderTitles[i], parentForNext, nil)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
folderID = id
|
||||
folderUID = uid
|
||||
parentForNext = &folderUID
|
||||
if cache != nil {
|
||||
cache[cumulativePath] = folderPathCacheEntry{id: id, uid: uid}
|
||||
}
|
||||
}
|
||||
return folderID, folderUID, nil
|
||||
}
|
||||
|
||||
func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) {
|
||||
checkFilepath, err := filepath.EvalSymlinks(path)
|
||||
if path != checkFilepath {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -26,7 +27,7 @@ func TestProvisionedSymlinkedFolder(t *testing.T) {
|
||||
Options: map[string]any{"path": symlinkedFolder},
|
||||
}
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil, nil)
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil, nil, setting.NewCfg())
|
||||
if err != nil {
|
||||
t.Error("expected err to be nil")
|
||||
}
|
||||
|
||||
@@ -22,8 +22,10 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
resourcepb "github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
@@ -41,6 +43,32 @@ const (
|
||||
configName = "default"
|
||||
)
|
||||
|
||||
// folderServiceForFoldersFromFilesStructure returns a fake folder service for tests using foldersFromFilesStructure.
|
||||
// The real folderimpl requires Kubernetes REST config (apiserver);
|
||||
// the test setup uses WithoutRestConfig which causes "rest config will not be available".
|
||||
// Get falls back to ExpectedError (ErrFolderNotFound) when no folder matches title+parent,
|
||||
// so folders are created via SaveFolderForProvisionedDashboards—typical for fresh provisioning.
|
||||
func folderServiceForFoldersFromFilesStructure() folder.Service {
|
||||
fakeFolder := foldertest.NewFakeService()
|
||||
fakeFolder.ExpectedFolder = nil
|
||||
fakeFolder.ExpectedError = dashboards.ErrFolderNotFound
|
||||
return fakeFolder
|
||||
}
|
||||
|
||||
// folderServiceWithPreexistingFilesStructureHierarchy seeds folders that mirror testdata/folders-from-files-structure
|
||||
// with UIDs that are not derived from any path hash, so tests can assert reuse by title+parent.
|
||||
func folderServiceWithPreexistingFilesStructureHierarchy() folder.Service {
|
||||
fakeFolder := foldertest.NewFakeService()
|
||||
const orgID int64 = 1
|
||||
fakeFolder.AddFolder(&folder.Folder{OrgID: orgID, UID: "preexist-folderOne", Title: "folderOne", ParentUID: ""})
|
||||
fakeFolder.AddFolder(&folder.Folder{OrgID: orgID, UID: "preexist-folderTwo", Title: "folderTwo", ParentUID: ""})
|
||||
fakeFolder.AddFolder(&folder.Folder{OrgID: orgID, UID: "preexist-folderThree", Title: "folderThree", ParentUID: "preexist-folderTwo"})
|
||||
fakeFolder.AddFolder(&folder.Folder{OrgID: orgID, UID: "preexist-folderFour", Title: "folderFour", ParentUID: "preexist-folderThree"})
|
||||
fakeFolder.ExpectedFolder = nil
|
||||
fakeFolder.ExpectedError = dashboards.ErrFolderNotFound
|
||||
return fakeFolder
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
@@ -59,7 +87,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
|
||||
t.Run("using path parameter", func(t *testing.T) {
|
||||
cfg := setup()
|
||||
cfg.Options["path"] = defaultDashboards
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil, nil)
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil, nil, setting.NewCfg())
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, reader.Path, "")
|
||||
})
|
||||
@@ -67,7 +95,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
|
||||
t.Run("using folder as options", func(t *testing.T) {
|
||||
cfg := setup()
|
||||
cfg.Options["folder"] = defaultDashboards
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil, nil)
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil, nil, setting.NewCfg())
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, reader.Path, "")
|
||||
})
|
||||
@@ -76,7 +104,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
|
||||
cfg := setup()
|
||||
cfg.Options["path"] = foldersFromFilesStructure
|
||||
cfg.Options["foldersFromFilesStructure"] = true
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil, nil)
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil, nil, setting.NewCfg())
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, reader.Path, "")
|
||||
})
|
||||
@@ -89,7 +117,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
|
||||
}
|
||||
|
||||
cfg.Options["folder"] = fullPath
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil, nil)
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil, nil, setting.NewCfg())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, reader.Path, fullPath)
|
||||
@@ -99,7 +127,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
|
||||
t.Run("using relative path", func(t *testing.T) {
|
||||
cfg := setup()
|
||||
cfg.Options["folder"] = defaultDashboards
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil, nil)
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil, nil, nil, setting.NewCfg())
|
||||
require.NoError(t, err)
|
||||
|
||||
resolvedPath := reader.resolvedPath()
|
||||
@@ -144,8 +172,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once()
|
||||
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, configName).Return(&folder.Folder{ID: 1}, nil).Once()
|
||||
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 2}, nil).Times(2)
|
||||
reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
reader.dashboardProvisioningService = fakeService
|
||||
reader, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reader.walkDisk(context.Background())
|
||||
@@ -164,8 +191,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
inserted++
|
||||
})
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
reader.dashboardProvisioningService = fakeService
|
||||
reader, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reader.walkDisk(context.Background())
|
||||
@@ -201,8 +227,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
|
||||
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once()
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
reader.dashboardProvisioningService = fakeService
|
||||
reader, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reader.walkDisk(context.Background())
|
||||
@@ -229,8 +254,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once()
|
||||
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once()
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
reader.dashboardProvisioningService = fakeService
|
||||
reader, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reader.walkDisk(context.Background())
|
||||
@@ -264,8 +288,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
|
||||
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once()
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
reader.dashboardProvisioningService = fakeService
|
||||
reader, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reader.walkDisk(context.Background())
|
||||
@@ -292,8 +315,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once()
|
||||
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once()
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
reader.dashboardProvisioningService = fakeService
|
||||
reader, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reader.walkDisk(context.Background())
|
||||
@@ -307,8 +329,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once()
|
||||
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once()
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
reader.dashboardProvisioningService = fakeService
|
||||
reader, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reader.walkDisk(context.Background())
|
||||
@@ -321,17 +342,142 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
cfg.Options["foldersFromFilesStructure"] = true
|
||||
|
||||
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once()
|
||||
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, configName).Return(&folder.Folder{}, nil).Times(2)
|
||||
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(3)
|
||||
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, configName).Return(&folder.Folder{}, nil).Times(4)
|
||||
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(5)
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
reader.dashboardProvisioningService = fakeService
|
||||
reader, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderServiceForFoldersFromFilesStructure(), cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reader.walkDisk(context.Background())
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Get nested folders from files structure", func(t *testing.T) {
|
||||
setup()
|
||||
cfg.Options["path"] = foldersFromFilesStructure
|
||||
cfg.Options["foldersFromFilesStructure"] = true
|
||||
|
||||
// root.json, folderOne/dashboard1, folderTwo/dashboard2, folderTwo/folderThree/dashboard3, folderTwo/folderThree/folderFour/dashboard4.
|
||||
// Cache: folderTwo and folderThree reused for dashboard4.
|
||||
const folderOneUID = "folderOne-uid"
|
||||
const folderTwoUID = "folderTwo-uid"
|
||||
const folderThreeUID = "folderThree-uid"
|
||||
|
||||
var folderCreateCalls []*folder.CreateFolderCommand
|
||||
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once()
|
||||
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.MatchedBy(func(cmd *folder.CreateFolderCommand) bool {
|
||||
return cmd.Title == "folderOne" && cmd.ParentUID == ""
|
||||
}), configName).Run(func(args mock.Arguments) {
|
||||
folderCreateCalls = append(folderCreateCalls, args[1].(*folder.CreateFolderCommand))
|
||||
}).Return(&folder.Folder{ID: 1, UID: folderOneUID, Title: "folderOne"}, nil).Once()
|
||||
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.MatchedBy(func(cmd *folder.CreateFolderCommand) bool {
|
||||
return cmd.Title == "folderTwo" && cmd.ParentUID == ""
|
||||
}), configName).Run(func(args mock.Arguments) {
|
||||
folderCreateCalls = append(folderCreateCalls, args[1].(*folder.CreateFolderCommand))
|
||||
}).Return(&folder.Folder{ID: 2, UID: folderTwoUID, Title: "folderTwo"}, nil).Once()
|
||||
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.MatchedBy(func(cmd *folder.CreateFolderCommand) bool {
|
||||
return cmd.Title == "folderThree" && cmd.ParentUID == folderTwoUID
|
||||
}), configName).Run(func(args mock.Arguments) {
|
||||
folderCreateCalls = append(folderCreateCalls, args[1].(*folder.CreateFolderCommand))
|
||||
}).Return(&folder.Folder{ID: 3, UID: folderThreeUID, Title: "folderThree"}, nil).Once()
|
||||
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.MatchedBy(func(cmd *folder.CreateFolderCommand) bool {
|
||||
return cmd.Title == "folderFour" && cmd.ParentUID == folderThreeUID
|
||||
}), configName).Run(func(args mock.Arguments) {
|
||||
folderCreateCalls = append(folderCreateCalls, args[1].(*folder.CreateFolderCommand))
|
||||
}).Return(&folder.Folder{ID: 4, UID: "folderFour-uid", Title: "folderFour"}, nil).Once()
|
||||
|
||||
// Map iteration order is undefined; record FolderUID/FolderID per provisioned file (ExternalID).
|
||||
savedFolderByExternalID := make(map[string]struct {
|
||||
folderUID string
|
||||
folderID int64
|
||||
})
|
||||
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).
|
||||
Run(func(args mock.Arguments) {
|
||||
dto := args.Get(1).(*dashboards.SaveDashboardDTO)
|
||||
dp := args.Get(2).(*dashboards.DashboardProvisioning)
|
||||
require.NotNil(t, dto)
|
||||
require.NotNil(t, dto.Dashboard)
|
||||
require.NotNil(t, dp)
|
||||
savedFolderByExternalID[dp.ExternalID] = struct {
|
||||
folderUID string
|
||||
folderID int64
|
||||
}{folderUID: dto.Dashboard.FolderUID, folderID: dto.Dashboard.FolderID} // nolint:staticcheck
|
||||
}).
|
||||
Return(&dashboards.Dashboard{}, nil).
|
||||
Times(5)
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderServiceForFoldersFromFilesStructure(), cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reader.walkDisk(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
basePath := reader.resolvedPath()
|
||||
dashboard3Path := filepath.Join(basePath, "folderTwo", "folderThree", "dashboard3.json")
|
||||
dashboard4Path := filepath.Join(basePath, "folderTwo", "folderThree", "folderFour", "dashboard4.json")
|
||||
require.Contains(t, savedFolderByExternalID, dashboard3Path)
|
||||
require.Equal(t, folderThreeUID, savedFolderByExternalID[dashboard3Path].folderUID)
|
||||
require.Equal(t, int64(3), savedFolderByExternalID[dashboard3Path].folderID)
|
||||
require.Contains(t, savedFolderByExternalID, dashboard4Path)
|
||||
require.Equal(t, "folderFour-uid", savedFolderByExternalID[dashboard4Path].folderUID)
|
||||
require.Equal(t, int64(4), savedFolderByExternalID[dashboard4Path].folderID)
|
||||
|
||||
require.Len(t, folderCreateCalls, 4, "cache: folderOne, folderTwo, folderThree created; folderFour reuses ancestors")
|
||||
folderByTitle := make(map[string]*folder.CreateFolderCommand)
|
||||
for _, c := range folderCreateCalls {
|
||||
folderByTitle[c.Title] = c
|
||||
}
|
||||
require.Equal(t, folderTwoUID, folderByTitle["folderThree"].ParentUID, "folderThree must be nested under folderTwo")
|
||||
require.Equal(t, folderThreeUID, folderByTitle["folderFour"].ParentUID, "folderFour must be nested under folderThree")
|
||||
})
|
||||
|
||||
t.Run("Reuses existing folders by title and parent across walks", func(t *testing.T) {
|
||||
setup()
|
||||
cfg.Options["path"] = foldersFromFilesStructure
|
||||
cfg.Options["foldersFromFilesStructure"] = true
|
||||
|
||||
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Twice()
|
||||
// Folders are pre-seeded in the fake with non-hash UIDs; provisioning must not create folders.
|
||||
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(10)
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderServiceWithPreexistingFilesStructureHierarchy(), cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reader.walkDisk(context.Background())
|
||||
require.NoError(t, err)
|
||||
err = reader.walkDisk(context.Background())
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Single-level and nested folders from files structure", func(t *testing.T) {
|
||||
setup()
|
||||
cfg.Options["path"] = foldersFromFilesStructure
|
||||
cfg.Options["foldersFromFilesStructure"] = true
|
||||
|
||||
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once()
|
||||
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, configName).Return(&folder.Folder{}, nil).Times(4)
|
||||
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(5)
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderServiceForFoldersFromFilesStructure(), cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reader.walkDisk(context.Background())
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Nested folder depth exceeds maximum returns error", func(t *testing.T) {
|
||||
setup()
|
||||
cfg.Options["path"] = oneDashboard
|
||||
cfg.Options["foldersFromFilesStructure"] = true
|
||||
// We test getOrCreateFolderFullpath directly; depth check runs before folder service is called.
|
||||
reader, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderServiceForFoldersFromFilesStructure(), cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = reader.getOrCreateFolderFullpath(context.Background(), "a/b/c/d/e", 1, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "exceeds maximum")
|
||||
})
|
||||
|
||||
t.Run("Invalid configuration should return error", func(t *testing.T) {
|
||||
setup()
|
||||
cfg := &config{
|
||||
@@ -341,7 +487,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
Folder: "",
|
||||
}
|
||||
|
||||
_, err := NewDashboardFileReader(cfg, logger, nil, nil, folderSvc)
|
||||
_, err := NewDashboardFileReader(cfg, logger, nil, nil, folderSvc, cfgT)
|
||||
require.NotNil(t, err)
|
||||
})
|
||||
|
||||
@@ -349,7 +495,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
setup()
|
||||
cfg.Options["path"] = brokenDashboards
|
||||
|
||||
_, err := NewDashboardFileReader(cfg, logger, nil, nil, folderSvc)
|
||||
_, err := NewDashboardFileReader(cfg, logger, nil, nil, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@@ -362,15 +508,13 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, mock.Anything).Return(&folder.Folder{}, nil).Times(2)
|
||||
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(2)
|
||||
|
||||
reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore, folderSvc)
|
||||
reader1.dashboardProvisioningService = fakeService
|
||||
reader1, err := NewDashboardFileReader(cfg1, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reader1.walkDisk(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore, folderSvc)
|
||||
reader2.dashboardProvisioningService = fakeService
|
||||
reader2, err := NewDashboardFileReader(cfg2, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reader2.walkDisk(context.Background())
|
||||
@@ -389,10 +533,10 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
"folder": defaultDashboards,
|
||||
},
|
||||
}
|
||||
r, err := NewDashboardFileReader(cfg, logger, nil, nil, folderSvc)
|
||||
r, err := NewDashboardFileReader(cfg, logger, nil, nil, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = r.getOrCreateFolder(context.Background(), cfg, fakeService, cfg.Folder)
|
||||
_, _, err = r.getOrCreateFolder(context.Background(), cfg, cfg.Folder)
|
||||
require.Equal(t, err, ErrFolderNameMissing)
|
||||
})
|
||||
|
||||
@@ -409,12 +553,12 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
}
|
||||
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, cfg.Name).Return(&folder.Folder{ID: 1}, nil).Once()
|
||||
|
||||
r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
r, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, _ = identity.WithServiceIdentity(ctx, 1)
|
||||
_, _, err = r.getOrCreateFolder(ctx, cfg, fakeService, cfg.Folder)
|
||||
_, _, err = r.getOrCreateFolder(ctx, cfg, cfg.Folder)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@@ -431,12 +575,12 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
r, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, _ = identity.WithServiceIdentity(ctx, 1)
|
||||
_, _, err = r.getOrCreateFolder(ctx, cfg, fakeService, cfg.Folder)
|
||||
_, _, err = r.getOrCreateFolder(ctx, cfg, cfg.Folder)
|
||||
require.ErrorIs(t, err, folder.ErrInvalidUID)
|
||||
})
|
||||
|
||||
@@ -466,8 +610,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
}
|
||||
|
||||
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once()
|
||||
reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
reader.dashboardProvisioningService = fakeService
|
||||
reader, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
resolvedPath := reader.resolvedPath()
|
||||
dashboards, err := reader.getProvisionedDashboardsByPath(context.Background(), fakeService, configName)
|
||||
@@ -503,15 +646,17 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
t.Run("Missing dashboard should be unprovisioned if DisableDeletion = true", func(t *testing.T) {
|
||||
setupFakeService()
|
||||
|
||||
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once()
|
||||
fakeService.On("UnprovisionDashboard", mock.Anything, mock.Anything).Return(nil).Once()
|
||||
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once()
|
||||
fakeForUnprovision := &dashboards.FakeDashboardProvisioning{}
|
||||
defer fakeForUnprovision.AssertExpectations(t)
|
||||
fakeForUnprovision.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once()
|
||||
fakeForUnprovision.On("UnprovisionDashboard", mock.Anything, mock.Anything).Return(nil).Once()
|
||||
fakeForUnprovision.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once()
|
||||
|
||||
cfg.DisableDeletion = true
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
reader.dashboardProvisioningService = fakeService
|
||||
reader.dashboardProvisioningService = fakeForUnprovision
|
||||
|
||||
err = reader.walkDisk(context.Background())
|
||||
require.NoError(t, err)
|
||||
@@ -520,12 +665,13 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
t.Run("Missing dashboard should be deleted if DisableDeletion = false", func(t *testing.T) {
|
||||
setupFakeService()
|
||||
|
||||
fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once()
|
||||
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once()
|
||||
fakeService.On("DeleteProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
||||
fakeForDelete := &dashboards.FakeDashboardProvisioning{}
|
||||
defer fakeForDelete.AssertExpectations(t)
|
||||
fakeForDelete.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once()
|
||||
fakeForDelete.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once()
|
||||
fakeForDelete.On("DeleteProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
reader.dashboardProvisioningService = fakeService
|
||||
reader, err := NewDashboardFileReader(cfg, logger, fakeForDelete, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reader.walkDisk(context.Background())
|
||||
@@ -534,6 +680,51 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSplitFolderFullpath(t *testing.T) {
|
||||
require.Equal(t, []string{"folderOne", "folderTwo"}, splitFolderFullpath("folderOne/folderTwo"))
|
||||
require.Equal(t, []string{"folderTwo", "folderThree", "folderFour"}, splitFolderFullpath("folderTwo/folderThree/folderFour"))
|
||||
require.Equal(t, []string{"folderOne", "folderTwo"}, splitFolderFullpath("folderOne//folderTwo"))
|
||||
require.Equal(t, []string{"folderOne"}, splitFolderFullpath("folderOne"))
|
||||
require.Empty(t, splitFolderFullpath(""))
|
||||
require.Empty(t, splitFolderFullpath("///"))
|
||||
}
|
||||
|
||||
func TestIntegrationFolderPathCacheReuse(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
fakeService := &dashboards.FakeDashboardProvisioning{}
|
||||
defer fakeService.AssertExpectations(t)
|
||||
fakeStore := &fakeDashboardStore{}
|
||||
|
||||
cfg := &config{
|
||||
Name: "cache-test",
|
||||
Type: "file",
|
||||
OrgID: 1,
|
||||
Folder: "",
|
||||
Options: map[string]any{"path": foldersFromFilesStructure},
|
||||
}
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test"), fakeService, fakeStore, folderServiceForFoldersFromFilesStructure(), setting.NewCfg())
|
||||
require.NoError(t, err)
|
||||
|
||||
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, "cache-test").
|
||||
Return(&folder.Folder{ID: 1, UID: "folderTwo-uid", Title: "folderTwo"}, nil).Once()
|
||||
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, "cache-test").
|
||||
Return(&folder.Folder{ID: 2, UID: "folderThree-uid", Title: "folderThree"}, nil).Once()
|
||||
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, "cache-test").
|
||||
Return(&folder.Folder{ID: 3, UID: "folderFour-uid", Title: "folderFour"}, nil).Once()
|
||||
|
||||
cache := make(map[string]folderPathCacheEntry)
|
||||
ctx := context.Background()
|
||||
|
||||
_, _, err = reader.getOrCreateFolderFullpath(ctx, "folderTwo/folderThree", 1, cache)
|
||||
require.NoError(t, err)
|
||||
_, _, err = reader.getOrCreateFolderFullpath(ctx, "folderTwo/folderThree/folderFour", 1, cache)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Second call must only create folderFour (folderTwo and folderThree from cache).
|
||||
fakeService.AssertExpectations(t)
|
||||
}
|
||||
|
||||
type FakeFileInfo struct {
|
||||
isDirectory bool
|
||||
name string
|
||||
|
||||
+156
-156
@@ -1,160 +1,160 @@
|
||||
{
|
||||
"title": "Grafana1",
|
||||
"tags": [],
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"rows": [
|
||||
{
|
||||
"title": "New row",
|
||||
"height": "150px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||
"style": {},
|
||||
"title": "Welcome to"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Welcome to Grafana",
|
||||
"height": "210px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 2,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
|
||||
"style": {},
|
||||
"title": "Documentation Links"
|
||||
"title": "Grafana1",
|
||||
"tags": [],
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"rows": [
|
||||
{
|
||||
"title": "New row",
|
||||
"height": "150px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||
"style": {},
|
||||
"title": "Welcome to"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Welcome to Grafana",
|
||||
"height": "210px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 2,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
|
||||
"style": {},
|
||||
"title": "Documentation Links"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
|
||||
"style": {},
|
||||
"title": "Tips & Shortcuts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "test",
|
||||
"height": "250px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"panels": [
|
||||
{
|
||||
"id": 4,
|
||||
"span": 12,
|
||||
"type": "graph",
|
||||
"x-axis": true,
|
||||
"y-axis": true,
|
||||
"scale": 1,
|
||||
"y_formats": [
|
||||
"short",
|
||||
"short"
|
||||
],
|
||||
"grid": {
|
||||
"max": null,
|
||||
"min": null,
|
||||
"leftMax": null,
|
||||
"rightMax": null,
|
||||
"leftMin": null,
|
||||
"rightMin": null,
|
||||
"threshold1": null,
|
||||
"threshold2": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
|
||||
"style": {},
|
||||
"title": "Tips & Shortcuts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "test",
|
||||
"height": "250px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"panels": [
|
||||
{
|
||||
"id": 4,
|
||||
"span": 12,
|
||||
"type": "graph",
|
||||
"x-axis": true,
|
||||
"y-axis": true,
|
||||
"scale": 1,
|
||||
"y_formats": [
|
||||
"short",
|
||||
"short"
|
||||
],
|
||||
"grid": {
|
||||
"max": null,
|
||||
"min": null,
|
||||
"leftMax": null,
|
||||
"rightMax": null,
|
||||
"leftMin": null,
|
||||
"rightMin": null,
|
||||
"threshold1": null,
|
||||
"threshold2": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
"resolution": 100,
|
||||
"lines": true,
|
||||
"fill": 1,
|
||||
"linewidth": 2,
|
||||
"dashes": false,
|
||||
"dashLength": 10,
|
||||
"spaceLength": 10,
|
||||
"points": false,
|
||||
"pointradius": 5,
|
||||
"bars": false,
|
||||
"stack": true,
|
||||
"spyable": true,
|
||||
"options": false,
|
||||
"legend": {
|
||||
"show": true,
|
||||
"values": false,
|
||||
"min": false,
|
||||
"max": false,
|
||||
"current": false,
|
||||
"total": false,
|
||||
"avg": false
|
||||
},
|
||||
"interactive": true,
|
||||
"legend_counts": true,
|
||||
"timezone": "browser",
|
||||
"percentage": false,
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
"value_type": "cumulative",
|
||||
"query_as_alias": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"target": "randomWalk('random walk')",
|
||||
"function": "mean",
|
||||
"column": "value"
|
||||
}
|
||||
],
|
||||
"aliasColors": {},
|
||||
"aliasYAxis": {},
|
||||
"title": "First Graph (click title to edit)",
|
||||
"datasource": "graphite",
|
||||
"renderer": "flot",
|
||||
"annotate": {
|
||||
"enable": false
|
||||
"resolution": 100,
|
||||
"lines": true,
|
||||
"fill": 1,
|
||||
"linewidth": 2,
|
||||
"dashes": false,
|
||||
"dashLength": 10,
|
||||
"spaceLength": 10,
|
||||
"points": false,
|
||||
"pointradius": 5,
|
||||
"bars": false,
|
||||
"stack": true,
|
||||
"spyable": true,
|
||||
"options": false,
|
||||
"legend": {
|
||||
"show": true,
|
||||
"values": false,
|
||||
"min": false,
|
||||
"max": false,
|
||||
"current": false,
|
||||
"total": false,
|
||||
"avg": false
|
||||
},
|
||||
"interactive": true,
|
||||
"legend_counts": true,
|
||||
"timezone": "browser",
|
||||
"percentage": false,
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
"value_type": "cumulative",
|
||||
"query_as_alias": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"target": "randomWalk('random walk')",
|
||||
"function": "mean",
|
||||
"column": "value"
|
||||
}
|
||||
],
|
||||
"aliasColors": {},
|
||||
"aliasYAxis": {},
|
||||
"title": "First Graph (click title to edit)",
|
||||
"datasource": "graphite",
|
||||
"renderer": "flot",
|
||||
"annotate": {
|
||||
"enable": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"nav": [
|
||||
{
|
||||
"type": "timepicker",
|
||||
"collapse": false,
|
||||
"enable": true,
|
||||
"status": "Stable",
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"now": true
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"nav": [
|
||||
{
|
||||
"type": "timepicker",
|
||||
"collapse": false,
|
||||
"enable": true,
|
||||
"status": "Stable",
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"now": true
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
|
||||
+156
-156
@@ -1,160 +1,160 @@
|
||||
{
|
||||
"title": "Grafana2",
|
||||
"tags": [],
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"rows": [
|
||||
{
|
||||
"title": "New row",
|
||||
"height": "150px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||
"style": {},
|
||||
"title": "Welcome to"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Welcome to Grafana",
|
||||
"height": "210px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 2,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
|
||||
"style": {},
|
||||
"title": "Documentation Links"
|
||||
"title": "Grafana2",
|
||||
"tags": [],
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"rows": [
|
||||
{
|
||||
"title": "New row",
|
||||
"height": "150px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||
"style": {},
|
||||
"title": "Welcome to"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Welcome to Grafana",
|
||||
"height": "210px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 2,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
|
||||
"style": {},
|
||||
"title": "Documentation Links"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
|
||||
"style": {},
|
||||
"title": "Tips & Shortcuts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "test",
|
||||
"height": "250px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"panels": [
|
||||
{
|
||||
"id": 4,
|
||||
"span": 12,
|
||||
"type": "graph",
|
||||
"x-axis": true,
|
||||
"y-axis": true,
|
||||
"scale": 1,
|
||||
"y_formats": [
|
||||
"short",
|
||||
"short"
|
||||
],
|
||||
"grid": {
|
||||
"max": null,
|
||||
"min": null,
|
||||
"leftMax": null,
|
||||
"rightMax": null,
|
||||
"leftMin": null,
|
||||
"rightMin": null,
|
||||
"threshold1": null,
|
||||
"threshold2": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
|
||||
"style": {},
|
||||
"title": "Tips & Shortcuts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "test",
|
||||
"height": "250px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"panels": [
|
||||
{
|
||||
"id": 4,
|
||||
"span": 12,
|
||||
"type": "graph",
|
||||
"x-axis": true,
|
||||
"y-axis": true,
|
||||
"scale": 1,
|
||||
"y_formats": [
|
||||
"short",
|
||||
"short"
|
||||
],
|
||||
"grid": {
|
||||
"max": null,
|
||||
"min": null,
|
||||
"leftMax": null,
|
||||
"rightMax": null,
|
||||
"leftMin": null,
|
||||
"rightMin": null,
|
||||
"threshold1": null,
|
||||
"threshold2": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
"resolution": 100,
|
||||
"lines": true,
|
||||
"fill": 1,
|
||||
"linewidth": 2,
|
||||
"dashes": false,
|
||||
"dashLength": 10,
|
||||
"spaceLength": 10,
|
||||
"points": false,
|
||||
"pointradius": 5,
|
||||
"bars": false,
|
||||
"stack": true,
|
||||
"spyable": true,
|
||||
"options": false,
|
||||
"legend": {
|
||||
"show": true,
|
||||
"values": false,
|
||||
"min": false,
|
||||
"max": false,
|
||||
"current": false,
|
||||
"total": false,
|
||||
"avg": false
|
||||
},
|
||||
"interactive": true,
|
||||
"legend_counts": true,
|
||||
"timezone": "browser",
|
||||
"percentage": false,
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
"value_type": "cumulative",
|
||||
"query_as_alias": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"target": "randomWalk('random walk')",
|
||||
"function": "mean",
|
||||
"column": "value"
|
||||
}
|
||||
],
|
||||
"aliasColors": {},
|
||||
"aliasYAxis": {},
|
||||
"title": "First Graph (click title to edit)",
|
||||
"datasource": "graphite",
|
||||
"renderer": "flot",
|
||||
"annotate": {
|
||||
"enable": false
|
||||
"resolution": 100,
|
||||
"lines": true,
|
||||
"fill": 1,
|
||||
"linewidth": 2,
|
||||
"dashes": false,
|
||||
"dashLength": 10,
|
||||
"spaceLength": 10,
|
||||
"points": false,
|
||||
"pointradius": 5,
|
||||
"bars": false,
|
||||
"stack": true,
|
||||
"spyable": true,
|
||||
"options": false,
|
||||
"legend": {
|
||||
"show": true,
|
||||
"values": false,
|
||||
"min": false,
|
||||
"max": false,
|
||||
"current": false,
|
||||
"total": false,
|
||||
"avg": false
|
||||
},
|
||||
"interactive": true,
|
||||
"legend_counts": true,
|
||||
"timezone": "browser",
|
||||
"percentage": false,
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
"value_type": "cumulative",
|
||||
"query_as_alias": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"target": "randomWalk('random walk')",
|
||||
"function": "mean",
|
||||
"column": "value"
|
||||
}
|
||||
],
|
||||
"aliasColors": {},
|
||||
"aliasYAxis": {},
|
||||
"title": "First Graph (click title to edit)",
|
||||
"datasource": "graphite",
|
||||
"renderer": "flot",
|
||||
"annotate": {
|
||||
"enable": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"nav": [
|
||||
{
|
||||
"type": "timepicker",
|
||||
"collapse": false,
|
||||
"enable": true,
|
||||
"status": "Stable",
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"now": true
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"nav": [
|
||||
{
|
||||
"type": "timepicker",
|
||||
"collapse": false,
|
||||
"enable": true,
|
||||
"status": "Stable",
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"now": true
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"title": "Grafana3",
|
||||
"tags": [],
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"rows": [
|
||||
{
|
||||
"title": "New row",
|
||||
"height": "150px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||
"style": {},
|
||||
"title": "Welcome to"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Welcome to Grafana",
|
||||
"height": "210px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 2,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
|
||||
"style": {},
|
||||
"title": "Documentation Links"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
|
||||
"style": {},
|
||||
"title": "Tips & Shortcuts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "test",
|
||||
"height": "250px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"panels": [
|
||||
{
|
||||
"id": 4,
|
||||
"span": 12,
|
||||
"type": "graph",
|
||||
"x-axis": true,
|
||||
"y-axis": true,
|
||||
"scale": 1,
|
||||
"y_formats": [
|
||||
"short",
|
||||
"short"
|
||||
],
|
||||
"grid": {
|
||||
"max": null,
|
||||
"min": null,
|
||||
"leftMax": null,
|
||||
"rightMax": null,
|
||||
"leftMin": null,
|
||||
"rightMin": null,
|
||||
"threshold1": null,
|
||||
"threshold2": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
"resolution": 100,
|
||||
"lines": true,
|
||||
"fill": 1,
|
||||
"linewidth": 2,
|
||||
"dashes": false,
|
||||
"dashLength": 10,
|
||||
"spaceLength": 10,
|
||||
"points": false,
|
||||
"pointradius": 5,
|
||||
"bars": false,
|
||||
"stack": true,
|
||||
"spyable": true,
|
||||
"options": false,
|
||||
"legend": {
|
||||
"show": true,
|
||||
"values": false,
|
||||
"min": false,
|
||||
"max": false,
|
||||
"current": false,
|
||||
"total": false,
|
||||
"avg": false
|
||||
},
|
||||
"interactive": true,
|
||||
"legend_counts": true,
|
||||
"timezone": "browser",
|
||||
"percentage": false,
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
"value_type": "cumulative",
|
||||
"query_as_alias": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"target": "randomWalk('random walk')",
|
||||
"function": "mean",
|
||||
"column": "value"
|
||||
}
|
||||
],
|
||||
"aliasColors": {},
|
||||
"aliasYAxis": {},
|
||||
"title": "First Graph (click title to edit)",
|
||||
"datasource": "graphite",
|
||||
"renderer": "flot",
|
||||
"annotate": {
|
||||
"enable": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"nav": [
|
||||
{
|
||||
"type": "timepicker",
|
||||
"collapse": false,
|
||||
"enable": true,
|
||||
"status": "Stable",
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"now": true
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"title": "Grafana4",
|
||||
"tags": [],
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"rows": [
|
||||
{
|
||||
"title": "New row",
|
||||
"height": "150px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||
"style": {},
|
||||
"title": "Welcome to"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Welcome to Grafana",
|
||||
"height": "210px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 2,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
|
||||
"style": {},
|
||||
"title": "Documentation Links"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
|
||||
"style": {},
|
||||
"title": "Tips & Shortcuts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "test",
|
||||
"height": "250px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"panels": [
|
||||
{
|
||||
"id": 4,
|
||||
"span": 12,
|
||||
"type": "graph",
|
||||
"x-axis": true,
|
||||
"y-axis": true,
|
||||
"scale": 1,
|
||||
"y_formats": [
|
||||
"short",
|
||||
"short"
|
||||
],
|
||||
"grid": {
|
||||
"max": null,
|
||||
"min": null,
|
||||
"leftMax": null,
|
||||
"rightMax": null,
|
||||
"leftMin": null,
|
||||
"rightMin": null,
|
||||
"threshold1": null,
|
||||
"threshold2": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
"resolution": 100,
|
||||
"lines": true,
|
||||
"fill": 1,
|
||||
"linewidth": 2,
|
||||
"dashes": false,
|
||||
"dashLength": 10,
|
||||
"spaceLength": 10,
|
||||
"points": false,
|
||||
"pointradius": 5,
|
||||
"bars": false,
|
||||
"stack": true,
|
||||
"spyable": true,
|
||||
"options": false,
|
||||
"legend": {
|
||||
"show": true,
|
||||
"values": false,
|
||||
"min": false,
|
||||
"max": false,
|
||||
"current": false,
|
||||
"total": false,
|
||||
"avg": false
|
||||
},
|
||||
"interactive": true,
|
||||
"legend_counts": true,
|
||||
"timezone": "browser",
|
||||
"percentage": false,
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
"value_type": "cumulative",
|
||||
"query_as_alias": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"target": "randomWalk('random walk')",
|
||||
"function": "mean",
|
||||
"column": "value"
|
||||
}
|
||||
],
|
||||
"aliasColors": {},
|
||||
"aliasYAxis": {},
|
||||
"title": "First Graph (click title to edit)",
|
||||
"datasource": "graphite",
|
||||
"renderer": "flot",
|
||||
"annotate": {
|
||||
"enable": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"nav": [
|
||||
{
|
||||
"type": "timepicker",
|
||||
"collapse": false,
|
||||
"enable": true,
|
||||
"status": "Stable",
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"now": true
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
@@ -62,12 +62,12 @@ func TestIntegrationDuplicatesValidator(t *testing.T) {
|
||||
ctx, _ = identity.WithServiceIdentity(ctx, 1)
|
||||
|
||||
fakeStore := &fakeDashboardStore{}
|
||||
r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
r, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything, mock.Anything).Return(&folder.Folder{}, nil).Times(6)
|
||||
fakeService.On("GetProvisionedDashboardData", mock.Anything, mock.AnythingOfType("string")).Return([]*dashboards.DashboardProvisioning{}, nil).Times(4)
|
||||
fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(5)
|
||||
_, folderUID, err := r.getOrCreateFolder(ctx, cfg, fakeService, folderName)
|
||||
_, folderUID, err := r.getOrCreateFolder(ctx, cfg, folderName)
|
||||
require.NoError(t, err)
|
||||
|
||||
identity := dashboardIdentity{folderUID: folderUID, title: "Grafana"}
|
||||
@@ -81,12 +81,10 @@ func TestIntegrationDuplicatesValidator(t *testing.T) {
|
||||
Options: map[string]any{"path": dashboardContainingUID},
|
||||
}
|
||||
|
||||
reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore, folderSvc)
|
||||
reader1.dashboardProvisioningService = fakeService
|
||||
reader1, err := NewDashboardFileReader(cfg1, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore, folderSvc)
|
||||
reader2.dashboardProvisioningService = fakeService
|
||||
reader2, err := NewDashboardFileReader(cfg2, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
duplicateValidator := newDuplicateValidator(logger, []*FileReader{reader1, reader2})
|
||||
@@ -121,9 +119,9 @@ func TestIntegrationDuplicatesValidator(t *testing.T) {
|
||||
ctx, _ = identity.WithServiceIdentity(ctx, 1)
|
||||
|
||||
fakeStore := &fakeDashboardStore{}
|
||||
r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
r, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
_, folderUID, err := r.getOrCreateFolder(ctx, cfg, fakeService, folderName)
|
||||
_, folderUID, err := r.getOrCreateFolder(ctx, cfg, folderName)
|
||||
require.NoError(t, err)
|
||||
|
||||
identity := dashboardIdentity{folderUID: folderUID, title: "Grafana"}
|
||||
@@ -137,12 +135,10 @@ func TestIntegrationDuplicatesValidator(t *testing.T) {
|
||||
Options: map[string]any{"path": dashboardContainingUID},
|
||||
}
|
||||
|
||||
reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore, folderSvc)
|
||||
reader1.dashboardProvisioningService = fakeService
|
||||
reader1, err := NewDashboardFileReader(cfg1, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore, folderSvc)
|
||||
reader2.dashboardProvisioningService = fakeService
|
||||
reader2, err := NewDashboardFileReader(cfg2, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
duplicateValidator := newDuplicateValidator(logger, []*FileReader{reader1, reader2})
|
||||
@@ -198,16 +194,13 @@ func TestIntegrationDuplicatesValidator(t *testing.T) {
|
||||
Name: "third", Type: "file", OrgID: 2, Folder: "duplicates-validator-folder",
|
||||
Options: map[string]any{"path": twoDashboardsWithUID},
|
||||
}
|
||||
reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore, folderSvc)
|
||||
reader1.dashboardProvisioningService = fakeService
|
||||
reader1, err := NewDashboardFileReader(cfg1, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
reader2, err := NewDashboardFileReader(cfg2, logger, nil, fakeStore, folderSvc)
|
||||
reader2.dashboardProvisioningService = fakeService
|
||||
reader2, err := NewDashboardFileReader(cfg2, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
reader3, err := NewDashboardFileReader(cfg3, logger, nil, fakeStore, folderSvc)
|
||||
reader3.dashboardProvisioningService = fakeService
|
||||
reader3, err := NewDashboardFileReader(cfg3, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
|
||||
duplicateValidator := newDuplicateValidator(logger, []*FileReader{reader1, reader2, reader3})
|
||||
@@ -226,9 +219,9 @@ func TestIntegrationDuplicatesValidator(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx, _ = identity.WithServiceIdentity(ctx, 1)
|
||||
|
||||
r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore, folderSvc)
|
||||
r, err := NewDashboardFileReader(cfg, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
_, folderUID, err := r.getOrCreateFolder(ctx, cfg, fakeService, cfg1.Folder)
|
||||
_, folderUID, err := r.getOrCreateFolder(ctx, cfg, cfg1.Folder)
|
||||
require.NoError(t, err)
|
||||
|
||||
identity := dashboardIdentity{folderUID: folderUID, title: "Grafana"}
|
||||
@@ -243,9 +236,9 @@ func TestIntegrationDuplicatesValidator(t *testing.T) {
|
||||
sort.Strings(titleUsageReaders)
|
||||
require.Equal(t, []string{"first"}, titleUsageReaders)
|
||||
|
||||
r, err = NewDashboardFileReader(cfg3, logger, nil, fakeStore, folderSvc)
|
||||
r, err = NewDashboardFileReader(cfg3, logger, fakeService, fakeStore, folderSvc, cfgT)
|
||||
require.NoError(t, err)
|
||||
_, folderUID, err = r.getOrCreateFolder(ctx, cfg3, fakeService, cfg3.Folder)
|
||||
_, folderUID, err = r.getOrCreateFolder(ctx, cfg3, cfg3.Folder)
|
||||
require.NoError(t, err)
|
||||
|
||||
identity = dashboardIdentity{folderUID: folderUID, title: "Grafana"}
|
||||
|
||||
Reference in New Issue
Block a user