K8s: Search fallback: Support all sort by methods (#100776)
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
||||
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||
@@ -68,8 +69,9 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider,
|
||||
dashStore dashboards.Store,
|
||||
provisioning provisioning.ProvisioningService,
|
||||
softDelete bool,
|
||||
sorter sort.Service,
|
||||
) DashboardAccess {
|
||||
dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore)
|
||||
dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
|
||||
return &dashboardSqlAccess{
|
||||
sql: sql,
|
||||
namespacer: namespacer,
|
||||
|
||||
@@ -14,9 +14,10 @@ import (
|
||||
folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
unisearch "github.com/grafana/grafana/pkg/storage/unified/search"
|
||||
"google.golang.org/grpc"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
)
|
||||
@@ -24,10 +25,19 @@ import (
|
||||
type DashboardSearchClient struct {
|
||||
resource.ResourceIndexClient
|
||||
dashboardStore dashboards.Store
|
||||
sorter sort.Service
|
||||
}
|
||||
|
||||
func NewDashboardSearchClient(dashboardStore dashboards.Store) *DashboardSearchClient {
|
||||
return &DashboardSearchClient{dashboardStore: dashboardStore}
|
||||
func NewDashboardSearchClient(dashboardStore dashboards.Store, sorter sort.Service) *DashboardSearchClient {
|
||||
return &DashboardSearchClient{dashboardStore: dashboardStore, sorter: sorter}
|
||||
}
|
||||
|
||||
var sortByMapping = map[string]string{
|
||||
unisearch.DASHBOARD_VIEWS_LAST_30_DAYS: "viewed-recently-",
|
||||
unisearch.DASHBOARD_VIEWS_TOTAL: "viewed-",
|
||||
unisearch.DASHBOARD_ERRORS_LAST_30_DAYS: "errors-recently-",
|
||||
unisearch.DASHBOARD_ERRORS_TOTAL: "errors-",
|
||||
"title": "alpha-",
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
@@ -78,16 +88,30 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
|
||||
query.Type = queryType
|
||||
}
|
||||
|
||||
// technically, there exists the ability to register multiple ways of sorting using the legacy database
|
||||
// see RegisterSortOption in pkg/services/search/sorting.go
|
||||
// however, it doesn't look like we are taking advantage of that. And since by default the legacy
|
||||
// sql will sort by title ascending, we only really need to handle the "alpha-desc" case
|
||||
if req.SortBy != nil {
|
||||
for _, sort := range req.SortBy {
|
||||
if sort.Field == "title" && sort.Desc {
|
||||
query.Sort = search.SortAlphaDesc
|
||||
}
|
||||
sortByField := ""
|
||||
if len(req.SortBy) != 0 {
|
||||
if len(req.SortBy) > 1 {
|
||||
return nil, fmt.Errorf("only one sort field is supported")
|
||||
}
|
||||
sort := req.SortBy[0]
|
||||
sortByField = strings.TrimPrefix(sort.Field, resource.SEARCH_FIELD_PREFIX)
|
||||
sorterName := sortByMapping[sortByField]
|
||||
|
||||
if sort.Desc {
|
||||
sorterName += "desc"
|
||||
} else {
|
||||
sorterName += "asc"
|
||||
}
|
||||
|
||||
if sorter, ok := c.sorter.GetSortOption(sorterName); ok {
|
||||
query.Sort = sorter
|
||||
}
|
||||
}
|
||||
|
||||
// the title search will not return any sortMeta (an int64), like
|
||||
// most sorting will. Without this, the title will be set to sortMeta (0)
|
||||
if sortByField == resource.SEARCH_FIELD_TITLE {
|
||||
sortByField = ""
|
||||
}
|
||||
|
||||
// if searching for tags, get those instead of the dashboards or folders
|
||||
@@ -178,7 +202,6 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
|
||||
query.ProvisionedRepo, _ = dashboard.GetProvisionedFileNameFromMeta(vals[0])
|
||||
}
|
||||
}
|
||||
|
||||
searchFields := resource.StandardSearchFields()
|
||||
list := &resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
@@ -186,6 +209,10 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
|
||||
searchFields.Field(resource.SEARCH_FIELD_TITLE),
|
||||
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
|
||||
searchFields.Field(resource.SEARCH_FIELD_TAGS),
|
||||
&resource.ResourceTableColumnDefinition{
|
||||
Name: sortByField,
|
||||
Type: resource.ResourceTableColumnDefinition_INT64,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -213,7 +240,7 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
|
||||
Key: getResourceKey(&dashboards.DashboardSearchProjection{
|
||||
UID: dashboard.UID,
|
||||
}, req.Options.Key.Namespace),
|
||||
Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), {}},
|
||||
Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), {}, {}},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -235,7 +262,7 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
|
||||
|
||||
list.Results.Rows = append(list.Results.Rows, &resource.ResourceTableRow{
|
||||
Key: getResourceKey(dashboard, req.Options.Key.Namespace),
|
||||
Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), tags},
|
||||
Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), tags, []byte(strconv.FormatInt(dashboard.SortMeta, 10))},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -276,6 +303,7 @@ func formatQueryResult(res []dashboards.DashboardSearchProjection) []*dashboards
|
||||
FolderUID: item.FolderUID,
|
||||
Tags: []string{},
|
||||
IsFolder: item.IsFolder,
|
||||
SortMeta: item.SortMeta,
|
||||
}
|
||||
hitList = append(hitList, hit)
|
||||
hits[key] = hit
|
||||
|
||||
@@ -3,14 +3,18 @@ package legacysearcher
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
unisearch "github.com/grafana/grafana/pkg/storage/unified/search"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
@@ -18,10 +22,13 @@ import (
|
||||
|
||||
func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
mockStore := dashboards.NewFakeDashboardStore(t)
|
||||
client := NewDashboardSearchClient(mockStore)
|
||||
sortSvc := sort.ProvideService()
|
||||
client := NewDashboardSearchClient(mockStore, sortSvc)
|
||||
ctx := context.Background()
|
||||
user := &user.SignedInUser{OrgID: 2}
|
||||
ctx = identity.WithRequester(ctx, user)
|
||||
emptyTags, err := json.Marshal([]string{})
|
||||
require.NoError(t, err)
|
||||
|
||||
dashboardKey := &resource.ResourceKey{
|
||||
Name: "uid",
|
||||
@@ -29,9 +36,11 @@ func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("Should parse results into GRPC", func(t *testing.T) {
|
||||
sorter, _ := sortSvc.GetSortOption("alpha-asc")
|
||||
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
|
||||
SignedInUser: user, // user from context should be used
|
||||
Type: "dash-db", // should set type based off of key
|
||||
Sort: sorter,
|
||||
}).Return([]dashboards.DashboardSearchProjection{
|
||||
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1", Term: "term"},
|
||||
{UID: "uid2", Title: "Test Dashboard2", FolderUID: "folder2"},
|
||||
@@ -41,17 +50,19 @@ func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
Options: &resource.ListOptions{
|
||||
Key: dashboardKey,
|
||||
},
|
||||
SortBy: []*resource.ResourceSearchRequest_Sort{
|
||||
{
|
||||
Field: resource.SEARCH_FIELD_TITLE,
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := client.Search(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
tags, err := json.Marshal([]string{"term"})
|
||||
require.NoError(t, err)
|
||||
|
||||
emptyTags, err := json.Marshal([]string{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, resp)
|
||||
|
||||
searchFields := resource.StandardSearchFields()
|
||||
require.Equal(t, &resource.ResourceSearchResponse{
|
||||
TotalHits: 2,
|
||||
@@ -60,6 +71,10 @@ func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
searchFields.Field(resource.SEARCH_FIELD_TITLE),
|
||||
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
|
||||
searchFields.Field(resource.SEARCH_FIELD_TAGS),
|
||||
&resource.ResourceTableColumnDefinition{
|
||||
Name: "", // sort by should be empty if title is what we sorted by
|
||||
Type: resource.ResourceTableColumnDefinition_INT64,
|
||||
},
|
||||
},
|
||||
Rows: []*resource.ResourceTableRow{
|
||||
{
|
||||
@@ -72,6 +87,7 @@ func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
[]byte("Test Dashboard"),
|
||||
[]byte("folder1"),
|
||||
tags,
|
||||
[]byte(strconv.FormatInt(0, 10)),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -84,6 +100,127 @@ func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
[]byte("Test Dashboard2"),
|
||||
[]byte("folder2"),
|
||||
emptyTags,
|
||||
[]byte(strconv.FormatInt(0, 10)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, resp)
|
||||
mockStore.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("Sorting should be properly parsed into legacy sorting options (asc), and results added", func(t *testing.T) {
|
||||
sortOptionAsc := model.SortOption{
|
||||
Name: "viewed-asc", // should add -asc to the sort field and match on that
|
||||
}
|
||||
sortSvc.RegisterSortOption(sortOptionAsc)
|
||||
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
|
||||
SignedInUser: user,
|
||||
Type: "dash-db",
|
||||
Sort: sortOptionAsc,
|
||||
}).Return([]dashboards.DashboardSearchProjection{
|
||||
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder", SortMeta: int64(50)},
|
||||
}, nil).Once()
|
||||
|
||||
req := &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: dashboardKey,
|
||||
},
|
||||
SortBy: []*resource.ResourceSearchRequest_Sort{
|
||||
{
|
||||
Field: resource.SEARCH_FIELD_PREFIX + unisearch.DASHBOARD_VIEWS_TOTAL, // "fields." prefix should be removed
|
||||
Desc: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := client.Search(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
searchFields := resource.StandardSearchFields()
|
||||
require.Equal(t, &resource.ResourceSearchResponse{
|
||||
TotalHits: 1,
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
searchFields.Field(resource.SEARCH_FIELD_TITLE),
|
||||
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
|
||||
searchFields.Field(resource.SEARCH_FIELD_TAGS),
|
||||
&resource.ResourceTableColumnDefinition{
|
||||
Name: "views_total",
|
||||
Type: resource.ResourceTableColumnDefinition_INT64,
|
||||
},
|
||||
},
|
||||
Rows: []*resource.ResourceTableRow{
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "uid",
|
||||
Group: dashboard.GROUP,
|
||||
Resource: dashboard.DASHBOARD_RESOURCE,
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("Test Dashboard"),
|
||||
[]byte("folder"),
|
||||
emptyTags,
|
||||
[]byte(strconv.FormatInt(50, 10)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, resp)
|
||||
mockStore.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("Sorting should be properly parsed into legacy sorting options (desc)", func(t *testing.T) {
|
||||
sortOptionAsc := model.SortOption{
|
||||
Name: "errors-recently-desc", // should add -asc to the sort field and match on that
|
||||
}
|
||||
sortSvc.RegisterSortOption(sortOptionAsc)
|
||||
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
|
||||
SignedInUser: user,
|
||||
Type: "dash-db",
|
||||
Sort: sortOptionAsc,
|
||||
}).Return([]dashboards.DashboardSearchProjection{
|
||||
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder", SortMeta: int64(2)},
|
||||
}, nil).Once()
|
||||
|
||||
req := &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: dashboardKey,
|
||||
},
|
||||
SortBy: []*resource.ResourceSearchRequest_Sort{
|
||||
{
|
||||
Field: unisearch.DASHBOARD_ERRORS_LAST_30_DAYS,
|
||||
Desc: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := client.Search(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
searchFields := resource.StandardSearchFields()
|
||||
require.Equal(t, &resource.ResourceSearchResponse{
|
||||
TotalHits: 1,
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
searchFields.Field(resource.SEARCH_FIELD_TITLE),
|
||||
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
|
||||
searchFields.Field(resource.SEARCH_FIELD_TAGS),
|
||||
&resource.ResourceTableColumnDefinition{
|
||||
Name: "errors_last_30_days",
|
||||
Type: resource.ResourceTableColumnDefinition_INT64,
|
||||
},
|
||||
},
|
||||
Rows: []*resource.ResourceTableRow{
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "uid",
|
||||
Group: dashboard.GROUP,
|
||||
Resource: dashboard.DASHBOARD_RESOURCE,
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("Test Dashboard"),
|
||||
[]byte("folder"),
|
||||
emptyTags,
|
||||
[]byte(strconv.FormatInt(2, 10)),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -290,7 +290,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
|
||||
if queryParams.Has("sort") {
|
||||
for _, sort := range queryParams["sort"] {
|
||||
if slices.Contains(search.DashboardFields(), sort) {
|
||||
sort = "fields." + sort
|
||||
sort = resource.SEARCH_FIELD_PREFIX + sort
|
||||
}
|
||||
s := &resource.ResourceSearchRequest_Sort{Field: sort}
|
||||
if strings.HasPrefix(sort, "-") {
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
@@ -68,11 +69,12 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
sql db.DB,
|
||||
tracing *tracing.TracingService,
|
||||
unified resource.ResourceClient,
|
||||
sorter sort.Service,
|
||||
) *DashboardsAPIBuilder {
|
||||
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
|
||||
dbp := legacysql.NewDatabaseProvider(sql)
|
||||
namespacer := request.GetNamespaceMapper(cfg)
|
||||
legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore)
|
||||
legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
|
||||
builder := &DashboardsAPIBuilder{
|
||||
log: log.New("grafana-apiserver.dashboards.v0alpha1"),
|
||||
DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{
|
||||
@@ -86,7 +88,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
|
||||
legacy: &dashboard.DashboardStorage{
|
||||
Resource: dashboardv0alpha1.DashboardResourceInfo,
|
||||
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete),
|
||||
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete, sorter),
|
||||
TableConverter: dashboardv0alpha1.DashboardResourceInfo.TableConverter(),
|
||||
Features: features,
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
@@ -65,6 +66,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
sql db.DB,
|
||||
tracing *tracing.TracingService,
|
||||
unified resource.ResourceClient,
|
||||
sorter sort.Service,
|
||||
) *DashboardsAPIBuilder {
|
||||
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
|
||||
dbp := legacysql.NewDatabaseProvider(sql)
|
||||
@@ -81,7 +83,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
|
||||
legacy: &dashboard.DashboardStorage{
|
||||
Resource: dashboardv1alpha1.DashboardResourceInfo,
|
||||
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete),
|
||||
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete, sorter),
|
||||
TableConverter: dashboardv1alpha1.DashboardResourceInfo.TableConverter(),
|
||||
Features: features,
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
@@ -65,6 +66,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
sql db.DB,
|
||||
tracing *tracing.TracingService,
|
||||
unified resource.ResourceClient,
|
||||
sorter sort.Service,
|
||||
) *DashboardsAPIBuilder {
|
||||
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
|
||||
dbp := legacysql.NewDatabaseProvider(sql)
|
||||
@@ -82,7 +84,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
|
||||
legacy: &dashboard.DashboardStorage{
|
||||
Resource: dashboardv2alpha1.DashboardResourceInfo,
|
||||
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete),
|
||||
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete, sorter),
|
||||
TableConverter: dashboardv2alpha1.DashboardResourceInfo.TableConverter(),
|
||||
Features: features,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user