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 22:31:45 +04:00
committed by GitHub
parent 23e04c0f9c
commit bf264d2f76
24 changed files with 1292 additions and 13 deletions

View File

@@ -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,
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,284 @@
package querylibrary_tests
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/querylibrary"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/user"
)
type queryLibraryAPIClient struct {
token string
url string
user *user.SignedInUser
sqlStore *sqlstore.SQLStore
}
func newQueryLibraryAPIClient(token string, baseUrl string, user *user.SignedInUser, sqlStore *sqlstore.SQLStore) *queryLibraryAPIClient {
return &queryLibraryAPIClient{
token: token,
url: baseUrl,
user: user,
sqlStore: sqlStore,
}
}
func (q *queryLibraryAPIClient) update(ctx context.Context, query *querylibrary.Query) error {
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
err := enc.Encode(query)
if err != nil {
return err
}
url := fmt.Sprintf("%s/query-library", q.url)
req, err := http.NewRequestWithContext(ctx, "POST", url, &buf)
if err != nil {
return err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
defer func() {
_ = resp.Body.Close()
}()
return err
}
func (q *queryLibraryAPIClient) delete(ctx context.Context, uid string) error {
url := fmt.Sprintf("%s/query-library?uid=%s", q.url, uid)
req, err := http.NewRequestWithContext(ctx, "DELETE", url, bytes.NewBuffer([]byte("")))
if err != nil {
return err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
defer func() {
_ = resp.Body.Close()
}()
return err
}
func (q *queryLibraryAPIClient) get(ctx context.Context, uid string) (*querylibrary.Query, error) {
url := fmt.Sprintf("%s/query-library?uid=%s", q.url, uid)
req, err := http.NewRequestWithContext(ctx, "GET", url, bytes.NewBuffer([]byte("")))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
query := make([]*querylibrary.Query, 0)
err = json.Unmarshal(b, &query)
if len(query) > 0 {
return query[0], err
}
return nil, err
}
type querySearchInfo struct {
kind string
uid string
name string
dsUIDs []string
location string
}
func (q *queryLibraryAPIClient) search(ctx context.Context, options querylibrary.QuerySearchOptions) ([]*querySearchInfo, error) {
return q.searchRetry(ctx, options, 1)
}
func (q *queryLibraryAPIClient) searchRetry(ctx context.Context, options querylibrary.QuerySearchOptions, attempt int) ([]*querySearchInfo, error) {
if attempt >= 3 {
return nil, errors.New("max attempts")
}
url := fmt.Sprintf("%s/search-v2", q.url)
text := "*"
if options.Query != "" {
text = options.Query
}
searchReq := map[string]interface{}{
"query": text,
"sort": "name_sort",
"kind": []string{"query"},
"limit": 50,
}
searchReqJson, err := simplejson.NewFromAny(searchReq).MarshalJSON()
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(searchReqJson))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
r := &backend.DataResponse{}
err = json.Unmarshal(b, r)
if len(r.Frames) != 1 {
return nil, fmt.Errorf("expected a single frame, received %s", string(b))
}
frame := r.Frames[0]
if frame.Name == "Loading" {
time.Sleep(100 * time.Millisecond)
return q.searchRetry(ctx, options, attempt+1)
}
res := make([]*querySearchInfo, 0)
frameLen, _ := frame.RowLen()
for i := 0; i < frameLen; i++ {
fKind, _ := frame.FieldByName("kind")
fUid, _ := frame.FieldByName("uid")
fName, _ := frame.FieldByName("name")
dsUID, _ := frame.FieldByName("ds_uid")
fLocation, _ := frame.FieldByName("location")
rawValue, ok := dsUID.At(i).(json.RawMessage)
if !ok || rawValue == nil {
return nil, errors.New("invalid ds_uid field")
}
jsonValue, err := rawValue.MarshalJSON()
if err != nil {
return nil, err
}
var uids []string
err = json.Unmarshal(jsonValue, &uids)
if err != nil {
return nil, err
}
res = append(res, &querySearchInfo{
kind: fKind.At(i).(string),
uid: fUid.At(i).(string),
name: fName.At(i).(string),
dsUIDs: uids,
location: fLocation.At(i).(string),
})
}
return res, err
}
func (q *queryLibraryAPIClient) getDashboard(ctx context.Context, uid string) (*dtos.DashboardFullWithMeta, error) {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/dashboards/uid/%s", q.url, uid), bytes.NewBuffer([]byte("")))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
res := &dtos.DashboardFullWithMeta{}
err = json.Unmarshal(b, res)
if err != nil {
return nil, err
}
return res, nil
}
func (q *queryLibraryAPIClient) createDashboard(ctx context.Context, dash *simplejson.Json) (string, error) {
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
dashMap, err := dash.Map()
if err != nil {
return "", err
}
err = enc.Encode(dashMap)
if err != nil {
return "", err
}
url := fmt.Sprintf("%s/dashboards/db", q.url)
req, err := http.NewRequestWithContext(ctx, "POST", url, &buf)
if err != nil {
return "", err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", q.token))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
defer func() {
_ = resp.Body.Close()
}()
if err != nil {
return "", err
}
jsonResp, err := simplejson.NewFromReader(resp.Body)
if err != nil {
return "", err
}
return jsonResp.Get("uid").MustString(), nil
}

View File

@@ -0,0 +1,72 @@
package querylibrary_tests
import (
"fmt"
"testing"
apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed"
"github.com/grafana/grafana/pkg/server"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
saAPI "github.com/grafana/grafana/pkg/services/serviceaccounts/api"
saTests "github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/stretchr/testify/require"
)
func createServiceAccountAdminToken(t *testing.T, name string, env *server.TestEnv) (string, *user.SignedInUser) {
t.Helper()
account := saTests.SetupUserServiceAccount(t, env.SQLStore, saTests.TestUser{
Name: name,
Role: string(org.RoleAdmin),
Login: name,
IsServiceAccount: true,
OrgID: 1,
})
keyGen, err := apikeygenprefix.New(saAPI.ServiceID)
require.NoError(t, err)
_ = saTests.SetupApiKey(t, env.SQLStore, saTests.TestApiKey{
Name: name,
Role: org.RoleAdmin,
OrgId: account.OrgID,
Key: keyGen.HashedKey,
ServiceAccountID: &account.ID,
})
return keyGen.ClientSecret, &user.SignedInUser{
UserID: account.ID,
Email: account.Email,
Name: account.Name,
Login: account.Login,
OrgID: account.OrgID,
}
}
type testContext struct {
authToken string
client *queryLibraryAPIClient
user *user.SignedInUser
}
func createTestContext(t *testing.T) testContext {
t.Helper()
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{featuremgmt.FlagPanelTitleSearch, featuremgmt.FlagQueryLibrary},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
authToken, serviceAccountUser := createServiceAccountAdminToken(t, "query-library", env)
client := newQueryLibraryAPIClient(authToken, fmt.Sprintf("http://%s/api", grafanaListedAddr), serviceAccountUser, env.SQLStore)
return testContext{
authToken: authToken,
client: client,
user: serviceAccountUser,
}
}

View File

@@ -0,0 +1,289 @@
package querylibrary_tests
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/querylibrary"
"github.com/grafana/grafana/pkg/tsdb/grafanads"
)
func TestCreateAndDelete(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
testCtx := createTestContext(t)
err := testCtx.client.update(ctx, &querylibrary.Query{
UID: "",
Title: "first query",
Tags: []string{},
Description: "",
Time: querylibrary.Time{
From: "now-15m",
To: "now-30m",
},
Queries: []*simplejson.Json{
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]string{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "randomWalk",
"refId": "A",
}),
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]string{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "list",
"path": "img",
"refId": "B",
}),
},
Variables: []*simplejson.Json{},
})
require.NoError(t, err)
search, err := testCtx.client.search(ctx, querylibrary.QuerySearchOptions{
Query: "",
})
require.NoError(t, err)
require.Len(t, search, 1)
info := search[0]
require.Equal(t, "query", info.kind)
require.Equal(t, "first query", info.name)
require.Equal(t, "General", info.location)
require.Equal(t, []string{grafanads.DatasourceUID, grafanads.DatasourceUID}, info.dsUIDs)
err = testCtx.client.delete(ctx, info.uid)
require.NoError(t, err)
search, err = testCtx.client.search(ctx, querylibrary.QuerySearchOptions{
Query: "",
})
require.NoError(t, err)
require.Len(t, search, 0)
query, err := testCtx.client.get(ctx, info.uid)
require.NoError(t, err)
require.Nil(t, query)
}
func createQuery(t *testing.T, ctx context.Context, testCtx testContext) string {
t.Helper()
err := testCtx.client.update(ctx, &querylibrary.Query{
UID: "",
Title: "first query",
Tags: []string{},
Description: "",
Time: querylibrary.Time{
From: "now-15m",
To: "now-30m",
},
Queries: []*simplejson.Json{
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]string{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "randomWalk",
"refId": "A",
}),
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]string{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "list",
"path": "img",
"refId": "B",
}),
},
Variables: []*simplejson.Json{},
})
require.NoError(t, err)
search, err := testCtx.client.search(ctx, querylibrary.QuerySearchOptions{
Query: "",
})
require.NoError(t, err)
require.Len(t, search, 1)
return search[0].uid
}
func TestDashboardGetWithLatestSavedQueries(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
testCtx := createTestContext(t)
queryUID := createQuery(t, ctx, testCtx)
dashUID, err := testCtx.client.createDashboard(ctx, simplejson.NewFromAny(map[string]interface{}{
"dashboard": map[string]interface{}{
"title": "my-new-dashboard",
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"savedQueryLink": map[string]interface{}{
"ref": map[string]string{
"uid": queryUID,
},
},
},
},
},
"folderId": 0,
"message": "",
"overwrite": true,
}))
require.NoError(t, err)
dashboard, err := testCtx.client.getDashboard(ctx, dashUID)
require.NoError(t, err)
panelsAsArray, err := dashboard.Dashboard.Get("panels").Array()
require.NoError(t, err)
require.Len(t, panelsAsArray, 2)
secondPanel := simplejson.NewFromAny(panelsAsArray[1])
require.Equal(t, []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "randomWalk",
"refId": "A",
},
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "list",
"path": "img",
"refId": "B",
},
}, secondPanel.Get("targets").MustArray())
require.Equal(t, map[string]interface{}{
"uid": grafanads.DatasourceUID,
"type": "datasource",
}, secondPanel.Get("datasource").MustMap())
// update, expect changes when getting dashboards
err = testCtx.client.update(ctx, &querylibrary.Query{
UID: queryUID,
Title: "first query",
Tags: []string{},
Description: "",
Time: querylibrary.Time{
From: "now-15m",
To: "now-30m",
},
Queries: []*simplejson.Json{
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]interface{}{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "randomWalk",
"refId": "A",
}),
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]interface{}{
"uid": "different-datasource-uid",
"type": "datasource",
},
"queryType": "randomWalk",
"path": "img",
"refId": "B",
}),
simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]interface{}{
"uid": "different-datasource-uid-2",
"type": "datasource",
},
"queryType": "randomWalk",
"path": "img",
"refId": "C",
}),
},
Variables: []*simplejson.Json{},
})
require.NoError(t, err)
dashboard, err = testCtx.client.getDashboard(ctx, dashUID)
require.NoError(t, err)
panelsAsArray, err = dashboard.Dashboard.Get("panels").Array()
require.NoError(t, err)
require.Len(t, panelsAsArray, 2)
secondPanel = simplejson.NewFromAny(panelsAsArray[1])
require.Equal(t, []interface{}{
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": grafanads.DatasourceUID,
"type": "datasource",
},
"queryType": "randomWalk",
"refId": "A",
},
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": "different-datasource-uid",
"type": "datasource",
},
"queryType": "randomWalk",
"path": "img",
"refId": "B",
},
map[string]interface{}{
"datasource": map[string]interface{}{
"uid": "different-datasource-uid-2",
"type": "datasource",
},
"queryType": "randomWalk",
"path": "img",
"refId": "C",
},
}, secondPanel.Get("targets").MustArray())
require.Equal(t, map[string]interface{}{
"uid": "-- Mixed --",
"type": "datasource",
}, secondPanel.Get("datasource").MustMap())
}

View File

@@ -0,0 +1,88 @@
package querylibrary
import (
"context"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
"github.com/grafana/grafana/pkg/services/user"
)
type Time struct {
// From Start time in epoch timestamps in milliseconds or relative using Grafana time units.
// required: true
// example: now-1h
From string `json:"from"`
// To End time in epoch timestamps in milliseconds or relative using Grafana time units.
// required: true
// example: now
To string `json:"to"`
}
type Query struct {
UID string `json:"uid"`
Title string `json:"title"`
Tags []string `json:"tags"`
Description string `json:"description"`
SchemaVersion int64 `json:"schemaVersion"`
Time Time `json:"time"`
// queries.refId Specifies an identifier of the query. Is optional and default to “A”.
// queries.datasourceId Specifies the data source to be queried. Each query in the request must have an unique datasourceId.
// queries.maxDataPoints - Species maximum amount of data points that dashboard panel can render. Is optional and default to 100.
// queries.intervalMs - Specifies the time interval in milliseconds of time series. Is optional and defaults to 1000.
// required: true
// example: [ { "refId": "A", "intervalMs": 86400000, "maxDataPoints": 1092, "datasource":{ "uid":"PD8C576611E62080A" }, "rawSql": "SELECT 1 as valueOne, 2 as valueTwo", "format": "table" } ]
Queries []*simplejson.Json `json:"queries"`
Variables []*simplejson.Json `json:"variables"`
}
type SavedQueryRef struct {
UID string `json:"uid"`
}
type SavedQueryLink struct {
Ref SavedQueryRef `json:"ref"`
}
type QueryInfo struct {
UID string `json:"uid"`
Title string `json:"title"`
Description string `json:"description"`
Tags []string `json:"tags"`
TimeFrom string `json:"timeFrom"`
TimeTo string `json:"timeTo"`
SchemaVersion int64 `json:"schemaVersion"`
Datasource []dslookup.DataSourceRef `json:"datasource,omitempty"` // UIDs
}
type QuerySearchOptions struct {
DatasourceUID string
Query string
DatasourceType string
}
type Service interface {
Search(ctx context.Context, user *user.SignedInUser, options QuerySearchOptions) ([]QueryInfo, error)
GetBatch(ctx context.Context, user *user.SignedInUser, uids []string) ([]*Query, error)
Update(ctx context.Context, user *user.SignedInUser, query *Query) error
Delete(ctx context.Context, user *user.SignedInUser, uid string) error
UpdateDashboardQueries(ctx context.Context, user *user.SignedInUser, dash *models.Dashboard) error
registry.CanBeDisabled
}
type HTTPService interface {
registry.CanBeDisabled
RegisterHTTPRoutes(routes routing.RouteRegister)
}