mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
ObjectStore: add a kind registry (#56507)
This commit is contained in:
parent
ac91df0ea2
commit
b24be6c0fc
@ -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>;
|
||||
/**
|
||||
|
@ -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]
|
||||
|
||||
///////////////////////////////////////
|
||||
|
@ -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
119
pkg/models/object.go
Normal 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)
|
@ -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,
|
||||
|
@ -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{
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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 {
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package object
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
@ -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":
|
@ -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
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
package dslookup
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
@ -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]
|
||||
}
|
99
pkg/services/store/kind/dashboard/summary.go
Normal file
99
pkg/services/store/kind/dashboard/summary.go
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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]
|
93
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-gradient-area-fills.json
vendored
Normal file
93
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-gradient-area-fills.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
181
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-shared-tooltips.json
vendored
Normal file
181
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-shared-tooltips.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
110
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-time-regions.json
vendored
Normal file
110
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-time-regions.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
385
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_tests.json
vendored
Normal file
385
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_tests.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
177
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_y_axis.json
vendored
Normal file
177
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_y_axis.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
32
pkg/services/store/kind/dashboard/types.go
Normal file
32
pkg/services/store/kind/dashboard/types.go
Normal 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
|
||||
}
|
4
pkg/services/store/kind/dummy/doc.go
Normal file
4
pkg/services/store/kind/dummy/doc.go
Normal 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
|
61
pkg/services/store/kind/dummy/summary.go
Normal file
61
pkg/services/store/kind/dummy/summary.go
Normal 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
|
||||
}
|
||||
}
|
67
pkg/services/store/kind/playlist/summary.go
Normal file
67
pkg/services/store/kind/playlist/summary.go
Normal 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
|
||||
}
|
39
pkg/services/store/kind/playlist/summary_test.go
Normal file
39
pkg/services/store/kind/playlist/summary_test.go
Normal 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))
|
||||
}
|
55
pkg/services/store/kind/png/summary.go
Normal file
55
pkg/services/store/kind/png/summary.go
Normal 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
|
||||
}
|
33
pkg/services/store/kind/png/summary_test.go
Normal file
33
pkg/services/store/kind/png/summary_test.go
Normal 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))
|
||||
}
|
144
pkg/services/store/kind/registry.go
Normal file
144
pkg/services/store/kind/registry.go
Normal 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 := ®istry{
|
||||
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
|
||||
}
|
42
pkg/services/store/kind/registry_test.go
Normal file
42
pkg/services/store/kind/registry_test.go
Normal 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)
|
||||
}
|
66
pkg/services/store/kind/svg/summary.go
Normal file
66
pkg/services/store/kind/svg/summary.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user