mirror of https://github.com/grafana/grafana.git
fix(dashboards): allow multiple types in search query (#111740)
This commit is contained in:
parent
0ac61983eb
commit
d46e3d0e5a
|
@ -302,20 +302,22 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
searchRequest.Fields = fields
|
searchRequest.Fields = fields
|
||||||
|
|
||||||
// Search dashboards or folders (or both)
|
// A search request can include multiple types, we need to acces the slice directly.
|
||||||
switch queryParams.Get("type") {
|
types := queryParams["type"]
|
||||||
case "folder":
|
hasDash := len(types) == 0 || slices.Contains(types, "dashboard")
|
||||||
searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), folders.RESOURCE)
|
hasFolder := len(types) == 0 || slices.Contains(types, "folder")
|
||||||
case "dashboard":
|
// If both types are present, we need to search both dashboards and folders, by default is nothing is set we also search both.
|
||||||
searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), dashboardv0alpha1.DASHBOARD_RESOURCE)
|
if (hasDash && hasFolder) || (!hasDash && !hasFolder) {
|
||||||
default:
|
|
||||||
searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), dashboardv0alpha1.DASHBOARD_RESOURCE)
|
searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), dashboardv0alpha1.DASHBOARD_RESOURCE)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
federate, _ := asResourceKey(user.GetNamespace(), folders.RESOURCE)
|
if federate, _ := asResourceKey(user.GetNamespace(), folders.RESOURCE); federate != nil {
|
||||||
if federate != nil {
|
|
||||||
searchRequest.Federated = []*resourcepb.ResourceKey{federate}
|
searchRequest.Federated = []*resourcepb.ResourceKey{federate}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if hasFolder {
|
||||||
|
searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), folders.RESOURCE)
|
||||||
|
} else if hasDash {
|
||||||
|
searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), dashboardv0alpha1.DASHBOARD_RESOURCE)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errhttp.Write(ctx, err, w)
|
errhttp.Write(ctx, err, w)
|
||||||
|
|
|
@ -2,7 +2,9 @@ package dashboards
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -11,11 +13,13 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
|
k8srest "k8s.io/client-go/rest"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
"github.com/grafana/grafana/pkg/apiserver/rest"
|
"github.com/grafana/grafana/pkg/apiserver/rest"
|
||||||
"github.com/grafana/grafana/pkg/infra/slugify"
|
"github.com/grafana/grafana/pkg/infra/slugify"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/tests/apis"
|
"github.com/grafana/grafana/pkg/tests/apis"
|
||||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||||
|
@ -312,3 +316,177 @@ func TestIntegrationLegacySupport(t *testing.T) {
|
||||||
}, &dtos.DashboardFullWithMeta{})
|
}, &dtos.DashboardFullWithMeta{})
|
||||||
require.Equal(t, 406, rsp.Response.StatusCode) // not acceptable
|
require.Equal(t, 406, rsp.Response.StatusCode) // not acceptable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIntegrationSearchTypeFiltering(t *testing.T) {
|
||||||
|
testutil.SkipIntegrationTestInShortMode(t)
|
||||||
|
|
||||||
|
modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5}
|
||||||
|
for _, mode := range modes {
|
||||||
|
runDashboardSearchTest(t, mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDashboardSearchTest(t *testing.T, mode rest.DualWriterMode) {
|
||||||
|
t.Run(fmt.Sprintf("search types with dual writer mode %d", mode), func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
flags := []string{}
|
||||||
|
if mode >= rest.Mode3 {
|
||||||
|
flags = append(flags, featuremgmt.FlagUnifiedStorageSearch)
|
||||||
|
}
|
||||||
|
|
||||||
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||||
|
AppModeProduction: true,
|
||||||
|
DisableAnonymous: true,
|
||||||
|
APIServerStorageType: "unified",
|
||||||
|
EnableFeatureToggles: flags,
|
||||||
|
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||||
|
"dashboards.dashboard.grafana.app": {DualWriterMode: mode},
|
||||||
|
"folders.folder.grafana.app": {DualWriterMode: mode},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
defer helper.Shutdown()
|
||||||
|
|
||||||
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||||
|
User: helper.Org1.Admin,
|
||||||
|
GVR: dashboardV0.DashboardResourceInfo.GroupVersionResource(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create one folder via legacy API
|
||||||
|
{
|
||||||
|
cfg := dynamic.ConfigFor(helper.Org1.Admin.NewRestConfig())
|
||||||
|
cfg.GroupVersion = &schema.GroupVersion{Group: "folder.grafana.app", Version: "v1beta1"}
|
||||||
|
restClient, err := k8srest.RESTClientFor(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var statusCode int
|
||||||
|
body := []byte(`{"uid":"sfolder","title":"Sample Folder"}`)
|
||||||
|
result := restClient.Post().AbsPath("api", "folders").
|
||||||
|
Body(body).
|
||||||
|
SetHeader("Content-type", "application/json").
|
||||||
|
Do(ctx).
|
||||||
|
StatusCode(&statusCode)
|
||||||
|
require.NoError(t, result.Error())
|
||||||
|
require.Equal(t, int(http.StatusOK), statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create one dashboard in root
|
||||||
|
{
|
||||||
|
obj := &unstructured.Unstructured{
|
||||||
|
Object: map[string]any{
|
||||||
|
"spec": map[string]any{
|
||||||
|
"title": "X",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
obj.SetGenerateName("x-")
|
||||||
|
obj.SetAPIVersion(dashboardV0.GroupVersion.String())
|
||||||
|
obj.SetKind("Dashboard")
|
||||||
|
_, err := client.Resource.Create(ctx, obj, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also create a dashboard via legacy API to ensure legacy search sees it in modes < 3
|
||||||
|
{
|
||||||
|
cfg := dynamic.ConfigFor(helper.Org1.Admin.NewRestConfig())
|
||||||
|
cfg.GroupVersion = &schema.GroupVersion{Group: "dashboard.grafana.app", Version: "v0alpha1"}
|
||||||
|
restClient, err := k8srest.RESTClientFor(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var statusCode int
|
||||||
|
body := []byte(`{"dashboard":{"title":"Legacy X"},"overwrite":true}`)
|
||||||
|
result := restClient.Post().AbsPath("api", "dashboards", "db").
|
||||||
|
Body(body).
|
||||||
|
SetHeader("Content-type", "application/json").
|
||||||
|
Do(ctx).
|
||||||
|
StatusCode(&statusCode)
|
||||||
|
require.NoError(t, result.Error())
|
||||||
|
require.Equal(t, int(http.StatusOK), statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := helper.Org1.Admin.Identity.GetNamespace()
|
||||||
|
cfg := dynamic.ConfigFor(helper.Org1.Admin.NewRestConfig())
|
||||||
|
cfg.GroupVersion = &schema.GroupVersion{Group: "dashboard.grafana.app", Version: "v0alpha1"}
|
||||||
|
restClient, err := k8srest.RESTClientFor(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
call := func(params string) dashboardV0.SearchResults {
|
||||||
|
var statusCode int
|
||||||
|
req := restClient.Get().AbsPath("apis", "dashboard.grafana.app", "v0alpha1", "namespaces", ns, "search").
|
||||||
|
Param("limit", "1000")
|
||||||
|
for _, kv := range strings.Split(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))
|
||||||
|
return sr
|
||||||
|
}
|
||||||
|
|
||||||
|
// No type => defaults to both
|
||||||
|
resAny := call("")
|
||||||
|
folders := 0
|
||||||
|
dashboards := 0
|
||||||
|
for _, h := range resAny.Hits {
|
||||||
|
if strings.HasPrefix(h.Resource, "folder") {
|
||||||
|
folders++
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(h.Resource, "dash") {
|
||||||
|
dashboards++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.GreaterOrEqual(t, dashboards, 1)
|
||||||
|
require.GreaterOrEqual(t, folders, 1)
|
||||||
|
|
||||||
|
// Only folder
|
||||||
|
resFolder := call("type=folder")
|
||||||
|
for _, h := range resFolder.Hits {
|
||||||
|
require.True(t, strings.HasPrefix(h.Resource, "folder"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only dashboard
|
||||||
|
resDash := call("type=dashboard")
|
||||||
|
require.GreaterOrEqual(t, len(resDash.Hits), 1)
|
||||||
|
for _, h := range resDash.Hits {
|
||||||
|
require.True(t, strings.HasPrefix(h.Resource, "dash"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both via repetition
|
||||||
|
resBoth := call("type=folder&type=dashboard")
|
||||||
|
folders, dashboards = 0, 0
|
||||||
|
for _, h := range resBoth.Hits {
|
||||||
|
if strings.HasPrefix(h.Resource, "folder") {
|
||||||
|
folders++
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(h.Resource, "dash") {
|
||||||
|
dashboards++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.GreaterOrEqual(t, dashboards, 1)
|
||||||
|
require.GreaterOrEqual(t, folders, 1)
|
||||||
|
|
||||||
|
// Invalid => defaults to both
|
||||||
|
resInvalid := call("type=invalid")
|
||||||
|
folders, dashboards = 0, 0
|
||||||
|
for _, h := range resInvalid.Hits {
|
||||||
|
if strings.HasPrefix(h.Resource, "folder") {
|
||||||
|
folders++
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(h.Resource, "dash") {
|
||||||
|
dashboards++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.GreaterOrEqual(t, dashboards, 1)
|
||||||
|
require.GreaterOrEqual(t, folders, 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue