Previews: datasource permissions (#52747)

* Previews: datasource permissions

* lint

* simplify - force non-null `ds_uids`

* add `canBeDisabled` to search service

* add `IncludeThumbnailsWithEmptyDsUids`

* remove force refresh migration

* refactor main preview service

* add safeguard

* revert ticker interval

* update testdata

* fix test

* add mock search service

* add datasources lookup test

* update migration

* extract ds lookup to its own package to avoid cyclic imports

* lint

* fix dashbaord extract, use the real datasource lookup in tests. IS IT BULLETPROOF YET?!

* fix dashbaord extract, use the real datasource lookup in tests. IS IT BULLETPROOF YET?!

* remove stale log

* consistent casing

* pass context to `createServiceAccount`

* filter out the special grafana ds
pull/52928/merge
Artur Wierzbicki 3 years ago committed by GitHub
parent d0e548c3e5
commit 18daa6754c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      pkg/models/dashboard_thumbs.go
  2. 4
      pkg/services/export/export_dash.go
  3. 5
      pkg/services/searchV2/allowed_actions.go
  4. 14
      pkg/services/searchV2/bluge.go
  5. 136
      pkg/services/searchV2/dslookup/ds_lookup.go
  6. 80
      pkg/services/searchV2/extract/dashboard.go
  7. 73
      pkg/services/searchV2/extract/dashboard_test.go
  8. 36
      pkg/services/searchV2/extract/targets.go
  9. 6
      pkg/services/searchV2/extract/testdata/check-string-datasource-id-info.json
  10. 4
      pkg/services/searchV2/extract/testdata/datasource-variable-info.json
  11. 4
      pkg/services/searchV2/extract/testdata/datasource-variable-no-curly-braces-info.json
  12. 16
      pkg/services/searchV2/extract/testdata/mixed-datasource-with-variable-info.json
  13. 8
      pkg/services/searchV2/extract/testdata/repeated-datasource-variables-info.json
  14. 16
      pkg/services/searchV2/extract/testdata/repeated-datasource-variables-with-default-info.json
  15. 4
      pkg/services/searchV2/extract/testdata/string-datasource-variable-info.json
  16. 57
      pkg/services/searchV2/extract/types.go
  17. 109
      pkg/services/searchV2/index.go
  18. 70
      pkg/services/searchV2/search_service_mock.go
  19. 2
      pkg/services/searchV2/service.go
  20. 4
      pkg/services/searchV2/stub.go
  21. 9
      pkg/services/searchV2/testdata/allowed_actions_scope_all.golden.jsonc
  22. 9
      pkg/services/searchV2/testdata/allowed_actions_scope_uids.golden.jsonc
  23. 15
      pkg/services/searchV2/testdata/basic-filter.jsonc
  24. 19
      pkg/services/searchV2/testdata/basic-search.jsonc
  25. 19
      pkg/services/searchV2/testdata/dashboard-create.jsonc
  26. 15
      pkg/services/searchV2/testdata/dashboard-delete.jsonc
  27. 19
      pkg/services/searchV2/testdata/dashboard-update.jsonc
  28. 19
      pkg/services/searchV2/testdata/folders-dashboard-removed-on-folder-removed.jsonc
  29. 19
      pkg/services/searchV2/testdata/folders-indexed.jsonc
  30. 19
      pkg/services/searchV2/testdata/multiple-tokens-beginning-lower.jsonc
  31. 19
      pkg/services/searchV2/testdata/multiple-tokens-beginning.jsonc
  32. 19
      pkg/services/searchV2/testdata/multiple-tokens-middle-lower.jsonc
  33. 19
      pkg/services/searchV2/testdata/multiple-tokens-middle.jsonc
  34. 19
      pkg/services/searchV2/testdata/ngram-camel-case-split.jsonc
  35. 19
      pkg/services/searchV2/testdata/ngram-punctuation-split.jsonc
  36. 23
      pkg/services/searchV2/testdata/ngram-simple.jsonc
  37. 15
      pkg/services/searchV2/testdata/panels-panel-removed-on-dashboard-removed.jsonc
  38. 19
      pkg/services/searchV2/testdata/prefix-search-beginning-lower.jsonc
  39. 19
      pkg/services/searchV2/testdata/prefix-search-beginning.jsonc
  40. 19
      pkg/services/searchV2/testdata/prefix-search-middle-lower.jsonc
  41. 19
      pkg/services/searchV2/testdata/prefix-search-middle.jsonc
  42. 19
      pkg/services/searchV2/testdata/prefix-search-ngram-exceeded.jsonc
  43. 23
      pkg/services/searchV2/testdata/scattered-tokens-match-reversed.jsonc
  44. 23
      pkg/services/searchV2/testdata/scattered-tokens-match.jsonc
  45. 4
      pkg/services/searchV2/testdata/search_response_frame.json
  46. 23
      pkg/services/searchV2/testdata/sort-asc.jsonc
  47. 23
      pkg/services/searchV2/testdata/sort-desc.jsonc
  48. 2
      pkg/services/searchV2/types.go
  49. 41
      pkg/services/sqlstore/dashboard_thumbs.go
  50. 17
      pkg/services/sqlstore/dashboard_thumbs_test.go
  51. 4
      pkg/services/sqlstore/migrations/dashboard_thumbs_mig.go
  52. 13
      pkg/services/thumbs/crawler.go
  53. 117
      pkg/services/thumbs/crawler_auth.go
  54. 91
      pkg/services/thumbs/datasources_lookup.go
  55. 57
      pkg/services/thumbs/datasources_lookup_test.go
  56. 4
      pkg/services/thumbs/models.go
  57. 21
      pkg/services/thumbs/repo.go
  58. 80
      pkg/services/thumbs/service.go
  59. 123
      pkg/services/thumbs/testdata/search_response_frame.json

@ -86,6 +86,7 @@ type DashboardThumbnail struct {
Image []byte `json:"image"`
MimeType string `json:"mimeType"`
Updated time.Time `json:"updated"`
DsUIDs string `json:"-" xorm:"ds_uids"`
}
//
@ -123,6 +124,7 @@ type FindDashboardThumbnailCountCommand struct {
type FindDashboardsWithStaleThumbnailsCommand struct {
IncludeManuallyUploadedThumbnails bool
IncludeThumbnailsWithEmptyDsUIDs bool
Theme Theme
Kind ThumbnailKind
Result []*DashboardWithStaleThumbnail
@ -133,6 +135,7 @@ type SaveDashboardThumbnailCommand struct {
DashboardVersion int
Image []byte
MimeType string
DatasourceUIDs []string
Result *DashboardThumbnail
}

@ -11,7 +11,7 @@ import (
"github.com/google/uuid"
"github.com/grafana/grafana/pkg/infra/filestorage"
"github.com/grafana/grafana/pkg/services/searchV2"
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
"github.com/grafana/grafana/pkg/services/searchV2/extract"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
@ -26,7 +26,7 @@ func exportDashboards(helper *commitHelper, job *gitExportJob) error {
folders[0] = job.cfg.GeneralFolderPath // "general"
}
lookup, err := searchV2.LoadDatasourceLookup(helper.ctx, helper.orgID, job.sql)
lookup, err := dslookup.LoadDatasourceLookup(helper.ctx, helper.orgID, job.sql)
if err != nil {
return err
}

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"github.com/grafana/grafana-plugin-sdk-go/backend"
@ -232,9 +233,9 @@ func getEntityReferences(resp *backend.DataResponse) ([]entityReferences, error)
return nil, errors.New("invalid value in dash_uid field")
}
rawDsUids, ok := dsUidField.At(i).(*json.RawMessage)
rawDsUids, ok := dsUidField.At(i).(json.RawMessage)
if !ok {
return nil, errors.New("invalid value in ds_uid field")
return nil, fmt.Errorf("invalid value for uid %s in ds_uid field: %s", uidField, dsUidField.At(i))
}
var uids []string

@ -496,7 +496,7 @@ func doSearchQuery(
fURL := data.NewFieldFromFieldType(data.FieldTypeString, 0)
fLocation := data.NewFieldFromFieldType(data.FieldTypeString, 0)
fTags := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)
fDSUIDs := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)
fDSUIDs := data.NewFieldFromFieldType(data.FieldTypeJSON, 0)
fExplain := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)
fScore.Name = "score"
@ -592,14 +592,14 @@ func doSearchQuery(
fTags.Append(nil)
}
if len(dsUIDs) > 0 {
js, _ := json.Marshal(dsUIDs)
jsb := json.RawMessage(js)
fDSUIDs.Append(&jsb)
} else {
fDSUIDs.Append(nil)
if len(dsUIDs) == 0 {
dsUIDs = []string{}
}
js, _ := json.Marshal(dsUIDs)
jsb := json.RawMessage(js)
fDSUIDs.Append(jsb)
if q.Explain {
if isMatchAllQuery {
fScore.Append(float64(fieldLen + q.From))

@ -0,0 +1,136 @@
package dslookup
import (
"context"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
type DataSourceRef struct {
UID string `json:"uid,omitempty"`
Type string `json:"type,omitempty"`
}
type DatasourceLookup interface {
// ByRef will return the default DS given empty reference (nil ref, or empty ref.uid and ref.type)
ByRef(ref *DataSourceRef) *DataSourceRef
ByType(dsType string) []DataSourceRef
}
type DatasourceQueryResult struct {
UID string `xorm:"uid"`
Type string `xorm:"type"`
Name string `xorm:"name"`
IsDefault bool `xorm:"is_default"`
}
func CreateDatasourceLookup(rows []*DatasourceQueryResult) DatasourceLookup {
byUID := make(map[string]*DataSourceRef, 50)
byName := make(map[string]*DataSourceRef, 50)
byType := make(map[string][]DataSourceRef, 50)
var defaultDS *DataSourceRef
for _, row := range rows {
ref := &DataSourceRef{
UID: row.UID,
Type: row.Type,
}
byUID[row.UID] = ref
byName[row.Name] = ref
if row.IsDefault {
defaultDS = ref
}
if _, ok := byType[row.Type]; !ok {
byType[row.Type] = make([]DataSourceRef, 0)
}
byType[row.Type] = append(byType[row.Type], *ref)
}
grafanaDs := &DataSourceRef{
UID: "grafana",
Type: "datasource",
}
if defaultDS == nil {
// fallback replicated from /pkg/api/frontendsettings.go
// https://github.com/grafana/grafana/blob/7ef21662f9ad74b80d832b9f2aa9db2fb4192741/pkg/api/frontendsettings.go#L51-L56
defaultDS = grafanaDs
}
if _, ok := byUID[grafanaDs.UID]; !ok {
byUID[grafanaDs.UID] = grafanaDs
}
grafanaDsName := "-- Grafana --"
if _, ok := byName[grafanaDsName]; !ok {
byName[grafanaDsName] = grafanaDs
}
return &DsLookup{
byName: byName,
byUID: byUID,
byType: byType,
defaultDS: defaultDS,
}
}
type DsLookup struct {
byName map[string]*DataSourceRef
byUID map[string]*DataSourceRef
byType map[string][]DataSourceRef
defaultDS *DataSourceRef
}
func (d *DsLookup) ByRef(ref *DataSourceRef) *DataSourceRef {
if ref == nil {
return d.defaultDS
}
key := ""
if ref.UID != "" {
ds, ok := d.byUID[ref.UID]
if ok {
return ds
}
key = ref.UID
}
if key == "" {
return d.defaultDS
}
ds, ok := d.byUID[key]
if ok {
return ds
}
return d.byName[key]
}
func (d *DsLookup) ByType(dsType string) []DataSourceRef {
ds, ok := d.byType[dsType]
if !ok {
return make([]DataSourceRef, 0)
}
return ds
}
func LoadDatasourceLookup(ctx context.Context, orgID int64, sql *sqlstore.SQLStore) (DatasourceLookup, error) {
rows := make([]*DatasourceQueryResult, 0)
if err := sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
sess.Table("data_source").
Where("org_id = ?", orgID).
Cols("uid", "name", "type", "is_default")
err := sess.Find(&rows)
if err != nil {
return err
}
return nil
}); err != nil {
return nil, err
}
return CreateDatasourceLookup(rows), nil
}

@ -6,6 +6,8 @@ import (
"strings"
jsoniter "github.com/json-iterator/go"
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
)
func logf(format string, a ...interface{}) {
@ -22,43 +24,46 @@ type templateVariable struct {
}
type datasourceVariableLookup struct {
variableNameToRefs map[string][]DataSourceRef
dsLookup DatasourceLookup
variableNameToRefs map[string][]dslookup.DataSourceRef
dsLookup dslookup.DatasourceLookup
}
func (d *datasourceVariableLookup) getDsRefsByTemplateVariableValue(value string, datasourceType string) []DataSourceRef {
func (d *datasourceVariableLookup) getDsRefsByTemplateVariableValue(value string, datasourceType string) []dslookup.DataSourceRef {
switch value {
case "default":
// can be the default DS, or a DS with UID="default"
candidateDs := d.dsLookup.ByRef(&DataSourceRef{UID: value})
candidateDs := d.dsLookup.ByRef(&dslookup.DataSourceRef{UID: value})
if candidateDs == nil {
// get the actual default DS
candidateDs = d.dsLookup.ByRef(nil)
}
if candidateDs != nil {
return []DataSourceRef{*candidateDs}
return []dslookup.DataSourceRef{*candidateDs}
}
return []DataSourceRef{}
return []dslookup.DataSourceRef{}
case "$__all":
// TODO: filter datasources by template variable's regex
return d.dsLookup.ByType(datasourceType)
case "":
return []DataSourceRef{}
return []dslookup.DataSourceRef{}
case "No data sources found":
return []DataSourceRef{}
return []dslookup.DataSourceRef{}
default:
return []DataSourceRef{
{
UID: value,
Type: datasourceType,
},
// some variables use `ds.name` rather `ds.uid`
if ref := d.dsLookup.ByRef(&dslookup.DataSourceRef{
UID: value,
}); ref != nil {
return []dslookup.DataSourceRef{*ref}
}
// discard variable
return []dslookup.DataSourceRef{}
}
}
func (d *datasourceVariableLookup) add(templateVariable templateVariable) {
var refs []DataSourceRef
var refs []dslookup.DataSourceRef
datasourceType, isDataSourceTypeValid := templateVariable.query.(string)
if !isDataSourceTypeValid {
@ -81,8 +86,8 @@ func (d *datasourceVariableLookup) add(templateVariable templateVariable) {
d.variableNameToRefs[templateVariable.name] = unique(refs)
}
func unique(refs []DataSourceRef) []DataSourceRef {
var uniqueRefs []DataSourceRef
func unique(refs []dslookup.DataSourceRef) []dslookup.DataSourceRef {
var uniqueRefs []dslookup.DataSourceRef
uidPresence := make(map[string]bool)
for _, ref := range refs {
if !uidPresence[ref.UID] {
@ -93,25 +98,25 @@ func unique(refs []DataSourceRef) []DataSourceRef {
return uniqueRefs
}
func (d *datasourceVariableLookup) getDatasourceRefs(name string) []DataSourceRef {
func (d *datasourceVariableLookup) getDatasourceRefs(name string) []dslookup.DataSourceRef {
refs, ok := d.variableNameToRefs[name]
if ok {
return refs
}
return []DataSourceRef{}
return []dslookup.DataSourceRef{}
}
func newDatasourceVariableLookup(dsLookup DatasourceLookup) *datasourceVariableLookup {
func newDatasourceVariableLookup(dsLookup dslookup.DatasourceLookup) *datasourceVariableLookup {
return &datasourceVariableLookup{
variableNameToRefs: make(map[string][]DataSourceRef),
variableNameToRefs: make(map[string][]dslookup.DataSourceRef),
dsLookup: dsLookup,
}
}
// nolint:gocyclo
// ReadDashboard will take a byte stream and return dashboard info
func ReadDashboard(stream io.Reader, lookup DatasourceLookup) (*DashboardInfo, error) {
func ReadDashboard(stream io.Reader, lookup dslookup.DatasourceLookup) (*DashboardInfo, error) {
dash := &DashboardInfo{}
iter := jsoniter.Parse(jsoniter.ConfigDefault, stream, 1024)
@ -277,7 +282,7 @@ func panelRequiresDatasource(panel PanelInfo) bool {
return panel.Type != "row"
}
func fillDefaultDatasources(dash *DashboardInfo, lookup DatasourceLookup) {
func fillDefaultDatasources(dash *DashboardInfo, lookup dslookup.DatasourceLookup) {
for i, panel := range dash.Panels {
if len(panel.Datasource) != 0 || !panelRequiresDatasource(panel) {
continue
@ -285,14 +290,14 @@ func fillDefaultDatasources(dash *DashboardInfo, lookup DatasourceLookup) {
defaultDs := lookup.ByRef(nil)
if defaultDs != nil {
dash.Panels[i].Datasource = []DataSourceRef{*defaultDs}
dash.Panels[i].Datasource = []dslookup.DataSourceRef{*defaultDs}
}
}
}
func filterOutSpecialDatasources(dash *DashboardInfo) {
for i, panel := range dash.Panels {
var dsRefs []DataSourceRef
var dsRefs []dslookup.DataSourceRef
// partition into actual datasource references and variables
for _, ds := range panel.Datasource {
@ -314,24 +319,33 @@ func filterOutSpecialDatasources(dash *DashboardInfo) {
func replaceDatasourceVariables(dash *DashboardInfo, datasourceVariablesLookup *datasourceVariableLookup) {
for i, panel := range dash.Panels {
var dsVariableRefs []DataSourceRef
var dsRefs []DataSourceRef
var dsVariableRefs []dslookup.DataSourceRef
var dsRefs []dslookup.DataSourceRef
// partition into actual datasource references and variables
for i := range panel.Datasource {
isVariableRef := strings.HasPrefix(panel.Datasource[i].UID, "$")
if isVariableRef {
uid := panel.Datasource[i].UID
if isVariableRef(uid) {
dsVariableRefs = append(dsVariableRefs, panel.Datasource[i])
} else {
dsRefs = append(dsRefs, panel.Datasource[i])
}
}
dash.Panels[i].Datasource = append(dsRefs, findDatasourceRefsForVariables(dsVariableRefs, datasourceVariablesLookup)...)
variables := findDatasourceRefsForVariables(dsVariableRefs, datasourceVariablesLookup)
dash.Panels[i].Datasource = append(dsRefs, variables...)
}
}
func getDataSourceVariableName(dsVariableRef DataSourceRef) string {
func isSpecialDatasource(uid string) bool {
return uid == "-- Mixed --" || uid == "-- Dashboard --"
}
func isVariableRef(uid string) bool {
return strings.HasPrefix(uid, "$")
}
func getDataSourceVariableName(dsVariableRef dslookup.DataSourceRef) string {
if strings.HasPrefix(dsVariableRef.UID, "${") {
return strings.TrimPrefix(strings.TrimSuffix(dsVariableRef.UID, "}"), "${")
}
@ -339,8 +353,8 @@ func getDataSourceVariableName(dsVariableRef DataSourceRef) string {
return strings.TrimPrefix(dsVariableRef.UID, "$")
}
func findDatasourceRefsForVariables(dsVariableRefs []DataSourceRef, datasourceVariablesLookup *datasourceVariableLookup) []DataSourceRef {
var referencedDs []DataSourceRef
func findDatasourceRefsForVariables(dsVariableRefs []dslookup.DataSourceRef, datasourceVariablesLookup *datasourceVariableLookup) []dslookup.DataSourceRef {
var referencedDs []dslookup.DataSourceRef
for _, dsVariableRef := range dsVariableRefs {
variableName := getDataSourceVariableName(dsVariableRef)
refs := datasourceVariablesLookup.getDatasourceRefs(variableName)
@ -350,7 +364,7 @@ func findDatasourceRefsForVariables(dsVariableRefs []DataSourceRef, datasourceVa
}
// will always return strings for now
func readPanelInfo(iter *jsoniter.Iterator, lookup DatasourceLookup) PanelInfo {
func readPanelInfo(iter *jsoniter.Iterator, lookup dslookup.DatasourceLookup) PanelInfo {
panel := PanelInfo{}
targets := newTargetInfo(lookup)

@ -8,41 +8,50 @@ import (
"strings"
"testing"
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type dsLookup struct {
}
func (d *dsLookup) ByRef(ref *DataSourceRef) *DataSourceRef {
if ref == nil || ref.UID == "" {
return &DataSourceRef{
UID: "default.uid",
Type: "default.type",
}
}
if ref.UID == "default" {
return nil
}
return ref
}
func (d *dsLookup) ByType(dsType string) []DataSourceRef {
if dsType == "sqlite-datasource" {
return []DataSourceRef{
{
UID: "sqlite-1",
Type: "sqlite-datasource",
},
{
UID: "sqlite-2",
Type: "sqlite-datasource",
},
}
}
return make([]DataSourceRef, 0)
func dsLookup() dslookup.DatasourceLookup {
return dslookup.CreateDatasourceLookup([]*dslookup.DatasourceQueryResult{
{
UID: "P8045C56BDA891CB2",
Type: "cloudwatch",
Name: "cloudwatch-name",
IsDefault: false,
},
{
UID: "PD8C576611E62080A",
Type: "testdata",
Name: "gdev-testdata",
IsDefault: false,
},
{
UID: "dgd92lq7k",
Type: "frser-sqlite-datasource",
Name: "frser-sqlite-datasource-name",
IsDefault: false,
},
{
UID: "sqlite-1",
Type: "sqlite-datasource",
Name: "SQLite Grafana",
IsDefault: false,
},
{
UID: "sqlite-2",
Type: "sqlite-datasource",
Name: "SQLite Grafana2",
IsDefault: false,
},
{
UID: "default.uid",
Type: "default.type",
Name: "default.name",
IsDefault: true,
},
})
}
func TestReadDashboard(t *testing.T) {
@ -80,7 +89,7 @@ func TestReadDashboard(t *testing.T) {
}
require.NoError(t, err)
dash, err := ReadDashboard(f, &dsLookup{})
dash, err := ReadDashboard(f, dsLookup())
sortDatasources(dash)
require.NoError(t, err)

@ -2,22 +2,24 @@ package extract
import (
jsoniter "github.com/json-iterator/go"
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
)
type targetInfo struct {
lookup DatasourceLookup
uids map[string]*DataSourceRef
lookup dslookup.DatasourceLookup
uids map[string]*dslookup.DataSourceRef
}
func newTargetInfo(lookup DatasourceLookup) targetInfo {
func newTargetInfo(lookup dslookup.DatasourceLookup) targetInfo {
return targetInfo{
lookup: lookup,
uids: make(map[string]*DataSourceRef),
uids: make(map[string]*dslookup.DataSourceRef),
}
}
func (s *targetInfo) GetDatasourceInfo() []DataSourceRef {
keys := make([]DataSourceRef, len(s.uids))
func (s *targetInfo) GetDatasourceInfo() []dslookup.DataSourceRef {
keys := make([]dslookup.DataSourceRef, len(s.uids))
i := 0
for _, v := range s.uids {
keys[i] = *v
@ -31,18 +33,28 @@ func (s *targetInfo) addDatasource(iter *jsoniter.Iterator) {
switch iter.WhatIsNext() {
case jsoniter.StringValue:
key := iter.ReadString()
ds := s.lookup.ByRef(&DataSourceRef{UID: key})
s.addRef(ds)
dsRef := &dslookup.DataSourceRef{UID: key}
if !isVariableRef(dsRef.UID) && !isSpecialDatasource(dsRef.UID) {
ds := s.lookup.ByRef(dsRef)
s.addRef(ds)
} else {
s.addRef(dsRef)
}
case jsoniter.NilValue:
s.addRef(s.lookup.ByRef(nil))
iter.Skip()
case jsoniter.ObjectValue:
ref := &DataSourceRef{}
ref := &dslookup.DataSourceRef{}
iter.ReadVal(ref)
ds := s.lookup.ByRef(ref)
s.addRef(ds)
if !isVariableRef(ref.UID) && !isSpecialDatasource(ref.UID) {
s.addRef(s.lookup.ByRef(ref))
} else {
s.addRef(ref)
}
default:
v := iter.Read()
@ -50,7 +62,7 @@ func (s *targetInfo) addDatasource(iter *jsoniter.Iterator) {
}
}
func (s *targetInfo) addRef(ref *DataSourceRef) {
func (s *targetInfo) addRef(ref *dslookup.DataSourceRef) {
if ref != nil && ref.UID != "" {
s.uids[ref.UID] = ref
}

@ -4,7 +4,8 @@
"tags": null,
"datasource": [
{
"uid": "-- Grafana --"
"uid": "grafana",
"type": "datasource"
}
],
"panels": [
@ -15,7 +16,8 @@
"pluginVersion": "7.5.0-pre",
"datasource": [
{
"uid": "-- Grafana --"
"uid": "grafana",
"type": "datasource"
}
]
}

@ -7,7 +7,7 @@
],
"datasource": [
{
"uid": "SQLite Grafana",
"uid": "sqlite-1",
"type": "sqlite-datasource"
}
],
@ -19,7 +19,7 @@
"pluginVersion": "9.1.0-pre",
"datasource": [
{
"uid": "SQLite Grafana",
"uid": "sqlite-1",
"type": "sqlite-datasource"
}
]

@ -7,7 +7,7 @@
],
"datasource": [
{
"uid": "SQLite Grafana",
"uid": "sqlite-1",
"type": "sqlite-datasource"
}
],
@ -19,7 +19,7 @@
"pluginVersion": "9.1.0-pre",
"datasource": [
{
"uid": "SQLite Grafana",
"uid": "sqlite-1",
"type": "sqlite-datasource"
}
]

@ -6,14 +6,14 @@
"dsVariable"
],
"datasource": [
{
"uid": "gdev-testdata",
"type": "testdata"
},
{
"uid": "default.uid",
"type": "default.type"
},
{
"uid": "PD8C576611E62080A",
"type": "testdata"
},
{
"uid": "P8045C56BDA891CB2",
"type": "cloudwatch"
@ -26,14 +26,14 @@
"type": "table",
"pluginVersion": "9.1.0-pre",
"datasource": [
{
"uid": "gdev-testdata",
"type": "testdata"
},
{
"uid": "default.uid",
"type": "default.type"
},
{
"uid": "PD8C576611E62080A",
"type": "testdata"
},
{
"uid": "P8045C56BDA891CB2",
"type": "cloudwatch"

@ -7,11 +7,11 @@
],
"datasource": [
{
"uid": "SQLite Grafana2",
"uid": "sqlite-2",
"type": "sqlite-datasource"
},
{
"uid": "SQLite Grafana",
"uid": "sqlite-1",
"type": "sqlite-datasource"
}
],
@ -23,11 +23,11 @@
"pluginVersion": "9.1.0-pre",
"datasource": [
{
"uid": "SQLite Grafana2",
"uid": "sqlite-2",
"type": "sqlite-datasource"
},
{
"uid": "SQLite Grafana",
"uid": "sqlite-1",
"type": "sqlite-datasource"
}
]

@ -6,13 +6,13 @@
"dsVariable"
],
"datasource": [
{
"uid": "gdev-testdata",
"type": "testdata"
},
{
"uid": "default.uid",
"type": "default.type"
},
{
"uid": "PD8C576611E62080A",
"type": "testdata"
}
],
"panels": [
@ -22,13 +22,13 @@
"type": "table",
"pluginVersion": "9.1.0-pre",
"datasource": [
{
"uid": "gdev-testdata",
"type": "testdata"
},
{
"uid": "default.uid",
"type": "default.type"
},
{
"uid": "PD8C576611E62080A",
"type": "testdata"
}
]
}

@ -7,7 +7,7 @@
],
"datasource": [
{
"uid": "SQLite Grafana",
"uid": "sqlite-1",
"type": "sqlite-datasource"
}
],
@ -19,7 +19,7 @@
"pluginVersion": "9.1.0-pre",
"datasource": [
{
"uid": "SQLite Grafana",
"uid": "sqlite-1",
"type": "sqlite-datasource"
}
]

@ -1,43 +1,36 @@
package extract
type DatasourceLookup interface {
// ByRef will return the default DS given empty reference (nil ref, or empty ref.uid and ref.type)
ByRef(ref *DataSourceRef) *DataSourceRef
ByType(dsType string) []DataSourceRef
}
type DataSourceRef struct {
UID string `json:"uid,omitempty"`
Type string `json:"type,omitempty"`
}
import (
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
)
type PanelInfo struct {
ID int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Type string `json:"type,omitempty"` // PluginID
PluginVersion string `json:"pluginVersion,omitempty"`
Datasource []DataSourceRef `json:"datasource,omitempty"` // UIDs
Transformer []string `json:"transformer,omitempty"` // ids of the transformation steps
ID int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Type string `json:"type,omitempty"` // PluginID
PluginVersion string `json:"pluginVersion,omitempty"`
Datasource []dslookup.DataSourceRef `json:"datasource,omitempty"` // UIDs
Transformer []string `json:"transformer,omitempty"` // ids of the transformation steps
// Rows define panels as sub objects
Collapsed []PanelInfo `json:"collapsed,omitempty"`
}
type DashboardInfo struct {
UID string `json:"uid,omitempty"`
ID int64 `json:"id,omitempty"` // internal ID
Title string `json:"title"`
Description string `json:"description,omitempty"`
Tags []string `json:"tags"`
TemplateVars []string `json:"templateVars,omitempty"` // the keys used
Datasource []DataSourceRef `json:"datasource,omitempty"` // UIDs
Panels []PanelInfo `json:"panels"` // nesed documents
SchemaVersion int64 `json:"schemaVersion"`
LinkCount int64 `json:"linkCount"`
TimeFrom string `json:"timeFrom"`
TimeTo string `json:"timeTo"`
TimeZone string `json:"timezone"`
Refresh string `json:"refresh,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"` // editable = false
UID string `json:"uid,omitempty"`
ID int64 `json:"id,omitempty"` // internal ID
Title string `json:"title"`
Description string `json:"description,omitempty"`
Tags []string `json:"tags"`
TemplateVars []string `json:"templateVars,omitempty"` // the keys used
Datasource []dslookup.DataSourceRef `json:"datasource,omitempty"` // UIDs
Panels []PanelInfo `json:"panels"` // nesed documents
SchemaVersion int64 `json:"schemaVersion"`
LinkCount int64 `json:"linkCount"`
TimeFrom string `json:"timeFrom"`
TimeTo string `json:"timeTo"`
TimeZone string `json:"timezone"`
Refresh string `json:"refresh,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"` // editable = false
}

@ -15,6 +15,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
"github.com/grafana/grafana/pkg/services/searchV2/extract"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/store"
@ -725,7 +726,7 @@ func (l sqlDashboardLoader) LoadDashboards(ctx context.Context, orgID int64, das
}
// key will allow name or uid
lookup, err := LoadDatasourceLookup(ctx, orgID, l.sql)
lookup, err := dslookup.LoadDatasourceLookup(ctx, orgID, l.sql)
if err != nil {
return dashboards, err
}
@ -813,109 +814,3 @@ type dashboardQueryResult struct {
Created time.Time
Updated time.Time
}
type datasourceQueryResult struct {
UID string `xorm:"uid"`
Type string `xorm:"type"`
Name string `xorm:"name"`
IsDefault bool `xorm:"is_default"`
}
func createDatasourceLookup(rows []*datasourceQueryResult) extract.DatasourceLookup {
byUID := make(map[string]*extract.DataSourceRef, 50)
byName := make(map[string]*extract.DataSourceRef, 50)
byType := make(map[string][]extract.DataSourceRef, 50)
var defaultDS *extract.DataSourceRef
for _, row := range rows {
ref := &extract.DataSourceRef{
UID: row.UID,
Type: row.Type,
}
byUID[row.UID] = ref
byName[row.Name] = ref
if row.IsDefault {
defaultDS = ref
}
if _, ok := byType[row.Type]; !ok {
byType[row.Type] = make([]extract.DataSourceRef, 5)
}
byType[row.Type] = append(byType[row.Type], *ref)
}
if defaultDS == nil {
// fallback replicated from /pkg/api/frontendsettings.go
// https://github.com/grafana/grafana/blob/7ef21662f9ad74b80d832b9f2aa9db2fb4192741/pkg/api/frontendsettings.go#L51-L56
defaultDS = &extract.DataSourceRef{
UID: "grafana",
Type: "datasource",
}
}
return &dsLookup{
byName: byName,
byUID: byUID,
byType: byType,
defaultDS: defaultDS,
}
}
type dsLookup struct {
byName map[string]*extract.DataSourceRef
byUID map[string]*extract.DataSourceRef
byType map[string][]extract.DataSourceRef
defaultDS *extract.DataSourceRef
}
func (d *dsLookup) ByRef(ref *extract.DataSourceRef) *extract.DataSourceRef {
if ref == nil {
return d.defaultDS
}
key := ""
if ref.UID != "" {
ds, ok := d.byUID[ref.UID]
if ok {
return ds
}
key = ref.UID
}
if key == "" {
return d.defaultDS
}
ds, ok := d.byUID[key]
if ok {
return ds
}
return d.byName[key]
}
func (d *dsLookup) ByType(dsType string) []extract.DataSourceRef {
ds, ok := d.byType[dsType]
if !ok {
return make([]extract.DataSourceRef, 0)
}
return ds
}
func LoadDatasourceLookup(ctx context.Context, orgID int64, sql *sqlstore.SQLStore) (extract.DatasourceLookup, error) {
rows := make([]*datasourceQueryResult, 0)
if err := sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
sess.Table("data_source").
Where("org_id = ?", orgID).
Cols("uid", "name", "type", "is_default")
err := sess.Find(&rows)
if err != nil {
return err
}
return nil
}); err != nil {
return nil, err
}
return createDatasourceLookup(rows), nil
}

@ -0,0 +1,70 @@
// Code generated by mockery v2.10.6. DO NOT EDIT.
package searchV2
import (
context "context"
backend "github.com/grafana/grafana-plugin-sdk-go/backend"
mock "github.com/stretchr/testify/mock"
)
// MockSearchService is an autogenerated mock type for the SearchService type
type MockSearchService struct {
mock.Mock
}
// DoDashboardQuery provides a mock function with given fields: ctx, user, orgId, query
func (_m *MockSearchService) DoDashboardQuery(ctx context.Context, user *backend.User, orgId int64, query DashboardQuery) *backend.DataResponse {
ret := _m.Called(ctx, user, orgId, query)
var r0 *backend.DataResponse
if rf, ok := ret.Get(0).(func(context.Context, *backend.User, int64, DashboardQuery) *backend.DataResponse); ok {
r0 = rf(ctx, user, orgId, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*backend.DataResponse)
}
}
return r0
}
// IsDisabled provides a mock function with given fields:
func (_m *MockSearchService) IsDisabled() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// RegisterDashboardIndexExtender provides a mock function with given fields: ext
func (_m *MockSearchService) RegisterDashboardIndexExtender(ext DashboardIndexExtender) {
_m.Called(ext)
}
// Run provides a mock function with given fields: ctx
func (_m *MockSearchService) Run(ctx context.Context) error {
ret := _m.Called(ctx)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
r0 = rf(ctx)
} else {
r0 = ret.Error(0)
}
return r0
}
// TriggerReIndex provides a mock function with given fields:
func (_m *MockSearchService) TriggerReIndex() {
_m.Called()
}

@ -112,7 +112,7 @@ func (s *StandardSearchService) getUser(ctx context.Context, backendUser *backen
}
err := s.sql.GetSignedInUser(ctx, getSignedInUserQuery)
if err != nil {
s.logger.Error("Error while retrieving user", "error", err, "email", backendUser.Email)
s.logger.Error("Error while retrieving user", "error", err, "email", backendUser.Email, "login", getSignedInUserQuery.Login)
return nil, errors.New("auth error")
}

@ -10,6 +10,10 @@ import (
type stubSearchService struct {
}
func (s *stubSearchService) IsDisabled() bool {
return true
}
func (s *stubSearchService) TriggerReIndex() {
// noop.
}

@ -19,9 +19,9 @@
// +----------------+----------------+-------------------------+------------------+--------------------------------------+--------------------------+---------------------------+----------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location | Name: allowed_actions |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | Type: []json.RawMessage |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | Type: []json.RawMessage |
// +----------------+----------------+-------------------------+------------------+--------------------------------------+--------------------------+---------------------------+----------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
// | folder | ujaM1h6nz | abc2 | | /dashboards/f/ujaM1h6nz/abc2 | null | null | | [{"kind":"folder","uid":"ujaM1h6nz","actions":["folders.permissions:read","folders.permissions:write","folders:create","folders:delete","folders:read","folders:write"]}] |
// | folder | ujaM1h6nz | abc2 | | /dashboards/f/ujaM1h6nz/abc2 | null | [] | | [{"kind":"folder","uid":"ujaM1h6nz","actions":["folders.permissions:read","folders.permissions:write","folders:create","folders:delete","folders:read","folders:write"]}] |
// | dashboard | 7MeksYbmk | Alerting with TestData | | /d/7MeksYbmk/alerting-with-testdata | [ | [ | yboVMzb7z | [{"kind":"dashboard","uid":"7MeksYbmk","actions":["dashboards.permissions:read","dashboards.permissions:write","dashboards:create","dashboards:delete","dashboards:read","dashboards:write"]},{"kind":"datasource","uid":"datasource-1","actions":["datasources.id:read","datasources.permissions:read","datasources.permissions:write","datasources:delete","datasources:explore","datasources:query","datasources:read","datasources:write"]}] |
// | | | | | | "gdev", | "datasource-1" | | |
// | | | | | | "alerting" | ] | | |
@ -115,8 +115,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -183,7 +182,7 @@
]
],
[
null,
[],
[
"datasource-1"
],

@ -19,9 +19,9 @@
// +----------------+----------------+-------------------------+------------------+--------------------------------------+--------------------------+---------------------------+----------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location | Name: allowed_actions |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | Type: []json.RawMessage |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | Type: []json.RawMessage |
// +----------------+----------------+-------------------------+------------------+--------------------------------------+--------------------------+---------------------------+----------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
// | folder | ujaM1h6nz | abc2 | | /dashboards/f/ujaM1h6nz/abc2 | null | null | | [{"kind":"folder","uid":"ujaM1h6nz","actions":["folders:read"]}] |
// | folder | ujaM1h6nz | abc2 | | /dashboards/f/ujaM1h6nz/abc2 | null | [] | | [{"kind":"folder","uid":"ujaM1h6nz","actions":["folders:read"]}] |
// | dashboard | 7MeksYbmk | Alerting with TestData | | /d/7MeksYbmk/alerting-with-testdata | [ | [ | yboVMzb7z | [{"kind":"dashboard","uid":"7MeksYbmk","actions":["dashboards:write"]},{"kind":"datasource","uid":"datasource-1","actions":[]}] |
// | | | | | | "gdev", | "datasource-1" | | |
// | | | | | | "alerting" | ] | | |
@ -115,8 +115,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -183,7 +182,7 @@
]
],
[
null,
[],
[
"datasource-1"
],

@ -8,12 +8,12 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 0 Rows
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -84,8 +84,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 2 | boom | | /pfix/d/2/ | null | null | |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 2 | boom | | /pfix/d/2/ | null | [] | |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
""

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 3 | created | | /pfix/d/3/ | null | null | general |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 3 | created | | /pfix/d/3/ | null | [] | general |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
"general"

@ -8,12 +8,12 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 0 Rows
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -84,8 +84,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 2 | nginx | | /pfix/d/2/ | null | null | general |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 2 | nginx | | /pfix/d/2/ | null | [] | general |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
"general"

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 4 | One more dash | | /pfix/d/4/ | null | null | |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 4 | One more dash | | /pfix/d/4/ | null | [] | |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
""

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+----------------+------------------+-----------------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+-----------------------+--------------------------+--------------------------+----------------+
// | folder | 1 | My folder | | /pfix/dashboards/f/1/ | null | null | |
// +----------------+----------------+----------------+------------------+-----------------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+----------------+------------------+-----------------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+-----------------------+--------------------------+-------------------------+----------------+
// | folder | 1 | My folder | | /pfix/dashboards/f/1/ | null | [] | |
// +----------------+----------------+----------------+------------------+-----------------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
""

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | null | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
""

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | null | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
""

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | null | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | [] | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
""

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | null | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
""

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 1 | heatTorkel | | /pfix/d/1/ | null | null | |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 1 | heatTorkel | | /pfix/d/1/ | null | [] | |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
""

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 1 | heat-torkel | | /pfix/d/1/ | null | null | |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 1 | heat-torkel | | /pfix/d/1/ | null | [] | |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
""

@ -8,14 +8,14 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 2 Rows
// +----------------+----------------+------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 1 | heat-torkel | | /pfix/d/1/ | null | null | |
// | dashboard | 2 | topology heatmap | | /pfix/d/2/ | null | null | |
// +----------------+----------------+------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 1 | heat-torkel | | /pfix/d/1/ | null | [] | |
// | dashboard | 2 | topology heatmap | | /pfix/d/2/ | null | [] | |
// +----------------+----------------+------------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -86,8 +86,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -126,8 +125,8 @@
null
],
[
null,
null
[],
[]
],
[
"",

@ -8,12 +8,12 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 0 Rows
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -84,8 +84,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | null | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
""

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | null | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 1 | Archer Data System | | /pfix/d/1/ | null | [] | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
""

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | null | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | [] | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
""

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | null | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 2 | Document Sync repo | | /pfix/d/2/ | null | [] | |
// +----------------+----------------+--------------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
""

@ -8,13 +8,13 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 1 Rows
// +----------------+----------------+--------------------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+--------------------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 1 | Eyjafjallajökull Eruption data | | /pfix/d/1/ | null | null | |
// +----------------+----------------+--------------------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+--------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+--------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 1 | Eyjafjallajökull Eruption data | | /pfix/d/1/ | null | [] | |
// +----------------+----------------+--------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -85,8 +85,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -119,7 +118,7 @@
null
],
[
null
[]
],
[
""

@ -8,14 +8,14 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 2 Rows
// +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 2 | A secret is powerful when it is empty (Umberto Eco) | | /pfix/d/2/ | null | null | |
// | dashboard | 1 | Three can keep a secret, if two of them are dead (Benjamin Franklin) | | /pfix/d/1/ | null | null | |
// +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 2 | A secret is powerful when it is empty (Umberto Eco) | | /pfix/d/2/ | null | [] | |
// | dashboard | 1 | Three can keep a secret, if two of them are dead (Benjamin Franklin) | | /pfix/d/1/ | null | [] | |
// +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -86,8 +86,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -126,8 +125,8 @@
null
],
[
null,
null
[],
[]
],
[
"",

@ -8,14 +8,14 @@
// }
// Name: Query results
// Dimensions: 8 Fields by 2 Rows
// +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
// +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// | dashboard | 1 | Three can keep a secret, if two of them are dead (Benjamin Franklin) | | /pfix/d/1/ | null | null | |
// | dashboard | 2 | A secret is powerful when it is empty (Umberto Eco) | | /pfix/d/2/ | null | null | |
// +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+--------------------------+----------------+
// +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string |
// +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+
// | dashboard | 1 | Three can keep a secret, if two of them are dead (Benjamin Franklin) | | /pfix/d/1/ | null | [] | |
// | dashboard | 2 | A secret is powerful when it is empty (Umberto Eco) | | /pfix/d/2/ | null | [] | |
// +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+-------------------------+----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -86,8 +86,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -126,8 +125,8 @@
null
],
[
null,
null
[],
[]
],
[
"",

@ -73,7 +73,7 @@
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"nullable": false
}
},
{
@ -133,7 +133,7 @@
]
],
[
null,
[],
[
"datasource-1"
],

@ -9,14 +9,14 @@
// }
// Name: Query results
// Dimensions: 9 Fields by 2 Rows
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+-----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location | Name: test num |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | Type: []float64 |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+-----------------+
// | dashboard | 1 | a-test | | /pfix/d/1/ | null | null | | 0 |
// | dashboard | 2 | z-test | | /pfix/d/2/ | null | null | | 1 |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+-----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+-----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location | Name: test num |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | Type: []float64 |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+-----------------+
// | dashboard | 1 | a-test | | /pfix/d/1/ | null | [] | | 0 |
// | dashboard | 2 | z-test | | /pfix/d/2/ | null | [] | | 1 |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+-----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -88,8 +88,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -135,8 +134,8 @@
null
],
[
null,
null
[],
[]
],
[
"",

@ -9,14 +9,14 @@
// }
// Name: Query results
// Dimensions: 9 Fields by 2 Rows
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+-----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location | Name: test num |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | Type: []float64 |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+-----------------+
// | dashboard | 2 | z-test | | /pfix/d/2/ | null | null | | 3 |
// | dashboard | 1 | a-test | | /pfix/d/1/ | null | null | | 2 |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+-----------------+
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+-----------------+
// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location | Name: test num |
// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []json.RawMessage | Type: []string | Type: []float64 |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+-----------------+
// | dashboard | 2 | z-test | | /pfix/d/2/ | null | [] | | 3 |
// | dashboard | 1 | a-test | | /pfix/d/1/ | null | [] | | 2 |
// +----------------+----------------+----------------+------------------+----------------+--------------------------+-------------------------+----------------+-----------------+
//
//
// 🌟 This was machine generated. Do not edit. 🌟
@ -88,8 +88,7 @@
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
"frame": "json.RawMessage"
}
},
{
@ -135,8 +134,8 @@
null
],
[
null,
null
[],
[]
],
[
"",

@ -31,7 +31,9 @@ type DashboardQuery struct {
From int `json:"from,omitempty"` // for paging
}
//go:generate mockery --name SearchService --structname MockSearchService --inpackage --filename search_service_mock.go
type SearchService interface {
registry.CanBeDisabled
registry.BackgroundService
DoDashboardQuery(ctx context.Context, user *backend.User, orgId int64, query DashboardQuery) *backend.DataResponse
RegisterDashboardIndexExtender(ext DashboardIndexExtender)

@ -2,6 +2,7 @@ package sqlstore
import (
"context"
"encoding/json"
"errors"
"time"
@ -22,6 +23,18 @@ func (ss *SQLStore) GetThumbnail(ctx context.Context, query *models.GetDashboard
return query.Result, err
}
func marshalDatasourceUids(dsUids []string) (string, error) {
if dsUids == nil {
return "", nil
}
b, err := json.Marshal(dsUids)
if err != nil {
return "", err
}
return string(b), nil
}
func (ss *SQLStore) SaveThumbnail(ctx context.Context, cmd *models.SaveDashboardThumbnailCommand) (*models.DashboardThumbnail, error) {
err := ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
existing, err := findThumbnailByMeta(sess, cmd.DashboardThumbnailMeta)
@ -30,11 +43,17 @@ func (ss *SQLStore) SaveThumbnail(ctx context.Context, cmd *models.SaveDashboard
return err
}
dsUids, err := marshalDatasourceUids(cmd.DatasourceUIDs)
if err != nil {
return err
}
if existing != nil {
existing.Image = cmd.Image
existing.MimeType = cmd.MimeType
existing.Updated = time.Now()
existing.DashboardVersion = cmd.DashboardVersion
existing.DsUIDs = dsUids
existing.State = models.ThumbnailStateDefault
_, err = sess.ID(existing.Id).Update(existing)
cmd.Result = existing
@ -53,6 +72,7 @@ func (ss *SQLStore) SaveThumbnail(ctx context.Context, cmd *models.SaveDashboard
thumb.Theme = cmd.Theme
thumb.Kind = cmd.Kind
thumb.Image = cmd.Image
thumb.DsUIDs = dsUids
thumb.MimeType = cmd.MimeType
thumb.DashboardId = dash.Id
thumb.DashboardVersion = cmd.DashboardVersion
@ -101,9 +121,17 @@ func (ss *SQLStore) FindDashboardsWithStaleThumbnails(ctx context.Context, cmd *
sess.Table("dashboard")
sess.Join("LEFT", "dashboard_thumbnail", "dashboard.id = dashboard_thumbnail.dashboard_id AND dashboard_thumbnail.theme = ? AND dashboard_thumbnail.kind = ?", cmd.Theme, cmd.Kind)
sess.Where("dashboard.is_folder = ?", dialect.BooleanStr(false))
sess.Where("(dashboard.version != dashboard_thumbnail.dashboard_version "+
"OR dashboard_thumbnail.state = ? "+
"OR dashboard_thumbnail.id IS NULL)", models.ThumbnailStateStale)
query := "(dashboard.version != dashboard_thumbnail.dashboard_version " +
"OR dashboard_thumbnail.state = ? " +
"OR dashboard_thumbnail.id IS NULL"
args := []interface{}{models.ThumbnailStateStale}
if cmd.IncludeThumbnailsWithEmptyDsUIDs {
query += " OR dashboard_thumbnail.ds_uids = ?"
args = append(args, "")
}
sess.Where(query+")", args...)
if !cmd.IncludeManuallyUploadedThumbnails {
sess.Where("(dashboard_thumbnail.id is not null AND dashboard_thumbnail.dashboard_version != ?) "+
@ -119,13 +147,13 @@ func (ss *SQLStore) FindDashboardsWithStaleThumbnails(ctx context.Context, cmd *
"dashboard.version",
"dashboard.slug")
var dashboards = make([]*models.DashboardWithStaleThumbnail, 0)
err := sess.Find(&dashboards)
var result = make([]*models.DashboardWithStaleThumbnail, 0)
err := sess.Find(&result)
if err != nil {
return err
}
cmd.Result = dashboards
cmd.Result = result
return err
})
@ -145,6 +173,7 @@ func findThumbnailByMeta(sess *DBSession, meta models.DashboardThumbnailMeta) (*
"dashboard_thumbnail.dashboard_version",
"dashboard_thumbnail.state",
"dashboard_thumbnail.kind",
"dashboard_thumbnail.ds_uids",
"dashboard_thumbnail.mime_type",
"dashboard_thumbnail.theme",
"dashboard_thumbnail.updated")

@ -69,6 +69,23 @@ func TestIntegrationSqlStorage(t *testing.T) {
require.Len(t, res, 0)
})
t.Run("Should return dashboards with thumbnails with empty ds_uids array", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.Id, false, "prod", "webapp")
upsertTestDashboardThumbnail(t, sqlStore, dash.Uid, dash.OrgId, dash.Version)
cmd := models.FindDashboardsWithStaleThumbnailsCommand{
Kind: kind,
IncludeThumbnailsWithEmptyDsUIDs: true,
Theme: theme,
}
res, err := sqlStore.FindDashboardsWithStaleThumbnails(context.Background(), &cmd)
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, dash.Id, res[0].Id)
})
t.Run("Should return dashboards with thumbnails marked as stale", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.Id, false, "prod", "webapp")

@ -24,4 +24,8 @@ func addDashboardThumbsMigrations(mg *migrator.Migrator) {
mg.AddMigration("create dashboard_thumbnail table", migrator.NewAddTableMigration(dashThumbs))
mg.AddMigration("add unique indexes for dashboard_thumbnail", migrator.NewAddIndexMigration(dashThumbs, dashThumbs.Indices[0]))
mg.AddMigration("Add ds_uids column to dashboard_thumbnail table", migrator.NewAddColumnMigration(dashThumbs,
// uids of datasources used in the dashboard when taking preview
&migrator.Column{Name: "ds_uids", Type: migrator.DB_Text, Nullable: false, Default: ""},
))
}

@ -37,9 +37,10 @@ type simpleCrawler struct {
queueMutex sync.Mutex
log log.Logger
renderingSessionByOrgId map[int64]rendering.Session
dsUidsLookup getDatasourceUidsForDashboard
}
func newSimpleCrawler(renderService rendering.Service, gl *live.GrafanaLive, repo thumbnailRepo, cfg *setting.Cfg, settings setting.DashboardPreviewsSettings) dashRenderer {
func newSimpleCrawler(renderService rendering.Service, gl *live.GrafanaLive, repo thumbnailRepo, cfg *setting.Cfg, settings setting.DashboardPreviewsSettings, dsUidsLookup getDatasourceUidsForDashboard) dashRenderer {
threadCount := int(settings.CrawlThreadCount)
c := &simpleCrawler{
// temporarily increases the concurrentLimit from the 'cfg.RendererConcurrentRequestLimit' to 'cfg.RendererConcurrentRequestLimit + crawlerThreadCount'
@ -48,6 +49,7 @@ func newSimpleCrawler(renderService rendering.Service, gl *live.GrafanaLive, rep
renderService: renderService,
threadCount: threadCount,
glive: gl,
dsUidsLookup: dsUidsLookup,
thumbnailRepo: repo,
log: log.New("thumbnails_crawler"),
status: crawlStatus{
@ -287,6 +289,13 @@ func (r *simpleCrawler) walk(ctx context.Context, id int) {
url := models.GetKioskModeDashboardUrl(item.Uid, item.Slug, r.opts.Theme)
r.log.Info("Getting dashboard thumbnail", "walkerId", id, "dashboardUID", item.Uid, "url", url)
dsUids, err := r.dsUidsLookup(ctx, item.Uid, item.OrgId)
if err != nil {
r.log.Warn("Error getting datasource uids", "walkerId", id, "dashboardUID", item.Uid, "url", url, "err", err)
r.newErrorResult()
continue
}
res, err := r.renderService.Render(ctx, rendering.Opts{
Width: 320,
Height: 240,
@ -321,7 +330,7 @@ func (r *simpleCrawler) walk(ctx context.Context, id int) {
OrgId: item.OrgId,
Theme: r.opts.Theme,
Kind: r.thumbnailKind,
}, item.Version)
}, item.Version, dsUids)
if err != nil {
r.log.Warn("Error saving image image", "walkerId", id, "dashboardUID", item.Uid, "url", url, "err", err, "itemTime", time.Since(itemStarted))

@ -2,40 +2,137 @@ package thumbs
import (
"context"
"errors"
"strconv"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
type CrawlerAuthSetupService interface {
Setup(ctx context.Context) (CrawlerAuth, error)
}
func ProvideCrawlerAuthSetupService() *OSSCrawlerAuthSetupService {
return &OSSCrawlerAuthSetupService{}
func ProvideCrawlerAuthSetupService(serviceAccounts serviceaccounts.Service, serviceAccountsStore serviceaccounts.Store, sqlStore *sqlstore.SQLStore) *OSSCrawlerAuthSetupService {
return &OSSCrawlerAuthSetupService{
serviceAccountNamePrefix: "dashboard-previews-crawler-org-",
serviceAccounts: serviceAccounts,
log: log.New("oss_crawler_account_setup_service"),
sqlStore: sqlStore,
serviceAccountsStore: serviceAccountsStore,
}
}
type OSSCrawlerAuthSetupService struct{}
type OSSCrawlerAuthSetupService struct {
log log.Logger
serviceAccountNamePrefix string
serviceAccounts serviceaccounts.Service
serviceAccountsStore serviceaccounts.Store
sqlStore *sqlstore.SQLStore
}
type CrawlerAuth interface {
GetUserId(orgId int64) int64
GetLogin(orgId int64) string
GetOrgRole() models.RoleType
}
type staticCrawlerAuth struct {
userId int64
orgRole models.RoleType
func (o *OSSCrawlerAuthSetupService) findAllOrgIds(ctx context.Context) ([]int64, error) {
searchAllOrgsQuery := &models.SearchOrgsQuery{}
if err := o.sqlStore.SearchOrgs(ctx, searchAllOrgsQuery); err != nil {
o.log.Error("Error when searching for orgs", "err", err)
return nil, err
}
orgIds := make([]int64, 0)
for i := range searchAllOrgsQuery.Result {
orgIds = append(orgIds, searchAllOrgsQuery.Result[i].Id)
}
return orgIds, nil
}
type crawlerAuth struct {
accountIdByOrgId map[int64]int64
loginByOrgId map[int64]string
orgRole models.RoleType
}
func (o *staticCrawlerAuth) GetOrgRole() models.RoleType {
func (o *crawlerAuth) GetOrgRole() models.RoleType {
return o.orgRole
}
func (o *staticCrawlerAuth) GetUserId(orgId int64) int64 {
return o.userId
func (o *crawlerAuth) GetUserId(orgId int64) int64 {
return o.accountIdByOrgId[orgId]
}
func (o *crawlerAuth) GetLogin(orgId int64) string {
return o.loginByOrgId[orgId]
}
func (o *OSSCrawlerAuthSetupService) Setup(ctx context.Context) (CrawlerAuth, error) {
orgIds, err := o.findAllOrgIds(ctx)
if err != nil {
return nil, err
}
// userId:0 and ROLE_ADMIN grants the crawler process permissions to view all dashboards in all folders & orgs
// the process doesn't and shouldn't actually need to edit/modify any resources from the UI
return &staticCrawlerAuth{userId: 0, orgRole: models.ROLE_ADMIN}, nil
orgRole := models.ROLE_ADMIN
accountIdByOrgId := make(map[int64]int64)
loginByOrgId := make(map[int64]string)
for _, orgId := range orgIds {
o.log.Info("Creating account for org", "orgId", orgId)
serviceAccountNameOrg := o.serviceAccountNamePrefix + strconv.FormatInt(orgId, 10)
saForm := serviceaccounts.CreateServiceAccountForm{
Name: serviceAccountNameOrg,
Role: &orgRole,
}
serviceAccount, err := o.serviceAccounts.CreateServiceAccount(ctx, orgId, &saForm)
accountAlreadyExists := errors.Is(err, database.ErrServiceAccountAlreadyExists)
if !accountAlreadyExists && err != nil {
o.log.Error("Failed to create the service account", "err", err, "accountName", serviceAccountNameOrg, "orgId", orgId)
return nil, err
}
var serviceAccountLogin string
var serviceAccountId int64
if accountAlreadyExists {
id, err := o.serviceAccounts.RetrieveServiceAccountIdByName(ctx, orgId, serviceAccountNameOrg)
if err != nil {
o.log.Error("Failed to retrieve service account", "err", err, "accountName", serviceAccountNameOrg)
return nil, err
}
// update org_role to make sure everything works properly if someone has changed the role since SA's original creation
dto, err := o.serviceAccountsStore.UpdateServiceAccount(ctx, orgId, id, &serviceaccounts.UpdateServiceAccountForm{
Name: &serviceAccountNameOrg,
Role: &orgRole,
})
if err != nil {
o.log.Error("Failed to update service account's role", "err", err, "accountName", serviceAccountNameOrg)
return nil, err
}
serviceAccountLogin = dto.Login
serviceAccountId = id
} else {
serviceAccountLogin = serviceAccount.Login
serviceAccountId = serviceAccount.Id
}
accountIdByOrgId[orgId] = serviceAccountId
loginByOrgId[orgId] = serviceAccountLogin
}
return &crawlerAuth{accountIdByOrgId: accountIdByOrgId, loginByOrgId: loginByOrgId, orgRole: orgRole}, nil
}

@ -0,0 +1,91 @@
package thumbs
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/searchV2"
"github.com/grafana/grafana/pkg/tsdb/grafanads"
)
type getDatasourceUidsForDashboard func(ctx context.Context, dashboardUid string, orgId int64) ([]string, error)
type dsUidsLookup struct {
searchService searchV2.SearchService
crawlerAuth CrawlerAuth
features featuremgmt.FeatureToggles
}
func getDatasourceUIDs(resp *backend.DataResponse, uid string) ([]string, error) {
if resp == nil {
return nil, errors.New("nil response")
}
if resp.Error != nil {
return nil, resp.Error
}
if len(resp.Frames) == 0 {
return nil, errors.New("empty response")
}
frame := resp.Frames[0]
field, idx := frame.FieldByName("ds_uid")
if field.Len() == 0 || idx == -1 {
return nil, fmt.Errorf("no ds_uid field for uid %s", uid)
}
rawValue, ok := field.At(0).(json.RawMessage)
if !ok || rawValue == nil {
return nil, fmt.Errorf("invalid value for uid %s in ds_uid field: %s", uid, field.At(0))
}
jsonValue, err := rawValue.MarshalJSON()
if err != nil {
return nil, err
}
var uids []string
err = json.Unmarshal(jsonValue, &uids)
if err != nil {
return nil, err
}
return uids, nil
}
func filterOutGrafanaDs(uids []string) []string {
var filtered []string
for _, uid := range uids {
if uid != grafanads.DatasourceUID {
filtered = append(filtered, uid)
}
}
return filtered
}
func (d *dsUidsLookup) getDatasourceUidsForDashboard(ctx context.Context, dashboardUid string, orgId int64) ([]string, error) {
if d.searchService.IsDisabled() {
return nil, nil
}
dashQueryResponse := d.searchService.DoDashboardQuery(ctx, &backend.User{
Login: d.crawlerAuth.GetLogin(orgId),
Role: string(d.crawlerAuth.GetOrgRole()),
}, orgId, searchV2.DashboardQuery{
UIDs: []string{dashboardUid},
})
uids, err := getDatasourceUIDs(dashQueryResponse, dashboardUid)
if err != nil {
return nil, err
}
return filterOutGrafanaDs(uids), nil
}

@ -0,0 +1,57 @@
package thumbs
import (
"context"
_ "embed"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/searchV2"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
var (
//go:embed testdata/search_response_frame.json
exampleListFrameJSON string
exampleListFrame = &data.Frame{}
_ = exampleListFrame.UnmarshalJSON([]byte(exampleListFrameJSON))
)
func TestShouldParseUidFromSearchResponseFrame(t *testing.T) {
searchService := &searchV2.MockSearchService{}
dsLookup := &dsUidsLookup{
searchService: searchService,
crawlerAuth: &crawlerAuth{},
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
}
dashboardUid := "abc"
searchService.On("IsDisabled").Return(false)
searchService.On("DoDashboardQuery", mock.Anything, mock.Anything, mock.Anything, searchV2.DashboardQuery{
UIDs: []string{dashboardUid},
}).Return(&backend.DataResponse{
Frames: []*data.Frame{exampleListFrame},
})
uids, err := dsLookup.getDatasourceUidsForDashboard(context.Background(), dashboardUid, 1)
require.NoError(t, err)
require.Equal(t, []string{"datasource-2", "datasource-3", "datasource-4"}, uids)
}
func TestShouldReturnNullIfSearchServiceIsDisabled(t *testing.T) {
searchService := &searchV2.MockSearchService{}
dsLookup := &dsUidsLookup{
searchService: searchService,
crawlerAuth: &crawlerAuth{},
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
}
dashboardUid := "abc"
searchService.On("IsDisabled").Return(true)
uids, err := dsLookup.getDatasourceUidsForDashboard(context.Background(), dashboardUid, 1)
require.NoError(t, err)
require.Nil(t, uids)
}

@ -79,8 +79,8 @@ type dashRenderer interface {
type thumbnailRepo interface {
updateThumbnailState(ctx context.Context, state models.ThumbnailState, meta models.DashboardThumbnailMeta) error
doThumbnailsExist(ctx context.Context) (bool, error)
saveFromFile(ctx context.Context, filePath string, meta models.DashboardThumbnailMeta, dashboardVersion int) (int64, error)
saveFromBytes(ctx context.Context, bytes []byte, mimeType string, meta models.DashboardThumbnailMeta, dashboardVersion int) (int64, error)
saveFromFile(ctx context.Context, filePath string, meta models.DashboardThumbnailMeta, dashboardVersion int, dsUids []string) (int64, error)
saveFromBytes(ctx context.Context, bytes []byte, mimeType string, meta models.DashboardThumbnailMeta, dashboardVersion int, dsUids []string) (int64, error)
getThumbnail(ctx context.Context, meta models.DashboardThumbnailMeta) (*models.DashboardThumbnail, error)
findDashboardsWithStaleThumbnails(ctx context.Context, theme models.Theme, thumbnailKind models.ThumbnailKind) ([]*models.DashboardWithStaleThumbnail, error)
}

@ -9,23 +9,26 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/searchV2"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
func newThumbnailRepo(store *sqlstore.SQLStore) thumbnailRepo {
func newThumbnailRepo(store *sqlstore.SQLStore, search searchV2.SearchService) thumbnailRepo {
repo := &sqlThumbnailRepository{
store: store,
log: log.New("thumbnails_repo"),
store: store,
search: search,
log: log.New("thumbnails_repo"),
}
return repo
}
type sqlThumbnailRepository struct {
store *sqlstore.SQLStore
log log.Logger
store *sqlstore.SQLStore
search searchV2.SearchService
log log.Logger
}
func (r *sqlThumbnailRepository) saveFromFile(ctx context.Context, filePath string, meta models.DashboardThumbnailMeta, dashboardVersion int) (int64, error) {
func (r *sqlThumbnailRepository) saveFromFile(ctx context.Context, filePath string, meta models.DashboardThumbnailMeta, dashboardVersion int, dsUids []string) (int64, error) {
// the filePath variable is never set by the user. it refers to a temporary file created either in
// 1. thumbs/service.go, when user uploads a thumbnail
// 2. the rendering service, when image-renderer returns a screenshot
@ -42,7 +45,7 @@ func (r *sqlThumbnailRepository) saveFromFile(ctx context.Context, filePath stri
return 0, err
}
return r.saveFromBytes(ctx, content, getMimeType(filePath), meta, dashboardVersion)
return r.saveFromBytes(ctx, content, getMimeType(filePath), meta, dashboardVersion, dsUids)
}
func getMimeType(filePath string) string {
@ -53,12 +56,13 @@ func getMimeType(filePath string) string {
return "image/png"
}
func (r *sqlThumbnailRepository) saveFromBytes(ctx context.Context, content []byte, mimeType string, meta models.DashboardThumbnailMeta, dashboardVersion int) (int64, error) {
func (r *sqlThumbnailRepository) saveFromBytes(ctx context.Context, content []byte, mimeType string, meta models.DashboardThumbnailMeta, dashboardVersion int, dsUids []string) (int64, error) {
cmd := &models.SaveDashboardThumbnailCommand{
DashboardThumbnailMeta: meta,
Image: content,
MimeType: mimeType,
DashboardVersion: dashboardVersion,
DatasourceUIDs: dsUids,
}
_, err := r.store.SaveThumbnail(ctx, cmd)
@ -87,6 +91,7 @@ func (r *sqlThumbnailRepository) getThumbnail(ctx context.Context, meta models.D
func (r *sqlThumbnailRepository) findDashboardsWithStaleThumbnails(ctx context.Context, theme models.Theme, kind models.ThumbnailKind) ([]*models.DashboardWithStaleThumbnail, error) {
return r.store.FindDashboardsWithStaleThumbnails(ctx, &models.FindDashboardsWithStaleThumbnailsCommand{
IncludeManuallyUploadedThumbnails: false,
IncludeThumbnailsWithEmptyDsUIDs: !r.search.IsDisabled(),
Theme: theme,
Kind: kind,
})

@ -9,6 +9,8 @@ import (
"net/http"
"time"
"github.com/grafana/grafana/pkg/services/datasources/permissions"
"github.com/grafana/grafana/pkg/services/searchV2"
"github.com/segmentio/encoding/json"
"github.com/grafana/grafana/pkg/api/response"
@ -56,6 +58,10 @@ type thumbService struct {
canRunCrawler bool
settings setting.DashboardPreviewsSettings
dashboardService dashboards.DashboardService
dsUidsLookup getDatasourceUidsForDashboard
dsPermissionsService permissions.DatasourcePermissionsService
licensing models.Licensing
searchService searchV2.SearchService
}
type crawlerScheduleOptions struct {
@ -71,13 +77,14 @@ type crawlerScheduleOptions struct {
func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
lockService *serverlock.ServerLockService, renderService rendering.Service,
gl *live.GrafanaLive, store *sqlstore.SQLStore, authSetupService CrawlerAuthSetupService,
dashboardService dashboards.DashboardService) Service {
dashboardService dashboards.DashboardService, searchService searchV2.SearchService,
dsPermissionsService permissions.DatasourcePermissionsService, licensing models.Licensing) Service {
if !features.IsEnabled(featuremgmt.FlagDashboardPreviews) {
return &dummyService{}
}
logger := log.New("previews_service")
thumbnailRepo := newThumbnailRepo(store)
thumbnailRepo := newThumbnailRepo(store, searchService)
canRunCrawler := true
@ -91,17 +98,27 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
logger.Info("Crawler auth setup complete", "crawlerAuthSetupTime", time.Since(authSetupStarted))
}
dsUidsLookup := &dsUidsLookup{
searchService: searchService,
crawlerAuth: crawlerAuth,
features: features,
}
t := &thumbService{
licensing: licensing,
renderingService: renderService,
renderer: newSimpleCrawler(renderService, gl, thumbnailRepo, cfg, cfg.DashboardPreviews),
renderer: newSimpleCrawler(renderService, gl, thumbnailRepo, cfg, cfg.DashboardPreviews, dsUidsLookup.getDatasourceUidsForDashboard),
thumbnailRepo: thumbnailRepo,
store: store,
features: features,
lockService: lockService,
crawlLockServiceActionName: "dashboard-crawler",
searchService: searchService,
log: logger,
canRunCrawler: canRunCrawler,
dsUidsLookup: dsUidsLookup.getDatasourceUidsForDashboard,
settings: cfg.DashboardPreviews,
dsPermissionsService: dsPermissionsService,
scheduleOptions: crawlerScheduleOptions{
tickerInterval: 5 * time.Minute,
crawlInterval: cfg.DashboardPreviews.SchedulerInterval,
@ -233,6 +250,10 @@ func (hs *thumbService) GetImage(c *models.ReqContext) {
return
}
if !hs.hasAccessToPreview(c, res, req) {
return
}
currentEtag := fmt.Sprintf("%d", res.Updated.Unix())
c.Resp.Header().Set("ETag", currentEtag)
@ -248,6 +269,50 @@ func (hs *thumbService) GetImage(c *models.ReqContext) {
}
}
func (hs *thumbService) hasAccessToPreview(c *models.ReqContext, res *models.DashboardThumbnail, req *previewRequest) bool {
if !hs.licensing.FeatureEnabled("accesscontrol.enforcement") {
return true
}
if hs.searchService.IsDisabled() {
c.JSON(404, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
return false
}
if res.DsUIDs == "" {
hs.log.Debug("dashboard preview is stale; no datasource uids", "dashboardUid", req.UID)
c.JSON(404, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
return false
}
var dsUids []string
err := json.Unmarshal([]byte(res.DsUIDs), &dsUids)
if err != nil {
hs.log.Error("Error when retrieving datasource uids", "dashboardUid", req.UID, "err", err)
c.JSON(404, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
return false
}
accessibleDatasources, err := hs.dsPermissionsService.FilterDatasourceUidsBasedOnQueryPermissions(c.Req.Context(), c.SignedInUser, dsUids)
if err != nil && !errors.Is(err, permissions.ErrNotImplemented) {
hs.log.Error("Error when filtering datasource uids", "dashboardUid", req.UID, "err", err)
c.JSON(500, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
return false
}
if !errors.Is(err, permissions.ErrNotImplemented) {
canQueryAllDatasources := len(accessibleDatasources) == len(dsUids)
if !canQueryAllDatasources {
hs.log.Info("Denied access to dashboard preview", "dashboardUid", req.UID, "err", err, "dashboardDatasources", dsUids, "accessibleDatasources", accessibleDatasources)
c.JSON(404, map[string]string{"dashboardUID": req.UID, "error": "unknown"})
return false
}
}
return true
}
func (hs *thumbService) GetDashboardPreviewsSetupSettings(c *models.ReqContext) dashboardPreviewsSetupConfig {
return hs.getDashboardPreviewsSetupSettings(c.Req.Context())
}
@ -330,12 +395,19 @@ func (hs *thumbService) SetImage(c *models.ReqContext) {
return
}
dsUids, err := hs.dsUidsLookup(c.Req.Context(), req.UID, req.OrgID)
if err != nil {
hs.log.Error("error looking up datasource ids", "err", err, "dashboardUid", req.UID)
c.JSON(500, map[string]string{"error": "internal server error"})
return
}
_, err = hs.thumbnailRepo.saveFromBytes(c.Req.Context(), fileBytes, getMimeType(handler.Filename), models.DashboardThumbnailMeta{
DashboardUID: req.UID,
OrgId: req.OrgID,
Theme: req.Theme,
Kind: req.Kind,
}, models.DashboardVersionForManualThumbnailUpload)
}, models.DashboardVersionForManualThumbnailUpload, dsUids)
if err != nil {
c.JSON(400, map[string]string{"error": "error saving thumbnail file"})

@ -0,0 +1,123 @@
{
"schema": {
"name": "Query results",
"refId": "Search",
"meta": {
"type": "search-results",
"custom": {
"count": 106,
"locationInfo": {
"yboVMzb7z": {
"name": "gdev dashboards",
"kind": "folder",
"url": "/dashboards/f/yboVMzb7z/gdev-dashboards"
}
},
"sortBy": "name_sort"
}
},
"fields": [
{
"name": "kind",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "uid",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "name",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "panel_type",
"type": "string",
"typeInfo": {
"frame": "string"
}
},
{
"name": "url",
"type": "string",
"typeInfo": {
"frame": "string"
},
"config": {
"links": [
{
"title": "link",
"url": "${__value.text}"
}
]
}
},
{
"name": "tags",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": true
}
},
{
"name": "ds_uid",
"type": "other",
"typeInfo": {
"frame": "json.RawMessage",
"nullable": false
}
},
{
"name": "location",
"type": "string",
"typeInfo": {
"frame": "string"
}
}
]
},
"data": {
"values": [
[
"dashboard"
],
[
"ujaM1h6nz"
],
[
"abc2"
],
[
""
],
[
"/dashboards/f/ujaM1h6nz/abc2"
],
[
[
"gdev"
]
],
[
[
"datasource-2",
"datasource-3",
"datasource-4",
"grafana"
]
],
[
""
]
]
}
}
Loading…
Cancel
Save