ObjectStore: add a kind registry (#56507)

This commit is contained in:
Ryan McKinley 2022-10-08 09:05:46 -07:00 committed by GitHub
parent ac91df0ea2
commit b24be6c0fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 1883 additions and 402 deletions

View File

@ -36,6 +36,7 @@ export interface Playlist {
interval: string;
/**
* The ordered list of items that the playlist will iterate over.
* FIXME! This should not be optional, but changing it makes the godegen awkward
*/
items?: Array<PlaylistItem>;
/**

View File

@ -22,6 +22,7 @@ seqs: [
interval: string | *"5m"
// The ordered list of items that the playlist will iterate over.
// FIXME! This should not be optional, but changing it makes the godegen awkward
items?: [...#PlaylistItem]
///////////////////////////////////////

View File

@ -36,6 +36,7 @@ type Model struct {
Interval string `json:"interval"`
// The ordered list of items that the playlist will iterate over.
// FIXME! This should not be optional, but changing it makes the godegen awkward
Items *[]PlaylistItem `json:"items,omitempty"`
// Name of the playlist.

119
pkg/models/object.go Normal file
View File

@ -0,0 +1,119 @@
package models
//-----------------------------------------------------------------------------------------------------
// NOTE: the object store is in heavy development, and the locations will likely continue to move
//-----------------------------------------------------------------------------------------------------
import "context"
const (
StandardKindDashboard = "dashboard"
StandardKindPlaylist = "playlist"
StandardKindFolder = "folder"
// StandardKindDataSource: not a real kind yet, but used to define references from dashboards
// Types: influx, prometheus, testdata, ...
StandardKindDataSource = "ds"
// StandardKindPanel: currently used in two ways :( in search it defines a panel within a dashboard
// This is also used to refer to a panel plugin with type: heatmap, timeseries, table, ...
StandardKindPanel = "panel"
// StandardKindTransform: used to show that a dashboard depends on a set of transformations
// NOTE: this should likey be replaced with kind:system/type:transform/uid:joinByField or something like that
StandardKindTransform = "transform"
// StandardKindSVG SVG file support
StandardKindSVG = "svg"
// StandardKindPNG PNG file support
StandardKindPNG = "png"
// StandardKindQuery early development on panel query library
// the kind may need to change to better encapsulate { targets:[], transforms:[] }
StandardKindQuery = "query"
)
// ObjectKindInfo describes information needed from the object store
// All non-raw types will have a schema that can be used to validate
type ObjectKindInfo struct {
// Unique short id for this kind
ID string `json:"id,omitempty"`
// Display name (may be equal to the ID)
Name string `json:"name,omitempty"`
// Kind description
Description string `json:"description,omitempty"`
// The format is not controlled by a schema
IsRaw bool `json:"isRaw,omitempty"`
// The preferred save extension (svg, png, parquet, etc) if one exists
FileExtension string `json:"fileExtension,omitempty"`
// The correct mime-type to return for raw objects
MimeType string `json:"mimeType,omitempty"`
}
// ObjectSummary represents common data derived from a raw object bytes.
// The values should not depend on system state, and are derived from the raw object.
// This summary is used for a unified search and object listing
type ObjectSummary struct {
UID string `json:"uid,omitempty"`
Kind string `json:"kind,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
// Key value pairs. Tags are are represented as keys with empty values
Labels map[string]string `json:"labels,omitempty"`
// URL should only be set if the value is not derived directly from kind+uid
// NOTE: this may go away with a more robust GRN solution /!\
URL string `json:"URL,omitempty"`
// When errors exist
Error *ObjectErrorInfo `json:"error,omitempty"`
// Optional field values. The schema will define and document possible values for a given kind
Fields map[string]interface{} `json:"fields,omitempty"`
// eg: panels within dashboard
Nested []*ObjectSummary `json:"nested,omitempty"`
// Optional references to external things
References []*ObjectExternalReference `json:"references,omitempty"`
// The summary can not be extended
_ interface{}
}
// This will likely get replaced with a more general error framework.
type ObjectErrorInfo struct {
// TODO: Match an error code registry?
Code int64 `json:"code,omitempty"`
// Simple error display
Message string `json:"message,omitempty"`
// Error details
Details interface{} `json:"details,omitempty"`
}
// Reference to another object outside itself
// This message is derived from the object body and can be used to search for references.
// This does not represent a method to declare a reference to another object.
type ObjectExternalReference struct {
// datasource (instance), dashboard (instance),
Kind string `json:"kind,omitempty"`
// prometheus / heatmap, heatamp|prometheus
Type string `json:"type,omitempty"` // flavor
// Unique ID for this object
UID string `json:"UID,omitempty"`
}
// ObjectSummaryBuilder will read an object, validate it, and return a summary, sanitized payload, or an error
// This should not include values that depend on system state, only the raw object
type ObjectSummaryBuilder = func(ctx context.Context, uid string, body []byte) (*ObjectSummary, []byte, error)

View File

@ -124,6 +124,7 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/services/star/starimpl"
"github.com/grafana/grafana/pkg/services/store"
"github.com/grafana/grafana/pkg/services/store/kind"
"github.com/grafana/grafana/pkg/services/store/object"
objectdummyserver "github.com/grafana/grafana/pkg/services/store/object/dummy"
"github.com/grafana/grafana/pkg/services/store/sanitizer"
@ -354,6 +355,7 @@ var wireBasicSet = wire.NewSet(
grpcserver.ProvideHealthService,
grpcserver.ProvideReflectionService,
interceptors.ProvideAuthenticator,
kind.ProvideService, // The registry known kinds
objectdummyserver.ProvideDummyObjectServer,
object.ProvideHTTPObjectStore,
teamimpl.ProvideService,

View File

@ -1,7 +1,6 @@
package export
import (
"bytes"
"encoding/json"
"fmt"
"path"
@ -11,9 +10,8 @@ import (
"github.com/google/uuid"
"github.com/grafana/grafana/pkg/infra/filestorage"
"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/kind/dashboard"
)
func exportDashboards(helper *commitHelper, job *gitExportJob) error {
@ -26,7 +24,7 @@ func exportDashboards(helper *commitHelper, job *gitExportJob) error {
folders[0] = job.cfg.GeneralFolderPath // "general"
}
lookup, err := dslookup.LoadDatasourceLookup(helper.ctx, helper.orgID, job.sql)
lookup, err := dashboard.LoadDatasourceLookup(helper.ctx, helper.orgID, job.sql)
if err != nil {
return err
}
@ -60,20 +58,22 @@ func exportDashboards(helper *commitHelper, job *gitExportJob) error {
return err
}
reader := dashboard.NewStaticDashboardSummaryBuilder(lookup)
// Process all folders
for _, row := range rows {
if !row.IsFolder {
continue
}
dash, err := extract.ReadDashboard(bytes.NewReader(row.Data), lookup)
dash, _, err := reader(helper.ctx, row.UID, row.Data)
if err != nil {
return err
}
dash.UID = row.UID
slug := cleanFileName(dash.Title)
slug := cleanFileName(dash.Name)
folder := map[string]string{
"title": dash.Title,
"title": dash.Name,
}
folderStructure.body = append(folderStructure.body, commitBody{

View File

@ -12,7 +12,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/querylibrary"
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
"github.com/grafana/grafana/pkg/services/store/kind/dashboard"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@ -205,7 +205,7 @@ func getDatasourceUID(q *simplejson.Json) string {
return uid
}
func isQueryWithMixedDataSource(q *querylibrary.Query) (isMixed bool, firstDsRef dslookup.DataSourceRef) {
func isQueryWithMixedDataSource(q *querylibrary.Query) (isMixed bool, firstDsRef dashboard.DataSourceRef) {
dsRefs := extractDataSources(q)
for _, dsRef := range dsRefs {
@ -226,8 +226,8 @@ func isQueryWithMixedDataSource(q *querylibrary.Query) (isMixed bool, firstDsRef
return false, firstDsRef
}
func extractDataSources(query *querylibrary.Query) []dslookup.DataSourceRef {
ds := make([]dslookup.DataSourceRef, 0)
func extractDataSources(query *querylibrary.Query) []dashboard.DataSourceRef {
ds := make([]dashboard.DataSourceRef, 0)
for _, q := range query.Queries {
dsUid := getDatasourceUID(q)
@ -236,7 +236,7 @@ func extractDataSources(query *querylibrary.Query) []dslookup.DataSourceRef {
dsType = expr.DatasourceType
}
ds = append(ds, dslookup.DataSourceRef{
ds = append(ds, dashboard.DataSourceRef{
UID: dsUid,
Type: dsType,
})

View File

@ -7,7 +7,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
"github.com/grafana/grafana/pkg/services/store/kind/dashboard"
"github.com/grafana/grafana/pkg/services/user"
)
@ -64,7 +64,7 @@ type QueryInfo struct {
TimeTo string `json:"timeTo"`
SchemaVersion int64 `json:"schemaVersion"`
Datasource []dslookup.DataSourceRef `json:"datasource,omitempty"` // UIDs
Datasource []dashboard.DataSourceRef `json:"datasource,omitempty"` // UIDs
}
type QuerySearchOptions struct {

View File

@ -14,7 +14,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/store/object"
"github.com/grafana/grafana/pkg/models"
)
const (
@ -168,7 +168,7 @@ func getNonFolderDashboardDoc(dash dashboard, location string) *bluge.Document {
}
for _, ref := range dash.summary.References {
if ref.Kind == object.StandardKindDataSource {
if ref.Kind == models.StandardKindDataSource {
if ref.Type != "" {
doc.AddField(bluge.NewKeywordField(documentFieldDSType, ref.Type).
StoreValue().
@ -200,7 +200,7 @@ func getDashboardPanelDocs(dash dashboard, location string) []*bluge.Document {
for _, ref := range dash.summary.References {
switch ref.Kind {
case object.StandardKindDashboard:
case models.StandardKindDashboard:
if ref.Type != "" {
doc.AddField(bluge.NewKeywordField(documentFieldDSType, ref.Type).
StoreValue().
@ -213,11 +213,11 @@ func getDashboardPanelDocs(dash dashboard, location string) []*bluge.Document {
Aggregatable().
SearchTermPositions())
}
case object.StandardKindPanel:
case models.StandardKindPanel:
if ref.Type != "" {
doc.AddField(bluge.NewKeywordField(documentFieldPanelType, ref.Type).Aggregatable().StoreValue())
}
case object.StandardKindTransform:
case models.StandardKindTransform:
if ref.Type != "" {
doc.AddField(bluge.NewKeywordField(documentFieldTransformer, ref.Type).Aggregatable())
}

View File

@ -1,36 +0,0 @@
package extract
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 []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 []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
}

View File

@ -8,7 +8,7 @@ import (
"github.com/blugelabs/bluge/search/searcher"
"github.com/blugelabs/bluge/search/similarity"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/store/object"
"github.com/grafana/grafana/pkg/models"
)
type PermissionFilter struct {
@ -19,11 +19,11 @@ type PermissionFilter struct {
type entityKind string
const (
entityKindPanel entityKind = object.StandardKindPanel
entityKindDashboard entityKind = object.StandardKindDashboard
entityKindFolder entityKind = object.StandardKindFolder
entityKindDatasource entityKind = object.StandardKindDataSource
entityKindQuery entityKind = object.StandardKindQuery
entityKindPanel entityKind = models.StandardKindPanel
entityKindDashboard entityKind = models.StandardKindDashboard
entityKindFolder entityKind = models.StandardKindFolder
entityKindDatasource entityKind = models.StandardKindDataSource
entityKindQuery entityKind = models.StandardKindQuery
)
func (r entityKind) IsValid() bool {

View File

@ -15,12 +15,11 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
"github.com/grafana/grafana/pkg/services/searchV2/object"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/store"
obj "github.com/grafana/grafana/pkg/services/store/object"
kdash "github.com/grafana/grafana/pkg/services/store/kind/dashboard"
"github.com/grafana/grafana/pkg/setting"
"go.opentelemetry.io/otel/attribute"
@ -55,7 +54,7 @@ type dashboard struct {
updated time.Time
// Use generic structure
summary *obj.ObjectSummary
summary *models.ObjectSummary
}
// buildSignal is sent when search index is accessed in organization for which
@ -839,7 +838,7 @@ func (l sqlDashboardLoader) LoadDashboards(ctx context.Context, orgID int64, das
slug: "",
created: time.Now(),
updated: time.Now(),
summary: &obj.ObjectSummary{
summary: &models.ObjectSummary{
//ID: 0,
Name: "General",
},
@ -850,7 +849,7 @@ func (l sqlDashboardLoader) LoadDashboards(ctx context.Context, orgID int64, das
loadDatasourceSpan.SetAttributes("orgID", orgID, attribute.Key("orgID").Int64(orgID))
// key will allow name or uid
lookup, err := dslookup.LoadDatasourceLookup(loadDatasourceCtx, orgID, l.sql)
lookup, err := kdash.LoadDatasourceLookup(loadDatasourceCtx, orgID, l.sql)
if err != nil {
loadDatasourceSpan.End()
return dashboards, err
@ -897,15 +896,10 @@ func (l sqlDashboardLoader) LoadDashboards(ctx context.Context, orgID int64, das
readDashboardSpan.SetAttributes("orgID", orgID, attribute.Key("orgID").Int64(orgID))
readDashboardSpan.SetAttributes("dashboardCount", len(rows), attribute.Key("dashboardCount").Int(len(rows)))
reader := object.NewDashboardSummaryBuilder(lookup)
reader := kdash.NewStaticDashboardSummaryBuilder(lookup)
for _, row := range rows {
obj := &obj.RawObject{
UID: row.Uid,
Kind: "dashboard",
Body: row.Data,
}
summary, err := reader(obj)
summary, _, err := reader(ctx, row.Uid, row.Data)
if err != nil {
l.logger.Warn("Error indexing dashboard data", "error", err, "dashboardId", row.Id, "dashboardSlug", row.Slug)
// But append info anyway for now, since we possibly extracted useful information.
@ -918,7 +912,7 @@ func (l sqlDashboardLoader) LoadDashboards(ctx context.Context, orgID int64, das
slug: row.Slug,
created: row.Created,
updated: row.Updated,
summary: &summary,
summary: summary,
})
lastID = row.Id
}

View File

@ -9,8 +9,8 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/store/object"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/infra/log"
@ -113,14 +113,14 @@ var testDashboards = []dashboard{
{
id: 1,
uid: "1",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "test",
},
},
{
id: 2,
uid: "2",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "boom",
},
},
@ -162,7 +162,7 @@ func TestDashboardIndexUpdates(t *testing.T) {
err := index.updateDashboard(context.Background(), testOrgID, orgIdx, dashboard{
id: 3,
uid: "3",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "created",
},
})
@ -181,7 +181,7 @@ func TestDashboardIndexUpdates(t *testing.T) {
err := index.updateDashboard(context.Background(), testOrgID, orgIdx, dashboard{
id: 2,
uid: "2",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "nginx",
},
})
@ -197,14 +197,14 @@ var testSortDashboards = []dashboard{
{
id: 1,
uid: "1",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "a-test",
},
},
{
id: 2,
uid: "2",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "z-test",
},
},
@ -288,14 +288,14 @@ var testPrefixDashboards = []dashboard{
{
id: 1,
uid: "1",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "Archer Data System",
},
},
{
id: 2,
uid: "2",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "Document Sync repo",
},
},
@ -366,7 +366,7 @@ var longPrefixDashboards = []dashboard{
{
id: 1,
uid: "1",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "Eyjafjallajökull Eruption data",
},
},
@ -385,14 +385,14 @@ var scatteredTokensDashboards = []dashboard{
{
id: 1,
uid: "1",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "Three can keep a secret, if two of them are dead (Benjamin Franklin)",
},
},
{
id: 3,
uid: "2",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "A secret is powerful when it is empty (Umberto Eco)",
},
},
@ -418,7 +418,7 @@ var dashboardsWithFolders = []dashboard{
id: 1,
uid: "1",
isFolder: true,
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "My folder",
},
},
@ -426,9 +426,9 @@ var dashboardsWithFolders = []dashboard{
id: 2,
uid: "2",
folderID: 1,
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "Dashboard in folder 1",
Nested: []*object.ObjectSummary{
Nested: []*models.ObjectSummary{
newNestedPanel(1, "Panel 1"),
newNestedPanel(2, "Panel 2"),
},
@ -438,9 +438,9 @@ var dashboardsWithFolders = []dashboard{
id: 3,
uid: "3",
folderID: 1,
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "Dashboard in folder 2",
Nested: []*object.ObjectSummary{
Nested: []*models.ObjectSummary{
newNestedPanel(3, "Panel 3"),
},
},
@ -448,9 +448,9 @@ var dashboardsWithFolders = []dashboard{
{
id: 4,
uid: "4",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "One more dash",
Nested: []*object.ObjectSummary{
Nested: []*models.ObjectSummary{
newNestedPanel(4, "Panel 4"),
},
},
@ -505,9 +505,9 @@ var dashboardsWithPanels = []dashboard{
{
id: 1,
uid: "1",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "My Dash",
Nested: []*object.ObjectSummary{
Nested: []*models.ObjectSummary{
newNestedPanel(1, "Panel 1"),
newNestedPanel(2, "Panel 2"),
},
@ -515,8 +515,8 @@ var dashboardsWithPanels = []dashboard{
},
}
func newNestedPanel(id int64, name string) *object.ObjectSummary {
summary := &object.ObjectSummary{
func newNestedPanel(id int64, name string) *models.ObjectSummary {
summary := &models.ObjectSummary{
Kind: "panel",
UID: fmt.Sprintf("???#%d", id),
}
@ -553,14 +553,14 @@ var punctuationSplitNgramDashboards = []dashboard{
{
id: 1,
uid: "1",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "heat-torkel",
},
},
{
id: 2,
uid: "2",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "topology heatmap",
},
},
@ -586,7 +586,7 @@ var camelCaseNgramDashboards = []dashboard{
{
id: 1,
uid: "1",
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: "heatTorkel",
},
},
@ -608,7 +608,7 @@ func dashboardsWithTitles(names ...string) []dashboard {
out = append(out, dashboard{
id: no,
uid: fmt.Sprintf("%d", no),
summary: &object.ObjectSummary{
summary: &models.ObjectSummary{
Name: name,
},
})

View File

@ -1,71 +0,0 @@
package object
import (
"bytes"
"fmt"
"strconv"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
"github.com/grafana/grafana/pkg/services/searchV2/extract"
"github.com/grafana/grafana/pkg/services/store/object"
)
func NewDashboardSummaryBuilder(lookup dslookup.DatasourceLookup) object.ObjectSummaryBuilder {
return func(obj *object.RawObject) (object.ObjectSummary, error) {
summary := object.ObjectSummary{
Labels: make(map[string]string),
Fields: make(map[string]interface{}),
}
stream := bytes.NewBuffer(obj.Body)
dash, err := extract.ReadDashboard(stream, lookup)
if err != nil {
summary.Error = &object.ObjectErrorInfo{
Code: 500, // generic bad error
Message: err.Error(),
}
return summary, err
}
dashboardRefs := object.NewReferenceAccumulator()
url := fmt.Sprintf("/d/%s/%s", obj.UID, models.SlugifyTitle(dash.Title))
summary.Name = dash.Title
summary.Description = dash.Description
summary.URL = url
for _, v := range dash.Tags {
summary.Labels[v] = ""
}
if len(dash.TemplateVars) > 0 {
summary.Fields["hasTemplateVars"] = true
}
for _, panel := range dash.Panels {
panelRefs := object.NewReferenceAccumulator()
p := &object.ObjectSummary{
UID: obj.UID + "#" + strconv.FormatInt(panel.ID, 10),
Kind: "panel",
}
p.Name = panel.Title
p.Description = panel.Description
p.URL = fmt.Sprintf("%s?viewPanel=%d", url, panel.ID)
p.Fields = make(map[string]interface{}, 0)
panelRefs.Add("panel", panel.Type, "")
for _, v := range panel.Datasource {
dashboardRefs.Add(object.StandardKindDataSource, v.Type, v.UID)
panelRefs.Add(object.StandardKindDataSource, v.Type, v.UID)
}
for _, v := range panel.Transformer {
panelRefs.Add(object.StandardKindTransform, v, "")
}
dashboardRefs.Add(object.StandardKindPanel, panel.Type, "")
p.References = panelRefs.Get()
summary.Nested = append(summary.Nested, p)
}
summary.References = dashboardRefs.Get()
return summary, nil
}
}

View File

@ -1,4 +1,4 @@
package object
package store
import (
"context"

View File

@ -1,4 +1,4 @@
package extract
package dashboard
import (
"io"
@ -6,8 +6,6 @@ import (
"strings"
jsoniter "github.com/json-iterator/go"
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
)
func logf(format string, a ...interface{}) {
@ -24,46 +22,46 @@ type templateVariable struct {
}
type datasourceVariableLookup struct {
variableNameToRefs map[string][]dslookup.DataSourceRef
dsLookup dslookup.DatasourceLookup
variableNameToRefs map[string][]DataSourceRef
dsLookup DatasourceLookup
}
func (d *datasourceVariableLookup) getDsRefsByTemplateVariableValue(value string, datasourceType string) []dslookup.DataSourceRef {
func (d *datasourceVariableLookup) getDsRefsByTemplateVariableValue(value string, datasourceType string) []DataSourceRef {
switch value {
case "default":
// can be the default DS, or a DS with UID="default"
candidateDs := d.dsLookup.ByRef(&dslookup.DataSourceRef{UID: value})
candidateDs := d.dsLookup.ByRef(&DataSourceRef{UID: value})
if candidateDs == nil {
// get the actual default DS
candidateDs = d.dsLookup.ByRef(nil)
}
if candidateDs != nil {
return []dslookup.DataSourceRef{*candidateDs}
return []DataSourceRef{*candidateDs}
}
return []dslookup.DataSourceRef{}
return []DataSourceRef{}
case "$__all":
// TODO: filter datasources by template variable's regex
return d.dsLookup.ByType(datasourceType)
case "":
return []dslookup.DataSourceRef{}
return []DataSourceRef{}
case "No data sources found":
return []dslookup.DataSourceRef{}
return []DataSourceRef{}
default:
// some variables use `ds.name` rather `ds.uid`
if ref := d.dsLookup.ByRef(&dslookup.DataSourceRef{
if ref := d.dsLookup.ByRef(&DataSourceRef{
UID: value,
}); ref != nil {
return []dslookup.DataSourceRef{*ref}
return []DataSourceRef{*ref}
}
// discard variable
return []dslookup.DataSourceRef{}
return []DataSourceRef{}
}
}
func (d *datasourceVariableLookup) add(templateVariable templateVariable) {
var refs []dslookup.DataSourceRef
var refs []DataSourceRef
datasourceType, isDataSourceTypeValid := templateVariable.query.(string)
if !isDataSourceTypeValid {
@ -86,8 +84,8 @@ func (d *datasourceVariableLookup) add(templateVariable templateVariable) {
d.variableNameToRefs[templateVariable.name] = unique(refs)
}
func unique(refs []dslookup.DataSourceRef) []dslookup.DataSourceRef {
var uniqueRefs []dslookup.DataSourceRef
func unique(refs []DataSourceRef) []DataSourceRef {
var uniqueRefs []DataSourceRef
uidPresence := make(map[string]bool)
for _, ref := range refs {
if !uidPresence[ref.UID] {
@ -98,26 +96,26 @@ func unique(refs []dslookup.DataSourceRef) []dslookup.DataSourceRef {
return uniqueRefs
}
func (d *datasourceVariableLookup) getDatasourceRefs(name string) []dslookup.DataSourceRef {
func (d *datasourceVariableLookup) getDatasourceRefs(name string) []DataSourceRef {
refs, ok := d.variableNameToRefs[name]
if ok {
return refs
}
return []dslookup.DataSourceRef{}
return []DataSourceRef{}
}
func newDatasourceVariableLookup(dsLookup dslookup.DatasourceLookup) *datasourceVariableLookup {
func newDatasourceVariableLookup(dsLookup DatasourceLookup) *datasourceVariableLookup {
return &datasourceVariableLookup{
variableNameToRefs: make(map[string][]dslookup.DataSourceRef),
variableNameToRefs: make(map[string][]DataSourceRef),
dsLookup: dsLookup,
}
}
// nolint:gocyclo
// ReadDashboard will take a byte stream and return dashboard info
func ReadDashboard(stream io.Reader, lookup dslookup.DatasourceLookup) (*DashboardInfo, error) {
dash := &DashboardInfo{}
func readDashboard(stream io.Reader, lookup DatasourceLookup) (*dashboardInfo, error) {
dash := &dashboardInfo{}
iter := jsoniter.Parse(jsoniter.ConfigDefault, stream, 1024)
@ -192,7 +190,7 @@ func ReadDashboard(stream io.Reader, lookup dslookup.DatasourceLookup) (*Dashboa
}
case "panels":
for iter.ReadArray() {
dash.Panels = append(dash.Panels, readPanelInfo(iter, lookup))
dash.Panels = append(dash.Panels, readpanelInfo(iter, lookup))
}
case "rows":
@ -278,11 +276,11 @@ func ReadDashboard(stream io.Reader, lookup dslookup.DatasourceLookup) (*Dashboa
return dash, iter.Error
}
func panelRequiresDatasource(panel PanelInfo) bool {
func panelRequiresDatasource(panel panelInfo) bool {
return panel.Type != "row"
}
func fillDefaultDatasources(dash *DashboardInfo, lookup dslookup.DatasourceLookup) {
func fillDefaultDatasources(dash *dashboardInfo, lookup DatasourceLookup) {
for i, panel := range dash.Panels {
if len(panel.Datasource) != 0 || !panelRequiresDatasource(panel) {
continue
@ -290,14 +288,14 @@ func fillDefaultDatasources(dash *DashboardInfo, lookup dslookup.DatasourceLooku
defaultDs := lookup.ByRef(nil)
if defaultDs != nil {
dash.Panels[i].Datasource = []dslookup.DataSourceRef{*defaultDs}
dash.Panels[i].Datasource = []DataSourceRef{*defaultDs}
}
}
}
func filterOutSpecialDatasources(dash *DashboardInfo) {
func filterOutSpecialDatasources(dash *dashboardInfo) {
for i, panel := range dash.Panels {
var dsRefs []dslookup.DataSourceRef
var dsRefs []DataSourceRef
// partition into actual datasource references and variables
for _, ds := range panel.Datasource {
@ -317,10 +315,10 @@ func filterOutSpecialDatasources(dash *DashboardInfo) {
}
}
func replaceDatasourceVariables(dash *DashboardInfo, datasourceVariablesLookup *datasourceVariableLookup) {
func replaceDatasourceVariables(dash *dashboardInfo, datasourceVariablesLookup *datasourceVariableLookup) {
for i, panel := range dash.Panels {
var dsVariableRefs []dslookup.DataSourceRef
var dsRefs []dslookup.DataSourceRef
var dsVariableRefs []DataSourceRef
var dsRefs []DataSourceRef
// partition into actual datasource references and variables
for i := range panel.Datasource {
@ -345,7 +343,7 @@ func isVariableRef(uid string) bool {
return strings.HasPrefix(uid, "$")
}
func getDataSourceVariableName(dsVariableRef dslookup.DataSourceRef) string {
func getDataSourceVariableName(dsVariableRef DataSourceRef) string {
if strings.HasPrefix(dsVariableRef.UID, "${") {
return strings.TrimPrefix(strings.TrimSuffix(dsVariableRef.UID, "}"), "${")
}
@ -353,8 +351,8 @@ func getDataSourceVariableName(dsVariableRef dslookup.DataSourceRef) string {
return strings.TrimPrefix(dsVariableRef.UID, "$")
}
func findDatasourceRefsForVariables(dsVariableRefs []dslookup.DataSourceRef, datasourceVariablesLookup *datasourceVariableLookup) []dslookup.DataSourceRef {
var referencedDs []dslookup.DataSourceRef
func findDatasourceRefsForVariables(dsVariableRefs []DataSourceRef, datasourceVariablesLookup *datasourceVariableLookup) []DataSourceRef {
var referencedDs []DataSourceRef
for _, dsVariableRef := range dsVariableRefs {
variableName := getDataSourceVariableName(dsVariableRef)
refs := datasourceVariablesLookup.getDatasourceRefs(variableName)
@ -364,8 +362,8 @@ func findDatasourceRefsForVariables(dsVariableRefs []dslookup.DataSourceRef, dat
}
// will always return strings for now
func readPanelInfo(iter *jsoniter.Iterator, lookup dslookup.DatasourceLookup) PanelInfo {
panel := PanelInfo{}
func readpanelInfo(iter *jsoniter.Iterator, lookup DatasourceLookup) panelInfo {
panel := panelInfo{}
targets := newTargetInfo(lookup)
@ -428,7 +426,7 @@ func readPanelInfo(iter *jsoniter.Iterator, lookup dslookup.DatasourceLookup) Pa
// Rows have nested panels
case "panels":
for iter.ReadArray() {
panel.Collapsed = append(panel.Collapsed, readPanelInfo(iter, lookup))
panel.Collapsed = append(panel.Collapsed, readpanelInfo(iter, lookup))
}
case "options":

View File

@ -1,4 +1,4 @@
package extract
package dashboard
import (
"encoding/json"
@ -10,12 +10,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
)
func dsLookup() dslookup.DatasourceLookup {
return dslookup.CreateDatasourceLookup([]*dslookup.DatasourceQueryResult{
func dsLookupForTests() DatasourceLookup {
return CreateDatasourceLookup([]*DatasourceQueryResult{
{
UID: "P8045C56BDA891CB2",
Type: "cloudwatch",
@ -74,7 +72,7 @@ func TestReadDashboard(t *testing.T) {
"panels-without-datasources",
}
devdash := "../../../../devenv/dev-dashboards/"
devdash := "../../../../../devenv/dev-dashboards/"
for _, input := range inputs {
// nolint:gosec
@ -90,7 +88,7 @@ func TestReadDashboard(t *testing.T) {
}
require.NoError(t, err)
dash, err := ReadDashboard(f, dsLookup())
dash, err := readDashboard(f, dsLookupForTests())
sortDatasources(dash)
require.NoError(t, err)
@ -115,7 +113,7 @@ func TestReadDashboard(t *testing.T) {
}
// assure consistent ordering of datasources to prevent random failures of `assert.JSONEq`
func sortDatasources(dash *DashboardInfo) {
func sortDatasources(dash *dashboardInfo) {
sort.Slice(dash.Datasource, func(i, j int) bool {
return strings.Compare(dash.Datasource[i].UID, dash.Datasource[j].UID) > 0
})

View File

@ -1,4 +1,4 @@
package dslookup
package dashboard
import (
"context"

View File

@ -1,8 +1,10 @@
package object
package dashboard
import (
"fmt"
"sort"
"github.com/grafana/grafana/pkg/models"
)
// A reference accumulator can combine
@ -11,24 +13,24 @@ type ReferenceAccumulator interface {
Add(kind string, subtype string, uid string)
// Returns the set of distinct references in a sorted order
Get() []*ExternalReference
Get() []*models.ObjectExternalReference
}
func NewReferenceAccumulator() ReferenceAccumulator {
return &referenceAccumulator{
refs: make(map[string]*ExternalReference),
refs: make(map[string]*models.ObjectExternalReference),
}
}
type referenceAccumulator struct {
refs map[string]*ExternalReference
refs map[string]*models.ObjectExternalReference
}
func (x *referenceAccumulator) Add(kind string, sub string, uid string) {
key := fmt.Sprintf("%s/%s/%s", kind, sub, uid)
_, ok := x.refs[key]
if !ok {
x.refs[key] = &ExternalReference{
x.refs[key] = &models.ObjectExternalReference{
Kind: kind,
Type: sub,
UID: uid,
@ -36,14 +38,14 @@ func (x *referenceAccumulator) Add(kind string, sub string, uid string) {
}
}
func (x *referenceAccumulator) Get() []*ExternalReference {
func (x *referenceAccumulator) Get() []*models.ObjectExternalReference {
keys := make([]string, 0, len(x.refs))
for k := range x.refs {
keys = append(keys, k)
}
sort.Strings(keys)
refs := make([]*ExternalReference, len(keys))
refs := make([]*models.ObjectExternalReference, len(keys))
for i, key := range keys {
refs[i] = x.refs[key]
}

View File

@ -0,0 +1,99 @@
package dashboard
import (
"bytes"
"context"
"fmt"
"strconv"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/store"
)
func GetObjectKindInfo() models.ObjectKindInfo {
return models.ObjectKindInfo{
ID: models.StandardKindDashboard,
Name: "Dashboard",
Description: "Define a grafana dashboard layout",
}
}
func NewDashboardSummary(sql *sqlstore.SQLStore) models.ObjectSummaryBuilder {
return func(ctx context.Context, uid string, body []byte) (*models.ObjectSummary, []byte, error) {
// This just gets the orgID (that will soon/eventually be encoded in a GRN and passed instead of a UID)
user := store.UserFromContext(ctx)
if user == nil {
return nil, nil, fmt.Errorf("can not find user in context")
}
// Totally inefficient to look this up every time, but for the current use case that is OK
// The lookup is currently structured to support searchV2, but I think should become a real fallback
// that is only executed when we find a legacy dashboard ref
lookup, err := LoadDatasourceLookup(ctx, user.OrgID, sql)
if err != nil {
return nil, nil, err
}
builder := NewStaticDashboardSummaryBuilder(lookup)
return builder(ctx, uid, body)
}
}
func NewStaticDashboardSummaryBuilder(lookup DatasourceLookup) models.ObjectSummaryBuilder {
return func(ctx context.Context, uid string, body []byte) (*models.ObjectSummary, []byte, error) {
summary := &models.ObjectSummary{
Labels: make(map[string]string),
Fields: make(map[string]interface{}),
}
stream := bytes.NewBuffer(body)
dash, err := readDashboard(stream, lookup)
if err != nil {
summary.Error = &models.ObjectErrorInfo{
Message: err.Error(),
}
return summary, body, err
}
dashboardRefs := NewReferenceAccumulator()
url := fmt.Sprintf("/d/%s/%s", uid, models.SlugifyTitle(dash.Title))
summary.Name = dash.Title
summary.Description = dash.Description
summary.URL = url
for _, v := range dash.Tags {
summary.Labels[v] = ""
}
if len(dash.TemplateVars) > 0 {
summary.Fields["hasTemplateVars"] = true
}
summary.Fields["schemaVersion"] = dash.SchemaVersion
for _, panel := range dash.Panels {
panelRefs := NewReferenceAccumulator()
p := &models.ObjectSummary{
UID: uid + "#" + strconv.FormatInt(panel.ID, 10),
Kind: "panel",
}
p.Name = panel.Title
p.Description = panel.Description
p.URL = fmt.Sprintf("%s?viewPanel=%d", url, panel.ID)
p.Fields = make(map[string]interface{}, 0)
panelRefs.Add("panel", panel.Type, "")
for _, v := range panel.Datasource {
dashboardRefs.Add(models.StandardKindDataSource, v.Type, v.UID)
panelRefs.Add(models.StandardKindDataSource, v.Type, v.UID)
}
for _, v := range panel.Transformer {
panelRefs.Add(models.StandardKindTransform, v, "")
}
dashboardRefs.Add(models.StandardKindPanel, panel.Type, "")
p.References = panelRefs.Get()
summary.Nested = append(summary.Nested, p)
}
summary.References = dashboardRefs.Get()
return summary, body, nil
}
}

View File

@ -1,39 +1,22 @@
package object
package dashboard
import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
"github.com/grafana/grafana/pkg/services/store/object"
"github.com/stretchr/testify/require"
)
func dsLookup() dslookup.DatasourceLookup {
return dslookup.CreateDatasourceLookup([]*dslookup.DatasourceQueryResult{
{
UID: "P8045C56BDA891CB2",
Type: "cloudwatch",
Name: "cloudwatch-name",
IsDefault: false,
},
{
UID: "default.uid",
Type: "default.type",
Name: "default.name",
IsDefault: true,
},
})
}
func TestReadSummaries(t *testing.T) {
devdash := "../../../../devenv/dev-dashboards/panel-graph/"
devdash := "../../../../../devenv/dev-dashboards/panel-graph/"
reader := NewDashboardSummaryBuilder(dsLookup())
ctx := context.Background()
reader := NewStaticDashboardSummaryBuilder(dsLookupForTests())
failed := make([]string, 0, 10)
err := filepath.Walk(devdash,
@ -51,10 +34,7 @@ func TestReadSummaries(t *testing.T) {
}
uid := path[len(devdash):]
summary, err := reader(&object.RawObject{
UID: uid,
Body: body,
})
summary, _, err := reader(ctx, uid, body)
if err != nil {
return err
}

View File

@ -1,25 +1,23 @@
package extract
package dashboard
import (
jsoniter "github.com/json-iterator/go"
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
)
type targetInfo struct {
lookup dslookup.DatasourceLookup
uids map[string]*dslookup.DataSourceRef
lookup DatasourceLookup
uids map[string]*DataSourceRef
}
func newTargetInfo(lookup dslookup.DatasourceLookup) targetInfo {
func newTargetInfo(lookup DatasourceLookup) targetInfo {
return targetInfo{
lookup: lookup,
uids: make(map[string]*dslookup.DataSourceRef),
uids: make(map[string]*DataSourceRef),
}
}
func (s *targetInfo) GetDatasourceInfo() []dslookup.DataSourceRef {
keys := make([]dslookup.DataSourceRef, len(s.uids))
func (s *targetInfo) GetDatasourceInfo() []DataSourceRef {
keys := make([]DataSourceRef, len(s.uids))
i := 0
for _, v := range s.uids {
keys[i] = *v
@ -34,7 +32,7 @@ func (s *targetInfo) addDatasource(iter *jsoniter.Iterator) {
case jsoniter.StringValue:
key := iter.ReadString()
dsRef := &dslookup.DataSourceRef{UID: key}
dsRef := &DataSourceRef{UID: key}
if !isVariableRef(dsRef.UID) && !isSpecialDatasource(dsRef.UID) {
ds := s.lookup.ByRef(dsRef)
s.addRef(ds)
@ -47,7 +45,7 @@ func (s *targetInfo) addDatasource(iter *jsoniter.Iterator) {
iter.Skip()
case jsoniter.ObjectValue:
ref := &dslookup.DataSourceRef{}
ref := &DataSourceRef{}
iter.ReadVal(ref)
if !isVariableRef(ref.UID) && !isSpecialDatasource(ref.UID) {
@ -62,7 +60,7 @@ func (s *targetInfo) addDatasource(iter *jsoniter.Iterator) {
}
}
func (s *targetInfo) addRef(ref *dslookup.DataSourceRef) {
func (s *targetInfo) addRef(ref *DataSourceRef) {
if ref != nil && ref.UID != "" {
s.uids[ref.UID] = ref
}
@ -84,7 +82,7 @@ func (s *targetInfo) addTarget(iter *jsoniter.Iterator) {
}
}
func (s *targetInfo) addPanel(panel PanelInfo) {
func (s *targetInfo) addPanel(panel panelInfo) {
for idx, v := range panel.Datasource {
if v.UID != "" {
s.uids[v.UID] = &panel.Datasource[idx]

View File

@ -0,0 +1,93 @@
{
"name": "Panel Tests - Graph - Gradient Area Fills",
"labels": {
"gdev": "",
"graph": "",
"panel-tests": ""
},
"URL": "/d/graph-gradient-area-fills.json/panel-tests-graph-gradient-area-fills",
"fields": {
"schemaVersion": 18
},
"nested": [
{
"uid": "graph-gradient-area-fills.json#2",
"kind": "panel",
"name": "Req/s",
"URL": "/d/graph-gradient-area-fills.json/panel-tests-graph-gradient-area-fills?viewPanel=2",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph-gradient-area-fills.json#11",
"kind": "panel",
"name": "Req/s",
"URL": "/d/graph-gradient-area-fills.json/panel-tests-graph-gradient-area-fills?viewPanel=11",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph-gradient-area-fills.json#7",
"kind": "panel",
"name": "Memory",
"URL": "/d/graph-gradient-area-fills.json/panel-tests-graph-gradient-area-fills?viewPanel=7",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph-gradient-area-fills.json#10",
"kind": "panel",
"name": "Req/s",
"URL": "/d/graph-gradient-area-fills.json/panel-tests-graph-gradient-area-fills?viewPanel=10",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
}
],
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
}

View File

@ -0,0 +1,181 @@
{
"name": "Panel Tests - shared tooltips",
"labels": {
"gdev": "",
"graph-ng": "",
"panel-tests": ""
},
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips",
"fields": {
"schemaVersion": 28
},
"nested": [
{
"uid": "graph-shared-tooltips.json#4",
"kind": "panel",
"name": "two units",
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=4",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "timeseries"
}
]
},
{
"uid": "graph-shared-tooltips.json#13",
"kind": "panel",
"name": "Speed vs Temperature (XY)",
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=13",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "xychart"
},
{
"kind": "transform",
"type": "organize"
},
{
"kind": "transform",
"type": "seriesToColumns"
}
]
},
{
"uid": "graph-shared-tooltips.json#2",
"kind": "panel",
"name": "Cursor info",
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=2",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "debug"
}
]
},
{
"uid": "graph-shared-tooltips.json#5",
"kind": "panel",
"name": "Only temperature",
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=5",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "timeseries"
}
]
},
{
"uid": "graph-shared-tooltips.json#9",
"kind": "panel",
"name": "Only Speed",
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=9",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "timeseries"
}
]
},
{
"uid": "graph-shared-tooltips.json#11",
"kind": "panel",
"name": "Panel Title",
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=11",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "timeseries"
}
]
},
{
"uid": "graph-shared-tooltips.json#8",
"kind": "panel",
"name": "flot panel (temperature)",
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=8",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph-shared-tooltips.json#10",
"kind": "panel",
"name": "flot panel (no units)",
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=10",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
}
],
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "debug"
},
{
"kind": "panel",
"type": "graph"
},
{
"kind": "panel",
"type": "timeseries"
},
{
"kind": "panel",
"type": "xychart"
}
]
}

View File

@ -0,0 +1,110 @@
{
"name": "Panel Tests - Graph Time Regions",
"labels": {
"gdev": "",
"graph": "",
"panel-tests": ""
},
"URL": "/d/graph-time-regions.json/panel-tests-graph-time-regions",
"fields": {
"schemaVersion": 18
},
"nested": [
{
"uid": "graph-time-regions.json#2",
"kind": "panel",
"name": "Business Hours",
"URL": "/d/graph-time-regions.json/panel-tests-graph-time-regions?viewPanel=2",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph-time-regions.json#4",
"kind": "panel",
"name": "Sunday's 20-23",
"URL": "/d/graph-time-regions.json/panel-tests-graph-time-regions?viewPanel=4",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph-time-regions.json#3",
"kind": "panel",
"name": "Each day of week",
"URL": "/d/graph-time-regions.json/panel-tests-graph-time-regions?viewPanel=3",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph-time-regions.json#5",
"kind": "panel",
"name": "05:00",
"URL": "/d/graph-time-regions.json/panel-tests-graph-time-regions?viewPanel=5",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph-time-regions.json#7",
"kind": "panel",
"name": "From 22:00 to 00:30 (crossing midnight)",
"URL": "/d/graph-time-regions.json/panel-tests-graph-time-regions?viewPanel=7",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
}
],
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
}

View File

@ -0,0 +1,385 @@
{
"name": "Panel Tests - Graph",
"labels": {
"gdev": "",
"graph": "",
"panel-tests": ""
},
"URL": "/d/graph_tests.json/panel-tests-graph",
"fields": {
"schemaVersion": 16
},
"nested": [
{
"uid": "graph_tests.json#1",
"kind": "panel",
"name": "No Data Points Warning",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=1",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_tests.json#2",
"kind": "panel",
"name": "Datapoints Outside Range Warning",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=2",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_tests.json#3",
"kind": "panel",
"name": "Random walk series",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=3",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_tests.json#4",
"kind": "panel",
"name": "Millisecond res x-axis and tooltip",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=4",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_tests.json#6",
"kind": "panel",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=6",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "text"
}
]
},
{
"uid": "graph_tests.json#5",
"kind": "panel",
"name": "2 yaxis and axis labels",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=5",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_tests.json#7",
"kind": "panel",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=7",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "text"
}
]
},
{
"uid": "graph_tests.json#8",
"kind": "panel",
"name": "null value connected",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=8",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_tests.json#10",
"kind": "panel",
"name": "null value null as zero",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=10",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_tests.json#13",
"kind": "panel",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=13",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "text"
}
]
},
{
"uid": "graph_tests.json#9",
"kind": "panel",
"name": "Stacking value ontop of nulls",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=9",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_tests.json#14",
"kind": "panel",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=14",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "text"
}
]
},
{
"uid": "graph_tests.json#12",
"kind": "panel",
"name": "Stacking all series null segment",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=12",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_tests.json#15",
"kind": "panel",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=15",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "text"
}
]
},
{
"uid": "graph_tests.json#21",
"kind": "panel",
"name": "Null between points",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=21",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_tests.json#22",
"kind": "panel",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=22",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "text"
}
]
},
{
"uid": "graph_tests.json#20",
"kind": "panel",
"name": "Legend Table Single Series Should Take Minimum Height",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=20",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_tests.json#16",
"kind": "panel",
"name": "Legend Table No Scroll Visible",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=16",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_tests.json#17",
"kind": "panel",
"name": "Legend Table Should Scroll",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=17",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_tests.json#18",
"kind": "panel",
"name": "Legend Table No Scroll Visible",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=18",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_tests.json#19",
"kind": "panel",
"name": "Legend Table No Scroll Visible",
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=19",
"references": [
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
}
]
}
],
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "ds",
"type": "testdata",
"UID": "PD8C576611E62080A"
},
{
"kind": "panel",
"type": "graph"
},
{
"kind": "panel",
"type": "text"
}
]
}

View File

@ -0,0 +1,177 @@
{
"name": "Panel Tests - Graph - Y axis ticks",
"labels": {
"gdev": "",
"panel-tests": ""
},
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks",
"fields": {
"schemaVersion": 19
},
"nested": [
{
"uid": "graph_y_axis.json#7",
"kind": "panel",
"name": "Data from 0 - 10K (unit short)",
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=7",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_y_axis.json#5",
"kind": "panel",
"name": "Data from 0 - 10K (unit bytes metric)",
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=5",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_y_axis.json#4",
"kind": "panel",
"name": "Data from 0 - 10K (unit bytes IEC)",
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=4",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_y_axis.json#2",
"kind": "panel",
"name": "Data from 0 - 10K (unit short)",
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=2",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_y_axis.json#3",
"kind": "panel",
"name": "Data from 0.0002 - 0.001 (unit short)",
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=3",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_y_axis.json#6",
"kind": "panel",
"name": "Data from 12000 - 30000 (unit ms)",
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=6",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_y_axis.json#9",
"kind": "panel",
"name": "Data from 0 - 1B (unit short)",
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=9",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_y_axis.json#10",
"kind": "panel",
"name": "Data from 0 - 1B (unit bytes)",
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=10",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
},
{
"uid": "graph_y_axis.json#8",
"kind": "panel",
"name": "Data from 12000 - 30000 (unit ms)",
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=8",
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
}
],
"references": [
{
"kind": "ds",
"type": "default.type",
"UID": "default.uid"
},
{
"kind": "panel",
"type": "graph"
}
]
}

View File

@ -0,0 +1,32 @@
package dashboard
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
// 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
}

View File

@ -0,0 +1,4 @@
// Package dummy provides a dummy kind useful for testing
//
// The dummy kind returns a complicated summary that can exercise most of the storage options
package dummy

View File

@ -0,0 +1,61 @@
package dummy
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana/pkg/models"
)
func GetObjectKindInfo(kind string) models.ObjectKindInfo {
return models.ObjectKindInfo{
ID: kind,
Name: kind,
Description: "Dummy kind used for testing.",
IsRaw: true,
}
}
func GetObjectSummaryBuilder(kind string) models.ObjectSummaryBuilder {
return func(ctx context.Context, uid string, body []byte) (*models.ObjectSummary, []byte, error) {
summary := &models.ObjectSummary{
Name: fmt.Sprintf("Dummy: %s", kind),
Kind: kind,
Description: fmt.Sprintf("Wrote at %s", time.Now().Local().String()),
Labels: map[string]string{
"hello": "world",
"tag1": "",
"tag2": "",
},
Fields: map[string]interface{}{
"field1": "a string",
"field2": 1.224,
"field4": true,
},
Error: nil, // ignore for now
Nested: nil, // ignore for now
References: []*models.ObjectExternalReference{
{
Kind: "ds",
Type: "influx",
UID: "xyz",
},
{
Kind: "panel",
Type: "heatmap",
},
{
Kind: "panel",
Type: "timeseries",
},
},
}
if summary.UID != "" && uid != summary.UID {
return summary, nil, fmt.Errorf("internal UID mismatch")
}
return summary, body, nil
}
}

View File

@ -0,0 +1,67 @@
package playlist
import (
"context"
"encoding/json"
"fmt"
"github.com/grafana/grafana/pkg/coremodel/playlist"
"github.com/grafana/grafana/pkg/models"
)
func GetObjectKindInfo() models.ObjectKindInfo {
return models.ObjectKindInfo{
ID: models.StandardKindPlaylist,
Name: "Playlist",
Description: "Cycle though a collection of dashboards automatically",
}
}
func GetObjectSummaryBuilder() models.ObjectSummaryBuilder {
return summaryBuilder
}
func summaryBuilder(ctx context.Context, uid string, body []byte) (*models.ObjectSummary, []byte, error) {
obj := &playlist.Model{}
err := json.Unmarshal(body, obj)
if err != nil {
return nil, nil, err // unable to read object
}
// TODO: fix model so this is not possible
if obj.Items == nil {
temp := make([]playlist.PlaylistItem, 0)
obj.Items = &temp
}
summary := &models.ObjectSummary{
UID: obj.Uid,
Name: obj.Name,
Description: fmt.Sprintf("%d items, refreshed every %s", len(*obj.Items), obj.Interval),
}
for _, item := range *obj.Items {
switch item.Type {
case playlist.PlaylistItemTypeDashboardByUid:
summary.References = append(summary.References, &models.ObjectExternalReference{
Kind: "dashboard",
UID: item.Value,
})
case playlist.PlaylistItemTypeDashboardByTag:
if summary.Labels == nil {
summary.Labels = make(map[string]string, 0)
}
summary.Labels[item.Value] = ""
case playlist.PlaylistItemTypeDashboardById:
// obviously insufficient long term... but good to have an example :)
summary.Error = &models.ObjectErrorInfo{
Message: "Playlist uses deprecated internal id system",
}
}
}
out, err := json.Marshal(obj)
return summary, out, err
}

View File

@ -0,0 +1,39 @@
package playlist
import (
"context"
"encoding/json"
"testing"
"github.com/grafana/grafana/pkg/coremodel/playlist"
"github.com/stretchr/testify/require"
)
func TestPlaylistSummary(t *testing.T) {
builder := GetObjectSummaryBuilder()
// Do not parse invalid input
_, _, err := builder(context.Background(), "abc", []byte("{invalid json"))
require.Error(t, err)
playlist := playlist.Model{
Interval: "30s",
Name: "test",
Items: &[]playlist.PlaylistItem{
{Type: playlist.PlaylistItemTypeDashboardByUid, Value: "D1"},
{Type: playlist.PlaylistItemTypeDashboardByTag, Value: "tagA"},
{Type: playlist.PlaylistItemTypeDashboardByUid, Value: "D3"},
},
}
out, err := json.Marshal(playlist)
require.NoError(t, err)
require.NotNil(t, out)
// Do not parse invalid input
summary, body, err := builder(context.Background(), "abc", out)
require.NoError(t, err)
require.Equal(t, "test", summary.Name)
require.Equal(t, 2, len(summary.References))
require.Equal(t, map[string]string{"tagA": ""}, summary.Labels)
require.True(t, json.Valid(body))
}

View File

@ -0,0 +1,55 @@
package png
import (
"bytes"
"context"
"image/png"
"strings"
"github.com/grafana/grafana/pkg/models"
)
func GetObjectKindInfo() models.ObjectKindInfo {
return models.ObjectKindInfo{
ID: models.StandardKindPNG,
Name: "PNG",
Description: "PNG Image file",
IsRaw: true,
FileExtension: "png",
MimeType: "image/png",
}
}
// SVG sanitizer based on the rendering service
func GetObjectSummaryBuilder() models.ObjectSummaryBuilder {
return func(ctx context.Context, uid string, body []byte) (*models.ObjectSummary, []byte, error) {
img, err := png.Decode(bytes.NewReader(body))
if err != nil {
return nil, nil, err
}
size := img.Bounds().Size()
summary := &models.ObjectSummary{
Kind: models.StandardKindSVG,
Name: guessNameFromUID(uid),
UID: uid,
Fields: map[string]interface{}{
"width": int64(size.X),
"height": int64(size.Y),
},
}
return summary, body, nil
}
}
func guessNameFromUID(uid string) string {
sidx := strings.LastIndex(uid, "/") + 1
didx := strings.LastIndex(uid, ".")
if didx > sidx && didx != sidx {
return uid[sidx:didx]
}
if sidx > 0 {
return uid[sidx:]
}
return uid
}

View File

@ -0,0 +1,33 @@
package png
import (
"context"
"encoding/base64"
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
func TestPNGSummary(t *testing.T) {
const gopher = `iVBORw0KGgoAAAANSUhEUgAAAEsAAAA8CAAAAAALAhhPAAAFfUlEQVRYw62XeWwUVRzHf2+OPbo9d7tsWyiyaZti6eWGAhISoIGKECEKCAiJJkYTiUgTMYSIosYYBBIUIxoSPIINEBDi2VhwkQrVsj1ESgu9doHWdrul7ba73WNm3vOPtsseM9MdwvvrzTs+8/t95ze/33sI5BqiabU6m9En8oNjduLnAEDLUsQXFF8tQ5oxK3vmnNmDSMtrncks9Hhtt/qeWZapHb1ha3UqYSWVl2ZmpWgaXMXGohQAvmeop3bjTRtv6SgaK/Pb9/bFzUrYslbFAmHPp+3WhAYdr+7GN/YnpN46Opv55VDsJkoEpMrY/vO2BIYQ6LLvm0ThY3MzDzzeSJeeWNyTkgnIE5ePKsvKlcg/0T9QMzXalwXMlj54z4c0rh/mzEfr+FgWEz2w6uk8dkzFAgcARAgNp1ZYef8bH2AgvuStbc2/i6CiWGj98y2tw2l4FAXKkQBIf+exyRnteY83LfEwDQAYCoK+P6bxkZm/0966LxcAAILHB56kgD95PPxltuYcMtFTWw/FKkY/6Opf3GGd9ZF+Qp6mzJxzuRSractOmJrH1u8XTvWFHINNkLQLMR+XHXvfPPHw967raE1xxwtA36IMRfkAAG29/7mLuQcb2WOnsJReZGfpiHsSBX81cvMKywYZHhX5hFPtOqPGWZCXnhWGAu6lX91ElKXSalcLXu3UaOXVay57ZSe5f6Gpx7J2MXAsi7EqSp09b/MirKSyJfnfEEgeDjl8FgDAfvewP03zZ+AJ0m9aFRM8eEHBDRKjfcreDXnZdQuAxXpT2NRJ7xl3UkLBhuVGU16gZiGOgZmrSbRdqkILuL/yYoSXHHkl9KXgqNu3PB8oRg0geC5vFmLjad6mUyTKLmF3OtraWDIfACyXqmephaDABawfpi6tqqBZytfQMqOz6S09iWXhktrRaB8Xz4Yi/8gyABDm5NVe6qq/3VzPrcjELWrebVuyY2T7ar4zQyybUCtsQ5Es1FGaZVrRVQwAgHGW2ZCRZshI5bGQi7HesyE972pOSeMM0dSktlzxRdrlqb3Osa6CCS8IJoQQQgBAbTAa5l5epO34rJszibJI8rxLfGzcp1dRosutGeb2VDNgqYrwTiPNsLxXiPi3dz7LiS1WBRBDBOnqEjyy3aQb+/bLiJzz9dIkscVBBLxMfSEac7kO4Fpkngi0ruNBeSOal+u8jgOuqPz12nryMLCniEjtOOOmpt+KEIqsEdocJjYXwrh9OZqWJQyPCTo67LNS/TdxLAv6R5ZNK9npEjbYdT33gRo4o5oTqR34R+OmaSzDBWsAIPhuRcgyoteNi9gF0KzNYWVItPf2TLoXEg+7isNC7uJkgo1iQWOfRSP9NR11RtbZZ3OMG/VhL6jvx+J1m87+RCfJChAtEBQkSBX2PnSiihc/Twh3j0h7qdYQAoRVsRGmq7HU2QRbaxVGa1D6nIOqaIWRjyRZpHMQKWKpZM5feA+lzC4ZFultV8S6T0mzQGhQohi5I8iw+CsqBSxhFMuwyLgSwbghGb0AiIKkSDmGZVmJSiKihsiyOAUs70UkywooYP0bii9GdH4sfr1UNysd3fUyLLMQN+rsmo3grHl9VNJHbbwxoa47Vw5gupIqrZcjPh9R4Nye3nRDk199V+aetmvVtDRE8/+cbgAAgMIWGb3UA0MGLE9SCbWX670TDy1y98c3D27eppUjsZ6fql3jcd5rUe7+ZIlLNQny3Rd+E5Tct3WVhTM5RBCEdiEK0b6B+/ca2gYU393nFj/n1AygRQxPIUA043M42u85+z2SnssKrPl8Mx76NL3E6eXc3be7OD+H4WHbJkKI8AU8irbITQjZ+0hQcPEgId/Fn/pl9crKH02+5o2b9T/eMx7pKoskYgAAAABJRU5ErkJggg==`
img, err := base64.StdEncoding.DecodeString(gopher)
require.NoError(t, err)
summary, out, err := GetObjectSummaryBuilder()(context.Background(), "hello.png", img)
require.NoError(t, err)
require.Equal(t, img, out) // same image
asjson, err := json.MarshalIndent(summary, "", " ")
//fmt.Printf(string(asjson))
require.NoError(t, err)
require.JSONEq(t, `{
"uid": "hello.png",
"kind": "svg",
"name": "hello",
"fields": {
"height": 60,
"width": 75
}
}`, string(asjson))
}

View File

@ -0,0 +1,144 @@
package kind
import (
"fmt"
"sort"
"sync"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/store/kind/dashboard"
"github.com/grafana/grafana/pkg/services/store/kind/dummy"
"github.com/grafana/grafana/pkg/services/store/kind/playlist"
"github.com/grafana/grafana/pkg/services/store/kind/png"
"github.com/grafana/grafana/pkg/services/store/kind/svg"
"github.com/grafana/grafana/pkg/setting"
)
type KindRegistry interface {
Register(info models.ObjectKindInfo, builder models.ObjectSummaryBuilder) error
GetSummaryBuilder(kind string) models.ObjectSummaryBuilder
GetInfo(kind string) (models.ObjectKindInfo, error)
GetKinds() []models.ObjectKindInfo
}
func NewKindRegistry() KindRegistry {
kinds := make(map[string]*kindValues)
kinds[models.StandardKindPlaylist] = &kindValues{
info: playlist.GetObjectKindInfo(),
builder: playlist.GetObjectSummaryBuilder(),
}
kinds[models.StandardKindPNG] = &kindValues{
info: png.GetObjectKindInfo(),
builder: png.GetObjectSummaryBuilder(),
}
// FIXME -- these are registered because existing tests use them
for _, k := range []string{"dummy", "kind1", "kind2", "kind3"} {
kinds[k] = &kindValues{
info: dummy.GetObjectKindInfo(k),
builder: dummy.GetObjectSummaryBuilder(k),
}
}
// create a registry
reg := &registry{
mutex: sync.RWMutex{},
kinds: kinds,
}
reg.updateInfoArray()
return reg
}
// TODO? This could be a zero dependency service that others are responsible for configuring
func ProvideService(cfg *setting.Cfg, renderer rendering.Service, sql *sqlstore.SQLStore) KindRegistry {
reg := NewKindRegistry()
// Register Dashboard support
//-----------------------
_ = reg.Register(dashboard.GetObjectKindInfo(), dashboard.NewDashboardSummary(sql))
// Register SVG support
//-----------------------
info := svg.GetObjectKindInfo()
allowUnsanitizedSvgUpload := cfg != nil && cfg.Storage.AllowUnsanitizedSvgUpload
support := svg.GetObjectSummaryBuilder(allowUnsanitizedSvgUpload, renderer)
_ = reg.Register(info, support)
return reg
}
type kindValues struct {
info models.ObjectKindInfo
builder models.ObjectSummaryBuilder
}
type registry struct {
mutex sync.RWMutex
kinds map[string]*kindValues
info []models.ObjectKindInfo
}
func (r *registry) updateInfoArray() {
info := make([]models.ObjectKindInfo, 0, len(r.kinds))
for _, v := range r.kinds {
info = append(info, v.info)
}
sort.Slice(info, func(i, j int) bool {
return info[i].ID < info[j].ID
})
r.info = info
}
func (r *registry) Register(info models.ObjectKindInfo, builder models.ObjectSummaryBuilder) error {
if info.ID == "" || builder == nil {
return fmt.Errorf("invalid kind")
}
r.mutex.Lock()
defer r.mutex.Unlock()
if r.kinds[info.ID] != nil {
return fmt.Errorf("already exits")
}
r.kinds[info.ID] = &kindValues{
info: info,
builder: builder,
}
r.updateInfoArray()
return nil
}
// GetSummaryBuilder returns a builder or nil if not found
func (r *registry) GetSummaryBuilder(kind string) models.ObjectSummaryBuilder {
r.mutex.RLock()
defer r.mutex.RUnlock()
v, ok := r.kinds[kind]
if ok {
return v.builder
}
return nil
}
// GetInfo returns the registered info
func (r *registry) GetInfo(kind string) (models.ObjectKindInfo, error) {
r.mutex.RLock()
defer r.mutex.RUnlock()
v, ok := r.kinds[kind]
if ok {
return v.info, nil
}
return models.ObjectKindInfo{}, fmt.Errorf("not found")
}
// GetSummaryBuilder returns a builder or nil if not found
func (r *registry) GetKinds() []models.ObjectKindInfo {
r.mutex.RLock()
defer r.mutex.RUnlock()
return r.info // returns a copy of the array
}

View File

@ -0,0 +1,42 @@
package kind
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/store/kind/dummy"
)
func TestKindRegistry(t *testing.T) {
registry := NewKindRegistry()
err := registry.Register(dummy.GetObjectKindInfo("test"), dummy.GetObjectSummaryBuilder("test"))
require.NoError(t, err)
ids := []string{}
for _, k := range registry.GetKinds() {
ids = append(ids, k.ID)
}
require.Equal(t, []string{
"dummy",
"kind1",
"kind2",
"kind3",
"playlist",
"png",
"test",
}, ids)
// Check playlist exists
info, err := registry.GetInfo(models.StandardKindPlaylist)
require.NoError(t, err)
require.Equal(t, "Playlist", info.Name)
require.False(t, info.IsRaw)
// Check that we registered a test item
info, err = registry.GetInfo("test")
require.NoError(t, err)
require.Equal(t, "test", info.Name)
require.True(t, info.IsRaw)
}

View File

@ -0,0 +1,66 @@
package svg
import (
"context"
"fmt"
"strings"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/rendering"
)
func GetObjectKindInfo() models.ObjectKindInfo {
return models.ObjectKindInfo{
ID: models.StandardKindSVG,
Name: "SVG",
Description: "Scalable Vector Graphics",
IsRaw: true,
FileExtension: "svg",
MimeType: "image/svg+xml",
}
}
// SVG sanitizer based on the rendering service
func GetObjectSummaryBuilder(allowUnsanitizedSvgUpload bool, renderer rendering.Service) models.ObjectSummaryBuilder {
return func(ctx context.Context, uid string, body []byte) (*models.ObjectSummary, []byte, error) {
if !IsSVG(body) {
return nil, nil, fmt.Errorf("invalid svg")
}
// When a renderer exists, we can return a sanitized version
var sanitized []byte
if renderer != nil {
rsp, err := renderer.SanitizeSVG(ctx, &rendering.SanitizeSVGRequest{
Content: body,
})
if err != nil && !allowUnsanitizedSvgUpload {
return nil, nil, err
}
sanitized = rsp.Sanitized
}
if sanitized == nil {
if !allowUnsanitizedSvgUpload {
return nil, nil, fmt.Errorf("unable to sanitize svg")
}
sanitized = body
}
return &models.ObjectSummary{
Kind: models.StandardKindSVG,
Name: guessNameFromUID(uid),
UID: uid,
}, sanitized, nil
}
}
func guessNameFromUID(uid string) string {
sidx := strings.LastIndex(uid, "/") + 1
didx := strings.LastIndex(uid, ".")
if didx > sidx && didx != sidx {
return uid[sidx:didx]
}
if sidx > 0 {
return uid[sidx:]
}
return uid
}

View File

@ -23,7 +23,7 @@
//FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
//OTHER DEALINGS IN THE SOFTWARE.
package issvg
package svg
import (
"regexp"
@ -51,12 +51,6 @@ func isBinary(buf []byte) bool {
}
// Is returns true if the given buffer is a valid SVG image.
func Is(buf []byte) bool {
func IsSVG(buf []byte) bool {
return !isBinary(buf) && svgRegex.Match(htmlCommentRegex.ReplaceAll(buf, []byte{}))
}
// IsSVG returns true if the given buffer is a valid SVG image.
// Alias to: Is()
func IsSVG(buf []byte) bool {
return Is(buf)
}

View File

@ -13,6 +13,8 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/x/persistentcollection"
"github.com/grafana/grafana/pkg/services/grpcserver"
"github.com/grafana/grafana/pkg/services/store"
"github.com/grafana/grafana/pkg/services/store/kind"
"github.com/grafana/grafana/pkg/services/store/object"
"github.com/grafana/grafana/pkg/setting"
)
@ -25,7 +27,6 @@ type ObjectVersionWithBody struct {
type RawObjectWithHistory struct {
Object *object.RawObject `json:"object,omitempty"`
Summary *object.ObjectSummary `json:"summary,omitempty"`
History []*ObjectVersionWithBody `json:"history,omitempty"`
}
@ -34,10 +35,11 @@ var (
rawObjectVersion = 6
)
func ProvideDummyObjectServer(cfg *setting.Cfg, grpcServerProvider grpcserver.Provider) object.ObjectStoreServer {
func ProvideDummyObjectServer(cfg *setting.Cfg, grpcServerProvider grpcserver.Provider, kinds kind.KindRegistry) object.ObjectStoreServer {
objectServer := &dummyObjectServer{
collection: persistentcollection.NewLocalFSPersistentCollection[*RawObjectWithHistory]("raw-object", cfg.DataPath, rawObjectVersion),
log: log.New("in-memory-object-server"),
kinds: kinds,
}
object.RegisterObjectStoreServer(grpcServerProvider.GetServer(), objectServer)
return objectServer
@ -46,6 +48,7 @@ func ProvideDummyObjectServer(cfg *setting.Cfg, grpcServerProvider grpcserver.Pr
type dummyObjectServer struct {
log log.Logger
collection persistentcollection.PersistentCollection[*RawObjectWithHistory]
kinds kind.KindRegistry
}
func namespaceFromUID(uid string) string {
@ -115,15 +118,15 @@ func (i dummyObjectServer) Read(ctx context.Context, r *object.ReadObjectRequest
Object: objVersion,
}
if r.WithSummary {
summary, _, e2 := object.GetSafeSaveObject(&object.WriteObjectRequest{
UID: r.UID,
Kind: r.Kind,
Body: objVersion.Body,
})
if e2 != nil {
return nil, e2
// Since we do not store the summary, we can just recreate on demand
builder := i.kinds.GetSummaryBuilder(r.Kind)
if builder != nil {
summary, _, e2 := builder(ctx, r.UID, objVersion.Body)
if e2 != nil {
return nil, e2
}
rsp.SummaryJson, err = json.Marshal(summary)
}
rsp.SummaryJson, err = json.Marshal(summary)
}
return rsp, err
}
@ -147,6 +150,10 @@ func createContentsHash(contents []byte) string {
}
func (i dummyObjectServer) update(ctx context.Context, r *object.WriteObjectRequest, namespace string) (*object.WriteObjectResponse, error) {
builder := i.kinds.GetSummaryBuilder(r.Kind)
if builder == nil {
return nil, fmt.Errorf("unsupported kind: " + r.Kind)
}
rsp := &object.WriteObjectResponse{}
updatedCount, err := i.collection.Update(ctx, namespace, func(i *RawObjectWithHistory) (bool, *RawObjectWithHistory, error) {
@ -164,7 +171,7 @@ func (i dummyObjectServer) update(ctx context.Context, r *object.WriteObjectRequ
return false, nil, err
}
modifier := object.UserFromContext(ctx)
modifier := store.UserFromContext(ctx)
updated := &object.RawObject{
UID: r.UID,
@ -172,7 +179,7 @@ func (i dummyObjectServer) update(ctx context.Context, r *object.WriteObjectRequ
Created: i.Object.Created,
CreatedBy: i.Object.CreatedBy,
Updated: time.Now().Unix(),
UpdatedBy: object.GetUserIDString(modifier),
UpdatedBy: store.GetUserIDString(modifier),
Size: int64(len(r.Body)),
ETag: createContentsHash(r.Body),
Body: r.Body,
@ -218,7 +225,7 @@ func (i dummyObjectServer) update(ctx context.Context, r *object.WriteObjectRequ
}
func (i dummyObjectServer) insert(ctx context.Context, r *object.WriteObjectRequest, namespace string) (*object.WriteObjectResponse, error) {
modifier := object.GetUserIDString(object.UserFromContext(ctx))
modifier := store.GetUserIDString(store.UserFromContext(ctx))
rawObj := &object.RawObject{
UID: r.UID,
Kind: r.Kind,

View File

@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/services/store/kind"
"github.com/grafana/grafana/pkg/web"
"github.com/grafana/grafana/pkg/api/response"
@ -22,12 +23,14 @@ type HTTPObjectStore interface {
type httpObjectStore struct {
store ObjectStoreServer
log log.Logger
kinds kind.KindRegistry
}
func ProvideHTTPObjectStore(store ObjectStoreServer) HTTPObjectStore {
func ProvideHTTPObjectStore(store ObjectStoreServer, kinds kind.KindRegistry) HTTPObjectStore {
return &httpObjectStore{
store: store,
log: log.New("http-object-store"),
kinds: kinds,
}
}
@ -116,6 +119,11 @@ func (s *httpObjectStore) doGetRawObject(c *models.ReqContext) response.Response
if err != nil {
return response.Error(500, "?", err)
}
info, err := s.kinds.GetInfo(kind)
if err != nil {
return response.Error(400, "Unsupported kind", err)
}
if rsp.Object != nil && rsp.Object.Body != nil {
// Configure etag support
currentEtag := rsp.Object.ETag
@ -129,10 +137,13 @@ func (s *httpObjectStore) doGetRawObject(c *models.ReqContext) response.Response
http.StatusNotModified, // 304
)
}
mime := info.MimeType
if mime == "" {
mime = "application/json"
}
return response.CreateNormalResponse(
http.Header{
"Content-Type": []string{"application/json"}, // TODO, based on kind!!!
"Content-Type": []string{mime},
"ETag": []string{currentEtag},
},
rsp.Object.Body,

View File

@ -1,63 +0,0 @@
package object
import (
"fmt"
"time"
)
// NOTE this is just a temporary registry/list so we can use constants
// TODO replace with codegen from kind schema system
const StandardKindDashboard = "dashboard"
const StandardKindFolder = "folder"
const StandardKindPanel = "panel" // types: heatmap, timeseries, table, ...
const StandardKindDataSource = "ds" // types: influx, prometheus, test, ...
const StandardKindTransform = "transform" // types: joinByField, pivot, organizeFields, ...
const StandardKindQuery = "query"
// This is a stub -- it will soon lookup in a registry of known "kinds"
// Each kind will be able to define:
// 1. sanitize/normalize function (ie get safe bytes)
// 2. SummaryProvier
func GetSafeSaveObject(r *WriteObjectRequest) (*ObjectSummary, []byte, error) {
summary := &ObjectSummary{
Name: fmt.Sprintf("hello: %s", r.Kind),
Description: fmt.Sprintf("Wrote at %s", time.Now().Local().String()),
Labels: map[string]string{
"hello": "world",
"tag1": "",
"tag2": "",
},
Fields: map[string]interface{}{
"field1": "a string",
"field2": 1.224,
"field4": true,
},
Error: nil, // ignore for now
Nested: nil, // ignore for now
References: []*ExternalReference{
{
Kind: "ds",
Type: "influx",
UID: "xyz",
},
{
Kind: "panel",
Type: "heatmap",
},
{
Kind: "panel",
Type: "timeseries",
},
},
}
if summary.UID != "" && r.UID != summary.UID {
return nil, nil, fmt.Errorf("internal UID mismatch")
}
if summary.Kind != "" && r.Kind != summary.Kind {
return nil, nil, fmt.Errorf("internal Kind mismatch")
}
return summary, r.Body, nil
}

View File

@ -1,43 +0,0 @@
package object
// ObjectSummary is derived from a RawObject and should not depend on system state
// The summary is used for a unified search and listings objects since the fully
type ObjectSummary struct {
UID string `json:"uid,omitempty"`
Kind string `json:"kind,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Labels map[string]string `json:"labels,omitempty"` // "tags" are represented as keys with empty values
URL string `json:"URL,omitempty"` // not great to save here, but maybe not terrible :shrug:
Error *ObjectErrorInfo `json:"error,omitempty"`
// Optional values -- schema will define the type
Fields map[string]interface{} `json:"fields,omitempty"` // Saved as JSON, returned in results, but values not sortable
// eg: panels within dashboard
Nested []*ObjectSummary `json:"nested,omitempty"`
// Optional references to external things
References []*ExternalReference `json:"references,omitempty"`
// struct can not be extended
_ interface{}
}
// Reference to another object outside itself
// This message is derived from the object body and can be used to search for references.
// This does not represent a method to declare a reference to another object.
type ExternalReference struct {
// datasource (instance), dashboard (instance),
Kind string `json:"kind,omitempty"`
// prometheus / heatmap, heatamp|prometheus
Type string `json:"type,omitempty"` // flavor
// Unique ID for this object
UID string `json:"UID,omitempty"`
}
// ObjectSummaryBuilder will read an object and create the summary.
// This should not include values that depend on system state, only the raw object
type ObjectSummaryBuilder = func(obj *RawObject) (ObjectSummary, error)

View File

@ -122,7 +122,7 @@ func TestObjectServer(t *testing.T) {
fakeUser := fmt.Sprintf("user:%d:%s", testCtx.user.UserID, testCtx.user.Login)
firstVersion := "1"
kind := "dashboard"
kind := "dummy"
uid := "my-test-entity"
body := []byte("{\"name\":\"John\"}")
@ -348,7 +348,7 @@ func TestObjectServer(t *testing.T) {
version = append(version, res.Version)
}
require.Equal(t, []string{"my-test-entity", "uid2", "uid3", "uid4"}, uids)
require.Equal(t, []string{"dashboard", "dashboard", "kind2", "kind2"}, kinds)
require.Equal(t, []string{"dummy", "dummy", "kind2", "kind2"}, kinds)
require.Equal(t, []string{
w1.Object.Version,
w2.Object.Version,
@ -370,7 +370,7 @@ func TestObjectServer(t *testing.T) {
version = append(version, res.Version)
}
require.Equal(t, []string{"my-test-entity", "uid2"}, uids)
require.Equal(t, []string{"dashboard", "dashboard"}, kinds)
require.Equal(t, []string{"dummy", "dummy"}, kinds)
require.Equal(t, []string{
w1.Object.Version,
w2.Object.Version,

View File

@ -9,7 +9,7 @@ import (
"strings"
"github.com/grafana/grafana/pkg/infra/filestorage"
issvg "github.com/grafana/grafana/pkg/services/store/go-is-svg"
"github.com/grafana/grafana/pkg/services/store/kind/svg"
"github.com/grafana/grafana/pkg/services/user"
)
@ -52,7 +52,7 @@ func fail(reason string) validationResult {
func (s *standardStorageService) detectMimeType(ctx context.Context, user *user.SignedInUser, uploadRequest *UploadRequest) string {
if strings.HasSuffix(uploadRequest.Path, ".svg") {
if issvg.IsSVG(uploadRequest.Contents) {
if svg.IsSVG(uploadRequest.Contents) {
return "image/svg+xml"
}
}