The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/store/entity/sqlstash/folder_support.go

168 lines
3.7 KiB

package sqlstash
import (
"context"
"encoding/json"
folder "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/grafana/grafana/pkg/services/sqlstore/session"
)
type folderInfo struct {
Guid string `json:"guid"`
UID string `json:"uid"`
Name string `json:"name"` // original display name
SlugPath string `json:"slug"` // full slug path
// original slug
Slug string `json:"-"`
depth int32
left int32
right int32
// Build the tree
parentUID string
// Calculated after query
parent *folderInfo
children []*folderInfo
stack []*folderInfo
}
// This will replace all entries in `entity_folder`
// This is pretty heavy weight, but it does give us a sorted folder list
// NOTE: this could be done async with a mutex/lock? reconciler pattern
func (s *sqlEntityServer) updateFolderTree(ctx context.Context, tx *session.SessionTx, namespace string) error {
_, err := tx.Exec(ctx, "DELETE FROM entity_folder WHERE namespace=?", namespace)
if err != nil {
return err
}
query := "SELECT guid,name,folder,name,slug" +
" FROM entity" +
" WHERE " + s.dialect.Quote("group") + "=? AND resource=? AND namespace=?" +
" ORDER BY slug asc"
args := []interface{}{folder.GROUP, folder.RESOURCE, namespace}
all := []*folderInfo{}
rows, err := tx.Query(ctx, query, args...)
if err != nil {
return err
}
defer func() { _ = rows.Close() }()
for rows.Next() {
folder := folderInfo{
children: []*folderInfo{},
}
err = rows.Scan(&folder.Guid, &folder.UID, &folder.parentUID, &folder.Name, &folder.Slug)
if err != nil {
return err
}
all = append(all, &folder)
}
root, lost, err := buildFolderTree(all)
if err != nil {
return err
}
err = insertFolderInfo(ctx, tx, namespace, root, false)
if err != nil {
return err
}
for _, folder := range lost {
err = insertFolderInfo(ctx, tx, namespace, folder, true)
if err != nil {
return err
}
}
return err
}
func buildFolderTree(all []*folderInfo) (*folderInfo, []*folderInfo, error) {
lost := []*folderInfo{}
lookup := make(map[string]*folderInfo)
for _, folder := range all {
lookup[folder.UID] = folder
}
root := &folderInfo{
Name: "Root",
UID: "",
children: []*folderInfo{},
left: 1,
}
lookup[""] = root
// already sorted by slug
for _, folder := range all {
parent, ok := lookup[folder.parentUID]
if ok {
folder.parent = parent
parent.children = append(parent.children, folder)
} else {
lost = append(lost, folder)
}
}
_, err := setMPTTOrder(root, []*folderInfo{}, int32(1))
return root, lost, err
}
// https://imrannazar.com/Modified-Preorder-Tree-Traversal
func setMPTTOrder(folder *folderInfo, stack []*folderInfo, idx int32) (int32, error) {
var err error
//nolint:gosec // G115
folder.depth = int32(len(stack))
folder.left = idx
folder.stack = stack
if folder.depth > 0 {
folder.SlugPath = "/"
for _, f := range stack {
folder.SlugPath += f.Slug + "/"
}
}
for _, child := range folder.children {
idx, err = setMPTTOrder(child, append(stack, child), idx+1)
if err != nil {
return idx, err
}
}
folder.right = idx + 1
return folder.right, nil
}
func insertFolderInfo(ctx context.Context, tx *session.SessionTx, namespace string, folder *folderInfo, isDetached bool) error {
js, _ := json.Marshal(folder.stack)
_, err := tx.Exec(ctx,
`INSERT INTO entity_folder `+
"(guid, namespace, name, slug_path, tree, depth, lft, rgt, detached) "+
`VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
folder.Guid,
namespace,
folder.UID,
folder.SlugPath,
string(js),
folder.depth,
folder.left,
folder.right,
isDetached,
)
if err != nil {
return err
}
for _, sub := range folder.children {
err := insertFolderInfo(ctx, tx, namespace, sub, isDetached)
if err != nil {
return err
}
}
return nil
}