Query library: requiresDevMode dummy backend (#56466)
* query library - dummy backend * fix tests * dont explicitly marshall backend dataresponse * skip integration tests * null check for tests * added query library to codeowners * null check for tests * lint
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
package querylibraryimpl
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/querylibrary"
|
||||
)
|
||||
|
||||
type queriesServiceHTTPHandler struct {
|
||||
service querylibrary.Service
|
||||
}
|
||||
|
||||
func (s *queriesServiceHTTPHandler) IsDisabled() bool {
|
||||
return s.service.IsDisabled()
|
||||
}
|
||||
|
||||
func (s *queriesServiceHTTPHandler) delete(c *models.ReqContext) response.Response {
|
||||
uid := c.Query("uid")
|
||||
err := s.service.Delete(c.Req.Context(), c.SignedInUser, uid)
|
||||
if err != nil {
|
||||
return response.Error(500, fmt.Sprintf("error deleting query with id %s", uid), err)
|
||||
}
|
||||
|
||||
return response.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *queriesServiceHTTPHandler) RegisterHTTPRoutes(routes routing.RouteRegister) {
|
||||
reqSignedIn := middleware.ReqSignedIn
|
||||
routes.Get("/", reqSignedIn, routing.Wrap(s.getBatch))
|
||||
routes.Post("/", reqSignedIn, routing.Wrap(s.update))
|
||||
routes.Delete("/", reqSignedIn, routing.Wrap(s.delete))
|
||||
}
|
||||
|
||||
func (s *queriesServiceHTTPHandler) getBatch(c *models.ReqContext) response.Response {
|
||||
uids := c.QueryStrings("uid")
|
||||
|
||||
queries, err := s.service.GetBatch(c.Req.Context(), c.SignedInUser, uids)
|
||||
if err != nil {
|
||||
return response.Error(500, fmt.Sprintf("error retrieving queries: [%s]", strings.Join(uids, ",")), err)
|
||||
}
|
||||
|
||||
return response.JSON(200, queries)
|
||||
}
|
||||
|
||||
func (s *queriesServiceHTTPHandler) update(c *models.ReqContext) response.Response {
|
||||
body, err := io.ReadAll(c.Req.Body)
|
||||
if err != nil {
|
||||
return response.Error(500, "error reading bytes", err)
|
||||
}
|
||||
|
||||
query := &querylibrary.Query{}
|
||||
err = json.Unmarshal(body, query)
|
||||
if err != nil {
|
||||
return response.Error(400, "error parsing body", err)
|
||||
}
|
||||
|
||||
if err := s.service.Update(c.Req.Context(), c.SignedInUser, query); err != nil {
|
||||
var msg string
|
||||
if len(query.UID) > 0 {
|
||||
msg = fmt.Sprintf("error updating query with UID %s: %s", query.UID, err.Error())
|
||||
} else {
|
||||
msg = fmt.Sprintf("error updating query with: %s", err.Error())
|
||||
}
|
||||
return response.Error(500, msg, err)
|
||||
}
|
||||
|
||||
return response.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
||||
func ProvideHTTPService(
|
||||
queriesService querylibrary.Service,
|
||||
) querylibrary.HTTPService {
|
||||
return &queriesServiceHTTPHandler{
|
||||
service: queriesService,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
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/searchV2/dslookup"
|
||||
"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 dslookup.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) []dslookup.DataSourceRef {
|
||||
ds := make([]dslookup.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, dslookup.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 == "" {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user