Search: Implement basic improved UI (#46758)

This commit is contained in:
Nathan Marrs
2022-03-30 09:50:32 -07:00
committed by GitHub
parent 851c54b3b1
commit 4449439a41
24 changed files with 1334 additions and 351 deletions

View File

@@ -12,7 +12,7 @@ func logf(format string, a ...interface{}) {
// nolint:gocyclo
// ReadDashboard will take a byte stream and return dashboard info
func ReadDashboard(stream io.Reader, datasource DatasourceLookup) *DashboardInfo {
func ReadDashboard(stream io.Reader, lookup DatasourceLookup) *DashboardInfo {
iter := jsoniter.Parse(jsoniter.ConfigDefault, stream, 1024)
dash := &DashboardInfo{}
@@ -73,7 +73,7 @@ func ReadDashboard(stream io.Reader, datasource DatasourceLookup) *DashboardInfo
case "panels":
for iter.ReadArray() {
dash.Panels = append(dash.Panels, readPanelInfo(iter))
dash.Panels = append(dash.Panels, readPanelInfo(iter, lookup))
}
case "rows":
@@ -129,16 +129,29 @@ func ReadDashboard(stream io.Reader, datasource DatasourceLookup) *DashboardInfo
logf("All dashbaords should have a UID defined")
}
targets := newTargetInfo(lookup)
for _, panel := range dash.Panels {
targets.addPanel(panel)
}
dash.Datasource = targets.GetDatasourceInfo()
return dash
}
// will always return strings for now
func readPanelInfo(iter *jsoniter.Iterator) PanelInfo {
func readPanelInfo(iter *jsoniter.Iterator, lookup DatasourceLookup) PanelInfo {
panel := PanelInfo{}
targets := newTargetInfo(lookup)
for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() {
// Skip null values so we don't need special int handling
if iter.WhatIsNext() == jsoniter.NilValue {
if l1Field == "datasource" {
targets.addDatasource(iter)
continue
}
// Skip null values so we don't need special int handling
iter.Skip()
continue
}
@@ -160,13 +173,11 @@ func readPanelInfo(iter *jsoniter.Iterator) PanelInfo {
panel.PluginVersion = iter.ReadString() // since 7x (the saved version for the plugin model)
case "datasource":
v := iter.Read()
logf(">>Panel.datasource = %v\n", v) // string or object!!!
targets.addDatasource(iter)
case "targets":
for iter.ReadArray() {
v := iter.Read()
logf("[Panel.TARGET] %v\n", v)
targets.addTarget(iter)
}
case "transformations":
@@ -183,7 +194,7 @@ func readPanelInfo(iter *jsoniter.Iterator) PanelInfo {
// Rows have nested panels
case "panels":
for iter.ReadArray() {
panel.Collapsed = append(panel.Collapsed, readPanelInfo(iter))
panel.Collapsed = append(panel.Collapsed, readPanelInfo(iter, lookup))
}
case "options":
@@ -201,5 +212,7 @@ func readPanelInfo(iter *jsoniter.Iterator) PanelInfo {
}
}
panel.Datasource = targets.GetDatasourceInfo()
return panel
}

View File

@@ -12,19 +12,36 @@ import (
func TestReadDashboard(t *testing.T) {
inputs := []string{
"all-panels.json",
"panel-graph/graph-shared-tooltips.json",
"check-string-datasource-id",
"all-panels",
"panel-graph/graph-shared-tooltips",
}
// key will allow name or uid
ds := func(key string) *DatasourceInfo {
return nil // TODO!
ds := func(ref *DataSourceRef) *DataSourceRef {
if ref == nil || ref.UID == "" {
return &DataSourceRef{
UID: "default.uid",
Type: "default.type",
}
}
return ref
}
devdash := "../../../../devenv/dev-dashboards/"
for _, input := range inputs {
// nolint:gosec
// We can ignore the gosec G304 warning because this is a test with hardcoded input values
f, err := os.Open("../../../../devenv/dev-dashboards/" + input)
f, err := os.Open(filepath.Join(devdash, input) + ".json")
if err == nil {
input = "devdash-" + filepath.Base(input)
}
if err != nil {
// nolint:gosec
// We can ignore the gosec G304 warning because this is a test with hardcoded input values
f, err = os.Open(filepath.Join("testdata", input) + ".json")
}
require.NoError(t, err)
dash := ReadDashboard(f, ds)
@@ -32,7 +49,7 @@ func TestReadDashboard(t *testing.T) {
require.NoError(t, err)
update := false
savedPath := "testdata/" + filepath.Base(input)
savedPath := "testdata/" + input + "-info.json"
saved, err := os.ReadFile(savedPath)
if err != nil {
update = true

View File

@@ -0,0 +1,81 @@
package extract
import (
jsoniter "github.com/json-iterator/go"
)
type targetInfo struct {
lookup DatasourceLookup
uids map[string]*DataSourceRef
}
func newTargetInfo(lookup DatasourceLookup) targetInfo {
return targetInfo{
lookup: lookup,
uids: make(map[string]*DataSourceRef),
}
}
func (s *targetInfo) GetDatasourceInfo() []DataSourceRef {
keys := make([]DataSourceRef, len(s.uids))
i := 0
for _, v := range s.uids {
keys[i] = *v
i++
}
return keys
}
// the node will either be string (name|uid) OR ref
func (s *targetInfo) addDatasource(iter *jsoniter.Iterator) {
switch iter.WhatIsNext() {
case jsoniter.StringValue:
key := iter.ReadString()
ds := s.lookup(&DataSourceRef{UID: key})
s.addRef(ds)
case jsoniter.NilValue:
s.addRef(s.lookup(nil))
iter.Skip()
case jsoniter.ObjectValue:
ref := &DataSourceRef{}
iter.ReadVal(ref)
ds := s.lookup(ref)
s.addRef(ds)
default:
v := iter.Read()
logf("[Panel.datasource.unknown] %v\n", v)
}
}
func (s *targetInfo) addRef(ref *DataSourceRef) {
if ref != nil && ref.UID != "" {
s.uids[ref.UID] = ref
}
}
func (s *targetInfo) addTarget(iter *jsoniter.Iterator) {
for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() {
switch l1Field {
case "datasource":
s.addDatasource(iter)
case "refId":
iter.Skip()
default:
v := iter.Read()
logf("[Panel.TARGET] %s=%v\n", l1Field, v)
}
}
}
func (s *targetInfo) addPanel(panel PanelInfo) {
for idx, v := range panel.Datasource {
if v.UID != "" {
s.uids[v.UID] = &panel.Datasource[idx]
}
}
}

View File

@@ -0,0 +1,29 @@
{
"id": 250,
"uid": "K2X7hzwGk",
"title": "fast streaming",
"tags": null,
"datasource": [
{
"uid": "-- Grafana --"
}
],
"panels": [
{
"id": 3,
"title": "Panel Title",
"type": "timeseries",
"pluginVersion": "7.5.0-pre",
"datasource": [
{
"uid": "-- Grafana --"
}
]
}
],
"schemaVersion": 27,
"linkCount": 0,
"timeFrom": "now-30s",
"timeTo": "now",
"timezone": ""
}

View File

@@ -0,0 +1,67 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations \u0026 Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 250,
"links": [],
"panels": [
{
"datasource": "-- Grafana --",
"fieldConfig": {},
"gridPos": {},
"id": 3,
"options": {
"graph": {},
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltipOptions": {
"mode": "single"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"channel": "stream/telegraf/cpu",
"filter": {
"fields": []
},
"queryType": "measurements",
"refId": "A"
}
],
"title": "Panel Title",
"type": "timeseries"
}
],
"schemaVersion": 27,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-30s",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "fast streaming",
"uid": "K2X7hzwGk",
"version": 8
}

View File

@@ -0,0 +1,122 @@
{
"uid": "TX2VU59MZ",
"title": "Panel Tests - shared tooltips",
"tags": [
"gdev",
"panel-tests",
"graph-ng"
],
"datasource": [
{
"uid": "default.uid",
"type": "default.type"
}
],
"panels": [
{
"id": 4,
"title": "two units",
"type": "timeseries",
"pluginVersion": "7.5.0-pre",
"datasource": [
{
"uid": "default.uid",
"type": "default.type"
}
]
},
{
"id": 13,
"title": "Speed vs Temperature (XY)",
"type": "xychart",
"pluginVersion": "7.5.0-pre",
"datasource": [
{
"uid": "default.uid",
"type": "default.type"
}
],
"transformations": [
"seriesToColumns",
"organize"
]
},
{
"id": 2,
"title": "Cursor info",
"type": "debug",
"pluginVersion": "7.5.0-pre",
"datasource": [
{
"uid": "default.uid",
"type": "default.type"
}
]
},
{
"id": 5,
"title": "Only temperature",
"type": "timeseries",
"pluginVersion": "7.5.0-pre",
"datasource": [
{
"uid": "default.uid",
"type": "default.type"
}
]
},
{
"id": 9,
"title": "Only Speed",
"type": "timeseries",
"pluginVersion": "7.5.0-pre",
"datasource": [
{
"uid": "default.uid",
"type": "default.type"
}
]
},
{
"id": 11,
"title": "Panel Title",
"type": "timeseries",
"pluginVersion": "7.5.0-pre",
"datasource": [
{
"uid": "default.uid",
"type": "default.type"
}
]
},
{
"id": 8,
"title": "flot panel (temperature)",
"type": "graph",
"pluginVersion": "7.5.0-pre",
"datasource": [
{
"uid": "default.uid",
"type": "default.type"
}
]
},
{
"id": 10,
"title": "flot panel (no units)",
"type": "graph",
"pluginVersion": "7.5.0-pre",
"datasource": [
{
"uid": "default.uid",
"type": "default.type"
}
]
}
],
"schemaVersion": 28,
"linkCount": 0,
"timeFrom": "2020-09-14T16:13:20.000Z",
"timeTo": "2020-09-15T20:00:00.000Z",
"timezone": ""
}

View File

@@ -1,68 +0,0 @@
{
"uid": "TX2VU59MZ",
"title": "Panel Tests - shared tooltips",
"tags": [
"gdev",
"panel-tests",
"graph-ng"
],
"panels": [
{
"id": 4,
"title": "two units",
"type": "timeseries",
"pluginVersion": "7.5.0-pre"
},
{
"id": 13,
"title": "Speed vs Temperature (XY)",
"type": "xychart",
"pluginVersion": "7.5.0-pre",
"transformations": [
"seriesToColumns",
"organize"
]
},
{
"id": 2,
"title": "Cursor info",
"type": "debug",
"pluginVersion": "7.5.0-pre"
},
{
"id": 5,
"title": "Only temperature",
"type": "timeseries",
"pluginVersion": "7.5.0-pre"
},
{
"id": 9,
"title": "Only Speed",
"type": "timeseries",
"pluginVersion": "7.5.0-pre"
},
{
"id": 11,
"title": "Panel Title",
"type": "timeseries",
"pluginVersion": "7.5.0-pre"
},
{
"id": 8,
"title": "flot panel (temperature)",
"type": "graph",
"pluginVersion": "7.5.0-pre"
},
{
"id": 10,
"title": "flot panel (no units)",
"type": "graph",
"pluginVersion": "7.5.0-pre"
}
],
"schemaVersion": 28,
"linkCount": 0,
"timeFrom": "2020-09-14T16:13:20.000Z",
"timeTo": "2020-09-15T20:00:00.000Z",
"timezone": ""
}

View File

@@ -1,45 +1,40 @@
package extract
type DatasourceLookup = func(key string) *DatasourceInfo
// empty everything will return the default
type DatasourceLookup = func(ref *DataSourceRef) *DataSourceRef
type DatasourceInfo struct {
UID string `json:"uid"`
Name string `json:"name"`
Type string `json:"type"` // plugin name
Version string `json:"version"`
Access string `json:"access,omitempty"` // proxy, direct, or empty
type DataSourceRef struct {
UID string `json:"uid,omitempty"`
Type string `json:"type,omitempty"`
}
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 []string `json:"datasource,omitempty"` // UIDs
DatasourceType []string `json:"datasourceType,omitempty"` // PluginIDs
Transformations []string `json:"transformations,omitempty"` // ids of the transformation steps
ID int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Type string `json:"type,omitempty"` // PluginID
PluginVersion string `json:"pluginVersion,omitempty"`
Datasource []DataSourceRef `json:"datasource,omitempty"` // UIDs
Transformations []string `json:"transformations,omitempty"` // ids of the transformation steps
// Rows define panels as sub objects
Collapsed []PanelInfo `json:"collapsed,omitempty"`
}
type DashboardInfo struct {
ID int64 `json:"id,omitempty"`
UID string `json:"uid,omitempty"`
Path string `json:"path,omitempty"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Tags []string `json:"tags"` // UIDs
Datasource []string `json:"datasource,omitempty"` // UIDs
DatasourceType []string `json:"datasourceType,omitempty"` // PluginIDs
TemplateVars []string `json:"templateVars,omitempty"` // the keys used
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
ID int64 `json:"id,omitempty"` // internal ID
UID string `json:"uid,omitempty"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Tags []string `json:"tags"`
TemplateVars []string `json:"templateVars,omitempty"` // the keys used
Datasource []DataSourceRef `json:"datasource,omitempty"` // UIDs
Panels []PanelInfo `json:"panels"` // nesed documents
SchemaVersion int64 `json:"schemaVersion"`
LinkCount int64 `json:"linkCount"`
TimeFrom string `json:"timeFrom"`
TimeTo string `json:"timeTo"`
TimeZone string `json:"timezone"`
Refresh string `json:"refresh,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"` // editable = false
}

View File

@@ -89,7 +89,7 @@ func (s *StandardSearchService) applyAuthFilter(user *models.SignedInUser, dash
// create a list of all viewable dashboards for this user
res := make([]dashMeta, 0, len(dash))
for _, dash := range dash {
if filter(dash.dash.UID) {
if filter(dash.dash.UID) || (dash.is_folder && dash.dash.UID == "") { // include the "General" folder
res = append(res, dash)
}
}
@@ -106,15 +106,38 @@ type dashDataQueryResult struct {
Updated time.Time
}
type dsQueryResult struct {
UID string `xorm:"uid"`
Type string `xorm:"type"`
Name string `xorm:"name"`
IsDefault bool `xorm:"is_default"`
}
func loadDashboards(ctx context.Context, orgID int64, sql *sqlstore.SQLStore) ([]dashMeta, error) {
meta := make([]dashMeta, 0, 200)
// Add the root folder ID (does not exist in SQL)
meta = append(meta, dashMeta{
id: 0,
is_folder: true,
folder_id: 0,
slug: "",
created: time.Now(),
updated: time.Now(),
dash: &extract.DashboardInfo{
ID: 0,
UID: "",
Title: "General",
},
})
// key will allow name or uid
lookup := func(key string) *extract.DatasourceInfo {
return nil // TODO!
lookup, err := loadDatasoureLookup(ctx, orgID, sql)
if err != nil {
return meta, err
}
err := sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
err = sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
rows := make([]*dashDataQueryResult, 0)
sess.Table("dashboard").
@@ -146,6 +169,64 @@ func loadDashboards(ctx context.Context, orgID int64, sql *sqlstore.SQLStore) ([
return meta, err
}
func loadDatasoureLookup(ctx context.Context, orgID int64, sql *sqlstore.SQLStore) (extract.DatasourceLookup, error) {
byUID := make(map[string]*extract.DataSourceRef, 50)
byName := make(map[string]*extract.DataSourceRef, 50)
var defaultDS *extract.DataSourceRef
err := sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
rows := make([]*dsQueryResult, 0)
sess.Table("data_source").
Where("org_id = ?", orgID).
Cols("uid", "name", "type", "is_default")
err := sess.Find(&rows)
if err != nil {
return err
}
for _, row := range rows {
ds := &extract.DataSourceRef{
UID: row.UID,
Type: row.Type,
}
byUID[row.UID] = ds
byName[row.Name] = ds
if row.IsDefault {
defaultDS = ds
}
}
return nil
})
if err != nil {
return nil, err
}
// Lookup by UID or name
return func(ref *extract.DataSourceRef) *extract.DataSourceRef {
if ref == nil {
return defaultDS
}
key := ""
if ref.UID != "" {
ds, ok := byUID[ref.UID]
if ok {
return ds
}
key = ref.UID
}
if key == "" {
return defaultDS
}
ds, ok := byUID[key]
if ok {
return ds
}
return byName[key]
}, err
}
type simpleCounter struct {
values map[string]int64
}
@@ -173,10 +254,12 @@ func metaToFrame(meta []dashMeta) data.Frames {
folderID := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
folderUID := data.NewFieldFromFieldType(data.FieldTypeString, 0)
folderName := data.NewFieldFromFieldType(data.FieldTypeString, 0)
folderDashCount := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
folderID.Name = "ID"
folderUID.Name = "UID"
folderName.Name = "Name"
folderID.Name = "id"
folderUID.Name = "uid"
folderName.Name = "name"
folderDashCount.Name = "DashCount"
dashID := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
dashUID := data.NewFieldFromFieldType(data.FieldTypeString, 0)
@@ -188,22 +271,28 @@ func metaToFrame(meta []dashMeta) data.Frames {
dashUpdated := data.NewFieldFromFieldType(data.FieldTypeTime, 0)
dashSchemaVersion := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
dashTags := data.NewFieldFromFieldType(data.FieldTypeNullableString, 0)
dashPanelCount := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
dashVarCount := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
dashDSList := data.NewFieldFromFieldType(data.FieldTypeNullableString, 0)
dashID.Name = "ID"
dashUID.Name = "UID"
dashFolderID.Name = "FolderID"
dashName.Name = "Name"
dashDescr.Name = "Description"
dashTags.Name = "Tags"
dashID.Name = "id"
dashUID.Name = "uid"
dashFolderID.Name = "folderID"
dashName.Name = "name"
dashDescr.Name = "description"
dashTags.Name = "tags"
dashSchemaVersion.Name = "SchemaVersion"
dashCreated.Name = "Created"
dashUpdated.Name = "Updated"
dashURL.Name = "URL"
dashURL.Name = "url"
dashURL.Config = &data.FieldConfig{
Links: []data.DataLink{
{Title: "link", URL: "${__value.text}"},
},
}
dashPanelCount.Name = "panelCount"
dashVarCount.Name = "varCount"
dashDSList.Name = "datasource"
dashTags.Config = &data.FieldConfig{
Custom: map[string]interface{}{
@@ -218,11 +307,11 @@ func metaToFrame(meta []dashMeta) data.Frames {
panelDescr := data.NewFieldFromFieldType(data.FieldTypeString, 0)
panelType := data.NewFieldFromFieldType(data.FieldTypeString, 0)
panelDashID.Name = "DashboardID"
panelID.Name = "ID"
panelName.Name = "Name"
panelDescr.Name = "Description"
panelType.Name = "Type"
panelDashID.Name = "dashboardID"
panelID.Name = "id"
panelName.Name = "name"
panelDescr.Name = "description"
panelType.Name = "type"
panelTypeCounter := simpleCounter{
values: make(map[string]int64, 30),
@@ -232,12 +321,14 @@ func metaToFrame(meta []dashMeta) data.Frames {
values: make(map[string]int64, 30),
}
var tags *string
folderCounter := make(map[int64]int64, 20)
for _, row := range meta {
if row.is_folder {
folderID.Append(row.id)
folderUID.Append(row.dash.UID)
folderName.Append(row.dash.Title)
folderDashCount.Append(int64(0)) // filled in later
continue
}
@@ -250,22 +341,23 @@ func metaToFrame(meta []dashMeta) data.Frames {
dashCreated.Append(row.created)
dashUpdated.Append(row.updated)
// Increment the folder counter
fcount, ok := folderCounter[row.folder_id]
if !ok {
fcount = 0
}
folderCounter[row.folder_id] = fcount + 1
url := fmt.Sprintf("/d/%s/%s", row.dash.UID, row.slug)
dashURL.Append(url)
// stats
schemaVersionCounter.add(strconv.FormatInt(row.dash.SchemaVersion, 10))
// Send tags as JSON array
tags = nil
if len(row.dash.Tags) > 0 {
b, err := json.Marshal(row.dash.Tags)
if err == nil {
s := string(b)
tags = &s
}
}
dashTags.Append(tags)
dashTags.Append(toJSONString(row.dash.Tags))
dashPanelCount.Append(int64(len(row.dash.Panels)))
dashVarCount.Append(int64(len(row.dash.TemplateVars)))
dashDSList.Append(dsAsJSONString(row.dash.Datasource))
// Row for each panel
for _, panel := range row.dash.Panels {
@@ -278,11 +370,47 @@ func metaToFrame(meta []dashMeta) data.Frames {
}
}
// Update the folder counts
for i := 0; i < folderID.Len(); i++ {
id, ok := folderID.At(i).(int64)
if ok {
folderDashCount.Set(i, folderCounter[id])
}
}
return data.Frames{
data.NewFrame("folders", folderID, folderUID, folderName),
data.NewFrame("dashboards", dashID, dashUID, dashURL, dashFolderID, dashName, dashDescr, dashTags, dashSchemaVersion, dashCreated, dashUpdated),
data.NewFrame("folders", folderID, folderUID, folderName, folderDashCount),
data.NewFrame("dashboards", dashID, dashUID, dashURL, dashFolderID,
dashName, dashDescr, dashTags,
dashSchemaVersion,
dashPanelCount, dashVarCount, dashDSList,
dashCreated, dashUpdated),
data.NewFrame("panels", panelDashID, panelID, panelName, panelDescr, panelType),
panelTypeCounter.toFrame("panel-type-counts"),
schemaVersionCounter.toFrame("schema-version-counts"),
}
}
func toJSONString(vals []string) *string {
if len(vals) < 1 {
return nil
}
b, err := json.Marshal(vals)
if err == nil {
s := string(b)
return &s
}
return nil
}
func dsAsJSONString(vals []extract.DataSourceRef) *string {
if len(vals) < 1 {
return nil
}
b, err := json.Marshal(vals)
if err == nil {
s := string(b)
return &s
}
return nil
}