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:
Artur Wierzbicki
2022-10-07 11:31:45 -07:00
committed by GitHub
parent 23e04c0f9c
commit bf264d2f76
24 changed files with 1292 additions and 13 deletions
@@ -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
}