Search: include panel titles and types in index (#115742)
This commit is contained in:
@@ -142,6 +142,24 @@ func (s *SearchHandler) GetAPIRoutes(defs map[string]common.OpenAPIDefinition) *
|
||||
Schema: spec.StringProperty(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "panelType",
|
||||
In: "query",
|
||||
Description: "find dashboards using panels of a given plugin type",
|
||||
Required: false,
|
||||
Schema: spec.StringProperty(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "dataSourceType",
|
||||
In: "query",
|
||||
Description: "find dashboards using datasources of a given plugin type",
|
||||
Required: false,
|
||||
Schema: spec.StringProperty(),
|
||||
},
|
||||
},
|
||||
{
|
||||
ParameterProps: spec3.ParameterProps{
|
||||
Name: "permission",
|
||||
@@ -430,14 +448,11 @@ func convertHttpSearchRequestToResourceSearchRequest(queryParams url.Values, use
|
||||
}
|
||||
}
|
||||
|
||||
// The facet term fields
|
||||
// Apply facet terms
|
||||
if facets, ok := queryParams["facet"]; ok {
|
||||
if queryParams.Has("facetLimit") {
|
||||
if parsed, err := strconv.Atoi(queryParams.Get("facetLimit")); err == nil && parsed > 0 {
|
||||
facetLimit = parsed
|
||||
if facetLimit > 1000 {
|
||||
facetLimit = 1000
|
||||
}
|
||||
facetLimit = min(parsed, 1000)
|
||||
}
|
||||
}
|
||||
searchRequest.Facet = make(map[string]*resourcepb.ResourceSearchRequest_Facet)
|
||||
@@ -449,21 +464,35 @@ func convertHttpSearchRequestToResourceSearchRequest(queryParams url.Values, use
|
||||
}
|
||||
}
|
||||
|
||||
// The tags filter
|
||||
if tags, ok := queryParams["tag"]; ok {
|
||||
if v, ok := queryParams["tag"]; ok {
|
||||
searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{
|
||||
Key: "tags",
|
||||
Operator: "=",
|
||||
Values: tags,
|
||||
Values: v,
|
||||
})
|
||||
}
|
||||
|
||||
// The libraryPanel filter
|
||||
if libraryPanel, ok := queryParams["libraryPanel"]; ok {
|
||||
if v, ok := queryParams["panelType"]; ok {
|
||||
searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{
|
||||
Key: resource.SEARCH_FIELD_PREFIX + builders.DASHBOARD_PANEL_TYPES,
|
||||
Operator: "=",
|
||||
Values: v,
|
||||
})
|
||||
}
|
||||
|
||||
if v, ok := queryParams["dataSourceType"]; ok {
|
||||
searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{
|
||||
Key: resource.SEARCH_FIELD_PREFIX + builders.DASHBOARD_DS_TYPES,
|
||||
Operator: "=",
|
||||
Values: v,
|
||||
})
|
||||
}
|
||||
|
||||
if v, ok := queryParams["libraryPanel"]; ok {
|
||||
searchRequest.Options.Fields = append(searchRequest.Options.Fields, &resourcepb.Requirement{
|
||||
Key: builders.DASHBOARD_LIBRARY_PANEL_REFERENCE,
|
||||
Operator: "=",
|
||||
Values: libraryPanel,
|
||||
Values: v,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,9 @@ func (d *DsLookup) ByRef(ref *DataSourceRef) *DataSourceRef {
|
||||
if ref == nil {
|
||||
return d.defaultDS
|
||||
}
|
||||
if ref.UID == "default" && ref.Type == "" {
|
||||
return d.defaultDS
|
||||
}
|
||||
|
||||
key := ""
|
||||
if ref.UID != "" {
|
||||
@@ -117,7 +120,13 @@ func (d *DsLookup) ByRef(ref *DataSourceRef) *DataSourceRef {
|
||||
return ds
|
||||
}
|
||||
|
||||
return d.byName[key]
|
||||
ds, ok = d.byName[key]
|
||||
if ok {
|
||||
return ds
|
||||
}
|
||||
|
||||
// With nothing was found (or configured), use the original reference
|
||||
return ref
|
||||
}
|
||||
|
||||
func (d *DsLookup) ByType(dsType string) []DataSourceRef {
|
||||
|
||||
+4
-4
@@ -4,8 +4,8 @@
|
||||
"tags": null,
|
||||
"datasource": [
|
||||
{
|
||||
"uid": "default.uid",
|
||||
"type": "default.type"
|
||||
"uid": "000000001",
|
||||
"type": "graphite"
|
||||
}
|
||||
],
|
||||
"panels": [
|
||||
@@ -16,8 +16,8 @@
|
||||
"libraryPanel": "dfkljg98345dkf",
|
||||
"datasource": [
|
||||
{
|
||||
"uid": "default.uid",
|
||||
"type": "default.type"
|
||||
"uid": "000000001",
|
||||
"type": "graphite"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package dashboard
|
||||
|
||||
import "iter"
|
||||
|
||||
type PanelSummaryInfo struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
@@ -30,3 +32,20 @@ type DashboardSummaryInfo struct {
|
||||
Refresh string `json:"refresh,omitempty"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"` // editable = false
|
||||
}
|
||||
|
||||
func (d *DashboardSummaryInfo) PanelIterator() iter.Seq[PanelSummaryInfo] {
|
||||
return func(yield func(PanelSummaryInfo) bool) {
|
||||
for _, p := range d.Panels {
|
||||
if len(p.Collapsed) > 0 {
|
||||
for _, c := range p.Collapsed {
|
||||
if !yield(c) { // NOTE, rows can only be one level deep!
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if !yield(p) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1253,21 +1253,23 @@ func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resourcepb.R
|
||||
queryExact.SetField(resource.SEARCH_FIELD_TITLE)
|
||||
queryExact.Analyzer = keyword.Name // don't analyze the query input - treat it as a single token
|
||||
queryExact.Operator = query.MatchQueryOperatorAnd // This doesn't make a difference for keyword analyzer, we add it just to be explicit.
|
||||
searchQuery := bleve.NewDisjunctionQuery(queryExact)
|
||||
|
||||
// Query 2: Phrase query with standard analyzer
|
||||
queryPhrase := bleve.NewMatchPhraseQuery(req.Query)
|
||||
queryPhrase.SetBoost(5.0)
|
||||
queryPhrase.SetField(resource.SEARCH_FIELD_TITLE)
|
||||
queryPhrase.Analyzer = standard.Name
|
||||
searchQuery.AddQuery(queryPhrase)
|
||||
|
||||
// Query 3: Match query with standard analyzer
|
||||
queryAnalyzed := bleve.NewMatchQuery(removeSmallTerms(req.Query))
|
||||
queryAnalyzed.SetField(resource.SEARCH_FIELD_TITLE)
|
||||
queryAnalyzed.SetBoost(2.0)
|
||||
queryAnalyzed.Analyzer = standard.Name
|
||||
queryAnalyzed.Operator = query.MatchQueryOperatorAnd // Make sure all terms from the query are matched
|
||||
searchQuery.AddQuery(queryAnalyzed)
|
||||
|
||||
// At least one of the queries must match
|
||||
searchQuery := bleve.NewDisjunctionQuery(queryExact, queryAnalyzed, queryPhrase)
|
||||
queries = append(queries, searchQuery)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"go.uber.org/goleak"
|
||||
|
||||
authlib "github.com/grafana/authlib/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
const DASHBOARD_SCHEMA_VERSION = "schema_version"
|
||||
const DASHBOARD_LINK_COUNT = "link_count"
|
||||
const DASHBOARD_PANEL_TYPES = "panel_types"
|
||||
const DASHBOARD_PANEL_TITLE = "panel_title"
|
||||
const DASHBOARD_DS_TYPES = "ds_types"
|
||||
const DASHBOARD_TRANSFORMATIONS = "transformation"
|
||||
const DASHBOARD_LIBRARY_PANEL_REFERENCE = "reference.LibraryPanel"
|
||||
@@ -53,11 +55,21 @@ func DashboardBuilder(namespaced resource.NamespacedDocumentSupplier) (resource.
|
||||
Type: resourcepb.ResourceTableColumnDefinition_INT32,
|
||||
Description: "How many links appear on the page",
|
||||
},
|
||||
{
|
||||
Name: DASHBOARD_PANEL_TITLE,
|
||||
Type: resourcepb.ResourceTableColumnDefinition_STRING,
|
||||
IsArray: true,
|
||||
Description: "The panel title text",
|
||||
Properties: &resourcepb.ResourceTableColumnDefinition_Properties{
|
||||
Filterable: false, // full text
|
||||
FreeText: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: DASHBOARD_PANEL_TYPES,
|
||||
Type: resourcepb.ResourceTableColumnDefinition_STRING,
|
||||
IsArray: true,
|
||||
Description: "How many links appear on the page",
|
||||
Description: "The panel types used in this dashboard",
|
||||
Properties: &resourcepb.ResourceTableColumnDefinition_Properties{
|
||||
Filterable: true,
|
||||
},
|
||||
@@ -269,14 +281,22 @@ func (s *DashboardDocumentBuilder) BuildDocument(ctx context.Context, key *resou
|
||||
doc.Description = summary.Description
|
||||
doc.Tags = summary.Tags
|
||||
|
||||
panelTitles := []string{}
|
||||
panelTypes := []string{}
|
||||
transformations := []string{}
|
||||
dsTypes := []string{}
|
||||
|
||||
for _, p := range summary.Panels {
|
||||
if p.Type != "" {
|
||||
for p := range summary.PanelIterator() {
|
||||
switch p.Type {
|
||||
case "": // ignore
|
||||
case "row": // row should map to a layout type when we support v2 constructs
|
||||
default:
|
||||
panelTypes = append(panelTypes, p.Type)
|
||||
}
|
||||
|
||||
if len(p.Title) > 0 {
|
||||
panelTitles = append(panelTitles, p.Title)
|
||||
}
|
||||
if len(p.Transformer) > 0 {
|
||||
transformations = append(transformations, p.Transformer...)
|
||||
}
|
||||
@@ -309,17 +329,20 @@ func (s *DashboardDocumentBuilder) BuildDocument(ctx context.Context, key *resou
|
||||
resource.SEARCH_FIELD_LEGACY_ID: summary.ID,
|
||||
}
|
||||
|
||||
if len(panelTitles) > 0 {
|
||||
doc.Fields[DASHBOARD_PANEL_TITLE] = panelTitles
|
||||
}
|
||||
if len(panelTypes) > 0 {
|
||||
sort.Strings(panelTypes)
|
||||
doc.Fields[DASHBOARD_PANEL_TYPES] = panelTypes
|
||||
doc.Fields[DASHBOARD_PANEL_TYPES] = slices.Compact(panelTypes) // distinct values
|
||||
}
|
||||
if len(dsTypes) > 0 {
|
||||
sort.Strings(dsTypes)
|
||||
doc.Fields[DASHBOARD_DS_TYPES] = dsTypes
|
||||
doc.Fields[DASHBOARD_DS_TYPES] = slices.Compact(dsTypes) // distinct values
|
||||
}
|
||||
if len(transformations) > 0 {
|
||||
sort.Strings(transformations)
|
||||
doc.Fields[DASHBOARD_TRANSFORMATIONS] = transformations
|
||||
doc.Fields[DASHBOARD_TRANSFORMATIONS] = slices.Compact(transformations) // distinct values
|
||||
}
|
||||
|
||||
for k, v := range s.Stats[summary.UID] {
|
||||
|
||||
@@ -32,10 +32,16 @@
|
||||
"errors_last_7_days": 1,
|
||||
"grafana.app/deprecatedInternalID": 141,
|
||||
"link_count": 0,
|
||||
"panel_title": [
|
||||
"green pie",
|
||||
"red pie",
|
||||
"blue pie",
|
||||
"collapsed row"
|
||||
],
|
||||
"panel_types": [
|
||||
"barchart",
|
||||
"graph",
|
||||
"row"
|
||||
"pie"
|
||||
],
|
||||
"schema_version": 38
|
||||
},
|
||||
@@ -46,6 +52,12 @@
|
||||
"kind": "DataSource",
|
||||
"name": "DSUID"
|
||||
},
|
||||
{
|
||||
"relation": "depends-on",
|
||||
"group": "dashboards.grafana.app",
|
||||
"kind": "LibraryPanel",
|
||||
"name": "l3d2s634-fdgf-75u4-3fg3-67j966ii7jur"
|
||||
},
|
||||
{
|
||||
"relation": "depends-on",
|
||||
"group": "dashboards.grafana.app",
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"name": "red pie",
|
||||
"uid": "e1d5f519-dabd-47c6-9ad7-83d181ce1cee"
|
||||
},
|
||||
"title": "green pie"
|
||||
"title": "red pie"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
@@ -78,6 +78,14 @@
|
||||
"id": 8,
|
||||
"type": "graph"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "graph"
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"type": "graph"
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"gridPos": {
|
||||
@@ -101,6 +109,10 @@
|
||||
"uid": "l3d2s634-fdgf-75u4-3fg3-67j966ii7jur"
|
||||
},
|
||||
"title": "blue pie"
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"type": "pie"
|
||||
}
|
||||
],
|
||||
"title": "collapsed row",
|
||||
|
||||
+11
-1
@@ -71,11 +71,18 @@
|
||||
"description": "How many links appear on the page",
|
||||
"priority": 0
|
||||
},
|
||||
{
|
||||
"name": "panel_title",
|
||||
"type": "string",
|
||||
"format": "",
|
||||
"description": "The panel title text",
|
||||
"priority": 0
|
||||
},
|
||||
{
|
||||
"name": "panel_types",
|
||||
"type": "string",
|
||||
"format": "",
|
||||
"description": "How many links appear on the page",
|
||||
"description": "The panel types used in this dashboard",
|
||||
"priority": 0
|
||||
},
|
||||
{
|
||||
@@ -214,6 +221,7 @@
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"object": {
|
||||
@@ -239,6 +247,7 @@
|
||||
"repo",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
[
|
||||
"timeseries"
|
||||
],
|
||||
@@ -282,6 +291,7 @@
|
||||
"repo",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
[
|
||||
"timeseries",
|
||||
"table"
|
||||
|
||||
@@ -4,10 +4,15 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
@@ -16,12 +21,167 @@ import (
|
||||
dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tests/apis"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
"github.com/grafana/grafana/pkg/util/testutil"
|
||||
)
|
||||
|
||||
func TestIntegrationSearchDevDashboards(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
ctx := context.Background()
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
DisableDataMigrations: true,
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode5},
|
||||
"folders.folder.grafana.app": {DualWriterMode: rest.Mode5},
|
||||
},
|
||||
UnifiedStorageEnableSearch: true,
|
||||
})
|
||||
defer helper.Shutdown()
|
||||
|
||||
// Create devenv dashboards from legacy API
|
||||
cfg := dynamic.ConfigFor(helper.Org1.Admin.NewRestConfig())
|
||||
cfg.GroupVersion = &dashboardV0.GroupVersion
|
||||
adminClient, err := k8srest.RESTClientFor(cfg)
|
||||
require.NoError(t, err)
|
||||
adminClient.Get()
|
||||
|
||||
fileCount := 0
|
||||
devenv := "../../../../devenv/dev-dashboards/panel-timeseries"
|
||||
err = filepath.WalkDir(devenv, func(p string, d fs.DirEntry, e error) error {
|
||||
require.NoError(t, err)
|
||||
if d.IsDir() || filepath.Ext(d.Name()) != ".json" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// use the filename as UID
|
||||
uid := strings.TrimSuffix(d.Name(), ".json")
|
||||
if len(uid) > 40 {
|
||||
uid = uid[:40] // avoid uid too long, max 40 characters
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
data, err := os.ReadFile(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := dashboards.SaveDashboardCommand{
|
||||
Dashboard: &simplejson.Json{},
|
||||
Overwrite: true,
|
||||
}
|
||||
err = cmd.Dashboard.FromDB(data)
|
||||
require.NoError(t, err)
|
||||
cmd.Dashboard.Set("id", nil)
|
||||
cmd.Dashboard.Set("uid", uid)
|
||||
data, err = json.Marshal(cmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
var statusCode int
|
||||
result := adminClient.Post().AbsPath("api", "dashboards", "db").
|
||||
Body(data).
|
||||
SetHeader("Content-type", "application/json").
|
||||
Do(ctx).
|
||||
StatusCode(&statusCode)
|
||||
require.NoError(t, result.Error(), "file: [%d] %s [status:%d]", fileCount, d.Name(), statusCode)
|
||||
require.Equal(t, int(http.StatusOK), statusCode)
|
||||
fileCount++
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 16, fileCount, "file count from %s", devenv)
|
||||
|
||||
// Helper to call search
|
||||
callSearch := func(user apis.User, params string) dashboardV0.SearchResults {
|
||||
require.NotNil(t, user)
|
||||
ns := user.Identity.GetNamespace()
|
||||
cfg := dynamic.ConfigFor(user.NewRestConfig())
|
||||
cfg.GroupVersion = &dashboardV0.GroupVersion
|
||||
restClient, err := k8srest.RESTClientFor(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
var statusCode int
|
||||
req := restClient.Get().AbsPath("apis", "dashboard.grafana.app", "v0alpha1", "namespaces", ns, "search").
|
||||
Param("limit", "1000").
|
||||
Param("type", "dashboard") // Only search dashboards
|
||||
|
||||
for kv := range strings.SplitSeq(params, "&") {
|
||||
if kv == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(kv, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
req = req.Param(parts[0], parts[1])
|
||||
}
|
||||
}
|
||||
res := req.Do(ctx).StatusCode(&statusCode)
|
||||
require.NoError(t, res.Error())
|
||||
require.Equal(t, int(http.StatusOK), statusCode)
|
||||
var sr dashboardV0.SearchResults
|
||||
raw, err := res.Raw()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, json.Unmarshal(raw, &sr))
|
||||
|
||||
// Normalize scores and query cost for snapshot comparison
|
||||
sr.QueryCost = 0 // this depends on the hardware
|
||||
sr.MaxScore = roundTo(sr.MaxScore, 3)
|
||||
for i := range sr.Hits {
|
||||
sr.Hits[i].Score = roundTo(sr.Hits[i].Score, 3) // 0.6250571494814442 -> 0.625
|
||||
}
|
||||
return sr
|
||||
}
|
||||
|
||||
// Compare a results to snapshots
|
||||
testCases := []struct {
|
||||
name string
|
||||
user apis.User
|
||||
params string
|
||||
}{
|
||||
{
|
||||
name: "all",
|
||||
user: helper.Org1.Admin,
|
||||
params: "", // only dashboards
|
||||
},
|
||||
{
|
||||
name: "simple-query",
|
||||
user: helper.Org1.Admin,
|
||||
params: "query=stacking",
|
||||
},
|
||||
{
|
||||
name: "with-text-panel",
|
||||
user: helper.Org1.Admin,
|
||||
params: "field=panel_types&panelType=text",
|
||||
},
|
||||
}
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res := callSearch(tc.user, tc.params)
|
||||
jj, err := json.MarshalIndent(res, "", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
fname := fmt.Sprintf("testdata/searchV0/t%02d-%s.json", i, tc.name)
|
||||
// nolint:gosec
|
||||
snapshot, err := os.ReadFile(fname)
|
||||
if err != nil {
|
||||
assert.Failf(t, "Failed to read snapshot", "file: %s", fname)
|
||||
err = os.WriteFile(fname, jj, 0o644)
|
||||
require.NoErrorf(t, err, "Failed to write snapshot file %s", fname)
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.JSONEq(t, string(snapshot), string(jj)) {
|
||||
err = os.WriteFile(fname, jj, 0o644)
|
||||
require.NoErrorf(t, err, "Failed to write snapshot file %s", fname)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationSearchPermissionFiltering(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
@@ -285,3 +445,11 @@ func setFolderPermissions(t *testing.T, helper *apis.K8sTestHelper, actingUser a
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.Response.StatusCode, "Failed to set permissions for folder %s", folderUID)
|
||||
}
|
||||
|
||||
// roundTo rounds a float64 to a specified number of decimal places.
|
||||
func roundTo(n float64, decimals uint32) float64 {
|
||||
// Calculate the power of 10 for the desired number of decimals
|
||||
scale := math.Pow(10, float64(decimals))
|
||||
// Multiply, round to the nearest integer, and then divide back
|
||||
return math.Round(n*scale) / scale
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
{
|
||||
"totalHits": 16,
|
||||
"hits": [
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries",
|
||||
"title": "Panel Tests - Graph NG",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-by-value-color-schemes",
|
||||
"title": "Panel Tests - Graph NG - By value color schemes",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-nulls",
|
||||
"title": "Panel Tests - Graph NG - Discrete panels",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng",
|
||||
"timeseries",
|
||||
"trend",
|
||||
"state-timeline",
|
||||
"transform"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-gradient-area",
|
||||
"title": "Panel Tests - Graph NG - Gradient Area Fills",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-soft-limits",
|
||||
"title": "Panel Tests - Graph NG - softMin/softMax",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-yaxis-ticks",
|
||||
"title": "Panel Tests - Graph NG - Y axis ticks",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-hue-gradients",
|
||||
"title": "Panel Tests - GraphNG - Hue Gradients",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-time",
|
||||
"title": "Panel Tests - GraphNG - Time Axis",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-thresholds",
|
||||
"title": "Panel Tests - GraphNG Thresholds",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-shared-tooltip-cursor-positio",
|
||||
"title": "Panel Tests - shared tooltips cursor positioning",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-bars-high-density",
|
||||
"title": "Panel Tests - TimeSeries - bars high density (stroke + fill)",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-out-of-rage",
|
||||
"title": "Panel Tests - Timeseries - Out of range",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-stacking",
|
||||
"title": "Panel Tests - TimeSeries - stacking",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-formats",
|
||||
"title": "Panel Tests - Timeseries - Supported input formats"
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-stacking2",
|
||||
"title": "TimeSeries \u0026 BarChart Stacking",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
]
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-y-ticks-zero-decimals",
|
||||
"title": "Zero Decimals Y Ticks",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
]
|
||||
}
|
||||
],
|
||||
"maxScore": 1
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"totalHits": 2,
|
||||
"hits": [
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-stacking",
|
||||
"title": "Panel Tests - TimeSeries - stacking",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
],
|
||||
"score": 0.658
|
||||
},
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-stacking2",
|
||||
"title": "TimeSeries \u0026 BarChart Stacking",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"graph-ng"
|
||||
],
|
||||
"score": 0.625
|
||||
}
|
||||
],
|
||||
"maxScore": 0.658
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"totalHits": 1,
|
||||
"hits": [
|
||||
{
|
||||
"resource": "dashboards",
|
||||
"name": "timeseries-formats",
|
||||
"title": "Panel Tests - Timeseries - Supported input formats",
|
||||
"field": {
|
||||
"panel_types": [
|
||||
"table",
|
||||
"text",
|
||||
"timeseries"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"maxScore": 1.778
|
||||
}
|
||||
@@ -1830,6 +1830,22 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "panelType",
|
||||
"in": "query",
|
||||
"description": "find dashboards using panels of a given plugin type",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dataSourceType",
|
||||
"in": "query",
|
||||
"description": "find dashboards using datasources of a given plugin type",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "permission",
|
||||
"in": "query",
|
||||
|
||||
Reference in New Issue
Block a user