ObjectStore: add a kind registry (#56507)
This commit is contained in:
1 parent
ac91df0ea2
commit
b24be6c0fc
72 files changed
+1883
-402
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
+36
-38
@@ -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":
|
||||
+6
-8
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package dslookup
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
+9
-7
@@ -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]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+6
-26
@@ -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
|
||||
}
|
||||
+11
-13
@@ -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]
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+93
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user