mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
ObjectStore: add a kind registry (#56507)
This commit is contained in:
parent
ac91df0ea2
commit
b24be6c0fc
@ -36,6 +36,7 @@ export interface Playlist {
|
|||||||
interval: string;
|
interval: string;
|
||||||
/**
|
/**
|
||||||
* The ordered list of items that the playlist will iterate over.
|
* 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>;
|
items?: Array<PlaylistItem>;
|
||||||
/**
|
/**
|
||||||
|
@ -22,6 +22,7 @@ seqs: [
|
|||||||
interval: string | *"5m"
|
interval: string | *"5m"
|
||||||
|
|
||||||
// The ordered list of items that the playlist will iterate over.
|
// 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]
|
items?: [...#PlaylistItem]
|
||||||
|
|
||||||
///////////////////////////////////////
|
///////////////////////////////////////
|
||||||
|
@ -36,6 +36,7 @@ type Model struct {
|
|||||||
Interval string `json:"interval"`
|
Interval string `json:"interval"`
|
||||||
|
|
||||||
// The ordered list of items that the playlist will iterate over.
|
// 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"`
|
Items *[]PlaylistItem `json:"items,omitempty"`
|
||||||
|
|
||||||
// Name of the playlist.
|
// Name of the playlist.
|
||||||
|
119
pkg/models/object.go
Normal file
119
pkg/models/object.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------------------------------
|
||||||
|
// NOTE: the object store is in heavy development, and the locations will likely continue to move
|
||||||
|
//-----------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
const (
|
||||||
|
StandardKindDashboard = "dashboard"
|
||||||
|
StandardKindPlaylist = "playlist"
|
||||||
|
StandardKindFolder = "folder"
|
||||||
|
|
||||||
|
// StandardKindDataSource: not a real kind yet, but used to define references from dashboards
|
||||||
|
// Types: influx, prometheus, testdata, ...
|
||||||
|
StandardKindDataSource = "ds"
|
||||||
|
|
||||||
|
// StandardKindPanel: currently used in two ways :( in search it defines a panel within a dashboard
|
||||||
|
// This is also used to refer to a panel plugin with type: heatmap, timeseries, table, ...
|
||||||
|
StandardKindPanel = "panel"
|
||||||
|
|
||||||
|
// StandardKindTransform: used to show that a dashboard depends on a set of transformations
|
||||||
|
// NOTE: this should likey be replaced with kind:system/type:transform/uid:joinByField or something like that
|
||||||
|
StandardKindTransform = "transform"
|
||||||
|
|
||||||
|
// StandardKindSVG SVG file support
|
||||||
|
StandardKindSVG = "svg"
|
||||||
|
|
||||||
|
// StandardKindPNG PNG file support
|
||||||
|
StandardKindPNG = "png"
|
||||||
|
|
||||||
|
// StandardKindQuery early development on panel query library
|
||||||
|
// the kind may need to change to better encapsulate { targets:[], transforms:[] }
|
||||||
|
StandardKindQuery = "query"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ObjectKindInfo describes information needed from the object store
|
||||||
|
// All non-raw types will have a schema that can be used to validate
|
||||||
|
type ObjectKindInfo struct {
|
||||||
|
// Unique short id for this kind
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
|
||||||
|
// Display name (may be equal to the ID)
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
|
||||||
|
// Kind description
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
|
||||||
|
// The format is not controlled by a schema
|
||||||
|
IsRaw bool `json:"isRaw,omitempty"`
|
||||||
|
|
||||||
|
// The preferred save extension (svg, png, parquet, etc) if one exists
|
||||||
|
FileExtension string `json:"fileExtension,omitempty"`
|
||||||
|
|
||||||
|
// The correct mime-type to return for raw objects
|
||||||
|
MimeType string `json:"mimeType,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectSummary represents common data derived from a raw object bytes.
|
||||||
|
// The values should not depend on system state, and are derived from the raw object.
|
||||||
|
// This summary is used for a unified search and object listing
|
||||||
|
type ObjectSummary struct {
|
||||||
|
UID string `json:"uid,omitempty"`
|
||||||
|
Kind string `json:"kind,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
|
||||||
|
// Key value pairs. Tags are are represented as keys with empty values
|
||||||
|
Labels map[string]string `json:"labels,omitempty"`
|
||||||
|
|
||||||
|
// URL should only be set if the value is not derived directly from kind+uid
|
||||||
|
// NOTE: this may go away with a more robust GRN solution /!\
|
||||||
|
URL string `json:"URL,omitempty"`
|
||||||
|
|
||||||
|
// When errors exist
|
||||||
|
Error *ObjectErrorInfo `json:"error,omitempty"`
|
||||||
|
|
||||||
|
// Optional field values. The schema will define and document possible values for a given kind
|
||||||
|
Fields map[string]interface{} `json:"fields,omitempty"`
|
||||||
|
|
||||||
|
// eg: panels within dashboard
|
||||||
|
Nested []*ObjectSummary `json:"nested,omitempty"`
|
||||||
|
|
||||||
|
// Optional references to external things
|
||||||
|
References []*ObjectExternalReference `json:"references,omitempty"`
|
||||||
|
|
||||||
|
// The summary can not be extended
|
||||||
|
_ interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will likely get replaced with a more general error framework.
|
||||||
|
type ObjectErrorInfo struct {
|
||||||
|
// TODO: Match an error code registry?
|
||||||
|
Code int64 `json:"code,omitempty"`
|
||||||
|
|
||||||
|
// Simple error display
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
|
||||||
|
// Error details
|
||||||
|
Details interface{} `json:"details,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference to another object outside itself
|
||||||
|
// This message is derived from the object body and can be used to search for references.
|
||||||
|
// This does not represent a method to declare a reference to another object.
|
||||||
|
type ObjectExternalReference struct {
|
||||||
|
// datasource (instance), dashboard (instance),
|
||||||
|
Kind string `json:"kind,omitempty"`
|
||||||
|
|
||||||
|
// prometheus / heatmap, heatamp|prometheus
|
||||||
|
Type string `json:"type,omitempty"` // flavor
|
||||||
|
|
||||||
|
// Unique ID for this object
|
||||||
|
UID string `json:"UID,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectSummaryBuilder will read an object, validate it, and return a summary, sanitized payload, or an error
|
||||||
|
// This should not include values that depend on system state, only the raw object
|
||||||
|
type ObjectSummaryBuilder = func(ctx context.Context, uid string, body []byte) (*ObjectSummary, []byte, error)
|
@ -124,6 +124,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
|
||||||
"github.com/grafana/grafana/pkg/services/star/starimpl"
|
"github.com/grafana/grafana/pkg/services/star/starimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/store"
|
"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/services/store/object"
|
||||||
objectdummyserver "github.com/grafana/grafana/pkg/services/store/object/dummy"
|
objectdummyserver "github.com/grafana/grafana/pkg/services/store/object/dummy"
|
||||||
"github.com/grafana/grafana/pkg/services/store/sanitizer"
|
"github.com/grafana/grafana/pkg/services/store/sanitizer"
|
||||||
@ -354,6 +355,7 @@ var wireBasicSet = wire.NewSet(
|
|||||||
grpcserver.ProvideHealthService,
|
grpcserver.ProvideHealthService,
|
||||||
grpcserver.ProvideReflectionService,
|
grpcserver.ProvideReflectionService,
|
||||||
interceptors.ProvideAuthenticator,
|
interceptors.ProvideAuthenticator,
|
||||||
|
kind.ProvideService, // The registry known kinds
|
||||||
objectdummyserver.ProvideDummyObjectServer,
|
objectdummyserver.ProvideDummyObjectServer,
|
||||||
object.ProvideHTTPObjectStore,
|
object.ProvideHTTPObjectStore,
|
||||||
teamimpl.ProvideService,
|
teamimpl.ProvideService,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package export
|
package export
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
@ -11,9 +10,8 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/grafana/grafana/pkg/infra/filestorage"
|
"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/sqlstore"
|
||||||
|
"github.com/grafana/grafana/pkg/services/store/kind/dashboard"
|
||||||
)
|
)
|
||||||
|
|
||||||
func exportDashboards(helper *commitHelper, job *gitExportJob) error {
|
func exportDashboards(helper *commitHelper, job *gitExportJob) error {
|
||||||
@ -26,7 +24,7 @@ func exportDashboards(helper *commitHelper, job *gitExportJob) error {
|
|||||||
folders[0] = job.cfg.GeneralFolderPath // "general"
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -60,20 +58,22 @@ func exportDashboards(helper *commitHelper, job *gitExportJob) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reader := dashboard.NewStaticDashboardSummaryBuilder(lookup)
|
||||||
|
|
||||||
// Process all folders
|
// Process all folders
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
if !row.IsFolder {
|
if !row.IsFolder {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dash, err := extract.ReadDashboard(bytes.NewReader(row.Data), lookup)
|
dash, _, err := reader(helper.ctx, row.UID, row.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
dash.UID = row.UID
|
dash.UID = row.UID
|
||||||
slug := cleanFileName(dash.Title)
|
slug := cleanFileName(dash.Name)
|
||||||
folder := map[string]string{
|
folder := map[string]string{
|
||||||
"title": dash.Title,
|
"title": dash.Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
folderStructure.body = append(folderStructure.body, commitBody{
|
folderStructure.body = append(folderStructure.body, commitBody{
|
||||||
|
@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/querylibrary"
|
"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/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
@ -205,7 +205,7 @@ func getDatasourceUID(q *simplejson.Json) string {
|
|||||||
return uid
|
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)
|
dsRefs := extractDataSources(q)
|
||||||
|
|
||||||
for _, dsRef := range dsRefs {
|
for _, dsRef := range dsRefs {
|
||||||
@ -226,8 +226,8 @@ func isQueryWithMixedDataSource(q *querylibrary.Query) (isMixed bool, firstDsRef
|
|||||||
return false, firstDsRef
|
return false, firstDsRef
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractDataSources(query *querylibrary.Query) []dslookup.DataSourceRef {
|
func extractDataSources(query *querylibrary.Query) []dashboard.DataSourceRef {
|
||||||
ds := make([]dslookup.DataSourceRef, 0)
|
ds := make([]dashboard.DataSourceRef, 0)
|
||||||
|
|
||||||
for _, q := range query.Queries {
|
for _, q := range query.Queries {
|
||||||
dsUid := getDatasourceUID(q)
|
dsUid := getDatasourceUID(q)
|
||||||
@ -236,7 +236,7 @@ func extractDataSources(query *querylibrary.Query) []dslookup.DataSourceRef {
|
|||||||
dsType = expr.DatasourceType
|
dsType = expr.DatasourceType
|
||||||
}
|
}
|
||||||
|
|
||||||
ds = append(ds, dslookup.DataSourceRef{
|
ds = append(ds, dashboard.DataSourceRef{
|
||||||
UID: dsUid,
|
UID: dsUid,
|
||||||
Type: dsType,
|
Type: dsType,
|
||||||
})
|
})
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/registry"
|
"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"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ type QueryInfo struct {
|
|||||||
TimeTo string `json:"timeTo"`
|
TimeTo string `json:"timeTo"`
|
||||||
SchemaVersion int64 `json:"schemaVersion"`
|
SchemaVersion int64 `json:"schemaVersion"`
|
||||||
|
|
||||||
Datasource []dslookup.DataSourceRef `json:"datasource,omitempty"` // UIDs
|
Datasource []dashboard.DataSourceRef `json:"datasource,omitempty"` // UIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
type QuerySearchOptions struct {
|
type QuerySearchOptions struct {
|
||||||
|
@ -14,7 +14,7 @@ import (
|
|||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/store/object"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -168,7 +168,7 @@ func getNonFolderDashboardDoc(dash dashboard, location string) *bluge.Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, ref := range dash.summary.References {
|
for _, ref := range dash.summary.References {
|
||||||
if ref.Kind == object.StandardKindDataSource {
|
if ref.Kind == models.StandardKindDataSource {
|
||||||
if ref.Type != "" {
|
if ref.Type != "" {
|
||||||
doc.AddField(bluge.NewKeywordField(documentFieldDSType, ref.Type).
|
doc.AddField(bluge.NewKeywordField(documentFieldDSType, ref.Type).
|
||||||
StoreValue().
|
StoreValue().
|
||||||
@ -200,7 +200,7 @@ func getDashboardPanelDocs(dash dashboard, location string) []*bluge.Document {
|
|||||||
|
|
||||||
for _, ref := range dash.summary.References {
|
for _, ref := range dash.summary.References {
|
||||||
switch ref.Kind {
|
switch ref.Kind {
|
||||||
case object.StandardKindDashboard:
|
case models.StandardKindDashboard:
|
||||||
if ref.Type != "" {
|
if ref.Type != "" {
|
||||||
doc.AddField(bluge.NewKeywordField(documentFieldDSType, ref.Type).
|
doc.AddField(bluge.NewKeywordField(documentFieldDSType, ref.Type).
|
||||||
StoreValue().
|
StoreValue().
|
||||||
@ -213,11 +213,11 @@ func getDashboardPanelDocs(dash dashboard, location string) []*bluge.Document {
|
|||||||
Aggregatable().
|
Aggregatable().
|
||||||
SearchTermPositions())
|
SearchTermPositions())
|
||||||
}
|
}
|
||||||
case object.StandardKindPanel:
|
case models.StandardKindPanel:
|
||||||
if ref.Type != "" {
|
if ref.Type != "" {
|
||||||
doc.AddField(bluge.NewKeywordField(documentFieldPanelType, ref.Type).Aggregatable().StoreValue())
|
doc.AddField(bluge.NewKeywordField(documentFieldPanelType, ref.Type).Aggregatable().StoreValue())
|
||||||
}
|
}
|
||||||
case object.StandardKindTransform:
|
case models.StandardKindTransform:
|
||||||
if ref.Type != "" {
|
if ref.Type != "" {
|
||||||
doc.AddField(bluge.NewKeywordField(documentFieldTransformer, ref.Type).Aggregatable())
|
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/searcher"
|
||||||
"github.com/blugelabs/bluge/search/similarity"
|
"github.com/blugelabs/bluge/search/similarity"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/store/object"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PermissionFilter struct {
|
type PermissionFilter struct {
|
||||||
@ -19,11 +19,11 @@ type PermissionFilter struct {
|
|||||||
type entityKind string
|
type entityKind string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
entityKindPanel entityKind = object.StandardKindPanel
|
entityKindPanel entityKind = models.StandardKindPanel
|
||||||
entityKindDashboard entityKind = object.StandardKindDashboard
|
entityKindDashboard entityKind = models.StandardKindDashboard
|
||||||
entityKindFolder entityKind = object.StandardKindFolder
|
entityKindFolder entityKind = models.StandardKindFolder
|
||||||
entityKindDatasource entityKind = object.StandardKindDataSource
|
entityKindDatasource entityKind = models.StandardKindDataSource
|
||||||
entityKindQuery entityKind = object.StandardKindQuery
|
entityKindQuery entityKind = models.StandardKindQuery
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r entityKind) IsValid() bool {
|
func (r entityKind) IsValid() bool {
|
||||||
|
@ -15,12 +15,11 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"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/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/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/services/store"
|
"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"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
|
||||||
@ -55,7 +54,7 @@ type dashboard struct {
|
|||||||
updated time.Time
|
updated time.Time
|
||||||
|
|
||||||
// Use generic structure
|
// Use generic structure
|
||||||
summary *obj.ObjectSummary
|
summary *models.ObjectSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildSignal is sent when search index is accessed in organization for which
|
// 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: "",
|
slug: "",
|
||||||
created: time.Now(),
|
created: time.Now(),
|
||||||
updated: time.Now(),
|
updated: time.Now(),
|
||||||
summary: &obj.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
//ID: 0,
|
//ID: 0,
|
||||||
Name: "General",
|
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))
|
loadDatasourceSpan.SetAttributes("orgID", orgID, attribute.Key("orgID").Int64(orgID))
|
||||||
|
|
||||||
// key will allow name or uid
|
// 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 {
|
if err != nil {
|
||||||
loadDatasourceSpan.End()
|
loadDatasourceSpan.End()
|
||||||
return dashboards, err
|
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("orgID", orgID, attribute.Key("orgID").Int64(orgID))
|
||||||
readDashboardSpan.SetAttributes("dashboardCount", len(rows), attribute.Key("dashboardCount").Int(len(rows)))
|
readDashboardSpan.SetAttributes("dashboardCount", len(rows), attribute.Key("dashboardCount").Int(len(rows)))
|
||||||
|
|
||||||
reader := object.NewDashboardSummaryBuilder(lookup)
|
reader := kdash.NewStaticDashboardSummaryBuilder(lookup)
|
||||||
|
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
obj := &obj.RawObject{
|
summary, _, err := reader(ctx, row.Uid, row.Data)
|
||||||
UID: row.Uid,
|
|
||||||
Kind: "dashboard",
|
|
||||||
Body: row.Data,
|
|
||||||
}
|
|
||||||
summary, err := reader(obj)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.logger.Warn("Error indexing dashboard data", "error", err, "dashboardId", row.Id, "dashboardSlug", row.Slug)
|
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.
|
// 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,
|
slug: row.Slug,
|
||||||
created: row.Created,
|
created: row.Created,
|
||||||
updated: row.Updated,
|
updated: row.Updated,
|
||||||
summary: &summary,
|
summary: summary,
|
||||||
})
|
})
|
||||||
lastID = row.Id
|
lastID = row.Id
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"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/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/store/object"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
@ -113,14 +113,14 @@ var testDashboards = []dashboard{
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
uid: "1",
|
uid: "1",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
uid: "2",
|
uid: "2",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "boom",
|
Name: "boom",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -162,7 +162,7 @@ func TestDashboardIndexUpdates(t *testing.T) {
|
|||||||
err := index.updateDashboard(context.Background(), testOrgID, orgIdx, dashboard{
|
err := index.updateDashboard(context.Background(), testOrgID, orgIdx, dashboard{
|
||||||
id: 3,
|
id: 3,
|
||||||
uid: "3",
|
uid: "3",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "created",
|
Name: "created",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -181,7 +181,7 @@ func TestDashboardIndexUpdates(t *testing.T) {
|
|||||||
err := index.updateDashboard(context.Background(), testOrgID, orgIdx, dashboard{
|
err := index.updateDashboard(context.Background(), testOrgID, orgIdx, dashboard{
|
||||||
id: 2,
|
id: 2,
|
||||||
uid: "2",
|
uid: "2",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "nginx",
|
Name: "nginx",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -197,14 +197,14 @@ var testSortDashboards = []dashboard{
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
uid: "1",
|
uid: "1",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "a-test",
|
Name: "a-test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
uid: "2",
|
uid: "2",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "z-test",
|
Name: "z-test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -288,14 +288,14 @@ var testPrefixDashboards = []dashboard{
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
uid: "1",
|
uid: "1",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "Archer Data System",
|
Name: "Archer Data System",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
uid: "2",
|
uid: "2",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "Document Sync repo",
|
Name: "Document Sync repo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -366,7 +366,7 @@ var longPrefixDashboards = []dashboard{
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
uid: "1",
|
uid: "1",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "Eyjafjallajökull Eruption data",
|
Name: "Eyjafjallajökull Eruption data",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -385,14 +385,14 @@ var scatteredTokensDashboards = []dashboard{
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
uid: "1",
|
uid: "1",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "Three can keep a secret, if two of them are dead (Benjamin Franklin)",
|
Name: "Three can keep a secret, if two of them are dead (Benjamin Franklin)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
uid: "2",
|
uid: "2",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "A secret is powerful when it is empty (Umberto Eco)",
|
Name: "A secret is powerful when it is empty (Umberto Eco)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -418,7 +418,7 @@ var dashboardsWithFolders = []dashboard{
|
|||||||
id: 1,
|
id: 1,
|
||||||
uid: "1",
|
uid: "1",
|
||||||
isFolder: true,
|
isFolder: true,
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "My folder",
|
Name: "My folder",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -426,9 +426,9 @@ var dashboardsWithFolders = []dashboard{
|
|||||||
id: 2,
|
id: 2,
|
||||||
uid: "2",
|
uid: "2",
|
||||||
folderID: 1,
|
folderID: 1,
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "Dashboard in folder 1",
|
Name: "Dashboard in folder 1",
|
||||||
Nested: []*object.ObjectSummary{
|
Nested: []*models.ObjectSummary{
|
||||||
newNestedPanel(1, "Panel 1"),
|
newNestedPanel(1, "Panel 1"),
|
||||||
newNestedPanel(2, "Panel 2"),
|
newNestedPanel(2, "Panel 2"),
|
||||||
},
|
},
|
||||||
@ -438,9 +438,9 @@ var dashboardsWithFolders = []dashboard{
|
|||||||
id: 3,
|
id: 3,
|
||||||
uid: "3",
|
uid: "3",
|
||||||
folderID: 1,
|
folderID: 1,
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "Dashboard in folder 2",
|
Name: "Dashboard in folder 2",
|
||||||
Nested: []*object.ObjectSummary{
|
Nested: []*models.ObjectSummary{
|
||||||
newNestedPanel(3, "Panel 3"),
|
newNestedPanel(3, "Panel 3"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -448,9 +448,9 @@ var dashboardsWithFolders = []dashboard{
|
|||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
uid: "4",
|
uid: "4",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "One more dash",
|
Name: "One more dash",
|
||||||
Nested: []*object.ObjectSummary{
|
Nested: []*models.ObjectSummary{
|
||||||
newNestedPanel(4, "Panel 4"),
|
newNestedPanel(4, "Panel 4"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -505,9 +505,9 @@ var dashboardsWithPanels = []dashboard{
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
uid: "1",
|
uid: "1",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "My Dash",
|
Name: "My Dash",
|
||||||
Nested: []*object.ObjectSummary{
|
Nested: []*models.ObjectSummary{
|
||||||
newNestedPanel(1, "Panel 1"),
|
newNestedPanel(1, "Panel 1"),
|
||||||
newNestedPanel(2, "Panel 2"),
|
newNestedPanel(2, "Panel 2"),
|
||||||
},
|
},
|
||||||
@ -515,8 +515,8 @@ var dashboardsWithPanels = []dashboard{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNestedPanel(id int64, name string) *object.ObjectSummary {
|
func newNestedPanel(id int64, name string) *models.ObjectSummary {
|
||||||
summary := &object.ObjectSummary{
|
summary := &models.ObjectSummary{
|
||||||
Kind: "panel",
|
Kind: "panel",
|
||||||
UID: fmt.Sprintf("???#%d", id),
|
UID: fmt.Sprintf("???#%d", id),
|
||||||
}
|
}
|
||||||
@ -553,14 +553,14 @@ var punctuationSplitNgramDashboards = []dashboard{
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
uid: "1",
|
uid: "1",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "heat-torkel",
|
Name: "heat-torkel",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
uid: "2",
|
uid: "2",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "topology heatmap",
|
Name: "topology heatmap",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -586,7 +586,7 @@ var camelCaseNgramDashboards = []dashboard{
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
uid: "1",
|
uid: "1",
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: "heatTorkel",
|
Name: "heatTorkel",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -608,7 +608,7 @@ func dashboardsWithTitles(names ...string) []dashboard {
|
|||||||
out = append(out, dashboard{
|
out = append(out, dashboard{
|
||||||
id: no,
|
id: no,
|
||||||
uid: fmt.Sprintf("%d", no),
|
uid: fmt.Sprintf("%d", no),
|
||||||
summary: &object.ObjectSummary{
|
summary: &models.ObjectSummary{
|
||||||
Name: name,
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
@ -1,4 +1,4 @@
|
|||||||
package extract
|
package dashboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
@ -6,8 +6,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func logf(format string, a ...interface{}) {
|
func logf(format string, a ...interface{}) {
|
||||||
@ -24,46 +22,46 @@ type templateVariable struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type datasourceVariableLookup struct {
|
type datasourceVariableLookup struct {
|
||||||
variableNameToRefs map[string][]dslookup.DataSourceRef
|
variableNameToRefs map[string][]DataSourceRef
|
||||||
dsLookup dslookup.DatasourceLookup
|
dsLookup DatasourceLookup
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *datasourceVariableLookup) getDsRefsByTemplateVariableValue(value string, datasourceType string) []dslookup.DataSourceRef {
|
func (d *datasourceVariableLookup) getDsRefsByTemplateVariableValue(value string, datasourceType string) []DataSourceRef {
|
||||||
switch value {
|
switch value {
|
||||||
case "default":
|
case "default":
|
||||||
// can be the default DS, or a DS with UID="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 {
|
if candidateDs == nil {
|
||||||
// get the actual default DS
|
// get the actual default DS
|
||||||
candidateDs = d.dsLookup.ByRef(nil)
|
candidateDs = d.dsLookup.ByRef(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if candidateDs != nil {
|
if candidateDs != nil {
|
||||||
return []dslookup.DataSourceRef{*candidateDs}
|
return []DataSourceRef{*candidateDs}
|
||||||
}
|
}
|
||||||
return []dslookup.DataSourceRef{}
|
return []DataSourceRef{}
|
||||||
case "$__all":
|
case "$__all":
|
||||||
// TODO: filter datasources by template variable's regex
|
// TODO: filter datasources by template variable's regex
|
||||||
return d.dsLookup.ByType(datasourceType)
|
return d.dsLookup.ByType(datasourceType)
|
||||||
case "":
|
case "":
|
||||||
return []dslookup.DataSourceRef{}
|
return []DataSourceRef{}
|
||||||
case "No data sources found":
|
case "No data sources found":
|
||||||
return []dslookup.DataSourceRef{}
|
return []DataSourceRef{}
|
||||||
default:
|
default:
|
||||||
// some variables use `ds.name` rather `ds.uid`
|
// some variables use `ds.name` rather `ds.uid`
|
||||||
if ref := d.dsLookup.ByRef(&dslookup.DataSourceRef{
|
if ref := d.dsLookup.ByRef(&DataSourceRef{
|
||||||
UID: value,
|
UID: value,
|
||||||
}); ref != nil {
|
}); ref != nil {
|
||||||
return []dslookup.DataSourceRef{*ref}
|
return []DataSourceRef{*ref}
|
||||||
}
|
}
|
||||||
|
|
||||||
// discard variable
|
// discard variable
|
||||||
return []dslookup.DataSourceRef{}
|
return []DataSourceRef{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *datasourceVariableLookup) add(templateVariable templateVariable) {
|
func (d *datasourceVariableLookup) add(templateVariable templateVariable) {
|
||||||
var refs []dslookup.DataSourceRef
|
var refs []DataSourceRef
|
||||||
|
|
||||||
datasourceType, isDataSourceTypeValid := templateVariable.query.(string)
|
datasourceType, isDataSourceTypeValid := templateVariable.query.(string)
|
||||||
if !isDataSourceTypeValid {
|
if !isDataSourceTypeValid {
|
||||||
@ -86,8 +84,8 @@ func (d *datasourceVariableLookup) add(templateVariable templateVariable) {
|
|||||||
d.variableNameToRefs[templateVariable.name] = unique(refs)
|
d.variableNameToRefs[templateVariable.name] = unique(refs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unique(refs []dslookup.DataSourceRef) []dslookup.DataSourceRef {
|
func unique(refs []DataSourceRef) []DataSourceRef {
|
||||||
var uniqueRefs []dslookup.DataSourceRef
|
var uniqueRefs []DataSourceRef
|
||||||
uidPresence := make(map[string]bool)
|
uidPresence := make(map[string]bool)
|
||||||
for _, ref := range refs {
|
for _, ref := range refs {
|
||||||
if !uidPresence[ref.UID] {
|
if !uidPresence[ref.UID] {
|
||||||
@ -98,26 +96,26 @@ func unique(refs []dslookup.DataSourceRef) []dslookup.DataSourceRef {
|
|||||||
return uniqueRefs
|
return uniqueRefs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *datasourceVariableLookup) getDatasourceRefs(name string) []dslookup.DataSourceRef {
|
func (d *datasourceVariableLookup) getDatasourceRefs(name string) []DataSourceRef {
|
||||||
refs, ok := d.variableNameToRefs[name]
|
refs, ok := d.variableNameToRefs[name]
|
||||||
if ok {
|
if ok {
|
||||||
return refs
|
return refs
|
||||||
}
|
}
|
||||||
|
|
||||||
return []dslookup.DataSourceRef{}
|
return []DataSourceRef{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDatasourceVariableLookup(dsLookup dslookup.DatasourceLookup) *datasourceVariableLookup {
|
func newDatasourceVariableLookup(dsLookup DatasourceLookup) *datasourceVariableLookup {
|
||||||
return &datasourceVariableLookup{
|
return &datasourceVariableLookup{
|
||||||
variableNameToRefs: make(map[string][]dslookup.DataSourceRef),
|
variableNameToRefs: make(map[string][]DataSourceRef),
|
||||||
dsLookup: dsLookup,
|
dsLookup: dsLookup,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:gocyclo
|
// nolint:gocyclo
|
||||||
// ReadDashboard will take a byte stream and return dashboard info
|
// ReadDashboard will take a byte stream and return dashboard info
|
||||||
func ReadDashboard(stream io.Reader, lookup dslookup.DatasourceLookup) (*DashboardInfo, error) {
|
func readDashboard(stream io.Reader, lookup DatasourceLookup) (*dashboardInfo, error) {
|
||||||
dash := &DashboardInfo{}
|
dash := &dashboardInfo{}
|
||||||
|
|
||||||
iter := jsoniter.Parse(jsoniter.ConfigDefault, stream, 1024)
|
iter := jsoniter.Parse(jsoniter.ConfigDefault, stream, 1024)
|
||||||
|
|
||||||
@ -192,7 +190,7 @@ func ReadDashboard(stream io.Reader, lookup dslookup.DatasourceLookup) (*Dashboa
|
|||||||
}
|
}
|
||||||
case "panels":
|
case "panels":
|
||||||
for iter.ReadArray() {
|
for iter.ReadArray() {
|
||||||
dash.Panels = append(dash.Panels, readPanelInfo(iter, lookup))
|
dash.Panels = append(dash.Panels, readpanelInfo(iter, lookup))
|
||||||
}
|
}
|
||||||
|
|
||||||
case "rows":
|
case "rows":
|
||||||
@ -278,11 +276,11 @@ func ReadDashboard(stream io.Reader, lookup dslookup.DatasourceLookup) (*Dashboa
|
|||||||
return dash, iter.Error
|
return dash, iter.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func panelRequiresDatasource(panel PanelInfo) bool {
|
func panelRequiresDatasource(panel panelInfo) bool {
|
||||||
return panel.Type != "row"
|
return panel.Type != "row"
|
||||||
}
|
}
|
||||||
|
|
||||||
func fillDefaultDatasources(dash *DashboardInfo, lookup dslookup.DatasourceLookup) {
|
func fillDefaultDatasources(dash *dashboardInfo, lookup DatasourceLookup) {
|
||||||
for i, panel := range dash.Panels {
|
for i, panel := range dash.Panels {
|
||||||
if len(panel.Datasource) != 0 || !panelRequiresDatasource(panel) {
|
if len(panel.Datasource) != 0 || !panelRequiresDatasource(panel) {
|
||||||
continue
|
continue
|
||||||
@ -290,14 +288,14 @@ func fillDefaultDatasources(dash *DashboardInfo, lookup dslookup.DatasourceLooku
|
|||||||
|
|
||||||
defaultDs := lookup.ByRef(nil)
|
defaultDs := lookup.ByRef(nil)
|
||||||
if defaultDs != 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 {
|
for i, panel := range dash.Panels {
|
||||||
var dsRefs []dslookup.DataSourceRef
|
var dsRefs []DataSourceRef
|
||||||
|
|
||||||
// partition into actual datasource references and variables
|
// partition into actual datasource references and variables
|
||||||
for _, ds := range panel.Datasource {
|
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 {
|
for i, panel := range dash.Panels {
|
||||||
var dsVariableRefs []dslookup.DataSourceRef
|
var dsVariableRefs []DataSourceRef
|
||||||
var dsRefs []dslookup.DataSourceRef
|
var dsRefs []DataSourceRef
|
||||||
|
|
||||||
// partition into actual datasource references and variables
|
// partition into actual datasource references and variables
|
||||||
for i := range panel.Datasource {
|
for i := range panel.Datasource {
|
||||||
@ -345,7 +343,7 @@ func isVariableRef(uid string) bool {
|
|||||||
return strings.HasPrefix(uid, "$")
|
return strings.HasPrefix(uid, "$")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDataSourceVariableName(dsVariableRef dslookup.DataSourceRef) string {
|
func getDataSourceVariableName(dsVariableRef DataSourceRef) string {
|
||||||
if strings.HasPrefix(dsVariableRef.UID, "${") {
|
if strings.HasPrefix(dsVariableRef.UID, "${") {
|
||||||
return strings.TrimPrefix(strings.TrimSuffix(dsVariableRef.UID, "}"), "${")
|
return strings.TrimPrefix(strings.TrimSuffix(dsVariableRef.UID, "}"), "${")
|
||||||
}
|
}
|
||||||
@ -353,8 +351,8 @@ func getDataSourceVariableName(dsVariableRef dslookup.DataSourceRef) string {
|
|||||||
return strings.TrimPrefix(dsVariableRef.UID, "$")
|
return strings.TrimPrefix(dsVariableRef.UID, "$")
|
||||||
}
|
}
|
||||||
|
|
||||||
func findDatasourceRefsForVariables(dsVariableRefs []dslookup.DataSourceRef, datasourceVariablesLookup *datasourceVariableLookup) []dslookup.DataSourceRef {
|
func findDatasourceRefsForVariables(dsVariableRefs []DataSourceRef, datasourceVariablesLookup *datasourceVariableLookup) []DataSourceRef {
|
||||||
var referencedDs []dslookup.DataSourceRef
|
var referencedDs []DataSourceRef
|
||||||
for _, dsVariableRef := range dsVariableRefs {
|
for _, dsVariableRef := range dsVariableRefs {
|
||||||
variableName := getDataSourceVariableName(dsVariableRef)
|
variableName := getDataSourceVariableName(dsVariableRef)
|
||||||
refs := datasourceVariablesLookup.getDatasourceRefs(variableName)
|
refs := datasourceVariablesLookup.getDatasourceRefs(variableName)
|
||||||
@ -364,8 +362,8 @@ func findDatasourceRefsForVariables(dsVariableRefs []dslookup.DataSourceRef, dat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// will always return strings for now
|
// will always return strings for now
|
||||||
func readPanelInfo(iter *jsoniter.Iterator, lookup dslookup.DatasourceLookup) PanelInfo {
|
func readpanelInfo(iter *jsoniter.Iterator, lookup DatasourceLookup) panelInfo {
|
||||||
panel := PanelInfo{}
|
panel := panelInfo{}
|
||||||
|
|
||||||
targets := newTargetInfo(lookup)
|
targets := newTargetInfo(lookup)
|
||||||
|
|
||||||
@ -428,7 +426,7 @@ func readPanelInfo(iter *jsoniter.Iterator, lookup dslookup.DatasourceLookup) Pa
|
|||||||
// Rows have nested panels
|
// Rows have nested panels
|
||||||
case "panels":
|
case "panels":
|
||||||
for iter.ReadArray() {
|
for iter.ReadArray() {
|
||||||
panel.Collapsed = append(panel.Collapsed, readPanelInfo(iter, lookup))
|
panel.Collapsed = append(panel.Collapsed, readpanelInfo(iter, lookup))
|
||||||
}
|
}
|
||||||
|
|
||||||
case "options":
|
case "options":
|
@ -1,4 +1,4 @@
|
|||||||
package extract
|
package dashboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -10,12 +10,10 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func dsLookup() dslookup.DatasourceLookup {
|
func dsLookupForTests() DatasourceLookup {
|
||||||
return dslookup.CreateDatasourceLookup([]*dslookup.DatasourceQueryResult{
|
return CreateDatasourceLookup([]*DatasourceQueryResult{
|
||||||
{
|
{
|
||||||
UID: "P8045C56BDA891CB2",
|
UID: "P8045C56BDA891CB2",
|
||||||
Type: "cloudwatch",
|
Type: "cloudwatch",
|
||||||
@ -74,7 +72,7 @@ func TestReadDashboard(t *testing.T) {
|
|||||||
"panels-without-datasources",
|
"panels-without-datasources",
|
||||||
}
|
}
|
||||||
|
|
||||||
devdash := "../../../../devenv/dev-dashboards/"
|
devdash := "../../../../../devenv/dev-dashboards/"
|
||||||
|
|
||||||
for _, input := range inputs {
|
for _, input := range inputs {
|
||||||
// nolint:gosec
|
// nolint:gosec
|
||||||
@ -90,7 +88,7 @@ func TestReadDashboard(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
dash, err := ReadDashboard(f, dsLookup())
|
dash, err := readDashboard(f, dsLookupForTests())
|
||||||
sortDatasources(dash)
|
sortDatasources(dash)
|
||||||
|
|
||||||
require.NoError(t, err)
|
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`
|
// 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 {
|
sort.Slice(dash.Datasource, func(i, j int) bool {
|
||||||
return strings.Compare(dash.Datasource[i].UID, dash.Datasource[j].UID) > 0
|
return strings.Compare(dash.Datasource[i].UID, dash.Datasource[j].UID) > 0
|
||||||
})
|
})
|
@ -1,4 +1,4 @@
|
|||||||
package dslookup
|
package dashboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@ -1,8 +1,10 @@
|
|||||||
package object
|
package dashboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A reference accumulator can combine
|
// A reference accumulator can combine
|
||||||
@ -11,24 +13,24 @@ type ReferenceAccumulator interface {
|
|||||||
Add(kind string, subtype string, uid string)
|
Add(kind string, subtype string, uid string)
|
||||||
|
|
||||||
// Returns the set of distinct references in a sorted order
|
// Returns the set of distinct references in a sorted order
|
||||||
Get() []*ExternalReference
|
Get() []*models.ObjectExternalReference
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReferenceAccumulator() ReferenceAccumulator {
|
func NewReferenceAccumulator() ReferenceAccumulator {
|
||||||
return &referenceAccumulator{
|
return &referenceAccumulator{
|
||||||
refs: make(map[string]*ExternalReference),
|
refs: make(map[string]*models.ObjectExternalReference),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type referenceAccumulator struct {
|
type referenceAccumulator struct {
|
||||||
refs map[string]*ExternalReference
|
refs map[string]*models.ObjectExternalReference
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *referenceAccumulator) Add(kind string, sub string, uid string) {
|
func (x *referenceAccumulator) Add(kind string, sub string, uid string) {
|
||||||
key := fmt.Sprintf("%s/%s/%s", kind, sub, uid)
|
key := fmt.Sprintf("%s/%s/%s", kind, sub, uid)
|
||||||
_, ok := x.refs[key]
|
_, ok := x.refs[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
x.refs[key] = &ExternalReference{
|
x.refs[key] = &models.ObjectExternalReference{
|
||||||
Kind: kind,
|
Kind: kind,
|
||||||
Type: sub,
|
Type: sub,
|
||||||
UID: uid,
|
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))
|
keys := make([]string, 0, len(x.refs))
|
||||||
for k := range x.refs {
|
for k := range x.refs {
|
||||||
keys = append(keys, k)
|
keys = append(keys, k)
|
||||||
}
|
}
|
||||||
sort.Strings(keys)
|
sort.Strings(keys)
|
||||||
|
|
||||||
refs := make([]*ExternalReference, len(keys))
|
refs := make([]*models.ObjectExternalReference, len(keys))
|
||||||
for i, key := range keys {
|
for i, key := range keys {
|
||||||
refs[i] = x.refs[key]
|
refs[i] = x.refs[key]
|
||||||
}
|
}
|
99
pkg/services/store/kind/dashboard/summary.go
Normal file
99
pkg/services/store/kind/dashboard/summary.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package dashboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
"github.com/grafana/grafana/pkg/services/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetObjectKindInfo() models.ObjectKindInfo {
|
||||||
|
return models.ObjectKindInfo{
|
||||||
|
ID: models.StandardKindDashboard,
|
||||||
|
Name: "Dashboard",
|
||||||
|
Description: "Define a grafana dashboard layout",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDashboardSummary(sql *sqlstore.SQLStore) models.ObjectSummaryBuilder {
|
||||||
|
return func(ctx context.Context, uid string, body []byte) (*models.ObjectSummary, []byte, error) {
|
||||||
|
// This just gets the orgID (that will soon/eventually be encoded in a GRN and passed instead of a UID)
|
||||||
|
user := store.UserFromContext(ctx)
|
||||||
|
if user == nil {
|
||||||
|
return nil, nil, fmt.Errorf("can not find user in context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Totally inefficient to look this up every time, but for the current use case that is OK
|
||||||
|
// The lookup is currently structured to support searchV2, but I think should become a real fallback
|
||||||
|
// that is only executed when we find a legacy dashboard ref
|
||||||
|
lookup, err := LoadDatasourceLookup(ctx, user.OrgID, sql)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
builder := NewStaticDashboardSummaryBuilder(lookup)
|
||||||
|
return builder(ctx, uid, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStaticDashboardSummaryBuilder(lookup DatasourceLookup) models.ObjectSummaryBuilder {
|
||||||
|
return func(ctx context.Context, uid string, body []byte) (*models.ObjectSummary, []byte, error) {
|
||||||
|
summary := &models.ObjectSummary{
|
||||||
|
Labels: make(map[string]string),
|
||||||
|
Fields: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
stream := bytes.NewBuffer(body)
|
||||||
|
dash, err := readDashboard(stream, lookup)
|
||||||
|
if err != nil {
|
||||||
|
summary.Error = &models.ObjectErrorInfo{
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
return summary, body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboardRefs := NewReferenceAccumulator()
|
||||||
|
url := fmt.Sprintf("/d/%s/%s", uid, models.SlugifyTitle(dash.Title))
|
||||||
|
summary.Name = dash.Title
|
||||||
|
summary.Description = dash.Description
|
||||||
|
summary.URL = url
|
||||||
|
for _, v := range dash.Tags {
|
||||||
|
summary.Labels[v] = ""
|
||||||
|
}
|
||||||
|
if len(dash.TemplateVars) > 0 {
|
||||||
|
summary.Fields["hasTemplateVars"] = true
|
||||||
|
}
|
||||||
|
summary.Fields["schemaVersion"] = dash.SchemaVersion
|
||||||
|
|
||||||
|
for _, panel := range dash.Panels {
|
||||||
|
panelRefs := NewReferenceAccumulator()
|
||||||
|
p := &models.ObjectSummary{
|
||||||
|
UID: uid + "#" + strconv.FormatInt(panel.ID, 10),
|
||||||
|
Kind: "panel",
|
||||||
|
}
|
||||||
|
p.Name = panel.Title
|
||||||
|
p.Description = panel.Description
|
||||||
|
p.URL = fmt.Sprintf("%s?viewPanel=%d", url, panel.ID)
|
||||||
|
p.Fields = make(map[string]interface{}, 0)
|
||||||
|
|
||||||
|
panelRefs.Add("panel", panel.Type, "")
|
||||||
|
for _, v := range panel.Datasource {
|
||||||
|
dashboardRefs.Add(models.StandardKindDataSource, v.Type, v.UID)
|
||||||
|
panelRefs.Add(models.StandardKindDataSource, v.Type, v.UID)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range panel.Transformer {
|
||||||
|
panelRefs.Add(models.StandardKindTransform, v, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboardRefs.Add(models.StandardKindPanel, panel.Type, "")
|
||||||
|
p.References = panelRefs.Get()
|
||||||
|
summary.Nested = append(summary.Nested, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.References = dashboardRefs.Get()
|
||||||
|
return summary, body, nil
|
||||||
|
}
|
||||||
|
}
|
@ -1,39 +1,22 @@
|
|||||||
package object
|
package dashboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
|
|
||||||
"github.com/grafana/grafana/pkg/services/store/object"
|
|
||||||
"github.com/stretchr/testify/require"
|
"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) {
|
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)
|
failed := make([]string, 0, 10)
|
||||||
|
|
||||||
err := filepath.Walk(devdash,
|
err := filepath.Walk(devdash,
|
||||||
@ -51,10 +34,7 @@ func TestReadSummaries(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uid := path[len(devdash):]
|
uid := path[len(devdash):]
|
||||||
summary, err := reader(&object.RawObject{
|
summary, _, err := reader(ctx, uid, body)
|
||||||
UID: uid,
|
|
||||||
Body: body,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
@ -1,25 +1,23 @@
|
|||||||
package extract
|
package dashboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/searchV2/dslookup"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type targetInfo struct {
|
type targetInfo struct {
|
||||||
lookup dslookup.DatasourceLookup
|
lookup DatasourceLookup
|
||||||
uids map[string]*dslookup.DataSourceRef
|
uids map[string]*DataSourceRef
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTargetInfo(lookup dslookup.DatasourceLookup) targetInfo {
|
func newTargetInfo(lookup DatasourceLookup) targetInfo {
|
||||||
return targetInfo{
|
return targetInfo{
|
||||||
lookup: lookup,
|
lookup: lookup,
|
||||||
uids: make(map[string]*dslookup.DataSourceRef),
|
uids: make(map[string]*DataSourceRef),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *targetInfo) GetDatasourceInfo() []dslookup.DataSourceRef {
|
func (s *targetInfo) GetDatasourceInfo() []DataSourceRef {
|
||||||
keys := make([]dslookup.DataSourceRef, len(s.uids))
|
keys := make([]DataSourceRef, len(s.uids))
|
||||||
i := 0
|
i := 0
|
||||||
for _, v := range s.uids {
|
for _, v := range s.uids {
|
||||||
keys[i] = *v
|
keys[i] = *v
|
||||||
@ -34,7 +32,7 @@ func (s *targetInfo) addDatasource(iter *jsoniter.Iterator) {
|
|||||||
case jsoniter.StringValue:
|
case jsoniter.StringValue:
|
||||||
key := iter.ReadString()
|
key := iter.ReadString()
|
||||||
|
|
||||||
dsRef := &dslookup.DataSourceRef{UID: key}
|
dsRef := &DataSourceRef{UID: key}
|
||||||
if !isVariableRef(dsRef.UID) && !isSpecialDatasource(dsRef.UID) {
|
if !isVariableRef(dsRef.UID) && !isSpecialDatasource(dsRef.UID) {
|
||||||
ds := s.lookup.ByRef(dsRef)
|
ds := s.lookup.ByRef(dsRef)
|
||||||
s.addRef(ds)
|
s.addRef(ds)
|
||||||
@ -47,7 +45,7 @@ func (s *targetInfo) addDatasource(iter *jsoniter.Iterator) {
|
|||||||
iter.Skip()
|
iter.Skip()
|
||||||
|
|
||||||
case jsoniter.ObjectValue:
|
case jsoniter.ObjectValue:
|
||||||
ref := &dslookup.DataSourceRef{}
|
ref := &DataSourceRef{}
|
||||||
iter.ReadVal(ref)
|
iter.ReadVal(ref)
|
||||||
|
|
||||||
if !isVariableRef(ref.UID) && !isSpecialDatasource(ref.UID) {
|
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 != "" {
|
if ref != nil && ref.UID != "" {
|
||||||
s.uids[ref.UID] = ref
|
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 {
|
for idx, v := range panel.Datasource {
|
||||||
if v.UID != "" {
|
if v.UID != "" {
|
||||||
s.uids[v.UID] = &panel.Datasource[idx]
|
s.uids[v.UID] = &panel.Datasource[idx]
|
93
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-gradient-area-fills.json
vendored
Normal file
93
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-gradient-area-fills.json
vendored
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
{
|
||||||
|
"name": "Panel Tests - Graph - Gradient Area Fills",
|
||||||
|
"labels": {
|
||||||
|
"gdev": "",
|
||||||
|
"graph": "",
|
||||||
|
"panel-tests": ""
|
||||||
|
},
|
||||||
|
"URL": "/d/graph-gradient-area-fills.json/panel-tests-graph-gradient-area-fills",
|
||||||
|
"fields": {
|
||||||
|
"schemaVersion": 18
|
||||||
|
},
|
||||||
|
"nested": [
|
||||||
|
{
|
||||||
|
"uid": "graph-gradient-area-fills.json#2",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Req/s",
|
||||||
|
"URL": "/d/graph-gradient-area-fills.json/panel-tests-graph-gradient-area-fills?viewPanel=2",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph-gradient-area-fills.json#11",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Req/s",
|
||||||
|
"URL": "/d/graph-gradient-area-fills.json/panel-tests-graph-gradient-area-fills?viewPanel=11",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph-gradient-area-fills.json#7",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Memory",
|
||||||
|
"URL": "/d/graph-gradient-area-fills.json/panel-tests-graph-gradient-area-fills?viewPanel=7",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph-gradient-area-fills.json#10",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Req/s",
|
||||||
|
"URL": "/d/graph-gradient-area-fills.json/panel-tests-graph-gradient-area-fills?viewPanel=10",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
181
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-shared-tooltips.json
vendored
Normal file
181
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-shared-tooltips.json
vendored
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
{
|
||||||
|
"name": "Panel Tests - shared tooltips",
|
||||||
|
"labels": {
|
||||||
|
"gdev": "",
|
||||||
|
"graph-ng": "",
|
||||||
|
"panel-tests": ""
|
||||||
|
},
|
||||||
|
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips",
|
||||||
|
"fields": {
|
||||||
|
"schemaVersion": 28
|
||||||
|
},
|
||||||
|
"nested": [
|
||||||
|
{
|
||||||
|
"uid": "graph-shared-tooltips.json#4",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "two units",
|
||||||
|
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=4",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph-shared-tooltips.json#13",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Speed vs Temperature (XY)",
|
||||||
|
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=13",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "xychart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "transform",
|
||||||
|
"type": "organize"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "transform",
|
||||||
|
"type": "seriesToColumns"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph-shared-tooltips.json#2",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Cursor info",
|
||||||
|
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=2",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "debug"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph-shared-tooltips.json#5",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Only temperature",
|
||||||
|
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=5",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph-shared-tooltips.json#9",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Only Speed",
|
||||||
|
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=9",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph-shared-tooltips.json#11",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Panel Title",
|
||||||
|
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=11",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph-shared-tooltips.json#8",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "flot panel (temperature)",
|
||||||
|
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=8",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph-shared-tooltips.json#10",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "flot panel (no units)",
|
||||||
|
"URL": "/d/graph-shared-tooltips.json/panel-tests-shared-tooltips?viewPanel=10",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "debug"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "xychart"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
110
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-time-regions.json
vendored
Normal file
110
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph-time-regions.json
vendored
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"name": "Panel Tests - Graph Time Regions",
|
||||||
|
"labels": {
|
||||||
|
"gdev": "",
|
||||||
|
"graph": "",
|
||||||
|
"panel-tests": ""
|
||||||
|
},
|
||||||
|
"URL": "/d/graph-time-regions.json/panel-tests-graph-time-regions",
|
||||||
|
"fields": {
|
||||||
|
"schemaVersion": 18
|
||||||
|
},
|
||||||
|
"nested": [
|
||||||
|
{
|
||||||
|
"uid": "graph-time-regions.json#2",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Business Hours",
|
||||||
|
"URL": "/d/graph-time-regions.json/panel-tests-graph-time-regions?viewPanel=2",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph-time-regions.json#4",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Sunday's 20-23",
|
||||||
|
"URL": "/d/graph-time-regions.json/panel-tests-graph-time-regions?viewPanel=4",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph-time-regions.json#3",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Each day of week",
|
||||||
|
"URL": "/d/graph-time-regions.json/panel-tests-graph-time-regions?viewPanel=3",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph-time-regions.json#5",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "05:00",
|
||||||
|
"URL": "/d/graph-time-regions.json/panel-tests-graph-time-regions?viewPanel=5",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph-time-regions.json#7",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "From 22:00 to 00:30 (crossing midnight)",
|
||||||
|
"URL": "/d/graph-time-regions.json/panel-tests-graph-time-regions?viewPanel=7",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
385
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_tests.json
vendored
Normal file
385
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_tests.json
vendored
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
{
|
||||||
|
"name": "Panel Tests - Graph",
|
||||||
|
"labels": {
|
||||||
|
"gdev": "",
|
||||||
|
"graph": "",
|
||||||
|
"panel-tests": ""
|
||||||
|
},
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph",
|
||||||
|
"fields": {
|
||||||
|
"schemaVersion": 16
|
||||||
|
},
|
||||||
|
"nested": [
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#1",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "No Data Points Warning",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=1",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#2",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Datapoints Outside Range Warning",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=2",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#3",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Random walk series",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=3",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#4",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Millisecond res x-axis and tooltip",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=4",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#6",
|
||||||
|
"kind": "panel",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=6",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#5",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "2 yaxis and axis labels",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=5",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#7",
|
||||||
|
"kind": "panel",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=7",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#8",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "null value connected",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=8",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#10",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "null value null as zero",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=10",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#13",
|
||||||
|
"kind": "panel",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=13",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#9",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Stacking value ontop of nulls",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=9",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#14",
|
||||||
|
"kind": "panel",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=14",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#12",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Stacking all series null segment",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=12",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#15",
|
||||||
|
"kind": "panel",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=15",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#21",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Null between points",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=21",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#22",
|
||||||
|
"kind": "panel",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=22",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#20",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Legend Table Single Series Should Take Minimum Height",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=20",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#16",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Legend Table No Scroll Visible",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=16",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#17",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Legend Table Should Scroll",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=17",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#18",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Legend Table No Scroll Visible",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=18",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_tests.json#19",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Legend Table No Scroll Visible",
|
||||||
|
"URL": "/d/graph_tests.json/panel-tests-graph?viewPanel=19",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "testdata",
|
||||||
|
"UID": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
177
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_y_axis.json
vendored
Normal file
177
pkg/services/store/kind/dashboard/testdata/gdev-walk-graph_y_axis.json
vendored
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
{
|
||||||
|
"name": "Panel Tests - Graph - Y axis ticks",
|
||||||
|
"labels": {
|
||||||
|
"gdev": "",
|
||||||
|
"panel-tests": ""
|
||||||
|
},
|
||||||
|
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks",
|
||||||
|
"fields": {
|
||||||
|
"schemaVersion": 19
|
||||||
|
},
|
||||||
|
"nested": [
|
||||||
|
{
|
||||||
|
"uid": "graph_y_axis.json#7",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Data from 0 - 10K (unit short)",
|
||||||
|
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=7",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_y_axis.json#5",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Data from 0 - 10K (unit bytes metric)",
|
||||||
|
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=5",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_y_axis.json#4",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Data from 0 - 10K (unit bytes IEC)",
|
||||||
|
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=4",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_y_axis.json#2",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Data from 0 - 10K (unit short)",
|
||||||
|
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=2",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_y_axis.json#3",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Data from 0.0002 - 0.001 (unit short)",
|
||||||
|
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=3",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_y_axis.json#6",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Data from 12000 - 30000 (unit ms)",
|
||||||
|
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=6",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_y_axis.json#9",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Data from 0 - 1B (unit short)",
|
||||||
|
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=9",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_y_axis.json#10",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Data from 0 - 1B (unit bytes)",
|
||||||
|
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=10",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "graph_y_axis.json#8",
|
||||||
|
"kind": "panel",
|
||||||
|
"name": "Data from 12000 - 30000 (unit ms)",
|
||||||
|
"URL": "/d/graph_y_axis.json/panel-tests-graph-y-axis-ticks?viewPanel=8",
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"kind": "ds",
|
||||||
|
"type": "default.type",
|
||||||
|
"UID": "default.uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "panel",
|
||||||
|
"type": "graph"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
32
pkg/services/store/kind/dashboard/types.go
Normal file
32
pkg/services/store/kind/dashboard/types.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package dashboard
|
||||||
|
|
||||||
|
type panelInfo struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"` // PluginID
|
||||||
|
PluginVersion string `json:"pluginVersion,omitempty"`
|
||||||
|
Datasource []DataSourceRef `json:"datasource,omitempty"` // UIDs
|
||||||
|
Transformer []string `json:"transformer,omitempty"` // ids of the transformation steps
|
||||||
|
|
||||||
|
// Rows define panels as sub objects
|
||||||
|
Collapsed []panelInfo `json:"collapsed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dashboardInfo struct {
|
||||||
|
UID string `json:"uid,omitempty"`
|
||||||
|
ID int64 `json:"id,omitempty"` // internal ID
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
TemplateVars []string `json:"templateVars,omitempty"` // the keys used
|
||||||
|
Datasource []DataSourceRef `json:"datasource,omitempty"` // UIDs
|
||||||
|
Panels []panelInfo `json:"panels"` // nesed documents
|
||||||
|
SchemaVersion int64 `json:"schemaVersion"`
|
||||||
|
LinkCount int64 `json:"linkCount"`
|
||||||
|
TimeFrom string `json:"timeFrom"`
|
||||||
|
TimeTo string `json:"timeTo"`
|
||||||
|
TimeZone string `json:"timezone"`
|
||||||
|
Refresh string `json:"refresh,omitempty"`
|
||||||
|
ReadOnly bool `json:"readOnly,omitempty"` // editable = false
|
||||||
|
}
|
4
pkg/services/store/kind/dummy/doc.go
Normal file
4
pkg/services/store/kind/dummy/doc.go
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Package dummy provides a dummy kind useful for testing
|
||||||
|
//
|
||||||
|
// The dummy kind returns a complicated summary that can exercise most of the storage options
|
||||||
|
package dummy
|
61
pkg/services/store/kind/dummy/summary.go
Normal file
61
pkg/services/store/kind/dummy/summary.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package dummy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetObjectKindInfo(kind string) models.ObjectKindInfo {
|
||||||
|
return models.ObjectKindInfo{
|
||||||
|
ID: kind,
|
||||||
|
Name: kind,
|
||||||
|
Description: "Dummy kind used for testing.",
|
||||||
|
IsRaw: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetObjectSummaryBuilder(kind string) models.ObjectSummaryBuilder {
|
||||||
|
return func(ctx context.Context, uid string, body []byte) (*models.ObjectSummary, []byte, error) {
|
||||||
|
summary := &models.ObjectSummary{
|
||||||
|
Name: fmt.Sprintf("Dummy: %s", kind),
|
||||||
|
Kind: kind,
|
||||||
|
Description: fmt.Sprintf("Wrote at %s", time.Now().Local().String()),
|
||||||
|
Labels: map[string]string{
|
||||||
|
"hello": "world",
|
||||||
|
"tag1": "",
|
||||||
|
"tag2": "",
|
||||||
|
},
|
||||||
|
Fields: map[string]interface{}{
|
||||||
|
"field1": "a string",
|
||||||
|
"field2": 1.224,
|
||||||
|
"field4": true,
|
||||||
|
},
|
||||||
|
Error: nil, // ignore for now
|
||||||
|
Nested: nil, // ignore for now
|
||||||
|
References: []*models.ObjectExternalReference{
|
||||||
|
{
|
||||||
|
Kind: "ds",
|
||||||
|
Type: "influx",
|
||||||
|
UID: "xyz",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: "panel",
|
||||||
|
Type: "heatmap",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: "panel",
|
||||||
|
Type: "timeseries",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if summary.UID != "" && uid != summary.UID {
|
||||||
|
return summary, nil, fmt.Errorf("internal UID mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary, body, nil
|
||||||
|
}
|
||||||
|
}
|
67
pkg/services/store/kind/playlist/summary.go
Normal file
67
pkg/services/store/kind/playlist/summary.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package playlist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/coremodel/playlist"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetObjectKindInfo() models.ObjectKindInfo {
|
||||||
|
return models.ObjectKindInfo{
|
||||||
|
ID: models.StandardKindPlaylist,
|
||||||
|
Name: "Playlist",
|
||||||
|
Description: "Cycle though a collection of dashboards automatically",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetObjectSummaryBuilder() models.ObjectSummaryBuilder {
|
||||||
|
return summaryBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
func summaryBuilder(ctx context.Context, uid string, body []byte) (*models.ObjectSummary, []byte, error) {
|
||||||
|
obj := &playlist.Model{}
|
||||||
|
err := json.Unmarshal(body, obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err // unable to read object
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: fix model so this is not possible
|
||||||
|
if obj.Items == nil {
|
||||||
|
temp := make([]playlist.PlaylistItem, 0)
|
||||||
|
obj.Items = &temp
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := &models.ObjectSummary{
|
||||||
|
UID: obj.Uid,
|
||||||
|
Name: obj.Name,
|
||||||
|
Description: fmt.Sprintf("%d items, refreshed every %s", len(*obj.Items), obj.Interval),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range *obj.Items {
|
||||||
|
switch item.Type {
|
||||||
|
case playlist.PlaylistItemTypeDashboardByUid:
|
||||||
|
summary.References = append(summary.References, &models.ObjectExternalReference{
|
||||||
|
Kind: "dashboard",
|
||||||
|
UID: item.Value,
|
||||||
|
})
|
||||||
|
|
||||||
|
case playlist.PlaylistItemTypeDashboardByTag:
|
||||||
|
if summary.Labels == nil {
|
||||||
|
summary.Labels = make(map[string]string, 0)
|
||||||
|
}
|
||||||
|
summary.Labels[item.Value] = ""
|
||||||
|
|
||||||
|
case playlist.PlaylistItemTypeDashboardById:
|
||||||
|
// obviously insufficient long term... but good to have an example :)
|
||||||
|
summary.Error = &models.ObjectErrorInfo{
|
||||||
|
Message: "Playlist uses deprecated internal id system",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(obj)
|
||||||
|
return summary, out, err
|
||||||
|
}
|
39
pkg/services/store/kind/playlist/summary_test.go
Normal file
39
pkg/services/store/kind/playlist/summary_test.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package playlist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/coremodel/playlist"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPlaylistSummary(t *testing.T) {
|
||||||
|
builder := GetObjectSummaryBuilder()
|
||||||
|
|
||||||
|
// Do not parse invalid input
|
||||||
|
_, _, err := builder(context.Background(), "abc", []byte("{invalid json"))
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
playlist := playlist.Model{
|
||||||
|
Interval: "30s",
|
||||||
|
Name: "test",
|
||||||
|
Items: &[]playlist.PlaylistItem{
|
||||||
|
{Type: playlist.PlaylistItemTypeDashboardByUid, Value: "D1"},
|
||||||
|
{Type: playlist.PlaylistItemTypeDashboardByTag, Value: "tagA"},
|
||||||
|
{Type: playlist.PlaylistItemTypeDashboardByUid, Value: "D3"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
out, err := json.Marshal(playlist)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
|
||||||
|
// Do not parse invalid input
|
||||||
|
summary, body, err := builder(context.Background(), "abc", out)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "test", summary.Name)
|
||||||
|
require.Equal(t, 2, len(summary.References))
|
||||||
|
require.Equal(t, map[string]string{"tagA": ""}, summary.Labels)
|
||||||
|
require.True(t, json.Valid(body))
|
||||||
|
}
|
55
pkg/services/store/kind/png/summary.go
Normal file
55
pkg/services/store/kind/png/summary.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package png
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"image/png"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetObjectKindInfo() models.ObjectKindInfo {
|
||||||
|
return models.ObjectKindInfo{
|
||||||
|
ID: models.StandardKindPNG,
|
||||||
|
Name: "PNG",
|
||||||
|
Description: "PNG Image file",
|
||||||
|
IsRaw: true,
|
||||||
|
FileExtension: "png",
|
||||||
|
MimeType: "image/png",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVG sanitizer based on the rendering service
|
||||||
|
func GetObjectSummaryBuilder() models.ObjectSummaryBuilder {
|
||||||
|
return func(ctx context.Context, uid string, body []byte) (*models.ObjectSummary, []byte, error) {
|
||||||
|
img, err := png.Decode(bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
size := img.Bounds().Size()
|
||||||
|
summary := &models.ObjectSummary{
|
||||||
|
Kind: models.StandardKindSVG,
|
||||||
|
Name: guessNameFromUID(uid),
|
||||||
|
UID: uid,
|
||||||
|
Fields: map[string]interface{}{
|
||||||
|
"width": int64(size.X),
|
||||||
|
"height": int64(size.Y),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return summary, body, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func guessNameFromUID(uid string) string {
|
||||||
|
sidx := strings.LastIndex(uid, "/") + 1
|
||||||
|
didx := strings.LastIndex(uid, ".")
|
||||||
|
if didx > sidx && didx != sidx {
|
||||||
|
return uid[sidx:didx]
|
||||||
|
}
|
||||||
|
if sidx > 0 {
|
||||||
|
return uid[sidx:]
|
||||||
|
}
|
||||||
|
return uid
|
||||||
|
}
|
33
pkg/services/store/kind/png/summary_test.go
Normal file
33
pkg/services/store/kind/png/summary_test.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package png
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPNGSummary(t *testing.T) {
|
||||||
|
const gopher = `iVBORw0KGgoAAAANSUhEUgAAAEsAAAA8CAAAAAALAhhPAAAFfUlEQVRYw62XeWwUVRzHf2+OPbo9d7tsWyiyaZti6eWGAhISoIGKECEKCAiJJkYTiUgTMYSIosYYBBIUIxoSPIINEBDi2VhwkQrVsj1ESgu9doHWdrul7ba73WNm3vOPtsseM9MdwvvrzTs+8/t95ze/33sI5BqiabU6m9En8oNjduLnAEDLUsQXFF8tQ5oxK3vmnNmDSMtrncks9Hhtt/qeWZapHb1ha3UqYSWVl2ZmpWgaXMXGohQAvmeop3bjTRtv6SgaK/Pb9/bFzUrYslbFAmHPp+3WhAYdr+7GN/YnpN46Opv55VDsJkoEpMrY/vO2BIYQ6LLvm0ThY3MzDzzeSJeeWNyTkgnIE5ePKsvKlcg/0T9QMzXalwXMlj54z4c0rh/mzEfr+FgWEz2w6uk8dkzFAgcARAgNp1ZYef8bH2AgvuStbc2/i6CiWGj98y2tw2l4FAXKkQBIf+exyRnteY83LfEwDQAYCoK+P6bxkZm/0966LxcAAILHB56kgD95PPxltuYcMtFTWw/FKkY/6Opf3GGd9ZF+Qp6mzJxzuRSractOmJrH1u8XTvWFHINNkLQLMR+XHXvfPPHw967raE1xxwtA36IMRfkAAG29/7mLuQcb2WOnsJReZGfpiHsSBX81cvMKywYZHhX5hFPtOqPGWZCXnhWGAu6lX91ElKXSalcLXu3UaOXVay57ZSe5f6Gpx7J2MXAsi7EqSp09b/MirKSyJfnfEEgeDjl8FgDAfvewP03zZ+AJ0m9aFRM8eEHBDRKjfcreDXnZdQuAxXpT2NRJ7xl3UkLBhuVGU16gZiGOgZmrSbRdqkILuL/yYoSXHHkl9KXgqNu3PB8oRg0geC5vFmLjad6mUyTKLmF3OtraWDIfACyXqmephaDABawfpi6tqqBZytfQMqOz6S09iWXhktrRaB8Xz4Yi/8gyABDm5NVe6qq/3VzPrcjELWrebVuyY2T7ar4zQyybUCtsQ5Es1FGaZVrRVQwAgHGW2ZCRZshI5bGQi7HesyE972pOSeMM0dSktlzxRdrlqb3Osa6CCS8IJoQQQgBAbTAa5l5epO34rJszibJI8rxLfGzcp1dRosutGeb2VDNgqYrwTiPNsLxXiPi3dz7LiS1WBRBDBOnqEjyy3aQb+/bLiJzz9dIkscVBBLxMfSEac7kO4Fpkngi0ruNBeSOal+u8jgOuqPz12nryMLCniEjtOOOmpt+KEIqsEdocJjYXwrh9OZqWJQyPCTo67LNS/TdxLAv6R5ZNK9npEjbYdT33gRo4o5oTqR34R+OmaSzDBWsAIPhuRcgyoteNi9gF0KzNYWVItPf2TLoXEg+7isNC7uJkgo1iQWOfRSP9NR11RtbZZ3OMG/VhL6jvx+J1m87+RCfJChAtEBQkSBX2PnSiihc/Twh3j0h7qdYQAoRVsRGmq7HU2QRbaxVGa1D6nIOqaIWRjyRZpHMQKWKpZM5feA+lzC4ZFultV8S6T0mzQGhQohi5I8iw+CsqBSxhFMuwyLgSwbghGb0AiIKkSDmGZVmJSiKihsiyOAUs70UkywooYP0bii9GdH4sfr1UNysd3fUyLLMQN+rsmo3grHl9VNJHbbwxoa47Vw5gupIqrZcjPh9R4Nye3nRDk199V+aetmvVtDRE8/+cbgAAgMIWGb3UA0MGLE9SCbWX670TDy1y98c3D27eppUjsZ6fql3jcd5rUe7+ZIlLNQny3Rd+E5Tct3WVhTM5RBCEdiEK0b6B+/ca2gYU393nFj/n1AygRQxPIUA043M42u85+z2SnssKrPl8Mx76NL3E6eXc3be7OD+H4WHbJkKI8AU8irbITQjZ+0hQcPEgId/Fn/pl9crKH02+5o2b9T/eMx7pKoskYgAAAABJRU5ErkJggg==`
|
||||||
|
img, err := base64.StdEncoding.DecodeString(gopher)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
summary, out, err := GetObjectSummaryBuilder()(context.Background(), "hello.png", img)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, img, out) // same image
|
||||||
|
|
||||||
|
asjson, err := json.MarshalIndent(summary, "", " ")
|
||||||
|
//fmt.Printf(string(asjson))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, `{
|
||||||
|
"uid": "hello.png",
|
||||||
|
"kind": "svg",
|
||||||
|
"name": "hello",
|
||||||
|
"fields": {
|
||||||
|
"height": 60,
|
||||||
|
"width": 75
|
||||||
|
}
|
||||||
|
}`, string(asjson))
|
||||||
|
}
|
144
pkg/services/store/kind/registry.go
Normal file
144
pkg/services/store/kind/registry.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package kind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
"github.com/grafana/grafana/pkg/services/store/kind/dashboard"
|
||||||
|
"github.com/grafana/grafana/pkg/services/store/kind/dummy"
|
||||||
|
"github.com/grafana/grafana/pkg/services/store/kind/playlist"
|
||||||
|
"github.com/grafana/grafana/pkg/services/store/kind/png"
|
||||||
|
"github.com/grafana/grafana/pkg/services/store/kind/svg"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KindRegistry interface {
|
||||||
|
Register(info models.ObjectKindInfo, builder models.ObjectSummaryBuilder) error
|
||||||
|
GetSummaryBuilder(kind string) models.ObjectSummaryBuilder
|
||||||
|
GetInfo(kind string) (models.ObjectKindInfo, error)
|
||||||
|
GetKinds() []models.ObjectKindInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKindRegistry() KindRegistry {
|
||||||
|
kinds := make(map[string]*kindValues)
|
||||||
|
kinds[models.StandardKindPlaylist] = &kindValues{
|
||||||
|
info: playlist.GetObjectKindInfo(),
|
||||||
|
builder: playlist.GetObjectSummaryBuilder(),
|
||||||
|
}
|
||||||
|
kinds[models.StandardKindPNG] = &kindValues{
|
||||||
|
info: png.GetObjectKindInfo(),
|
||||||
|
builder: png.GetObjectSummaryBuilder(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME -- these are registered because existing tests use them
|
||||||
|
for _, k := range []string{"dummy", "kind1", "kind2", "kind3"} {
|
||||||
|
kinds[k] = &kindValues{
|
||||||
|
info: dummy.GetObjectKindInfo(k),
|
||||||
|
builder: dummy.GetObjectSummaryBuilder(k),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a registry
|
||||||
|
reg := ®istry{
|
||||||
|
mutex: sync.RWMutex{},
|
||||||
|
kinds: kinds,
|
||||||
|
}
|
||||||
|
reg.updateInfoArray()
|
||||||
|
return reg
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO? This could be a zero dependency service that others are responsible for configuring
|
||||||
|
func ProvideService(cfg *setting.Cfg, renderer rendering.Service, sql *sqlstore.SQLStore) KindRegistry {
|
||||||
|
reg := NewKindRegistry()
|
||||||
|
|
||||||
|
// Register Dashboard support
|
||||||
|
//-----------------------
|
||||||
|
_ = reg.Register(dashboard.GetObjectKindInfo(), dashboard.NewDashboardSummary(sql))
|
||||||
|
|
||||||
|
// Register SVG support
|
||||||
|
//-----------------------
|
||||||
|
info := svg.GetObjectKindInfo()
|
||||||
|
allowUnsanitizedSvgUpload := cfg != nil && cfg.Storage.AllowUnsanitizedSvgUpload
|
||||||
|
support := svg.GetObjectSummaryBuilder(allowUnsanitizedSvgUpload, renderer)
|
||||||
|
_ = reg.Register(info, support)
|
||||||
|
|
||||||
|
return reg
|
||||||
|
}
|
||||||
|
|
||||||
|
type kindValues struct {
|
||||||
|
info models.ObjectKindInfo
|
||||||
|
builder models.ObjectSummaryBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
type registry struct {
|
||||||
|
mutex sync.RWMutex
|
||||||
|
kinds map[string]*kindValues
|
||||||
|
info []models.ObjectKindInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registry) updateInfoArray() {
|
||||||
|
info := make([]models.ObjectKindInfo, 0, len(r.kinds))
|
||||||
|
for _, v := range r.kinds {
|
||||||
|
info = append(info, v.info)
|
||||||
|
}
|
||||||
|
sort.Slice(info, func(i, j int) bool {
|
||||||
|
return info[i].ID < info[j].ID
|
||||||
|
})
|
||||||
|
r.info = info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registry) Register(info models.ObjectKindInfo, builder models.ObjectSummaryBuilder) error {
|
||||||
|
if info.ID == "" || builder == nil {
|
||||||
|
return fmt.Errorf("invalid kind")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mutex.Lock()
|
||||||
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
|
if r.kinds[info.ID] != nil {
|
||||||
|
return fmt.Errorf("already exits")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.kinds[info.ID] = &kindValues{
|
||||||
|
info: info,
|
||||||
|
builder: builder,
|
||||||
|
}
|
||||||
|
r.updateInfoArray()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSummaryBuilder returns a builder or nil if not found
|
||||||
|
func (r *registry) GetSummaryBuilder(kind string) models.ObjectSummaryBuilder {
|
||||||
|
r.mutex.RLock()
|
||||||
|
defer r.mutex.RUnlock()
|
||||||
|
|
||||||
|
v, ok := r.kinds[kind]
|
||||||
|
if ok {
|
||||||
|
return v.builder
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInfo returns the registered info
|
||||||
|
func (r *registry) GetInfo(kind string) (models.ObjectKindInfo, error) {
|
||||||
|
r.mutex.RLock()
|
||||||
|
defer r.mutex.RUnlock()
|
||||||
|
|
||||||
|
v, ok := r.kinds[kind]
|
||||||
|
if ok {
|
||||||
|
return v.info, nil
|
||||||
|
}
|
||||||
|
return models.ObjectKindInfo{}, fmt.Errorf("not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSummaryBuilder returns a builder or nil if not found
|
||||||
|
func (r *registry) GetKinds() []models.ObjectKindInfo {
|
||||||
|
r.mutex.RLock()
|
||||||
|
defer r.mutex.RUnlock()
|
||||||
|
|
||||||
|
return r.info // returns a copy of the array
|
||||||
|
}
|
42
pkg/services/store/kind/registry_test.go
Normal file
42
pkg/services/store/kind/registry_test.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package kind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/store/kind/dummy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKindRegistry(t *testing.T) {
|
||||||
|
registry := NewKindRegistry()
|
||||||
|
err := registry.Register(dummy.GetObjectKindInfo("test"), dummy.GetObjectSummaryBuilder("test"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ids := []string{}
|
||||||
|
for _, k := range registry.GetKinds() {
|
||||||
|
ids = append(ids, k.ID)
|
||||||
|
}
|
||||||
|
require.Equal(t, []string{
|
||||||
|
"dummy",
|
||||||
|
"kind1",
|
||||||
|
"kind2",
|
||||||
|
"kind3",
|
||||||
|
"playlist",
|
||||||
|
"png",
|
||||||
|
"test",
|
||||||
|
}, ids)
|
||||||
|
|
||||||
|
// Check playlist exists
|
||||||
|
info, err := registry.GetInfo(models.StandardKindPlaylist)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "Playlist", info.Name)
|
||||||
|
require.False(t, info.IsRaw)
|
||||||
|
|
||||||
|
// Check that we registered a test item
|
||||||
|
info, err = registry.GetInfo("test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "test", info.Name)
|
||||||
|
require.True(t, info.IsRaw)
|
||||||
|
}
|
66
pkg/services/store/kind/svg/summary.go
Normal file
66
pkg/services/store/kind/svg/summary.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetObjectKindInfo() models.ObjectKindInfo {
|
||||||
|
return models.ObjectKindInfo{
|
||||||
|
ID: models.StandardKindSVG,
|
||||||
|
Name: "SVG",
|
||||||
|
Description: "Scalable Vector Graphics",
|
||||||
|
IsRaw: true,
|
||||||
|
FileExtension: "svg",
|
||||||
|
MimeType: "image/svg+xml",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SVG sanitizer based on the rendering service
|
||||||
|
func GetObjectSummaryBuilder(allowUnsanitizedSvgUpload bool, renderer rendering.Service) models.ObjectSummaryBuilder {
|
||||||
|
return func(ctx context.Context, uid string, body []byte) (*models.ObjectSummary, []byte, error) {
|
||||||
|
if !IsSVG(body) {
|
||||||
|
return nil, nil, fmt.Errorf("invalid svg")
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a renderer exists, we can return a sanitized version
|
||||||
|
var sanitized []byte
|
||||||
|
if renderer != nil {
|
||||||
|
rsp, err := renderer.SanitizeSVG(ctx, &rendering.SanitizeSVGRequest{
|
||||||
|
Content: body,
|
||||||
|
})
|
||||||
|
if err != nil && !allowUnsanitizedSvgUpload {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
sanitized = rsp.Sanitized
|
||||||
|
}
|
||||||
|
if sanitized == nil {
|
||||||
|
if !allowUnsanitizedSvgUpload {
|
||||||
|
return nil, nil, fmt.Errorf("unable to sanitize svg")
|
||||||
|
}
|
||||||
|
sanitized = body
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.ObjectSummary{
|
||||||
|
Kind: models.StandardKindSVG,
|
||||||
|
Name: guessNameFromUID(uid),
|
||||||
|
UID: uid,
|
||||||
|
}, sanitized, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func guessNameFromUID(uid string) string {
|
||||||
|
sidx := strings.LastIndex(uid, "/") + 1
|
||||||
|
didx := strings.LastIndex(uid, ".")
|
||||||
|
if didx > sidx && didx != sidx {
|
||||||
|
return uid[sidx:didx]
|
||||||
|
}
|
||||||
|
if sidx > 0 {
|
||||||
|
return uid[sidx:]
|
||||||
|
}
|
||||||
|
return uid
|
||||||
|
}
|
@ -23,7 +23,7 @@
|
|||||||
//FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
//FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
//OTHER DEALINGS IN THE SOFTWARE.
|
//OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
package issvg
|
package svg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -51,12 +51,6 @@ func isBinary(buf []byte) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Is returns true if the given buffer is a valid SVG image.
|
// 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{}))
|
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/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/x/persistentcollection"
|
"github.com/grafana/grafana/pkg/infra/x/persistentcollection"
|
||||||
"github.com/grafana/grafana/pkg/services/grpcserver"
|
"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/services/store/object"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
@ -25,7 +27,6 @@ type ObjectVersionWithBody struct {
|
|||||||
|
|
||||||
type RawObjectWithHistory struct {
|
type RawObjectWithHistory struct {
|
||||||
Object *object.RawObject `json:"object,omitempty"`
|
Object *object.RawObject `json:"object,omitempty"`
|
||||||
Summary *object.ObjectSummary `json:"summary,omitempty"`
|
|
||||||
History []*ObjectVersionWithBody `json:"history,omitempty"`
|
History []*ObjectVersionWithBody `json:"history,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,10 +35,11 @@ var (
|
|||||||
rawObjectVersion = 6
|
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{
|
objectServer := &dummyObjectServer{
|
||||||
collection: persistentcollection.NewLocalFSPersistentCollection[*RawObjectWithHistory]("raw-object", cfg.DataPath, rawObjectVersion),
|
collection: persistentcollection.NewLocalFSPersistentCollection[*RawObjectWithHistory]("raw-object", cfg.DataPath, rawObjectVersion),
|
||||||
log: log.New("in-memory-object-server"),
|
log: log.New("in-memory-object-server"),
|
||||||
|
kinds: kinds,
|
||||||
}
|
}
|
||||||
object.RegisterObjectStoreServer(grpcServerProvider.GetServer(), objectServer)
|
object.RegisterObjectStoreServer(grpcServerProvider.GetServer(), objectServer)
|
||||||
return objectServer
|
return objectServer
|
||||||
@ -46,6 +48,7 @@ func ProvideDummyObjectServer(cfg *setting.Cfg, grpcServerProvider grpcserver.Pr
|
|||||||
type dummyObjectServer struct {
|
type dummyObjectServer struct {
|
||||||
log log.Logger
|
log log.Logger
|
||||||
collection persistentcollection.PersistentCollection[*RawObjectWithHistory]
|
collection persistentcollection.PersistentCollection[*RawObjectWithHistory]
|
||||||
|
kinds kind.KindRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
func namespaceFromUID(uid string) string {
|
func namespaceFromUID(uid string) string {
|
||||||
@ -115,15 +118,15 @@ func (i dummyObjectServer) Read(ctx context.Context, r *object.ReadObjectRequest
|
|||||||
Object: objVersion,
|
Object: objVersion,
|
||||||
}
|
}
|
||||||
if r.WithSummary {
|
if r.WithSummary {
|
||||||
summary, _, e2 := object.GetSafeSaveObject(&object.WriteObjectRequest{
|
// Since we do not store the summary, we can just recreate on demand
|
||||||
UID: r.UID,
|
builder := i.kinds.GetSummaryBuilder(r.Kind)
|
||||||
Kind: r.Kind,
|
if builder != nil {
|
||||||
Body: objVersion.Body,
|
summary, _, e2 := builder(ctx, r.UID, objVersion.Body)
|
||||||
})
|
if e2 != nil {
|
||||||
if e2 != nil {
|
return nil, e2
|
||||||
return nil, e2
|
}
|
||||||
|
rsp.SummaryJson, err = json.Marshal(summary)
|
||||||
}
|
}
|
||||||
rsp.SummaryJson, err = json.Marshal(summary)
|
|
||||||
}
|
}
|
||||||
return rsp, err
|
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) {
|
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{}
|
rsp := &object.WriteObjectResponse{}
|
||||||
|
|
||||||
updatedCount, err := i.collection.Update(ctx, namespace, func(i *RawObjectWithHistory) (bool, *RawObjectWithHistory, error) {
|
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
|
return false, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
modifier := object.UserFromContext(ctx)
|
modifier := store.UserFromContext(ctx)
|
||||||
|
|
||||||
updated := &object.RawObject{
|
updated := &object.RawObject{
|
||||||
UID: r.UID,
|
UID: r.UID,
|
||||||
@ -172,7 +179,7 @@ func (i dummyObjectServer) update(ctx context.Context, r *object.WriteObjectRequ
|
|||||||
Created: i.Object.Created,
|
Created: i.Object.Created,
|
||||||
CreatedBy: i.Object.CreatedBy,
|
CreatedBy: i.Object.CreatedBy,
|
||||||
Updated: time.Now().Unix(),
|
Updated: time.Now().Unix(),
|
||||||
UpdatedBy: object.GetUserIDString(modifier),
|
UpdatedBy: store.GetUserIDString(modifier),
|
||||||
Size: int64(len(r.Body)),
|
Size: int64(len(r.Body)),
|
||||||
ETag: createContentsHash(r.Body),
|
ETag: createContentsHash(r.Body),
|
||||||
Body: 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) {
|
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{
|
rawObj := &object.RawObject{
|
||||||
UID: r.UID,
|
UID: r.UID,
|
||||||
Kind: r.Kind,
|
Kind: r.Kind,
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"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/web"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
@ -22,12 +23,14 @@ type HTTPObjectStore interface {
|
|||||||
type httpObjectStore struct {
|
type httpObjectStore struct {
|
||||||
store ObjectStoreServer
|
store ObjectStoreServer
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
kinds kind.KindRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideHTTPObjectStore(store ObjectStoreServer) HTTPObjectStore {
|
func ProvideHTTPObjectStore(store ObjectStoreServer, kinds kind.KindRegistry) HTTPObjectStore {
|
||||||
return &httpObjectStore{
|
return &httpObjectStore{
|
||||||
store: store,
|
store: store,
|
||||||
log: log.New("http-object-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 {
|
if err != nil {
|
||||||
return response.Error(500, "?", err)
|
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 {
|
if rsp.Object != nil && rsp.Object.Body != nil {
|
||||||
// Configure etag support
|
// Configure etag support
|
||||||
currentEtag := rsp.Object.ETag
|
currentEtag := rsp.Object.ETag
|
||||||
@ -129,10 +137,13 @@ func (s *httpObjectStore) doGetRawObject(c *models.ReqContext) response.Response
|
|||||||
http.StatusNotModified, // 304
|
http.StatusNotModified, // 304
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
mime := info.MimeType
|
||||||
|
if mime == "" {
|
||||||
|
mime = "application/json"
|
||||||
|
}
|
||||||
return response.CreateNormalResponse(
|
return response.CreateNormalResponse(
|
||||||
http.Header{
|
http.Header{
|
||||||
"Content-Type": []string{"application/json"}, // TODO, based on kind!!!
|
"Content-Type": []string{mime},
|
||||||
"ETag": []string{currentEtag},
|
"ETag": []string{currentEtag},
|
||||||
},
|
},
|
||||||
rsp.Object.Body,
|
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)
|
fakeUser := fmt.Sprintf("user:%d:%s", testCtx.user.UserID, testCtx.user.Login)
|
||||||
firstVersion := "1"
|
firstVersion := "1"
|
||||||
kind := "dashboard"
|
kind := "dummy"
|
||||||
uid := "my-test-entity"
|
uid := "my-test-entity"
|
||||||
body := []byte("{\"name\":\"John\"}")
|
body := []byte("{\"name\":\"John\"}")
|
||||||
|
|
||||||
@ -348,7 +348,7 @@ func TestObjectServer(t *testing.T) {
|
|||||||
version = append(version, res.Version)
|
version = append(version, res.Version)
|
||||||
}
|
}
|
||||||
require.Equal(t, []string{"my-test-entity", "uid2", "uid3", "uid4"}, uids)
|
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{
|
require.Equal(t, []string{
|
||||||
w1.Object.Version,
|
w1.Object.Version,
|
||||||
w2.Object.Version,
|
w2.Object.Version,
|
||||||
@ -370,7 +370,7 @@ func TestObjectServer(t *testing.T) {
|
|||||||
version = append(version, res.Version)
|
version = append(version, res.Version)
|
||||||
}
|
}
|
||||||
require.Equal(t, []string{"my-test-entity", "uid2"}, uids)
|
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{
|
require.Equal(t, []string{
|
||||||
w1.Object.Version,
|
w1.Object.Version,
|
||||||
w2.Object.Version,
|
w2.Object.Version,
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/filestorage"
|
"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"
|
"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 {
|
func (s *standardStorageService) detectMimeType(ctx context.Context, user *user.SignedInUser, uploadRequest *UploadRequest) string {
|
||||||
if strings.HasSuffix(uploadRequest.Path, ".svg") {
|
if strings.HasSuffix(uploadRequest.Path, ".svg") {
|
||||||
if issvg.IsSVG(uploadRequest.Contents) {
|
if svg.IsSVG(uploadRequest.Contents) {
|
||||||
return "image/svg+xml"
|
return "image/svg+xml"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user