Unified Storage: Make all dashboard fields searchable (#98899)

* wip. adding sprinkles fields.

* some refactoring. Works with sprinkles now.

* exclude top level dashboard hit fields from hit "fields"

* adds unit test for DecodeCell helper

* test can search for specific dashboard fields on bleve index

* adds search handler tests for the fields and tests for fields when transforming the search req to a bleve search req

* fix panic when calling fields.Set() with int32

* adds regression test

* remove unneeded method on test mock client

* fix linter issues

* updates dashboard test data for bleve tests

* remove DASHBOARD_LEGACY_ID from bleve_tests

* dont cast twice

* updates test to sort by dashboard_views_last_1_days

* declare excludedFields outside of function

* fixes sorting by dashboard fields - prepends "fields." to any dashboard fields we try to sort by

* uses map for excludedFields

* expects fields to be array-style url param

* change method name

* fixes failing tests - needed to add column type to mocks
This commit is contained in:
owensmallwood
2025-01-15 10:23:05 -06:00
committed by GitHub
parent 8415089534
commit d00592ffa0
12 changed files with 629 additions and 20 deletions
@@ -12,6 +12,7 @@ import (
"time"
"github.com/google/uuid"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel"
"golang.org/x/exp/slices"
@@ -71,6 +72,16 @@ var (
tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/dashboards/service")
)
var (
excludedFields = map[string]string{
resource.SEARCH_FIELD_EXPLAIN: "",
resource.SEARCH_FIELD_SCORE: "",
resource.SEARCH_FIELD_TITLE: "",
resource.SEARCH_FIELD_FOLDER: "",
resource.SEARCH_FIELD_TAGS: "",
}
)
type DashboardServiceImpl struct {
cfg *setting.Cfg
log log.Logger
@@ -1949,11 +1960,11 @@ func ParseResults(result *resource.ResourceSearchResponse, offset int64) (*v0alp
explainIDX = i
case resource.SEARCH_FIELD_SCORE:
scoreIDX = i
case "title":
case resource.SEARCH_FIELD_TITLE:
titleIDX = i
case "folder":
case resource.SEARCH_FIELD_FOLDER:
folderIDX = i
case "tags":
case resource.SEARCH_FIELD_TAGS:
tagsIDX = i
}
}
@@ -1967,11 +1978,28 @@ func ParseResults(result *resource.ResourceSearchResponse, offset int64) (*v0alp
}
for i, row := range result.Results.Rows {
fields := &common.Unstructured{}
for colIndex, col := range result.Results.Columns {
if _, ok := excludedFields[col.Name]; ok {
val, err := resource.DecodeCell(col, colIndex, row.Cells[colIndex])
if err != nil {
return nil, err
}
// Some of the dashboard fields come in as int32, but we need to convert them to int64 or else fields.Set() will panic
int32Val, ok := val.(int32)
if ok {
val = int64(int32Val)
}
fields.Set(col.Name, val)
}
}
hit := &v0alpha1.DashboardHit{
Resource: row.Key.Resource, // folders | dashboards
Name: row.Key.Name, // The Grafana UID
Title: string(row.Cells[titleIDX]),
Folder: string(row.Cells[folderIDX]),
Field: fields,
}
if tagsIDX > 0 && row.Cells[tagsIDX] != nil {
_ = json.Unmarshal(row.Cells[tagsIDX], &hit.Tags)
@@ -7,6 +7,7 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/storage/unified/search"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@@ -417,9 +418,11 @@ func TestGetDashboard(t *testing.T) {
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "title",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "folder",
Type: resource.ResourceTableColumnDefinition_STRING,
},
},
Rows: []*resource.ResourceTableRow{
@@ -636,9 +639,11 @@ func TestGetProvisionedDashboardData(t *testing.T) {
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "title",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "folder",
Type: resource.ResourceTableColumnDefinition_STRING,
},
},
Rows: []*resource.ResourceTableRow{
@@ -729,9 +734,11 @@ func TestGetProvisionedDashboardDataByDashboardID(t *testing.T) {
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "title",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "folder",
Type: resource.ResourceTableColumnDefinition_STRING,
},
},
Rows: []*resource.ResourceTableRow{
@@ -810,9 +817,11 @@ func TestGetProvisionedDashboardDataByDashboardUID(t *testing.T) {
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "title",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "folder",
Type: resource.ResourceTableColumnDefinition_STRING,
},
},
Rows: []*resource.ResourceTableRow{
@@ -917,9 +926,11 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "title",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "folder",
Type: resource.ResourceTableColumnDefinition_STRING,
},
},
Rows: []*resource.ResourceTableRow{
@@ -946,9 +957,11 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "title",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "folder",
Type: resource.ResourceTableColumnDefinition_STRING,
},
},
Rows: []*resource.ResourceTableRow{
@@ -1039,9 +1052,11 @@ func TestUnprovisionDashboard(t *testing.T) {
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "title",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "folder",
Type: resource.ResourceTableColumnDefinition_STRING,
},
},
Rows: []*resource.ResourceTableRow{
@@ -1095,9 +1110,11 @@ func TestGetDashboardsByPluginID(t *testing.T) {
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "title",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "folder",
Type: resource.ResourceTableColumnDefinition_STRING,
},
},
Rows: []*resource.ResourceTableRow{
@@ -1303,9 +1320,11 @@ func TestDeleteDashboard(t *testing.T) {
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "title",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "folder",
Type: resource.ResourceTableColumnDefinition_STRING,
},
},
Rows: []*resource.ResourceTableRow{
@@ -1422,12 +1441,15 @@ func TestSearchDashboards(t *testing.T) {
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "title",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "folder",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "tags",
Type: resource.ResourceTableColumnDefinition_STRING,
},
},
Rows: []*resource.ResourceTableRow{
@@ -1521,9 +1543,11 @@ func TestGetDashboards(t *testing.T) {
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "title",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "folder",
Type: resource.ResourceTableColumnDefinition_STRING,
},
},
Rows: []*resource.ResourceTableRow{
@@ -1599,9 +1623,11 @@ func TestGetDashboardUIDByID(t *testing.T) {
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "title",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "folder",
Type: resource.ResourceTableColumnDefinition_STRING,
},
},
Rows: []*resource.ResourceTableRow{
@@ -1877,3 +1903,47 @@ func TestToUID(t *testing.T) {
assert.Equal(t, "", result)
})
}
// regression test - parsing int32 values from search results was causing a panic
func TestParseResults(t *testing.T) {
resSearchResp := &resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
{
Name: "title",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: "folder",
Type: resource.ResourceTableColumnDefinition_STRING,
},
{
Name: search.DASHBOARD_ERRORS_LAST_1_DAYS,
Type: resource.ResourceTableColumnDefinition_INT64,
},
{
Name: search.DASHBOARD_LINK_COUNT,
Type: resource.ResourceTableColumnDefinition_INT32,
},
},
Rows: []*resource.ResourceTableRow{
{
Key: &resource.ResourceKey{
Name: "uid",
Resource: "dashboard",
},
Cells: [][]byte{
[]byte("Dashboard 1"),
[]byte("folder1"),
[]byte("100"),
[]byte("25"),
},
},
},
},
TotalHits: 1,
}
_, err := ParseResults(resSearchResp, 0)
require.NoError(t, err)
}