mirror of
https://github.com/grafana/grafana.git
synced 2025-09-20 14:02:52 +08:00

Co-authored-by: nmarrs <nathanielmarrs@gmail.com> Co-authored-by: Adela Almasan <adela.almasan@grafana.com> Co-authored-by: drew08t <drew08@gmail.com> Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
299 lines
7.8 KiB
Go
299 lines
7.8 KiB
Go
package querylibraryimpl
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/expr"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/infra/x/persistentcollection"
|
|
"github.com/grafana/grafana/pkg/models"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/querylibrary"
|
|
"github.com/grafana/grafana/pkg/services/store/kind/dashboard"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles) querylibrary.Service {
|
|
return &service{
|
|
cfg: cfg,
|
|
log: log.New("queryLibraryService"),
|
|
features: features,
|
|
collection: persistentcollection.NewLocalFSPersistentCollection[*querylibrary.Query]("query-library", cfg.DataPath, 1),
|
|
}
|
|
}
|
|
|
|
type service struct {
|
|
cfg *setting.Cfg
|
|
features featuremgmt.FeatureToggles
|
|
log log.Logger
|
|
collection persistentcollection.PersistentCollection[*querylibrary.Query]
|
|
}
|
|
|
|
type perRequestQueryLoader struct {
|
|
service querylibrary.Service
|
|
queries map[string]*querylibrary.Query
|
|
ctx context.Context
|
|
user *user.SignedInUser
|
|
}
|
|
|
|
func (q *perRequestQueryLoader) byUID(uid string) (*querylibrary.Query, error) {
|
|
if q, ok := q.queries[uid]; ok {
|
|
return q, nil
|
|
}
|
|
|
|
queries, err := q.service.GetBatch(q.ctx, q.user, []string{uid})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(queries) != 1 {
|
|
return nil, err
|
|
}
|
|
|
|
q.queries[uid] = queries[0]
|
|
return queries[0], nil
|
|
}
|
|
|
|
func newPerRequestQueryLoader(ctx context.Context, user *user.SignedInUser, service querylibrary.Service) queryLoader {
|
|
return &perRequestQueryLoader{queries: make(map[string]*querylibrary.Query), ctx: ctx, user: user, service: service}
|
|
}
|
|
|
|
type queryLoader interface {
|
|
byUID(uid string) (*querylibrary.Query, error)
|
|
}
|
|
|
|
func (s *service) UpdateDashboardQueries(ctx context.Context, user *user.SignedInUser, dash *models.Dashboard) error {
|
|
queryLoader := newPerRequestQueryLoader(ctx, user, s)
|
|
return s.updateQueriesRecursively(queryLoader, dash.Data)
|
|
}
|
|
|
|
func (s *service) updateQueriesRecursively(loader queryLoader, parent *simplejson.Json) error {
|
|
panels := parent.Get("panels").MustArray()
|
|
for i := range panels {
|
|
panelAsJSON := simplejson.NewFromAny(panels[i])
|
|
panelType := panelAsJSON.Get("type").MustString()
|
|
|
|
if panelType == "row" {
|
|
err := s.updateQueriesRecursively(loader, panelAsJSON)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
queryUID := panelAsJSON.GetPath("savedQueryLink", "ref", "uid").MustString()
|
|
if queryUID == "" {
|
|
continue
|
|
}
|
|
|
|
query, err := loader.byUID(queryUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if query == nil {
|
|
// query deleted - unlink
|
|
panelAsJSON.Set("savedQueryLink", nil)
|
|
continue
|
|
}
|
|
|
|
queriesAsMap := make([]interface{}, 0)
|
|
for idx := range query.Queries {
|
|
queriesAsMap = append(queriesAsMap, query.Queries[idx].MustMap())
|
|
}
|
|
panelAsJSON.Set("targets", queriesAsMap)
|
|
|
|
isMixed, firstDsRef := isQueryWithMixedDataSource(query)
|
|
if isMixed {
|
|
panelAsJSON.Set("datasource", map[string]interface{}{
|
|
"uid": "-- Mixed --",
|
|
"type": "datasource",
|
|
})
|
|
} else {
|
|
panelAsJSON.Set("datasource", map[string]interface{}{
|
|
"uid": firstDsRef.UID,
|
|
"type": firstDsRef.Type,
|
|
})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *service) IsDisabled() bool {
|
|
return !s.features.IsEnabled(featuremgmt.FlagQueryLibrary) || !s.features.IsEnabled(featuremgmt.FlagPanelTitleSearch)
|
|
}
|
|
|
|
func namespaceFromUser(user *user.SignedInUser) string {
|
|
return fmt.Sprintf("orgId-%d", user.OrgID)
|
|
}
|
|
|
|
func (s *service) Search(ctx context.Context, user *user.SignedInUser, options querylibrary.QuerySearchOptions) ([]querylibrary.QueryInfo, error) {
|
|
queries, err := s.collection.Find(ctx, namespaceFromUser(user), func(_ *querylibrary.Query) (bool, error) { return true, nil })
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
queryInfo := asQueryInfo(queries)
|
|
filteredQueryInfo := make([]querylibrary.QueryInfo, 0)
|
|
for _, q := range queryInfo {
|
|
if len(options.Query) > 0 {
|
|
lowerTitle := strings.ReplaceAll(strings.ToLower(q.Title), " ", "")
|
|
lowerQuery := strings.ReplaceAll(strings.ToLower(options.Query), " ", "")
|
|
|
|
if !strings.Contains(lowerTitle, lowerQuery) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if len(options.DatasourceUID) > 0 || len(options.DatasourceType) > 0 {
|
|
dsUids := make(map[string]bool)
|
|
dsTypes := make(map[string]bool)
|
|
for _, ds := range q.Datasource {
|
|
dsUids[ds.UID] = true
|
|
dsTypes[ds.Type] = true
|
|
}
|
|
|
|
if len(options.DatasourceType) > 0 && !dsTypes[options.DatasourceType] {
|
|
continue
|
|
}
|
|
|
|
if len(options.DatasourceUID) > 0 && !dsUids[options.DatasourceUID] {
|
|
continue
|
|
}
|
|
}
|
|
|
|
filteredQueryInfo = append(filteredQueryInfo, q)
|
|
}
|
|
|
|
return filteredQueryInfo, nil
|
|
}
|
|
|
|
func asQueryInfo(queries []*querylibrary.Query) []querylibrary.QueryInfo {
|
|
res := make([]querylibrary.QueryInfo, 0)
|
|
for _, query := range queries {
|
|
res = append(res, querylibrary.QueryInfo{
|
|
UID: query.UID,
|
|
Title: query.Title,
|
|
Description: query.Description,
|
|
Tags: query.Tags,
|
|
TimeFrom: query.Time.From,
|
|
TimeTo: query.Time.To,
|
|
SchemaVersion: query.SchemaVersion,
|
|
Datasource: extractDataSources(query),
|
|
})
|
|
}
|
|
return res
|
|
}
|
|
|
|
func getDatasourceUID(q *simplejson.Json) string {
|
|
uid := q.Get("datasource").Get("uid").MustString()
|
|
|
|
if uid == "" {
|
|
uid = q.Get("datasource").MustString()
|
|
}
|
|
|
|
if expr.IsDataSource(uid) {
|
|
return expr.DatasourceUID
|
|
}
|
|
|
|
return uid
|
|
}
|
|
|
|
func isQueryWithMixedDataSource(q *querylibrary.Query) (isMixed bool, firstDsRef dashboard.DataSourceRef) {
|
|
dsRefs := extractDataSources(q)
|
|
|
|
for _, dsRef := range dsRefs {
|
|
if dsRef.Type == expr.DatasourceType {
|
|
continue
|
|
}
|
|
|
|
if firstDsRef.UID == "" {
|
|
firstDsRef = dsRef
|
|
continue
|
|
}
|
|
|
|
if firstDsRef.UID != dsRef.UID || firstDsRef.Type != dsRef.Type {
|
|
return true, firstDsRef
|
|
}
|
|
}
|
|
|
|
return false, firstDsRef
|
|
}
|
|
|
|
func extractDataSources(query *querylibrary.Query) []dashboard.DataSourceRef {
|
|
ds := make([]dashboard.DataSourceRef, 0)
|
|
|
|
for _, q := range query.Queries {
|
|
dsUid := getDatasourceUID(q)
|
|
dsType := q.Get("datasource").Get("type").MustString()
|
|
if expr.IsDataSource(dsUid) {
|
|
dsType = expr.DatasourceType
|
|
}
|
|
|
|
ds = append(ds, dashboard.DataSourceRef{
|
|
UID: dsUid,
|
|
Type: dsType,
|
|
})
|
|
}
|
|
|
|
return ds
|
|
}
|
|
|
|
func (s *service) GetBatch(ctx context.Context, user *user.SignedInUser, uids []string) ([]*querylibrary.Query, error) {
|
|
uidMap := make(map[string]bool)
|
|
for _, uid := range uids {
|
|
uidMap[uid] = true
|
|
}
|
|
|
|
return s.collection.Find(ctx, namespaceFromUser(user), func(q *querylibrary.Query) (bool, error) {
|
|
if _, ok := uidMap[q.UID]; ok {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
})
|
|
}
|
|
|
|
func (s *service) Update(ctx context.Context, user *user.SignedInUser, query *querylibrary.Query) error {
|
|
if query.UID == "" {
|
|
queriesWithTheSameTitle, err := s.Search(ctx, user, querylibrary.QuerySearchOptions{Query: query.Title})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(queriesWithTheSameTitle) != 0 {
|
|
return fmt.Errorf("can't create query with title '%s'. existing query with similar name: '%s'", query.Title, queriesWithTheSameTitle[0].Title)
|
|
}
|
|
|
|
query.UID = util.GenerateShortUID()
|
|
return s.collection.Insert(ctx, namespaceFromUser(user), query)
|
|
}
|
|
|
|
_, err := s.collection.Update(ctx, namespaceFromUser(user), func(q *querylibrary.Query) (updated bool, updatedItem *querylibrary.Query, err error) {
|
|
if q.UID == query.UID {
|
|
return true, query, nil
|
|
}
|
|
|
|
return false, nil, nil
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (s *service) Delete(ctx context.Context, user *user.SignedInUser, uid string) error {
|
|
_, err := s.collection.Delete(ctx, namespaceFromUser(user), func(q *querylibrary.Query) (bool, error) {
|
|
if q.UID == uid {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
})
|
|
|
|
return err
|
|
}
|