wire up the ui to the new search api (#97866)
wire up the ui to the new search api Co-authored-by: Scott Lepper <scott.lepper@gmail.com> Co-authored-by: Ryan McKinley <ryantxu@gmail.com> Co-authored-by: Dan Cech <dcech@grafana.com>
This commit is contained in:
co-authored by
Ryan McKinley
Dan Cech
parent
86a13ea4bd
commit
a8f347144d
@@ -53,12 +53,18 @@ type IndexableDocument struct {
|
||||
// The resource key
|
||||
Key *ResourceKey `json:"key"`
|
||||
|
||||
// The resource type ( for federated indexes )
|
||||
Kind string `json:"kind,omitempty"`
|
||||
|
||||
// Resource version for the resource (if known)
|
||||
RV int64 `json:"rv,omitempty"`
|
||||
|
||||
// The generic display name
|
||||
Title string `json:"title,omitempty"`
|
||||
|
||||
// internal sort field for title ( don't set this directly )
|
||||
TitleSort string `json:"title_sort,omitempty"`
|
||||
|
||||
// A generic description -- helpful in global search
|
||||
Description string `json:"description,omitempty"`
|
||||
|
||||
@@ -99,6 +105,10 @@ type IndexableDocument struct {
|
||||
RepoInfo *utils.ResourceRepositoryInfo `json:"repository,omitempty"`
|
||||
}
|
||||
|
||||
func (m *IndexableDocument) Type() string {
|
||||
return m.Key.Resource
|
||||
}
|
||||
|
||||
type ResourceReference struct {
|
||||
Relation string `json:"relation"` // eg: depends-on
|
||||
Group string `json:"group,omitempty"` // the api group
|
||||
@@ -138,10 +148,26 @@ func (m ResourceReferences) Less(i, j int) bool {
|
||||
|
||||
// Create a new indexable document based on a generic k8s resource
|
||||
func NewIndexableDocument(key *ResourceKey, rv int64, obj utils.GrafanaMetaAccessor) *IndexableDocument {
|
||||
title := obj.FindTitle(key.Name)
|
||||
if title == key.Name {
|
||||
// TODO: something wrong with FindTitle
|
||||
spec, err := obj.GetSpec()
|
||||
if err == nil {
|
||||
specValue, ok := spec.(map[string]any)
|
||||
if ok {
|
||||
specTitle, ok := specValue["title"].(string)
|
||||
if ok {
|
||||
title = specTitle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
doc := &IndexableDocument{
|
||||
Key: key,
|
||||
Kind: key.Resource,
|
||||
RV: rv,
|
||||
Title: obj.FindTitle(key.Name), // We always want *something* to display
|
||||
Title: title, // We always want *something* to display
|
||||
TitleSort: title,
|
||||
Labels: obj.GetLabels(),
|
||||
Folder: obj.GetFolder(),
|
||||
CreatedBy: obj.GetCreatedBy(),
|
||||
@@ -178,7 +204,6 @@ func (s *standardDocumentBuilder) BuildDocument(ctx context.Context, key *Resour
|
||||
}
|
||||
|
||||
doc := NewIndexableDocument(key, rv, obj)
|
||||
doc.Title = obj.FindTitle(key.Name)
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
@@ -220,11 +245,13 @@ func (x *searchableDocumentFields) Field(name string) *ResourceTableColumnDefini
|
||||
}
|
||||
|
||||
const SEARCH_FIELD_ID = "_id" // {namespace}/{group}/{resource}/{name}
|
||||
const SEARCH_FIELD_KIND = "kind" // resource ( for federated index filtering )
|
||||
const SEARCH_FIELD_GROUP_RESOURCE = "gr" // group/resource
|
||||
const SEARCH_FIELD_NAMESPACE = "namespace"
|
||||
const SEARCH_FIELD_NAME = "name"
|
||||
const SEARCH_FIELD_RV = "rv"
|
||||
const SEARCH_FIELD_TITLE = "title"
|
||||
const SEARCH_FIELD_TITLE_SORT = "title_sort"
|
||||
const SEARCH_FIELD_DESCRIPTION = "description"
|
||||
const SEARCH_FIELD_TAGS = "tags"
|
||||
const SEARCH_FIELD_LABELS = "labels" // All labels, not a specific one
|
||||
|
||||
@@ -33,8 +33,10 @@ func TestStandardDocumentBuilder(t *testing.T) {
|
||||
"resource": "playlists",
|
||||
"name": "test1"
|
||||
},
|
||||
"kind": "playlists",
|
||||
"rv": 10,
|
||||
"title": "test1",
|
||||
"title": "test playlist unified storage",
|
||||
"title_sort": "test playlist unified storage",
|
||||
"created": 1717236672000,
|
||||
"createdBy": "user:ABC",
|
||||
"updatedBy": "user:XYZ",
|
||||
@@ -43,5 +45,5 @@ func TestStandardDocumentBuilder(t *testing.T) {
|
||||
"path": "15",
|
||||
"hash": "xyz"
|
||||
}
|
||||
}`, string(jj))
|
||||
}`, string(jj))
|
||||
}
|
||||
|
||||
@@ -305,27 +305,9 @@ func (b *bleveIndex) Search(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write frame as JSON
|
||||
//response.Frame, err = frame.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse the facet fields
|
||||
for k, v := range res.Facets {
|
||||
f := &resource.ResourceSearchResponse_Facet{
|
||||
Field: v.Field,
|
||||
Total: int64(v.Total),
|
||||
Missing: int64(v.Missing),
|
||||
}
|
||||
if v.Terms != nil {
|
||||
for _, t := range v.Terms.Terms() {
|
||||
f.Terms = append(f.Terms, &resource.ResourceSearchResponse_TermFacet{
|
||||
Term: t.Term,
|
||||
Count: int64(t.Count),
|
||||
})
|
||||
}
|
||||
}
|
||||
f := newResponseFacet(v)
|
||||
if response.Facet == nil {
|
||||
response.Facet = make(map[string]*resource.ResourceSearchResponse_Facet)
|
||||
}
|
||||
@@ -387,7 +369,7 @@ func (b *bleveIndex) getIndex(
|
||||
return nil, fmt.Errorf("federated indexes must be the same type")
|
||||
}
|
||||
if typedindex.verifyKey(req.Federated[i]) != nil {
|
||||
return nil, fmt.Errorf("federated index keys do not match")
|
||||
return nil, fmt.Errorf("federated index keys do not match (%v != %v)", typedindex, req.Federated[i])
|
||||
}
|
||||
all = append(all, typedindex.index)
|
||||
}
|
||||
@@ -397,11 +379,16 @@ func (b *bleveIndex) getIndex(
|
||||
}
|
||||
|
||||
func toBleveSearchRequest(req *resource.ResourceSearchRequest, access authz.AccessClient) (*bleve.SearchRequest, *resource.ErrorResult) {
|
||||
facets := bleve.FacetsRequest{}
|
||||
for _, f := range req.Facet {
|
||||
facets[f.Field] = bleve.NewFacetRequest(f.Field, int(f.Limit))
|
||||
}
|
||||
searchrequest := &bleve.SearchRequest{
|
||||
Fields: req.Fields,
|
||||
Size: int(req.Limit),
|
||||
From: int(req.Offset),
|
||||
Explain: req.Explain,
|
||||
Facets: facets,
|
||||
}
|
||||
|
||||
// Currently everything is within an AND query
|
||||
@@ -415,6 +402,7 @@ func toBleveSearchRequest(req *resource.ResourceSearchRequest, access authz.Acce
|
||||
queries = append(queries, q)
|
||||
}
|
||||
}
|
||||
// filters
|
||||
if len(req.Options.Fields) > 0 {
|
||||
for _, v := range req.Options.Fields {
|
||||
q, err := requirementQuery(v, "")
|
||||
@@ -425,12 +413,7 @@ func toBleveSearchRequest(req *resource.ResourceSearchRequest, access authz.Acce
|
||||
}
|
||||
}
|
||||
|
||||
if req.Query != "" {
|
||||
// ??? Should expose the full power of query parsing here?
|
||||
// it is great for exploration, but also hard to change in the future
|
||||
q := bleve.NewQueryStringQuery(req.Query)
|
||||
queries = append(queries, q)
|
||||
}
|
||||
queries = append(queries, newTextQuery(req))
|
||||
|
||||
if access != nil {
|
||||
// TODO AUTHZ!!!!
|
||||
@@ -458,30 +441,11 @@ func toBleveSearchRequest(req *resource.ResourceSearchRequest, access authz.Acce
|
||||
}
|
||||
|
||||
// Add the sort fields
|
||||
for _, sort := range req.SortBy {
|
||||
// hardcoded (for now)
|
||||
if strings.HasPrefix(sort.Field, "stats.") {
|
||||
searchrequest.Sort = append(searchrequest.Sort, &search.SortField{
|
||||
Field: sort.Field,
|
||||
Desc: sort.Desc,
|
||||
Type: search.SortFieldAsNumber, // force for now!
|
||||
Mode: search.SortFieldDefault, // ???
|
||||
Missing: search.SortFieldMissingLast,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Default support
|
||||
input := sort.Field
|
||||
if sort.Desc {
|
||||
input = "-" + sort.Field
|
||||
}
|
||||
s := search.ParseSearchSortString(input)
|
||||
searchrequest.Sort = append(searchrequest.Sort, s)
|
||||
}
|
||||
sorting := getSortFields(req)
|
||||
searchrequest.SortBy(sorting)
|
||||
|
||||
// Always sort by *something*, otherwise the order is unstable
|
||||
if len(searchrequest.Sort) == 0 {
|
||||
if len(sorting) == 0 {
|
||||
searchrequest.Sort = append(searchrequest.Sort, &search.SortDocID{
|
||||
Desc: false,
|
||||
})
|
||||
@@ -490,23 +454,71 @@ func toBleveSearchRequest(req *resource.ResourceSearchRequest, access authz.Acce
|
||||
return searchrequest, nil
|
||||
}
|
||||
|
||||
func getSortFields(req *resource.ResourceSearchRequest) []string {
|
||||
sorting := []string{}
|
||||
for _, sort := range req.SortBy {
|
||||
input := sort.Field
|
||||
if field, ok := textSortFields[input]; ok {
|
||||
input = field
|
||||
}
|
||||
|
||||
if sort.Desc {
|
||||
input = "-" + input
|
||||
}
|
||||
sorting = append(sorting, input)
|
||||
}
|
||||
return sorting
|
||||
}
|
||||
|
||||
// fields that we went to sort by the full text
|
||||
var textSortFields = map[string]string{
|
||||
resource.SEARCH_FIELD_TITLE: resource.SEARCH_FIELD_TITLE + "_sort",
|
||||
}
|
||||
|
||||
// Convert a "requirement" into a bleve query
|
||||
func requirementQuery(req *resource.Requirement, prefix string) (query.Query, *resource.ErrorResult) {
|
||||
switch selection.Operator(req.Operator) {
|
||||
case selection.Equals, selection.DoubleEquals:
|
||||
if len(req.Values) != 1 {
|
||||
return nil, resource.NewBadRequestError("equals query can have one value")
|
||||
if len(req.Values) == 0 {
|
||||
return query.NewMatchAllQuery(), nil
|
||||
}
|
||||
if len(req.Values[0]) == 1 {
|
||||
q := query.NewMatchQuery(req.Values[0])
|
||||
q.FieldVal = prefix + req.Key
|
||||
return q, nil
|
||||
}
|
||||
q := query.NewMatchQuery(req.Values[0])
|
||||
q.FieldVal = prefix + req.Key
|
||||
return q, nil
|
||||
|
||||
conjuncts := []query.Query{}
|
||||
for _, v := range req.Values {
|
||||
q := query.NewMatchQuery(v)
|
||||
q.FieldVal = prefix + req.Key
|
||||
conjuncts = append(conjuncts, q)
|
||||
}
|
||||
|
||||
return query.NewConjunctionQuery(conjuncts), nil
|
||||
case selection.NotEquals:
|
||||
case selection.DoesNotExist:
|
||||
case selection.GreaterThan:
|
||||
case selection.LessThan:
|
||||
case selection.Exists:
|
||||
case selection.In:
|
||||
if len(req.Values) == 0 {
|
||||
return query.NewMatchAllQuery(), nil
|
||||
}
|
||||
if len(req.Values) == 1 {
|
||||
q := query.NewMatchQuery(req.Values[0])
|
||||
q.FieldVal = prefix + req.Key
|
||||
return q, nil
|
||||
}
|
||||
|
||||
disjuncts := []query.Query{}
|
||||
for _, v := range req.Values {
|
||||
q := query.NewMatchQuery(v)
|
||||
q.FieldVal = prefix + req.Key
|
||||
disjuncts = append(disjuncts, q)
|
||||
}
|
||||
|
||||
return query.NewDisjunctionQuery(disjuncts), nil
|
||||
case selection.NotIn:
|
||||
}
|
||||
return nil, resource.NewBadRequestError(
|
||||
@@ -615,3 +627,29 @@ func getAllFields(standard resource.SearchableDocumentFields, custom resource.Se
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func newResponseFacet(v *search.FacetResult) *resource.ResourceSearchResponse_Facet {
|
||||
f := &resource.ResourceSearchResponse_Facet{
|
||||
Field: v.Field,
|
||||
Total: int64(v.Total),
|
||||
Missing: int64(v.Missing),
|
||||
}
|
||||
if v.Terms != nil {
|
||||
for _, t := range v.Terms.Terms() {
|
||||
f.Terms = append(f.Terms, &resource.ResourceSearchResponse_TermFacet{
|
||||
Term: t.Term,
|
||||
Count: int64(t.Count),
|
||||
})
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func newTextQuery(req *resource.ResourceSearchRequest) query.Query {
|
||||
if req.Query == "" || req.Query == "*" {
|
||||
return bleve.NewMatchAllQuery()
|
||||
}
|
||||
// TODO: wildcard query?
|
||||
// return bleve.NewWildcardQuery(req.Query)
|
||||
return bleve.NewFuzzyQuery(req.Query)
|
||||
}
|
||||
|
||||
@@ -16,21 +16,20 @@ func getBleveMappings(fields resource.SearchableDocumentFields) mapping.IndexMap
|
||||
|
||||
func getBleveDocMappings(_ resource.SearchableDocumentFields) *mapping.DocumentMapping {
|
||||
mapper := bleve.NewDocumentStaticMapping()
|
||||
mapper.AddFieldMapping(&mapping.FieldMapping{
|
||||
Name: resource.SEARCH_FIELD_TITLE,
|
||||
Type: "text",
|
||||
// TODO - if we don't want title to be a keyword, we can use this
|
||||
// set the title field to use keyword analyzer so it sorts by the whole phrase
|
||||
// https://github.com/blevesearch/bleve/issues/417#issuecomment-245273022
|
||||
Analyzer: keyword.Name,
|
||||
Store: true,
|
||||
Index: true,
|
||||
IncludeTermVectors: true,
|
||||
IncludeInAll: true,
|
||||
DocValues: false,
|
||||
})
|
||||
|
||||
mapper.AddFieldMapping(&mapping.FieldMapping{
|
||||
// for sorting by title
|
||||
titleSortMapping := bleve.NewKeywordFieldMapping()
|
||||
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE_SORT, titleSortMapping)
|
||||
|
||||
// for searching by title
|
||||
titleSearchMapping := bleve.NewTextFieldMapping()
|
||||
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE, titleSearchMapping)
|
||||
|
||||
// for filtering by kind/resource ( federated search )
|
||||
kindMapping := bleve.NewTextFieldMapping()
|
||||
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_KIND, kindMapping)
|
||||
|
||||
descriptionMapping := &mapping.FieldMapping{
|
||||
Name: resource.SEARCH_FIELD_DESCRIPTION,
|
||||
Type: "text",
|
||||
Store: true,
|
||||
@@ -38,9 +37,10 @@ func getBleveDocMappings(_ resource.SearchableDocumentFields) *mapping.DocumentM
|
||||
IncludeTermVectors: false,
|
||||
IncludeInAll: false,
|
||||
DocValues: false,
|
||||
})
|
||||
}
|
||||
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_DESCRIPTION, descriptionMapping)
|
||||
|
||||
mapper.AddFieldMapping(&mapping.FieldMapping{
|
||||
tagsMapping := &mapping.FieldMapping{
|
||||
Name: resource.SEARCH_FIELD_TAGS,
|
||||
Type: "text",
|
||||
Analyzer: keyword.Name,
|
||||
@@ -49,9 +49,10 @@ func getBleveDocMappings(_ resource.SearchableDocumentFields) *mapping.DocumentM
|
||||
IncludeTermVectors: false,
|
||||
IncludeInAll: true,
|
||||
DocValues: false,
|
||||
})
|
||||
}
|
||||
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TAGS, tagsMapping)
|
||||
|
||||
mapper.AddFieldMapping(&mapping.FieldMapping{
|
||||
folderMapping := &mapping.FieldMapping{
|
||||
Name: resource.SEARCH_FIELD_FOLDER,
|
||||
Type: "text",
|
||||
Analyzer: keyword.Name,
|
||||
@@ -60,9 +61,10 @@ func getBleveDocMappings(_ resource.SearchableDocumentFields) *mapping.DocumentM
|
||||
IncludeTermVectors: false,
|
||||
IncludeInAll: true,
|
||||
DocValues: true, // will be needed for authz client
|
||||
})
|
||||
}
|
||||
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_FOLDER, folderMapping)
|
||||
|
||||
mapper.AddFieldMapping(&mapping.FieldMapping{
|
||||
repoMapping := &mapping.FieldMapping{
|
||||
Name: resource.SEARCH_FIELD_REPOSITORY,
|
||||
Type: "text",
|
||||
Analyzer: keyword.Name,
|
||||
@@ -71,8 +73,10 @@ func getBleveDocMappings(_ resource.SearchableDocumentFields) *mapping.DocumentM
|
||||
IncludeTermVectors: false,
|
||||
IncludeInAll: true,
|
||||
DocValues: true,
|
||||
})
|
||||
}
|
||||
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_REPOSITORY, repoMapping)
|
||||
|
||||
// TODO: we use the static mapper. why set dynamic to true?
|
||||
mapper.Dynamic = true
|
||||
|
||||
return mapper
|
||||
|
||||
@@ -42,5 +42,5 @@ func TestDocumentMapping(t *testing.T) {
|
||||
|
||||
fmt.Printf("DOC: fields %d\n", len(doc.Fields))
|
||||
fmt.Printf("DOC: size %d\n", doc.Size())
|
||||
require.Equal(t, 15, len(doc.Fields))
|
||||
require.Equal(t, 17, len(doc.Fields))
|
||||
}
|
||||
|
||||
@@ -66,11 +66,12 @@ func TestBleveBackend(t *testing.T) {
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "aaa",
|
||||
Namespace: "ns",
|
||||
Group: "g",
|
||||
Resource: "dash",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
},
|
||||
Title: "aaa (dash)",
|
||||
Folder: "xxx",
|
||||
Title: "aaa (dash)",
|
||||
TitleSort: "aaa (dash)",
|
||||
Folder: "xxx",
|
||||
Fields: map[string]any{
|
||||
DASHBOARD_LEGACY_ID: 12,
|
||||
DASHBOARD_PANEL_TYPES: []string{"timeseries", "table"},
|
||||
@@ -83,11 +84,12 @@ func TestBleveBackend(t *testing.T) {
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "bbb",
|
||||
Namespace: "ns",
|
||||
Group: "g",
|
||||
Resource: "dash",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
},
|
||||
Title: "bbb (dash)",
|
||||
Folder: "xxx",
|
||||
Title: "bbb (dash)",
|
||||
TitleSort: "bbb (dash)",
|
||||
Folder: "xxx",
|
||||
Fields: map[string]any{
|
||||
DASHBOARD_LEGACY_ID: 12,
|
||||
DASHBOARD_PANEL_TYPES: []string{"timeseries"},
|
||||
@@ -103,11 +105,12 @@ func TestBleveBackend(t *testing.T) {
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "ccc",
|
||||
Namespace: "ns",
|
||||
Group: "g",
|
||||
Resource: "dash",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
},
|
||||
Title: "ccc (dash)",
|
||||
Folder: "zzz",
|
||||
Title: "ccc (dash)",
|
||||
TitleSort: "ccc (dash)",
|
||||
Folder: "zzz",
|
||||
RepoInfo: &utils.ResourceRepositoryInfo{
|
||||
Name: "r0",
|
||||
},
|
||||
@@ -145,11 +148,7 @@ func TestBleveBackend(t *testing.T) {
|
||||
require.NotNil(t, rsp.Results)
|
||||
require.NotNil(t, rsp.Facet)
|
||||
|
||||
t.Run("x", func(t *testing.T) {
|
||||
t.Skip("flakey tests - skipping") // sort seems different in CI... sometimes!
|
||||
// Match the results
|
||||
resource.AssertTableSnapshot(t, filepath.Join("testdata", "manual-dashboard.json"), rsp.Results)
|
||||
})
|
||||
resource.AssertTableSnapshot(t, filepath.Join("testdata", "manual-dashboard.json"), rsp.Results)
|
||||
|
||||
// Get the tags facets
|
||||
facet, ok := rsp.Facet["tags"]
|
||||
@@ -193,20 +192,22 @@ func TestBleveBackend(t *testing.T) {
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "zzz",
|
||||
Namespace: "ns",
|
||||
Group: "g",
|
||||
Resource: "folder",
|
||||
Group: "folder.grafana.app",
|
||||
Resource: "folders",
|
||||
},
|
||||
Title: "zzz (folder)",
|
||||
Title: "zzz (folder)",
|
||||
TitleSort: "zzz (folder)",
|
||||
})
|
||||
_ = index.Write(&resource.IndexableDocument{
|
||||
RV: 2,
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "yyy",
|
||||
Namespace: "ns",
|
||||
Group: "g",
|
||||
Resource: "folder",
|
||||
Group: "folder.grafana.app",
|
||||
Resource: "folders",
|
||||
},
|
||||
Title: "yyy (folder)",
|
||||
Title: "yyy (folder)",
|
||||
TitleSort: "yyy (folder)",
|
||||
Labels: map[string]string{
|
||||
"region": "west",
|
||||
},
|
||||
@@ -232,8 +233,6 @@ func TestBleveBackend(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("simple federation", func(t *testing.T) {
|
||||
t.Skip("flakey tests - skipping") // sort seems different in CI... sometimes!
|
||||
|
||||
// The other tests must run first to build the indexes
|
||||
require.NotNil(t, dashboardsIndex)
|
||||
require.NotNil(t, foldersIndex)
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
"resource": "dashboards",
|
||||
"name": "aaa"
|
||||
},
|
||||
"kind": "dashboards",
|
||||
"rv": 1234,
|
||||
"title": "Test title",
|
||||
"title_sort": "Test title",
|
||||
"description": "test description",
|
||||
"tags": [
|
||||
"a",
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
"resource": "dashboards",
|
||||
"name": "aaa"
|
||||
},
|
||||
"kind": "dashboards",
|
||||
"rv": 1234,
|
||||
"title": "aaa",
|
||||
"title": "test-aaa",
|
||||
"title_sort": "test-aaa",
|
||||
"created": 1730490142000,
|
||||
"createdBy": "user:1",
|
||||
"repository": {
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
"resource": "dashboards",
|
||||
"name": "bbb"
|
||||
},
|
||||
"kind": "dashboards",
|
||||
"rv": 1234,
|
||||
"title": "bbb",
|
||||
"title": "test-bbb",
|
||||
"title_sort": "test-bbb",
|
||||
"created": 1730490142000,
|
||||
"createdBy": "user:1",
|
||||
"repository": {
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
"resource": "dashboards",
|
||||
"name": "aaa"
|
||||
},
|
||||
"kind": "dashboards",
|
||||
"rv": 1234,
|
||||
"title": "aaa",
|
||||
"title": "Test AAA",
|
||||
"title_sort": "Test AAA",
|
||||
"created": 1731336353000,
|
||||
"createdBy": "user:t000000001",
|
||||
"repository": {
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
"resource": "dashboards",
|
||||
"name": "aaa"
|
||||
},
|
||||
"kind": "dashboards",
|
||||
"rv": 1234,
|
||||
"title": "aaa",
|
||||
"title": "Test AAA",
|
||||
"title_sort": "Test AAA",
|
||||
"created": 1706690655000,
|
||||
"createdBy": "user:abc",
|
||||
"repository": {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"metadata": {},
|
||||
"columnDefinitions": [
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"format": "",
|
||||
"description": "Display name for the resource",
|
||||
"priority": 0
|
||||
},
|
||||
{
|
||||
"name": "_id",
|
||||
"type": "string",
|
||||
"format": "",
|
||||
"description": "Unique Identifier. {namespace}/{group}/{resource}/{name}",
|
||||
"priority": 0
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"cells": [
|
||||
"yyy (folder)",
|
||||
"ns/folder.grafana.app/folders/yyy"
|
||||
],
|
||||
"object": {
|
||||
"kind": "folders",
|
||||
"apiVersion": "folder.grafana.app",
|
||||
"metadata": {
|
||||
"name": "yyy",
|
||||
"namespace": "ns",
|
||||
"creationTimestamp": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"zzz (folder)",
|
||||
"ns/folder.grafana.app/folders/zzz"
|
||||
],
|
||||
"object": {
|
||||
"kind": "folders",
|
||||
"apiVersion": "folder.grafana.app",
|
||||
"metadata": {
|
||||
"name": "zzz",
|
||||
"namespace": "ns",
|
||||
"creationTimestamp": null
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -68,7 +68,7 @@
|
||||
"rows": [
|
||||
{
|
||||
"cells": [
|
||||
"ns/g/dash/ccc",
|
||||
"ns/dashboard.grafana.app/dashboards/ccc",
|
||||
"ccc (dash)",
|
||||
[
|
||||
"aa"
|
||||
@@ -81,8 +81,8 @@
|
||||
null
|
||||
],
|
||||
"object": {
|
||||
"kind": "dash",
|
||||
"apiVersion": "g",
|
||||
"kind": "dashboards",
|
||||
"apiVersion": "dashboard.grafana.app",
|
||||
"metadata": {
|
||||
"name": "ccc",
|
||||
"namespace": "ns",
|
||||
@@ -92,7 +92,7 @@
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"ns/g/dash/bbb",
|
||||
"ns/dashboard.grafana.app/dashboards/bbb",
|
||||
"bbb (dash)",
|
||||
[
|
||||
"aa"
|
||||
@@ -105,8 +105,8 @@
|
||||
null
|
||||
],
|
||||
"object": {
|
||||
"kind": "dash",
|
||||
"apiVersion": "g",
|
||||
"kind": "dashboards",
|
||||
"apiVersion": "dashboard.grafana.app",
|
||||
"metadata": {
|
||||
"name": "bbb",
|
||||
"namespace": "ns",
|
||||
@@ -116,7 +116,7 @@
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"ns/g/dash/aaa",
|
||||
"ns/dashboard.grafana.app/dashboards/aaa",
|
||||
"aaa (dash)",
|
||||
[
|
||||
"aa",
|
||||
@@ -130,8 +130,8 @@
|
||||
null
|
||||
],
|
||||
"object": {
|
||||
"kind": "dash",
|
||||
"apiVersion": "g",
|
||||
"kind": "dashboards",
|
||||
"apiVersion": "dashboard.grafana.app",
|
||||
"metadata": {
|
||||
"name": "aaa",
|
||||
"namespace": "ns",
|
||||
|
||||
+28
-28
@@ -20,26 +20,11 @@
|
||||
{
|
||||
"cells": [
|
||||
"aaa (dash)",
|
||||
"ns/g/dash/bbb"
|
||||
"ns/dashboard.grafana.app/dashboards/aaa"
|
||||
],
|
||||
"object": {
|
||||
"kind": "dash",
|
||||
"apiVersion": "g",
|
||||
"metadata": {
|
||||
"name": "bbb",
|
||||
"namespace": "ns",
|
||||
"creationTimestamp": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"bbb (dash)",
|
||||
"ns/g/dash/aaa"
|
||||
],
|
||||
"object": {
|
||||
"kind": "dash",
|
||||
"apiVersion": "g",
|
||||
"kind": "dashboards",
|
||||
"apiVersion": "dashboard.grafana.app",
|
||||
"metadata": {
|
||||
"name": "aaa",
|
||||
"namespace": "ns",
|
||||
@@ -49,12 +34,27 @@
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"ccc (dash)",
|
||||
"ns/g/dash/ccc"
|
||||
"bbb (dash)",
|
||||
"ns/dashboard.grafana.app/dashboards/bbb"
|
||||
],
|
||||
"object": {
|
||||
"kind": "dash",
|
||||
"apiVersion": "g",
|
||||
"kind": "dashboards",
|
||||
"apiVersion": "dashboard.grafana.app",
|
||||
"metadata": {
|
||||
"name": "bbb",
|
||||
"namespace": "ns",
|
||||
"creationTimestamp": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"ccc (dash)",
|
||||
"ns/dashboard.grafana.app/dashboards/ccc"
|
||||
],
|
||||
"object": {
|
||||
"kind": "dashboards",
|
||||
"apiVersion": "dashboard.grafana.app",
|
||||
"metadata": {
|
||||
"name": "ccc",
|
||||
"namespace": "ns",
|
||||
@@ -65,11 +65,11 @@
|
||||
{
|
||||
"cells": [
|
||||
"yyy (folder)",
|
||||
"ns/g/folder/yyy"
|
||||
"ns/folder.grafana.app/folders/yyy"
|
||||
],
|
||||
"object": {
|
||||
"kind": "folder",
|
||||
"apiVersion": "g",
|
||||
"kind": "folders",
|
||||
"apiVersion": "folder.grafana.app",
|
||||
"metadata": {
|
||||
"name": "yyy",
|
||||
"namespace": "ns",
|
||||
@@ -80,11 +80,11 @@
|
||||
{
|
||||
"cells": [
|
||||
"zzz (folder)",
|
||||
"ns/g/folder/zzz"
|
||||
"ns/folder.grafana.app/folders/zzz"
|
||||
],
|
||||
"object": {
|
||||
"kind": "folder",
|
||||
"apiVersion": "g",
|
||||
"kind": "folders",
|
||||
"apiVersion": "folder.grafana.app",
|
||||
"metadata": {
|
||||
"name": "zzz",
|
||||
"namespace": "ns",
|
||||
|
||||
+6
-6
@@ -47,7 +47,7 @@
|
||||
"rows": [
|
||||
{
|
||||
"cells": [
|
||||
"ns/g/folder/yyy",
|
||||
"ns/folder.grafana.app/folders/yyy",
|
||||
"yyy (folder)",
|
||||
null,
|
||||
null,
|
||||
@@ -55,8 +55,8 @@
|
||||
0
|
||||
],
|
||||
"object": {
|
||||
"kind": "folder",
|
||||
"apiVersion": "g",
|
||||
"kind": "folders",
|
||||
"apiVersion": "folder.grafana.app",
|
||||
"metadata": {
|
||||
"name": "yyy",
|
||||
"namespace": "ns",
|
||||
@@ -66,7 +66,7 @@
|
||||
},
|
||||
{
|
||||
"cells": [
|
||||
"ns/g/folder/zzz",
|
||||
"ns/folder.grafana.app/folders/zzz",
|
||||
"zzz (folder)",
|
||||
null,
|
||||
null,
|
||||
@@ -74,8 +74,8 @@
|
||||
0
|
||||
],
|
||||
"object": {
|
||||
"kind": "folder",
|
||||
"apiVersion": "g",
|
||||
"kind": "folders",
|
||||
"apiVersion": "folder.grafana.app",
|
||||
"metadata": {
|
||||
"name": "zzz",
|
||||
"namespace": "ns",
|
||||
|
||||
Reference in New Issue
Block a user