Merge remote-tracking branch 'origin' into attempt_at_short_url_updates

This commit is contained in:
nmarrs
2025-12-09 09:36:43 -08:00
110 changed files with 2616 additions and 387 deletions
+1
View File
@@ -85,6 +85,7 @@
# Git Sync frontend owned by frontend team as a whole.
/apps/alerting/ @grafana/alerting-backend
/apps/quotas/ @grafana/grafana-search-and-storage
/apps/dashboard/ @grafana/grafana-app-platform-squad @grafana/dashboards-squad
/apps/folder/ @grafana/grafana-app-platform-squad
/apps/playlist/ @grafana/grafana-app-platform-squad
+1
View File
@@ -93,6 +93,7 @@ COPY pkg/storage/unified/apistore pkg/storage/unified/apistore
COPY pkg/semconv pkg/semconv
COPY pkg/aggregator pkg/aggregator
COPY apps/playlist apps/playlist
COPY apps/quotas apps/quotas
COPY apps/plugins apps/plugins
COPY apps/shorturl apps/shorturl
COPY apps/annotation apps/annotation
+2
View File
@@ -224,6 +224,8 @@ github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba h1:psKWNETD5nGxmF
github.com/grafana/alerting v0.0.0-20251204145817-de8c2bbf9eba/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
github.com/grafana/grafana-app-sdk/logging v0.48.3 h1:72NUpGNiJXCNQz/on++YSsl38xuVYYBKv5kKQaOClX4=
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
github.com/grafana/loki/pkg/push v0.0.0-20250823105456-332df2b20000 h1:/5LKSYgLmAhwA4m6iGUD4w1YkydEWWjazn9qxCFT8W0=
@@ -768,6 +768,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged"
// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing).
VariableHide: *"dontHide" | "hideLabel" | "hideVariable"
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
VariableRegexApplyTo: *"value" | "text"
// Determine the origin of the adhoc variable filter
FilterOrigin: "dashboard"
@@ -803,6 +807,7 @@ QueryVariableSpec: {
datasource?: DataSourceRef
query: DataQueryKind
regex: string | *""
regexApplyTo?: VariableRegexApplyTo
sort: VariableSort
definition?: string
options: [...VariableOption] | *[]
@@ -772,6 +772,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged"
// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing), `inControlsMenu` (show in a drop-down menu).
VariableHide: *"dontHide" | "hideLabel" | "hideVariable" | "inControlsMenu"
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
VariableRegexApplyTo: *"value" | "text"
// Determine the origin of the adhoc variable filter
FilterOrigin: "dashboard"
@@ -806,6 +810,7 @@ QueryVariableSpec: {
description?: string
query: DataQueryKind
regex: string | *""
regexApplyTo?: VariableRegexApplyTo
sort: VariableSort
definition?: string
options: [...VariableOption] | *[]
@@ -222,6 +222,8 @@ lineage: schemas: [{
// Optional field, if you want to extract part of a series name or metric node segment.
// Named capture groups can be used to separate the display text and value.
regex?: string
// Determine whether regex applies to variable value or display text
regexApplyTo?: #VariableRegexApplyTo
// Additional static options for query variable
staticOptions?: [...#VariableOption]
// Ordering of static options in relation to options returned from data source for query variable
@@ -249,6 +251,10 @@ lineage: schemas: [{
// Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing), 3 (show under the controls dropdown menu).
#VariableHide: 0 | 1 | 2 | 3 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable|inControlsMenu") @grafana(TSVeneer="type")
// Determine whether regex applies to variable value or display text
// Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users)
#VariableRegexApplyTo: "value" | "text" @cuetsy(kind="type")
// Sort variable options
// Accepted values are:
// `0`: No sorting
@@ -222,6 +222,8 @@ lineage: schemas: [{
// Optional field, if you want to extract part of a series name or metric node segment.
// Named capture groups can be used to separate the display text and value.
regex?: string
// Determine whether regex applies to variable value or display text
regexApplyTo?: #VariableRegexApplyTo
// Additional static options for query variable
staticOptions?: [...#VariableOption]
// Ordering of static options in relation to options returned from data source for query variable
@@ -249,6 +251,10 @@ lineage: schemas: [{
// Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing), 3 (show under the controls dropdown menu).
#VariableHide: 0 | 1 | 2 | 3 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable|inControlsMenu") @grafana(TSVeneer="type")
// Determine whether regex applies to variable value or display text
// Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users)
#VariableRegexApplyTo: "value" | "text" @cuetsy(kind="type")
// Sort variable options
// Accepted values are:
// `0`: No sorting
@@ -772,6 +772,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged"
// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing).
VariableHide: *"dontHide" | "hideLabel" | "hideVariable"
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
VariableRegexApplyTo: *"value" | "text"
// Determine the origin of the adhoc variable filter
FilterOrigin: "dashboard"
@@ -807,6 +811,7 @@ QueryVariableSpec: {
datasource?: DataSourceRef
query: DataQueryKind
regex: string | *""
regexApplyTo?: VariableRegexApplyTo
sort: VariableSort
definition?: string
options: [...VariableOption] | *[]
@@ -1364,6 +1364,7 @@ type DashboardQueryVariableSpec struct {
Datasource *DashboardDataSourceRef `json:"datasource,omitempty"`
Query DashboardDataQueryKind `json:"query"`
Regex string `json:"regex"`
RegexApplyTo *DashboardVariableRegexApplyTo `json:"regexApplyTo,omitempty"`
Sort DashboardVariableSort `json:"sort"`
Definition *string `json:"definition,omitempty"`
Options []DashboardVariableOption `json:"options"`
@@ -1393,6 +1394,7 @@ func NewDashboardQueryVariableSpec() *DashboardQueryVariableSpec {
SkipUrlSync: false,
Query: *NewDashboardDataQueryKind(),
Regex: "",
RegexApplyTo: (func(input DashboardVariableRegexApplyTo) *DashboardVariableRegexApplyTo { return &input })(DashboardVariableRegexApplyToValue),
Options: []DashboardVariableOption{},
Multi: false,
IncludeAll: false,
@@ -1443,6 +1445,16 @@ const (
DashboardVariableRefreshOnTimeRangeChanged DashboardVariableRefresh = "onTimeRangeChanged"
)
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
// +k8s:openapi-gen=true
type DashboardVariableRegexApplyTo string
const (
DashboardVariableRegexApplyToValue DashboardVariableRegexApplyTo = "value"
DashboardVariableRegexApplyToText DashboardVariableRegexApplyTo = "text"
)
// Sort variable options
// Accepted values are:
// `disabled`: No sorting
@@ -3646,6 +3646,12 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableSpec(ref common.Re
Format: "",
},
},
"regexApplyTo": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"sort": {
SchemaProps: spec.SchemaProps{
Default: "",
@@ -776,6 +776,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged"
// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing), `inControlsMenu` (show in a drop-down menu).
VariableHide: *"dontHide" | "hideLabel" | "hideVariable" | "inControlsMenu"
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
VariableRegexApplyTo: *"value" | "text"
// Determine the origin of the adhoc variable filter
FilterOrigin: "dashboard"
@@ -810,6 +814,7 @@ QueryVariableSpec: {
description?: string
query: DataQueryKind
regex: string | *""
regexApplyTo?: VariableRegexApplyTo
sort: VariableSort
definition?: string
options: [...VariableOption] | *[]
@@ -1367,6 +1367,7 @@ type DashboardQueryVariableSpec struct {
Description *string `json:"description,omitempty"`
Query DashboardDataQueryKind `json:"query"`
Regex string `json:"regex"`
RegexApplyTo *DashboardVariableRegexApplyTo `json:"regexApplyTo,omitempty"`
Sort DashboardVariableSort `json:"sort"`
Definition *string `json:"definition,omitempty"`
Options []DashboardVariableOption `json:"options"`
@@ -1396,6 +1397,7 @@ func NewDashboardQueryVariableSpec() *DashboardQueryVariableSpec {
SkipUrlSync: false,
Query: *NewDashboardDataQueryKind(),
Regex: "",
RegexApplyTo: (func(input DashboardVariableRegexApplyTo) *DashboardVariableRegexApplyTo { return &input })(DashboardVariableRegexApplyToValue),
Options: []DashboardVariableOption{},
Multi: false,
IncludeAll: false,
@@ -1447,6 +1449,16 @@ const (
DashboardVariableRefreshOnTimeRangeChanged DashboardVariableRefresh = "onTimeRangeChanged"
)
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
// +k8s:openapi-gen=true
type DashboardVariableRegexApplyTo string
const (
DashboardVariableRegexApplyToValue DashboardVariableRegexApplyTo = "value"
DashboardVariableRegexApplyToText DashboardVariableRegexApplyTo = "text"
)
// Sort variable options
// Accepted values are:
// `disabled`: No sorting
@@ -3656,6 +3656,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardQueryVariableSpec(ref common.Ref
Format: "",
},
},
"regexApplyTo": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"sort": {
SchemaProps: spec.SchemaProps{
Default: "",
File diff suppressed because one or more lines are too long
@@ -12,13 +12,6 @@ import (
)
func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error {
// Wrap the provider once with 10s caching for all conversions.
// This prevents repeated DB queries across multiple conversion calls while allowing
// the cache to refresh periodically, making it suitable for long-lived singleton usage.
dsIndexProvider = schemaversion.WrapIndexProviderWithCache(dsIndexProvider)
// Wrap library element provider with caching as well
leIndexProvider = schemaversion.WrapLibraryElementProviderWithCache(leIndexProvider)
// v0 conversions
if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv1.Dashboard)(nil),
withConversionMetrics(dashv0.APIVERSION, dashv1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error {
@@ -62,13 +55,13 @@ func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSo
// v2alpha1 conversions
if err := s.AddConversionFunc((*dashv2alpha1.Dashboard)(nil), (*dashv0.Dashboard)(nil),
withConversionMetrics(dashv2alpha1.APIVERSION, dashv0.APIVERSION, func(a, b interface{}, scope conversion.Scope) error {
return Convert_V2alpha1_to_V0(a.(*dashv2alpha1.Dashboard), b.(*dashv0.Dashboard), scope, dsIndexProvider)
return Convert_V2alpha1_to_V0(a.(*dashv2alpha1.Dashboard), b.(*dashv0.Dashboard), scope)
})); err != nil {
return err
}
if err := s.AddConversionFunc((*dashv2alpha1.Dashboard)(nil), (*dashv1.Dashboard)(nil),
withConversionMetrics(dashv2alpha1.APIVERSION, dashv1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error {
return Convert_V2alpha1_to_V1beta1(a.(*dashv2alpha1.Dashboard), b.(*dashv1.Dashboard), scope, dsIndexProvider)
return Convert_V2alpha1_to_V1beta1(a.(*dashv2alpha1.Dashboard), b.(*dashv1.Dashboard), scope)
})); err != nil {
return err
}
@@ -42,7 +42,7 @@
"regex": "",
"skipUrlSync": false,
"refresh": 1
},
},
{
"name": "query_var",
"type": "query",
@@ -81,6 +81,7 @@
"allValue": ".*",
"multi": true,
"regex": "/.*9090.*/",
"regexApplyTo": "text",
"skipUrlSync": false,
"refresh": 2,
"sort": 1,
@@ -107,7 +108,7 @@
},
{
"selected": false,
"text": "staging",
"text": "staging",
"value": "staging"
},
{
@@ -335,6 +336,7 @@
"allValue": "*",
"multi": true,
"regex": "/host[0-9]+/",
"regexApplyTo": "value",
"skipUrlSync": false,
"refresh": 1,
"sort": 2,
@@ -354,4 +356,4 @@
},
"links": []
}
}
}
@@ -94,6 +94,7 @@
"query": "label_values(up, instance)",
"refresh": 2,
"regex": "/.*9090.*/",
"regexApplyTo": "text",
"skipUrlSync": false,
"sort": 1,
"tagValuesQuery": "",
@@ -362,6 +363,7 @@
},
"refresh": 1,
"regex": "/host[0-9]+/",
"regexApplyTo": "value",
"skipUrlSync": false,
"sort": 2,
"tagValuesQuery": "",
@@ -110,6 +110,7 @@
}
},
"regex": "/.*9090.*/",
"regexApplyTo": "text",
"sort": "alphabeticalAsc",
"definition": "label_values(up, instance)",
"options": [
@@ -401,6 +402,7 @@
}
},
"regex": "/host[0-9]+/",
"regexApplyTo": "value",
"sort": "alphabeticalDesc",
"definition": "terms field:@host size:100",
"options": [],
@@ -111,6 +111,7 @@
}
},
"regex": "/.*9090.*/",
"regexApplyTo": "text",
"sort": "alphabeticalAsc",
"definition": "label_values(up, instance)",
"options": [
@@ -404,6 +405,7 @@
}
},
"regex": "/host[0-9]+/",
"regexApplyTo": "value",
"sort": "alphabeticalDesc",
"definition": "terms field:@host size:100",
"options": [],
@@ -229,6 +229,16 @@ func getBoolField(m map[string]interface{}, key string, defaultValue bool) bool
return defaultValue
}
func getUnionField[T ~string](m map[string]interface{}, key string) *T {
if val, ok := m[key]; ok {
if str, ok := val.(string); ok && str != "" {
result := T(str)
return &result
}
}
return nil
}
// Helper function to create int64 pointer
func int64Ptr(i int64) *int64 {
return &i
@@ -1195,6 +1205,7 @@ func buildQueryVariable(ctx context.Context, varMap map[string]interface{}, comm
Refresh: transformVariableRefreshToEnum(varMap["refresh"]),
Sort: transformVariableSortToEnum(varMap["sort"]),
Regex: schemaversion.GetStringValue(varMap, "regex"),
RegexApplyTo: getUnionField[dashv2alpha1.DashboardVariableRegexApplyTo](varMap, "regexApplyTo"),
Query: buildDataQueryKindForVariable(varMap["query"], datasourceType),
AllowCustomValue: getBoolField(varMap, "allowCustomValue", true),
},
@@ -11,10 +11,10 @@ import (
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
)
func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error {
func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error {
// Convert v2alpha1 → v1beta1 first, then v1beta1 → v0
v1beta1 := &dashv1.Dashboard{}
if err := ConvertDashboard_V2alpha1_to_V1beta1(in, v1beta1, scope, dsIndexProvider); err != nil {
if err := ConvertDashboard_V2alpha1_to_V1beta1(in, v1beta1, scope); err != nil {
out.ObjectMeta = in.ObjectMeta
out.APIVersion = dashv0.APIVERSION
out.Kind = in.Kind
@@ -53,13 +53,13 @@ func Convert_V2alpha1_to_V0(in *dashv2alpha1.Dashboard, out *dashv0.Dashboard, s
return nil
}
func Convert_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error {
func Convert_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
out.APIVersion = dashv1.APIVERSION
out.Kind = in.Kind
// Convert the spec
if err := ConvertDashboard_V2alpha1_to_V1beta1(in, out, scope, dsIndexProvider); err != nil {
if err := ConvertDashboard_V2alpha1_to_V1beta1(in, out, scope); err != nil {
out.Status = dashv1.DashboardStatus{
Conversion: &dashv1.DashboardConversionStatus{
StoredVersion: ptr.To(dashv2alpha1.VERSION),
@@ -179,7 +179,7 @@ func Convert_V2beta1_to_V1beta1(in *dashv2beta1.Dashboard, out *dashv1.Dashboard
// Convert v2alpha1 → v1beta1
// Note: ConvertDashboard_V2alpha1_to_V1beta1 will set out.ObjectMeta from v2alpha1,
// but we've already set it from the original input, so it will be preserved
if err := ConvertDashboard_V2alpha1_to_V1beta1(v2alpha1, out, scope, dsIndexProvider); err != nil {
if err := ConvertDashboard_V2alpha1_to_V1beta1(v2alpha1, out, scope); err != nil {
out.Status = dashv1.DashboardStatus{
Conversion: &dashv1.DashboardConversionStatus{
StoredVersion: ptr.To(dashv2beta1.VERSION),
@@ -39,7 +39,7 @@ func TestV2alpha1ConversionErrorHandling(t *testing.T) {
}
target := &dashv1.Dashboard{}
err := Convert_V2alpha1_to_V1beta1(source, target, nil, dsProvider)
err := Convert_V2alpha1_to_V1beta1(source, target, nil)
// Convert_V2alpha1_to_V1beta1 doesn't return error, just sets status
require.NoError(t, err, "Convert_V2alpha1_to_V1beta1 doesn't return error")
@@ -1,14 +1,12 @@
package conversion
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/conversion"
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
"github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion"
"k8s.io/apimachinery/pkg/conversion"
)
// ConvertDashboard_V2alpha1_to_V1beta1 converts a v2alpha1 dashboard to v1beta1 format.
@@ -16,19 +14,13 @@ import (
// that represents the v1 dashboard JSON format.
// The dsIndexProvider is used to resolve default datasources when queries/variables/annotations
// don't have explicit datasource references.
func ConvertDashboard_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope, dsIndexProvider schemaversion.DataSourceIndexProvider) error {
func ConvertDashboard_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
out.APIVersion = dashv1.APIVERSION
out.Kind = in.Kind // Preserve the Kind from input (should be "Dashboard")
// Get datasource index for resolving default datasources
var dsIndex *schemaversion.DatasourceIndex
if dsIndexProvider != nil {
dsIndex = dsIndexProvider.Index(context.Background())
}
// Convert the spec to v1beta1 unstructured format
dashboardJSON, err := convertDashboardSpec_V2alpha1_to_V1beta1(&in.Spec, dsIndex)
dashboardJSON, err := convertDashboardSpec_V2alpha1_to_V1beta1(&in.Spec)
if err != nil {
return fmt.Errorf("failed to convert dashboard spec: %w", err)
}
@@ -39,7 +31,7 @@ func ConvertDashboard_V2alpha1_to_V1beta1(in *dashv2alpha1.Dashboard, out *dashv
return nil
}
func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) {
func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec) (map[string]interface{}, error) {
dashboard := make(map[string]interface{})
// Convert basic fields
@@ -75,7 +67,7 @@ func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec, ds
}
// Convert panels from elements and layout
panels, err := convertPanelsFromElementsAndLayout(in.Elements, in.Layout, dsIndex)
panels, err := convertPanelsFromElementsAndLayout(in.Elements, in.Layout)
if err != nil {
return nil, fmt.Errorf("failed to convert panels: %w", err)
}
@@ -90,7 +82,7 @@ func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec, ds
}
// Convert variables
variables := convertVariablesToV1(in.Variables, dsIndex)
variables := convertVariablesToV1(in.Variables)
if len(variables) > 0 {
dashboard["templating"] = map[string]interface{}{
"list": variables,
@@ -98,7 +90,7 @@ func convertDashboardSpec_V2alpha1_to_V1beta1(in *dashv2alpha1.DashboardSpec, ds
}
// Convert annotations - always include even if empty to prevent DashboardModel from adding built-in
annotations := convertAnnotationsToV1(in.Annotations, dsIndex)
annotations := convertAnnotationsToV1(in.Annotations)
dashboard["annotations"] = map[string]interface{}{
"list": annotations,
}
@@ -236,28 +228,28 @@ func countTotalPanels(panels []interface{}) int {
// - RowsLayout: Rows become row panels; nested structures are flattened
// - AutoGridLayout: Calculates gridPos based on column count and row height
// - TabsLayout: Tabs become expanded row panels; content is flattened
func convertPanelsFromElementsAndLayout(elements map[string]dashv2alpha1.DashboardElement, layout dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, dsIndex *schemaversion.DatasourceIndex) ([]interface{}, error) {
func convertPanelsFromElementsAndLayout(elements map[string]dashv2alpha1.DashboardElement, layout dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind) ([]interface{}, error) {
if layout.GridLayoutKind != nil {
return convertGridLayoutToPanels(elements, layout.GridLayoutKind, dsIndex)
return convertGridLayoutToPanels(elements, layout.GridLayoutKind)
}
if layout.RowsLayoutKind != nil {
return convertRowsLayoutToPanels(elements, layout.RowsLayoutKind, dsIndex)
return convertRowsLayoutToPanels(elements, layout.RowsLayoutKind)
}
if layout.AutoGridLayoutKind != nil {
return convertAutoGridLayoutToPanels(elements, layout.AutoGridLayoutKind, dsIndex)
return convertAutoGridLayoutToPanels(elements, layout.AutoGridLayoutKind)
}
if layout.TabsLayoutKind != nil {
return convertTabsLayoutToPanels(elements, layout.TabsLayoutKind, dsIndex)
return convertTabsLayoutToPanels(elements, layout.TabsLayoutKind)
}
// No layout specified, return empty panels
return []interface{}{}, nil
}
func convertGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, gridLayout *dashv2alpha1.DashboardGridLayoutKind, dsIndex *schemaversion.DatasourceIndex) ([]interface{}, error) {
func convertGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, gridLayout *dashv2alpha1.DashboardGridLayoutKind) ([]interface{}, error) {
panels := make([]interface{}, 0, len(gridLayout.Spec.Items))
for _, item := range gridLayout.Spec.Items {
@@ -266,7 +258,7 @@ func convertGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement
return nil, fmt.Errorf("panel with uid %s not found in the dashboard elements", item.Spec.Element.Name)
}
panel, err := convertPanelFromElement(&element, &item, dsIndex)
panel, err := convertPanelFromElement(&element, &item)
if err != nil {
return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err)
}
@@ -279,21 +271,21 @@ func convertGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement
// convertRowsLayoutToPanels converts a RowsLayout to V1 panels.
// All nested structures (rows within rows, tabs within rows) are flattened to the root level.
// Each row becomes a row panel, and nested content is added sequentially after it.
func convertRowsLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, rowsLayout *dashv2alpha1.DashboardRowsLayoutKind, dsIndex *schemaversion.DatasourceIndex) ([]interface{}, error) {
return convertNestedLayoutToPanels(elements, rowsLayout, nil, dsIndex, 0)
func convertRowsLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, rowsLayout *dashv2alpha1.DashboardRowsLayoutKind) ([]interface{}, error) {
return convertNestedLayoutToPanels(elements, rowsLayout, nil, 0)
}
// convertNestedLayoutToPanels handles arbitrary nesting of RowsLayout and TabsLayout.
// It processes each row/tab in order, tracking Y position to ensure panels don't overlap.
// The function recursively flattens nested structures to produce a flat V1 panel array.
func convertNestedLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, rowsLayout *dashv2alpha1.DashboardRowsLayoutKind, tabsLayout *dashv2alpha1.DashboardTabsLayoutKind, dsIndex *schemaversion.DatasourceIndex, yOffset int64) ([]interface{}, error) {
func convertNestedLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, rowsLayout *dashv2alpha1.DashboardRowsLayoutKind, tabsLayout *dashv2alpha1.DashboardTabsLayoutKind, yOffset int64) ([]interface{}, error) {
panels := make([]interface{}, 0)
currentY := yOffset
// Process RowsLayout
if rowsLayout != nil {
for _, row := range rowsLayout.Spec.Rows {
rowPanels, newY, err := processRowItem(elements, &row, dsIndex, currentY)
rowPanels, newY, err := processRowItem(elements, &row, currentY)
if err != nil {
return nil, err
}
@@ -305,7 +297,7 @@ func convertNestedLayoutToPanels(elements map[string]dashv2alpha1.DashboardEleme
// Process TabsLayout (tabs are converted to rows)
if tabsLayout != nil {
for _, tab := range tabsLayout.Spec.Tabs {
tabPanels, newY, err := processTabItem(elements, &tab, dsIndex, currentY)
tabPanels, newY, err := processTabItem(elements, &tab, currentY)
if err != nil {
return nil, err
}
@@ -324,7 +316,7 @@ func convertNestedLayoutToPanels(elements map[string]dashv2alpha1.DashboardEleme
// - Collapsed row: Panels stored inside row.panels with absolute Y positions
// - Expanded row: Panels added to top level after the row panel
// - Nested layouts: Parent row is preserved; nested content is flattened after it
func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dashv2alpha1.DashboardRowsLayoutRowKind, dsIndex *schemaversion.DatasourceIndex, startY int64) ([]interface{}, int64, error) {
func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dashv2alpha1.DashboardRowsLayoutRowKind, startY int64) ([]interface{}, int64, error) {
panels := make([]interface{}, 0)
currentY := startY
@@ -354,7 +346,7 @@ func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dash
}
// Then process nested rows
nestedPanels, err := convertNestedLayoutToPanels(elements, row.Spec.Layout.RowsLayoutKind, nil, dsIndex, currentY)
nestedPanels, err := convertNestedLayoutToPanels(elements, row.Spec.Layout.RowsLayoutKind, nil, currentY)
if err != nil {
return nil, 0, err
}
@@ -387,7 +379,7 @@ func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dash
}
// Then process nested tabs
nestedPanels, err := convertNestedLayoutToPanels(elements, nil, row.Spec.Layout.TabsLayoutKind, dsIndex, currentY)
nestedPanels, err := convertNestedLayoutToPanels(elements, nil, row.Spec.Layout.TabsLayoutKind, currentY)
if err != nil {
return nil, 0, err
}
@@ -429,7 +421,7 @@ func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dash
// Add collapsed panels if row is collapsed (panels use absolute Y positions)
if isCollapsed {
collapsedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, dsIndex, currentY+1)
collapsedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, currentY+1)
if err != nil {
return nil, 0, err
}
@@ -444,7 +436,7 @@ func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dash
// Add panels from row layout (only for expanded rows or hidden header rows)
if !isCollapsed || isHiddenHeader {
rowPanels, newY, err := extractExpandedPanels(elements, &row.Spec.Layout, dsIndex, currentY, isHiddenHeader, startY)
rowPanels, newY, err := extractExpandedPanels(elements, &row.Spec.Layout, currentY, isHiddenHeader, startY)
if err != nil {
return nil, 0, err
}
@@ -459,7 +451,7 @@ func processRowItem(elements map[string]dashv2alpha1.DashboardElement, row *dash
// Each tab becomes an expanded row panel (collapsed=false) with an empty panels array.
// The tab's content is flattened and added to the top level after the row panel.
// Nested layouts within the tab are recursively processed.
func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dashv2alpha1.DashboardTabsLayoutTabKind, dsIndex *schemaversion.DatasourceIndex, startY int64) ([]interface{}, int64, error) {
func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dashv2alpha1.DashboardTabsLayoutTabKind, startY int64) ([]interface{}, int64, error) {
panels := make([]interface{}, 0)
currentY := startY
@@ -487,7 +479,7 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash
// Handle nested layouts inside the tab
if tab.Spec.Layout.RowsLayoutKind != nil {
// Nested RowsLayout inside tab
nestedPanels, err := convertNestedLayoutToPanels(elements, tab.Spec.Layout.RowsLayoutKind, nil, dsIndex, currentY)
nestedPanels, err := convertNestedLayoutToPanels(elements, tab.Spec.Layout.RowsLayoutKind, nil, currentY)
if err != nil {
return nil, 0, err
}
@@ -495,7 +487,7 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash
currentY = getMaxYFromPanels(nestedPanels, currentY)
} else if tab.Spec.Layout.TabsLayoutKind != nil {
// Nested TabsLayout inside tab
nestedPanels, err := convertNestedLayoutToPanels(elements, nil, tab.Spec.Layout.TabsLayoutKind, dsIndex, currentY)
nestedPanels, err := convertNestedLayoutToPanels(elements, nil, tab.Spec.Layout.TabsLayoutKind, currentY)
if err != nil {
return nil, 0, err
}
@@ -512,7 +504,7 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash
adjustedItem := item
adjustedItem.Spec.Y = item.Spec.Y + currentY
panel, err := convertPanelFromElement(&element, &adjustedItem, dsIndex)
panel, err := convertPanelFromElement(&element, &adjustedItem)
if err != nil {
return nil, 0, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err)
}
@@ -525,7 +517,7 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash
}
} else if tab.Spec.Layout.AutoGridLayoutKind != nil {
// AutoGridLayout inside tab - convert with Y offset
autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, tab.Spec.Layout.AutoGridLayoutKind, dsIndex, currentY)
autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, tab.Spec.Layout.AutoGridLayoutKind, currentY)
if err != nil {
return nil, 0, err
}
@@ -540,7 +532,7 @@ func processTabItem(elements map[string]dashv2alpha1.DashboardElement, tab *dash
// Panels are positioned with absolute Y coordinates (baseY + relative Y).
// This matches V1 behavior where collapsed row panels store their children
// with Y positions as if the row were expanded at that location.
func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, dsIndex *schemaversion.DatasourceIndex, baseY int64) ([]interface{}, error) {
func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, baseY int64) ([]interface{}, error) {
panels := make([]interface{}, 0)
if layout.GridLayoutKind != nil {
@@ -552,7 +544,7 @@ func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.Dashbo
// Create a copy with adjusted Y position
adjustedItem := item
adjustedItem.Spec.Y = item.Spec.Y + baseY
panel, err := convertPanelFromElement(&element, &adjustedItem, dsIndex)
panel, err := convertPanelFromElement(&element, &adjustedItem)
if err != nil {
return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err)
}
@@ -561,7 +553,7 @@ func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.Dashbo
}
// Handle AutoGridLayout for collapsed rows with Y offset
if layout.AutoGridLayoutKind != nil {
autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, dsIndex, baseY)
autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, baseY)
if err != nil {
return nil, err
}
@@ -571,7 +563,7 @@ func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.Dashbo
if layout.RowsLayoutKind != nil {
currentY := baseY
for _, row := range layout.RowsLayoutKind.Spec.Rows {
nestedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, dsIndex, currentY)
nestedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, currentY)
if err != nil {
return nil, err
}
@@ -582,7 +574,7 @@ func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.Dashbo
if layout.TabsLayoutKind != nil {
currentY := baseY
for _, tab := range layout.TabsLayoutKind.Spec.Tabs {
nestedPanels, err := extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements, &tab.Spec.Layout, dsIndex, currentY)
nestedPanels, err := extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements, &tab.Spec.Layout, currentY)
if err != nil {
return nil, err
}
@@ -596,7 +588,7 @@ func extractCollapsedPanelsWithAbsoluteY(elements map[string]dashv2alpha1.Dashbo
// extractCollapsedPanelsFromTabLayoutWithAbsoluteY extracts panels from a tab layout with absolute Y.
// Similar to extractCollapsedPanelsWithAbsoluteY but handles the tab-specific layout type.
func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, dsIndex *schemaversion.DatasourceIndex, baseY int64) ([]interface{}, error) {
func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind, baseY int64) ([]interface{}, error) {
panels := make([]interface{}, 0)
if layout.GridLayoutKind != nil {
@@ -607,7 +599,7 @@ func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2
}
adjustedItem := item
adjustedItem.Spec.Y = item.Spec.Y + baseY
panel, err := convertPanelFromElement(&element, &adjustedItem, dsIndex)
panel, err := convertPanelFromElement(&element, &adjustedItem)
if err != nil {
return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err)
}
@@ -615,7 +607,7 @@ func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2
}
}
if layout.AutoGridLayoutKind != nil {
autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, dsIndex, baseY)
autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, baseY)
if err != nil {
return nil, err
}
@@ -624,7 +616,7 @@ func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2
if layout.RowsLayoutKind != nil {
currentY := baseY
for _, row := range layout.RowsLayoutKind.Spec.Rows {
nestedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, dsIndex, currentY)
nestedPanels, err := extractCollapsedPanelsWithAbsoluteY(elements, &row.Spec.Layout, currentY)
if err != nil {
return nil, err
}
@@ -635,7 +627,7 @@ func extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements map[string]dashv2
if layout.TabsLayoutKind != nil {
currentY := baseY
for _, tab := range layout.TabsLayoutKind.Spec.Tabs {
nestedPanels, err := extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements, &tab.Spec.Layout, dsIndex, currentY)
nestedPanels, err := extractCollapsedPanelsFromTabLayoutWithAbsoluteY(elements, &tab.Spec.Layout, currentY)
if err != nil {
return nil, err
}
@@ -679,7 +671,7 @@ func getLayoutHeightFromTab(layout *dashv2alpha1.DashboardGridLayoutKindOrRowsLa
// - Explicit row: Add (currentY - 1) to relative Y for absolute positioning
//
// Returns the panels and the new Y position for the next row.
func extractExpandedPanels(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, dsIndex *schemaversion.DatasourceIndex, currentY int64, isHiddenHeader bool, startY int64) ([]interface{}, int64, error) {
func extractExpandedPanels(elements map[string]dashv2alpha1.DashboardElement, layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayoutKindOrTabsLayoutKindOrRowsLayoutKind, currentY int64, isHiddenHeader bool, startY int64) ([]interface{}, int64, error) {
panels := make([]interface{}, 0)
// For hidden headers, don't track Y changes (matches original behavior)
maxY := startY
@@ -700,7 +692,7 @@ func extractExpandedPanels(elements map[string]dashv2alpha1.DashboardElement, la
}
// For hidden headers: don't adjust Y, keep item.Spec.Y as-is
panel, err := convertPanelFromElement(&element, &adjustedItem, dsIndex)
panel, err := convertPanelFromElement(&element, &adjustedItem)
if err != nil {
return nil, 0, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err)
}
@@ -725,7 +717,7 @@ func extractExpandedPanels(elements map[string]dashv2alpha1.DashboardElement, la
yOffset = currentY - 1
}
autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, dsIndex, yOffset)
autoGridPanels, err := convertAutoGridLayoutToPanelsWithOffset(elements, layout.AutoGridLayoutKind, yOffset)
if err != nil {
return nil, 0, err
}
@@ -788,7 +780,7 @@ func getLayoutHeight(layout *dashv2alpha1.DashboardGridLayoutKindOrAutoGridLayou
// convertAutoGridLayoutToPanelsWithOffset converts AutoGridLayout with a Y offset.
// Same as convertAutoGridLayoutToPanels but starts at yOffset instead of 0.
// Used when AutoGridLayout appears inside rows or tabs.
func convertAutoGridLayoutToPanelsWithOffset(elements map[string]dashv2alpha1.DashboardElement, autoGridLayout *dashv2alpha1.DashboardAutoGridLayoutKind, dsIndex *schemaversion.DatasourceIndex, yOffset int64) ([]interface{}, error) {
func convertAutoGridLayoutToPanelsWithOffset(elements map[string]dashv2alpha1.DashboardElement, autoGridLayout *dashv2alpha1.DashboardAutoGridLayoutKind, yOffset int64) ([]interface{}, error) {
panels := make([]interface{}, 0, len(autoGridLayout.Spec.Items))
const (
@@ -850,7 +842,7 @@ func convertAutoGridLayoutToPanelsWithOffset(elements map[string]dashv2alpha1.Da
},
}
panel, err := convertPanelFromElement(&element, &gridItem, dsIndex)
panel, err := convertPanelFromElement(&element, &gridItem)
if err != nil {
return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err)
}
@@ -876,7 +868,7 @@ func convertAutoGridLayoutToPanelsWithOffset(elements map[string]dashv2alpha1.Da
//
// Width: 24 / maxColumnCount (default 3 columns = 8 units wide)
// Height: Predefined grid units per mode (see pixelsToGridUnits for custom)
func convertAutoGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, autoGridLayout *dashv2alpha1.DashboardAutoGridLayoutKind, dsIndex *schemaversion.DatasourceIndex) ([]interface{}, error) {
func convertAutoGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, autoGridLayout *dashv2alpha1.DashboardAutoGridLayoutKind) ([]interface{}, error) {
panels := make([]interface{}, 0, len(autoGridLayout.Spec.Items))
const (
@@ -963,7 +955,7 @@ func convertAutoGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardEle
}
}
panel, err := convertPanelFromElement(&element, &gridItem, dsIndex)
panel, err := convertPanelFromElement(&element, &gridItem)
if err != nil {
return nil, fmt.Errorf("failed to convert panel %s: %w", item.Spec.Element.Name, err)
}
@@ -984,11 +976,11 @@ func convertAutoGridLayoutToPanels(elements map[string]dashv2alpha1.DashboardEle
// V1 has no native tab concept, so tabs are converted to expanded row panels.
// Each tab becomes a row panel (collapsed=false, panels=[]) with its content
// flattened to the top level. Tab order is preserved in the output.
func convertTabsLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, tabsLayout *dashv2alpha1.DashboardTabsLayoutKind, dsIndex *schemaversion.DatasourceIndex) ([]interface{}, error) {
return convertNestedLayoutToPanels(elements, nil, tabsLayout, dsIndex, 0)
func convertTabsLayoutToPanels(elements map[string]dashv2alpha1.DashboardElement, tabsLayout *dashv2alpha1.DashboardTabsLayoutKind) ([]interface{}, error) {
return convertNestedLayoutToPanels(elements, nil, tabsLayout, 0)
}
func convertPanelFromElement(element *dashv2alpha1.DashboardElement, layoutItem *dashv2alpha1.DashboardGridLayoutItemKind, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) {
func convertPanelFromElement(element *dashv2alpha1.DashboardElement, layoutItem *dashv2alpha1.DashboardGridLayoutItemKind) (map[string]interface{}, error) {
panel := make(map[string]interface{})
// Set grid position
@@ -1017,7 +1009,7 @@ func convertPanelFromElement(element *dashv2alpha1.DashboardElement, layoutItem
}
if element.PanelKind != nil {
return convertPanelKindToV1(element.PanelKind, panel, dsIndex)
return convertPanelKindToV1(element.PanelKind, panel)
}
if element.LibraryPanelKind != nil {
@@ -1027,7 +1019,7 @@ func convertPanelFromElement(element *dashv2alpha1.DashboardElement, layoutItem
return nil, fmt.Errorf("element has neither PanelKind nor LibraryPanelKind")
}
func convertPanelKindToV1(panelKind *dashv2alpha1.DashboardPanelKind, panel map[string]interface{}, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) {
func convertPanelKindToV1(panelKind *dashv2alpha1.DashboardPanelKind, panel map[string]interface{}) (map[string]interface{}, error) {
spec := panelKind.Spec
panel["id"] = int(spec.Id)
@@ -1069,14 +1061,14 @@ func convertPanelKindToV1(panelKind *dashv2alpha1.DashboardPanelKind, panel map[
// Convert queries (targets)
targets := make([]map[string]interface{}, 0, len(spec.Data.Spec.Queries))
for _, query := range spec.Data.Spec.Queries {
target := convertPanelQueryToV1(&query, dsIndex)
target := convertPanelQueryToV1(&query)
targets = append(targets, target)
}
panel["targets"] = targets
// Detect mixed datasource - set panel.datasource to "mixed" if queries use different datasources
// This matches the frontend behavior in getPanelDataSource (layoutSerializers/utils.ts)
if mixedDS := detectMixedDatasource(spec.Data.Spec.Queries, dsIndex); mixedDS != nil {
if mixedDS := detectMixedDatasource(spec.Data.Spec.Queries); mixedDS != nil {
panel["datasource"] = mixedDS
}
@@ -1125,7 +1117,7 @@ func convertPanelKindToV1(panelKind *dashv2alpha1.DashboardPanelKind, panel map[
return panel, nil
}
func convertPanelQueryToV1(query *dashv2alpha1.DashboardPanelQueryKind, dsIndex *schemaversion.DatasourceIndex) map[string]interface{} {
func convertPanelQueryToV1(query *dashv2alpha1.DashboardPanelQueryKind) map[string]interface{} {
target := make(map[string]interface{})
// Copy query spec (excluding refId, hide, datasource which are handled separately)
@@ -1150,7 +1142,7 @@ func convertPanelQueryToV1(query *dashv2alpha1.DashboardPanelQueryKind, dsIndex
}
// Resolve datasource based on V2 input (reuse shared function)
datasource := getDataSourceForQuery(query.Spec.Datasource, query.Spec.Query.Kind, nil)
datasource := getDataSourceForQuery(query.Spec.Datasource, query.Spec.Query.Kind)
if datasource != nil {
target["datasource"] = datasource
}
@@ -1164,7 +1156,7 @@ func convertPanelQueryToV1(query *dashv2alpha1.DashboardPanelQueryKind, dsIndex
// - Else if queryKind (type) is non-empty → return {type} only
// - Else → return nil (no datasource)
// Used for variables and annotations. Panel queries use convertPanelQueryToV1Target.
func getDataSourceForQuery(explicitDS *dashv2alpha1.DashboardDataSourceRef, queryKind string, _ *schemaversion.DatasourceIndex) map[string]interface{} {
func getDataSourceForQuery(explicitDS *dashv2alpha1.DashboardDataSourceRef, queryKind string) map[string]interface{} {
// Case 1: Explicit datasource with UID provided
if explicitDS != nil && explicitDS.Uid != nil && *explicitDS.Uid != "" {
datasource := map[string]interface{}{
@@ -1195,7 +1187,7 @@ func getDataSourceForQuery(explicitDS *dashv2alpha1.DashboardDataSourceRef, quer
// Compares based on V2 input without runtime resolution:
// - If query has explicit datasource.uid → use that UID and type
// - Else → use query.Kind as type (empty UID)
func detectMixedDatasource(queries []dashv2alpha1.DashboardPanelQueryKind, _ *schemaversion.DatasourceIndex) map[string]interface{} {
func detectMixedDatasource(queries []dashv2alpha1.DashboardPanelQueryKind) map[string]interface{} {
if len(queries) == 0 {
return nil
}
@@ -1254,7 +1246,7 @@ func convertLibraryPanelKindToV1(libPanelKind *dashv2alpha1.DashboardLibraryPane
return panel, nil
}
func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind, dsIndex *schemaversion.DatasourceIndex) []map[string]interface{} {
func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind) []map[string]interface{} {
result := make([]map[string]interface{}, 0, len(variables))
for _, variable := range variables {
@@ -1262,7 +1254,7 @@ func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind, dsInde
var err error
if variable.QueryVariableKind != nil {
varMap, err = convertQueryVariableToV1(variable.QueryVariableKind, dsIndex)
varMap, err = convertQueryVariableToV1(variable.QueryVariableKind)
} else if variable.DatasourceVariableKind != nil {
varMap, err = convertDatasourceVariableToV1(variable.DatasourceVariableKind)
} else if variable.CustomVariableKind != nil {
@@ -1274,9 +1266,9 @@ func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind, dsInde
} else if variable.TextVariableKind != nil {
varMap, err = convertTextVariableToV1(variable.TextVariableKind)
} else if variable.GroupByVariableKind != nil {
varMap, err = convertGroupByVariableToV1(variable.GroupByVariableKind, dsIndex)
varMap, err = convertGroupByVariableToV1(variable.GroupByVariableKind)
} else if variable.AdhocVariableKind != nil {
varMap, err = convertAdhocVariableToV1(variable.AdhocVariableKind, dsIndex)
varMap, err = convertAdhocVariableToV1(variable.AdhocVariableKind)
} else if variable.SwitchVariableKind != nil {
varMap, err = convertSwitchVariableToV1(variable.SwitchVariableKind)
}
@@ -1289,7 +1281,7 @@ func convertVariablesToV1(variables []dashv2alpha1.DashboardVariableKind, dsInde
return result
}
func convertQueryVariableToV1(variable *dashv2alpha1.DashboardQueryVariableKind, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) {
func convertQueryVariableToV1(variable *dashv2alpha1.DashboardQueryVariableKind) (map[string]interface{}, error) {
spec := variable.Spec
varMap := map[string]interface{}{
"name": spec.Name,
@@ -1320,6 +1312,9 @@ func convertQueryVariableToV1(variable *dashv2alpha1.DashboardQueryVariableKind,
if spec.Definition != nil {
varMap["definition"] = *spec.Definition
}
if spec.RegexApplyTo != nil {
varMap["regexApplyTo"] = string(*spec.RegexApplyTo)
}
varMap["allowCustomValue"] = spec.AllowCustomValue
// Convert query - handle LEGACY_STRING_VALUE_KEY
@@ -1336,7 +1331,7 @@ func convertQueryVariableToV1(variable *dashv2alpha1.DashboardQueryVariableKind,
}
// Resolve datasource - use explicit datasource or resolve from query kind (datasource type)/default
datasource := getDataSourceForQuery(spec.Datasource, spec.Query.Kind, dsIndex)
datasource := getDataSourceForQuery(spec.Datasource, spec.Query.Kind)
if datasource != nil {
varMap["datasource"] = datasource
}
@@ -1486,7 +1481,7 @@ func convertTextVariableToV1(variable *dashv2alpha1.DashboardTextVariableKind) (
return varMap, nil
}
func convertGroupByVariableToV1(variable *dashv2alpha1.DashboardGroupByVariableKind, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) {
func convertGroupByVariableToV1(variable *dashv2alpha1.DashboardGroupByVariableKind) (map[string]interface{}, error) {
spec := variable.Spec
varMap := map[string]interface{}{
"name": spec.Name,
@@ -1509,7 +1504,7 @@ func convertGroupByVariableToV1(variable *dashv2alpha1.DashboardGroupByVariableK
}
// Resolve datasource - GroupBy variables don't have a query kind, so use empty string (will fall back to default)
datasource := getDataSourceForQuery(spec.Datasource, "", dsIndex)
datasource := getDataSourceForQuery(spec.Datasource, "")
if datasource != nil {
varMap["datasource"] = datasource
}
@@ -1517,7 +1512,7 @@ func convertGroupByVariableToV1(variable *dashv2alpha1.DashboardGroupByVariableK
return varMap, nil
}
func convertAdhocVariableToV1(variable *dashv2alpha1.DashboardAdhocVariableKind, dsIndex *schemaversion.DatasourceIndex) (map[string]interface{}, error) {
func convertAdhocVariableToV1(variable *dashv2alpha1.DashboardAdhocVariableKind) (map[string]interface{}, error) {
spec := variable.Spec
varMap := map[string]interface{}{
"name": spec.Name,
@@ -1536,7 +1531,7 @@ func convertAdhocVariableToV1(variable *dashv2alpha1.DashboardAdhocVariableKind,
varMap["allowCustomValue"] = spec.AllowCustomValue
// Resolve datasource - Adhoc variables don't have a query kind, so use empty string (will fall back to default)
datasource := getDataSourceForQuery(spec.Datasource, "", dsIndex)
datasource := getDataSourceForQuery(spec.Datasource, "")
if datasource != nil {
varMap["datasource"] = datasource
}
@@ -1663,7 +1658,7 @@ func convertSwitchVariableToV1(variable *dashv2alpha1.DashboardSwitchVariableKin
return varMap, nil
}
func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryKind, dsIndex *schemaversion.DatasourceIndex) []map[string]interface{} {
func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryKind) []map[string]interface{} {
result := make([]map[string]interface{}, 0, len(annotations))
for _, annotation := range annotations {
@@ -1686,7 +1681,7 @@ func convertAnnotationsToV1(annotations []dashv2alpha1.DashboardAnnotationQueryK
if annotation.Spec.Query != nil {
queryKind = annotation.Spec.Query.Kind
}
datasource := getDataSourceForQuery(annotation.Spec.Datasource, queryKind, dsIndex)
datasource := getDataSourceForQuery(annotation.Spec.Datasource, queryKind)
if datasource != nil {
annotationMap["datasource"] = datasource
}
@@ -767,6 +767,7 @@ func convertQueryVariableSpec_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardQuer
out.SkipUrlSync = in.SkipUrlSync
out.Description = in.Description
out.Regex = in.Regex
out.RegexApplyTo = (*dashv2beta1.DashboardVariableRegexApplyTo)(in.RegexApplyTo)
out.Sort = dashv2beta1.DashboardVariableSort(in.Sort)
out.Definition = in.Definition
out.Options = convertVariableOptions_V2alpha1_to_V2beta1(in.Options)
@@ -806,6 +806,7 @@ func convertQueryVariableSpec_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardQuery
out.SkipUrlSync = in.SkipUrlSync
out.Description = in.Description
out.Regex = in.Regex
out.RegexApplyTo = (*dashv2alpha1.DashboardVariableRegexApplyTo)(in.RegexApplyTo)
out.Sort = dashv2alpha1.DashboardVariableSort(in.Sort)
out.Definition = in.Definition
out.Options = convertVariableOptions_V2beta1_to_V2alpha1(in.Options)
+7 -3
View File
@@ -61,9 +61,13 @@ type migrator struct {
func (m *migrator) init(dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) {
initOnce.Do(func() {
m.dsIndexProvider = dsIndexProvider
m.leIndexProvider = leIndexProvider
m.migrations = schemaversion.GetMigrations(dsIndexProvider, leIndexProvider)
// Wrap the provider once with 10s caching for all conversions.
// This prevents repeated DB queries across multiple conversion calls while allowing
// the cache to refresh periodically, making it suitable for long-lived singleton usage.
m.dsIndexProvider = schemaversion.WrapIndexProviderWithCache(dsIndexProvider)
// Wrap library element provider with caching as well
m.leIndexProvider = schemaversion.WrapLibraryElementProviderWithCache(leIndexProvider)
m.migrations = schemaversion.GetMigrations(m.dsIndexProvider, m.leIndexProvider)
close(m.ready)
})
}
+5 -5
View File
@@ -34,7 +34,7 @@ manifest: {
v0alpha1: {
kinds: [examplev0alpha1]
// This is explicitly set to false to keep the example app disabled by default.
// This is explicitly set to false to keep the example app disabled by default.
// It can be enabled via conf overrides, or by setting this value to true and regenerating.
served: false
}
@@ -48,14 +48,14 @@ v1alpha1: {
// served indicates whether this particular version is served by the API server.
// served should be set to false before a version is removed from the manifest entirely.
// served defaults to true if not present.
// This is explicitly set to false to keep the example app disabled by default.
// This is explicitly set to false to keep the example app disabled by default.
// It can be enabled via conf overrides, or by setting this value to true and regenerating.
served: false
// routes contains resource routes for the version, which are split into 'namespaced' and 'cluster' scoped routes.
// This allows you to add additional non-storage- and non-kind- based handlers for your app.
// These should only be used if the behavior cannot be accomplished by reconciliation on storage events or subresource routes on a kind.
routes: {
// namespaced contains namespace-scoped resource routes for the version,
// namespaced contains namespace-scoped resource routes for the version,
// which are exposed as HTTP handlers on '<version>/namespaces/<namespace>/<route>'.
namespaced: {
"/something": {
@@ -72,7 +72,7 @@ v1alpha1: {
}
}
}
// cluster contains cluster-scoped resource routes for the version,
// cluster contains cluster-scoped resource routes for the version,
// which are exposed as HTTP handlers on '<version>/<route>'.
cluster: {
"/other": {
@@ -113,4 +113,4 @@ v1alpha1: {
enabled: true
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
include ../sdk.mk
.PHONY: generate # Run Grafana App SDK code generation
generate: install-app-sdk update-app-sdk
@$(APP_SDK_BIN) generate \
--source=./kinds/ \
--gogenpath=./pkg/apis \
--grouping=group \
--defencoding=none
+92
View File
@@ -0,0 +1,92 @@
module github.com/grafana/grafana/apps/quotas
go 1.25.3
require (
github.com/grafana/grafana-app-sdk v0.48.5
github.com/grafana/grafana-app-sdk/logging v0.48.3
k8s.io/apimachinery v0.34.2
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-test/deep v1.1.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/onsi/gomega v1.36.2 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.3 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/woodsbury/decimal128 v1.3.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.14.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.34.2 // indirect
k8s.io/apiextensions-apiserver v0.34.2 // indirect
k8s.io/client-go v0.34.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
+252
View File
@@ -0,0 +1,252 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/grafana-app-sdk v0.48.2 h1:CQQDhwo1fWaXQVKvxxOcK6azbuY3E2TgJHNAZlYYn7U=
github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk=
github.com/grafana/grafana-app-sdk v0.48.4 h1:t9r+Y6E7D832ZxQ2c1n0lp6cvsYKhhrAodVYzE1y0s0=
github.com/grafana/grafana-app-sdk v0.48.4/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
github.com/grafana/grafana-app-sdk v0.48.5 h1:MS8l9fTZz+VbTfgApn09jw27GxhQ6fNOWGhC4ydvZmM=
github.com/grafana/grafana-app-sdk v0.48.5/go.mod h1:HJsMOSBmt/D/Ihs1SvagOwmXKi0coBMVHlfvdd+qe9Y=
github.com/grafana/grafana-app-sdk/logging v0.48.1 h1:veM0X5LAPyN3KsDLglWjIofndbGuf7MqnrDuDN+F/Ng=
github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
github.com/grafana/grafana-app-sdk/logging v0.48.3/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q=
github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/puzpuzpuz/xsync/v2 v2.5.1 h1:mVGYAvzDSu52+zaGyNjC+24Xw2bQi3kTr4QJ6N9pIIU=
github.com/puzpuzpuz/xsync/v2 v2.5.1/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo=
k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE=
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+2
View File
@@ -0,0 +1,2 @@
module: "github.com/grafana/grafana/apps/quotas/kinds"
language: version: "v0.8.2"
+92
View File
@@ -0,0 +1,92 @@
package kinds
manifest: {
// appName is the unique name of your app. It is used to reference the app from other config objects,
// and to generate the group used by your app in the app platform API.
appName: "quotas"
// groupOverride can be used to specify a non-appName-based API group.
// By default, an app's API group is LOWER(REPLACE(appName, '-', '')).ext.grafana.com,
// but there are cases where this needs to be changed.
// Keep in mind that changing this after an app is deployed can cause problems with clients and/or kind data.
groupOverride: "quotas.grafana.app"
// versions is a map of versions supported by your app. Version names should follow the format "v<integer>" or
// "v<integer>(alpha|beta)<integer>". Each version contains the kinds your app manages for that version.
// If your app needs access to kinds managed by another app, use permissions.accessKinds to allow your app access.
versions: {
"v0alpha1": v0alpha1
}
// extraPermissions contains any additional permissions your app may require to function.
// Your app will always have all permissions for each kind it manages (the items defined in 'kinds').
extraPermissions: {
// If your app needs access to additional kinds supplied by other apps, you can list them here
accessKinds: [
// Here is an example for your app accessing the playlist kind for reads and watch
// {
// group: "playlist.grafana.app"
// resource: "playlists"
// actions: ["get","list","watch"]
// }
]
}
}
// v1alpha1 is the v1alpha1 version of the app's API.
// It includes kinds which the v1alpha1 API serves, and (future) custom routes served globally from the v1alpha1 version.
v0alpha1: {
// kinds is the list of kinds served by this version
kinds: []
// [OPTIONAL]
// served indicates whether this particular version is served by the API server.
// served should be set to false before a version is removed from the manifest entirely.
// served defaults to true if not present.
served: true
routes: {
// namespaced contains namespace-scoped resource routes for the version,
// which are exposed as HTTP handlers on '<version>/namespaces/<namespace>/<route>'.
namespaced: {
"/usage": {
"GET": {
response: {
namespace: string
resource: string
group: string
usage: int64
limit: int64
}
request: {
query: {
group: string
resource: string
}
}
}
}
}
}
// [OPTIONAL]
// Codegen is a trait that tells the grafana-app-sdk, or other code generation tooling, how to process this kind.
// If not present, default values within the codegen trait are used.
// If you wish to specify codegen per-version, put this section in the version's object
// (for example, <no value>v1alpha1) instead.
codegen: {
// [OPTIONAL]
// ts contains TypeScript code generation properties for the kind
ts: {
// [OPTIONAL]
// enabled indicates whether the CLI should generate front-end TypeScript code for the kind.
// Defaults to true if not present.
enabled: true
}
// [OPTIONAL]
// go contains go code generation properties for the kind
go: {
// [OPTIONAL]
// enabled indicates whether the CLI should generate back-end go code for the kind.
// Defaults to true if not present.
enabled: true
}
}
}
@@ -0,0 +1,33 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
import (
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
type GetUsageRequestParamsObject struct {
metav1.TypeMeta `json:",inline"`
GetUsageRequestParams `json:",inline"`
}
func NewGetUsageRequestParamsObject() *GetUsageRequestParamsObject {
return &GetUsageRequestParamsObject{}
}
func (o *GetUsageRequestParamsObject) DeepCopyObject() runtime.Object {
dst := NewGetUsageRequestParamsObject()
o.DeepCopyInto(dst)
return dst
}
func (o *GetUsageRequestParamsObject) DeepCopyInto(dst *GetUsageRequestParamsObject) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
dstGetUsageRequestParams := GetUsageRequestParams{}
_ = resource.CopyObjectInto(&dstGetUsageRequestParams, &o.GetUsageRequestParams)
}
var _ runtime.Object = NewGetUsageRequestParamsObject()
@@ -0,0 +1,13 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
type GetUsageRequestParams struct {
Group string `json:"group"`
Resource string `json:"resource"`
}
// NewGetUsageRequestParams creates a new GetUsageRequestParams object.
func NewGetUsageRequestParams() *GetUsageRequestParams {
return &GetUsageRequestParams{}
}
@@ -0,0 +1,17 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
// +k8s:openapi-gen=true
type GetUsageBody struct {
Namespace string `json:"namespace"`
Resource string `json:"resource"`
Group string `json:"group"`
Usage int64 `json:"usage"`
Limit int64 `json:"limit"`
}
// NewGetUsageBody creates a new GetUsageBody object.
func NewGetUsageBody() *GetUsageBody {
return &GetUsageBody{}
}
@@ -0,0 +1,37 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
package v0alpha1
import (
"github.com/grafana/grafana-app-sdk/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// +k8s:openapi-gen=true
type GetUsage struct {
metav1.TypeMeta `json:",inline"`
GetUsageBody `json:",inline"`
}
func NewGetUsage() *GetUsage {
return &GetUsage{}
}
func (t *GetUsageBody) DeepCopyInto(dst *GetUsageBody) {
_ = resource.CopyObjectInto(dst, t)
}
func (o *GetUsage) DeepCopyObject() runtime.Object {
dst := NewGetUsage()
o.DeepCopyInto(dst)
return dst
}
func (o *GetUsage) DeepCopyInto(dst *GetUsage) {
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
dst.TypeMeta.Kind = o.TypeMeta.Kind
o.GetUsageBody.DeepCopyInto(&dst.GetUsageBody)
}
var _ runtime.Object = NewGetUsage()
+213
View File
@@ -0,0 +1,213 @@
//
// This file is generated by grafana-app-sdk
// DO NOT EDIT
//
package apis
import (
"fmt"
"strings"
"github.com/grafana/grafana-app-sdk/app"
"github.com/grafana/grafana-app-sdk/resource"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
v0alpha1 "github.com/grafana/grafana/apps/quotas/pkg/apis/quotas/v0alpha1"
)
var appManifestData = app.ManifestData{
AppName: "quotas",
Group: "quotas.grafana.app",
PreferredVersion: "v0alpha1",
Versions: []app.ManifestVersion{
{
Name: "v0alpha1",
Served: true,
Kinds: []app.ManifestVersionKind{},
Routes: app.ManifestVersionRoutes{
Namespaced: map[string]spec3.PathProps{
"/usage": {
Get: &spec3.Operation{
OperationProps: spec3.OperationProps{
OperationId: "getUsage",
Parameters: []*spec3.Parameter{
{
ParameterProps: spec3.ParameterProps{
Name: "group",
In: "query",
Required: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
},
},
{
ParameterProps: spec3.ParameterProps{
Name: "resource",
In: "query",
Required: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
},
},
},
Responses: &spec3.Responses{
ResponsesProps: spec3.ResponsesProps{
Default: &spec3.Response{
ResponseProps: spec3.ResponseProps{
Description: "Default OK response",
Content: map[string]*spec3.MediaType{
"application/json": {
MediaTypeProps: spec3.MediaTypeProps{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"apiVersion": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
},
},
"group": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
"kind": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
},
},
"limit": {
SchemaProps: spec.SchemaProps{
Type: []string{"integer"},
},
},
"namespace": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
"resource": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
"usage": {
SchemaProps: spec.SchemaProps{
Type: []string{"integer"},
},
},
},
Required: []string{
"namespace",
"resource",
"group",
"usage",
"limit",
"apiVersion",
"kind",
},
}},
}},
},
},
},
}},
},
},
},
},
Cluster: map[string]spec3.PathProps{},
Schemas: map[string]spec.Schema{},
},
},
},
}
func LocalManifest() app.Manifest {
return app.NewEmbeddedManifest(appManifestData)
}
func RemoteManifest() app.Manifest {
return app.NewAPIServerManifest("quotas")
}
var kindVersionToGoType = map[string]resource.Kind{}
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
// If there is no association for the provided Kind and Version, exists will return false.
func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) {
goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)]
return goType, exists
}
var customRouteToGoResponseType = map[string]any{
"v0alpha1||<namespace>/usage|GET": v0alpha1.GetUsage{},
}
// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists.
// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths.
// If there is no association for the provided kind, version, custom route path, and method, exists will return false.
// Resource routes (those without a kind) should prefix their route with "<namespace>/" if the route is namespaced (otherwise the route is assumed to be cluster-scope)
func ManifestCustomRouteResponsesAssociator(kind, version, path, verb string) (goType any, exists bool) {
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
goType, exists = customRouteToGoResponseType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
return goType, exists
}
var customRouteToGoParamsType = map[string]runtime.Object{}
func ManifestCustomRouteQueryAssociator(kind, version, path, verb string) (goType runtime.Object, exists bool) {
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
goType, exists = customRouteToGoParamsType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
return goType, exists
}
var customRouteToGoRequestBodyType = map[string]any{}
func ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb string) (goType any, exists bool) {
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
goType, exists = customRouteToGoRequestBodyType[fmt.Sprintf("%s|%s|%s|%s", version, kind, path, strings.ToUpper(verb))]
return goType, exists
}
type GoTypeAssociator struct{}
func NewGoTypeAssociator() *GoTypeAssociator {
return &GoTypeAssociator{}
}
func (g *GoTypeAssociator) KindToGoType(kind, version string) (goType resource.Kind, exists bool) {
return ManifestGoTypeAssociator(kind, version)
}
func (g *GoTypeAssociator) CustomRouteReturnGoType(kind, version, path, verb string) (goType any, exists bool) {
return ManifestCustomRouteResponsesAssociator(kind, version, path, verb)
}
func (g *GoTypeAssociator) CustomRouteQueryGoType(kind, version, path, verb string) (goType runtime.Object, exists bool) {
return ManifestCustomRouteQueryAssociator(kind, version, path, verb)
}
func (g *GoTypeAssociator) CustomRouteRequestBodyGoType(kind, version, path, verb string) (goType any, exists bool) {
return ManifestCustomRouteRequestBodyAssociator(kind, version, path, verb)
}
+123
View File
@@ -0,0 +1,123 @@
package app
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/grafana/grafana-app-sdk/app"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana-app-sdk/operator"
"github.com/grafana/grafana-app-sdk/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
unifiedStorage "github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana-app-sdk/simple"
quotasv0alpha1 "github.com/grafana/grafana/apps/quotas/pkg/apis/quotas/v0alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type QuotasAppConfig struct {
ResourceClient unifiedStorage.ResourceClient
}
type QuotasHandler struct {
ResourceClient unifiedStorage.ResourceClient
}
func NewQuotasHandler(cfg *QuotasAppConfig) *QuotasHandler {
return &QuotasHandler{
ResourceClient: cfg.ResourceClient,
}
}
// GetQuota handles requests for the GET /usage resource route
func (h *QuotasHandler) GetQuota(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error {
if !request.URL.Query().Has("group") {
// TODO its returning a 500 instead of 400 bad request
writer.WriteHeader(http.StatusBadRequest)
return fmt.Errorf("missing required query parameters: group")
}
if !request.URL.Query().Has("resource") {
writer.WriteHeader(http.StatusBadRequest)
return fmt.Errorf("missing required query parameters: resource")
}
group := request.URL.Query().Get("group")
res := request.URL.Query().Get("resource")
quotaReq := &resourcepb.QuotaUsageRequest{
Key: &resourcepb.ResourceKey{
Namespace: request.ResourceIdentifier.Namespace,
Group: group,
Resource: res,
},
}
quota, err := h.ResourceClient.GetQuotaUsage(ctx, quotaReq)
if err != nil {
return err
}
writer.Header().Set("Content-Type", "application/json")
return json.NewEncoder(writer).Encode(quotasv0alpha1.GetUsage{
TypeMeta: metav1.TypeMeta{
APIVersion: "quotas.grafana.com/v0alpha1",
Kind: "Quotas",
},
GetUsageBody: quotasv0alpha1.GetUsageBody{
Namespace: request.ResourceIdentifier.Namespace,
Group: group,
Resource: res,
Usage: quota.Usage,
Limit: quota.Limit,
},
})
}
func New(cfg app.Config) (app.App, error) {
appConfig, ok := cfg.SpecificConfig.(*QuotasAppConfig)
if !ok {
return nil, fmt.Errorf("expected QuotasAppConfig but got %T", cfg.SpecificConfig)
}
handler := NewQuotasHandler(appConfig)
simpleConfig := simple.AppConfig{
Name: "quotas",
KubeConfig: cfg.KubeConfig,
InformerConfig: simple.AppInformerConfig{
InformerOptions: operator.InformerOptions{
ErrorHandler: func(ctx context.Context, err error) {
logging.FromContext(ctx).Error("Informer processing error", "error", err)
},
},
},
ManagedKinds: []simple.AppManagedKind{},
VersionedCustomRoutes: map[string]simple.AppVersionRouteHandlers{
"v0alpha1": {
{
Namespaced: true,
Path: "usage",
Method: "GET",
}: handler.GetQuota,
},
},
}
a, err := simple.NewApp(simpleConfig)
if err != nil {
return nil, err
}
err = a.ValidateManifest(cfg.ManifestData)
if err != nil {
return nil, err
}
return a, nil
}
func GetKinds() map[schema.GroupVersion][]resource.Kind {
return map[schema.GroupVersion][]resource.Kind{}
}
+71
View File
@@ -0,0 +1,71 @@
package app
import (
"context"
"net/http/httptest"
"net/url"
"testing"
"github.com/grafana/grafana-app-sdk/app"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestGetQuota(t *testing.T) {
t.Run("will return error when resource param is missing", func(t *testing.T) {
clientMock := resource.NewMockResourceClient(t)
handler := NewQuotasHandler(&QuotasAppConfig{
ResourceClient: clientMock,
})
url, err := url.Parse("http://localhost:3000/apis/quotas.grafana.app/v0alpha1/namespaces/stacks-1/usage?group=dashboard.grafana.app")
require.NoError(t, err)
req := &app.CustomRouteRequest{
URL: url,
Method: "GET",
}
recorder := &httptest.ResponseRecorder{}
err = handler.GetQuota(context.Background(), recorder, req)
require.Error(t, err)
})
t.Run("will return error when group param is missing", func(t *testing.T) {
clientMock := resource.NewMockResourceClient(t)
handler := NewQuotasHandler(&QuotasAppConfig{
ResourceClient: clientMock,
})
url, err := url.Parse("http://localhost:3000/apis/quotas.grafana.app/v0alpha1/namespaces/stacks-1/usage?resource=dashboards")
require.NoError(t, err)
req := &app.CustomRouteRequest{
URL: url,
Method: "GET",
}
recorder := &httptest.ResponseRecorder{}
err = handler.GetQuota(context.Background(), recorder, req)
require.Error(t, err)
})
t.Run("will return quotas response when params are valid", func(t *testing.T) {
clientMock := resource.NewMockResourceClient(t)
clientMock.On("GetQuotaUsage", mock.Anything, mock.Anything, mock.Anything).Return(&resourcepb.QuotaUsageResponse{
Error: nil,
Usage: 1,
Limit: 2,
}, nil)
handler := NewQuotasHandler(&QuotasAppConfig{
ResourceClient: clientMock,
})
url, err := url.Parse("http://localhost:3000/apis/quotas.grafana.app/v0alpha1/namespaces/stacks-1/usage?group=dashboard.grafana.app&resource=dashboards")
require.NoError(t, err)
req := &app.CustomRouteRequest{
URL: url,
Method: "GET",
}
recorder := &httptest.ResponseRecorder{}
err = handler.GetQuota(context.Background(), recorder, req)
require.NoError(t, err)
require.Equal(t, 200, recorder.Code)
})
}
@@ -0,0 +1,49 @@
/*
* This file was generated by grafana-app-sdk. DO NOT EDIT.
*/
import { Spec } from './types.spec.gen';
import { Status } from './types.status.gen';
export interface Metadata {
name: string;
namespace: string;
generateName?: string;
selfLink?: string;
uid?: string;
resourceVersion?: string;
generation?: number;
creationTimestamp?: string;
deletionTimestamp?: string;
deletionGracePeriodSeconds?: number;
labels?: Record<string, string>;
annotations?: Record<string, string>;
ownerReferences?: OwnerReference[];
finalizers?: string[];
managedFields?: ManagedFieldsEntry[];
}
export interface OwnerReference {
apiVersion: string;
kind: string;
name: string;
uid: string;
controller?: boolean;
blockOwnerDeletion?: boolean;
}
export interface ManagedFieldsEntry {
manager?: string;
operation?: string;
apiVersion?: string;
time?: string;
fieldsType?: string;
subresource?: string;
}
export interface Quota {
kind: string;
apiVersion: string;
metadata: Metadata;
spec: Spec;
status: Status;
}
@@ -0,0 +1,30 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
// metadata contains embedded CommonMetadata and can be extended with custom string fields
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
// without external reference as using the CommonMetadata reference breaks thema codegen.
export interface Metadata {
updateTimestamp: string;
createdBy: string;
uid: string;
creationTimestamp: string;
deletionTimestamp?: string;
finalizers: string[];
resourceVersion: string;
generation: number;
updatedBy: string;
labels: Record<string, string>;
}
export const defaultMetadata = (): Metadata => ({
updateTimestamp: "",
createdBy: "",
uid: "",
creationTimestamp: "",
finalizers: [],
resourceVersion: "",
generation: 0,
updatedBy: "",
labels: {},
});
@@ -0,0 +1,14 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
export interface Spec {
count: string;
limit: string;
kind: string;
}
export const defaultSpec = (): Spec => ({
count: "",
limit: "",
kind: "",
});
@@ -0,0 +1,30 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
export interface OperatorState {
// lastEvaluation is the ResourceVersion last evaluated
lastEvaluation: string;
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
state: "success" | "in_progress" | "failed";
// descriptiveState is an optional more descriptive state field which has no requirements on format
descriptiveState?: string;
// details contains any extra information that is operator-specific
details?: Record<string, any>;
}
export const defaultOperatorState = (): OperatorState => ({
lastEvaluation: "",
state: "success",
});
export interface Status {
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
operatorStates?: Record<string, OperatorState>;
// additionalFields is reserved for future use
additionalFields?: Record<string, any>;
}
export const defaultStatus = (): Status => ({
});
@@ -171,6 +171,7 @@ Query expressions are different for each data source. For more information, refe
- If you need more room in a single input field query editor, then hover your cursor over the lines in the lower right corner of the field and drag downward to expand.
1. (Optional) In the **Regex** field, type a regular expression to filter or capture specific parts of the names returned by your data source query. To see examples, refer to [Filter variables with a regular expression](#filter-variables-with-regex).
1. Under **Apply regex to**, select **Variable value** or **Display text** to choose where the regex pattern is applied. The default is **Variable value**.
1. In the **Sort** drop-down list, select the sort order for values to be displayed in the dropdown list. The default option, **Disabled**, means that the order of options returned by your data source query is used.
1. Under **Refresh**, select when the variable should update options:
- **On dashboard load** - Queries the data source every time the dashboard loads. This slows down dashboard loading, because the variable query needs to be completed before dashboard can be initialized.
@@ -79,6 +79,16 @@ test.describe(
await expect(regexInput).toHaveAttribute('placeholder', '/.*-(?<text>.*)-(?<value>.*)-.*/');
await expect(regexInput).toHaveValue('');
// Check regex apply to field - should default to "Variable value"
const regexApplyToField = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2
);
await expect(regexApplyToField).toBeVisible();
const variableValueRadio = page.getByRole('radio', { name: 'Variable value' });
await expect(variableValueRadio).toBeChecked();
const displayTextRadio = page.getByRole('radio', { name: 'Display text' });
await expect(displayTextRadio).not.toBeChecked();
const sortSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2
);
-5
View File
@@ -2001,11 +2001,6 @@
"count": 1
}
},
"public/app/features/dashboard-scene/settings/variables/components/VariableTextAreaField.tsx": {
"no-restricted-syntax": {
"count": 1
}
},
"public/app/features/dashboard-scene/settings/variables/components/VariableTextField.tsx": {
"no-restricted-syntax": {
"count": 1
+2 -2
View File
@@ -665,7 +665,7 @@ require (
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-openapi/swag/conv v0.25.1 // indirect
github.com/go-openapi/swag/fileutils v0.25.1 // indirect
@@ -686,7 +686,7 @@ require (
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
github.com/shirou/gopsutil/v4 v4.25.3 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
+4 -6
View File
@@ -646,7 +646,6 @@ gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0p
github.com/1NCE-GmbH/grpc-go-pool v0.0.0-20231117122434-2a5bb974daa2 h1:qFYgLH2zZe3WHpQgUrzeazC+ebDebwAQqS9yE1cP5Bs=
github.com/1NCE-GmbH/grpc-go-pool v0.0.0-20231117122434-2a5bb974daa2/go.mod h1:09/ALd1AXCTCOfcJYD8+jIYKmFmi6PVCkTsipC18F7E=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
github.com/Azure/azure-sdk-for-go v23.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
@@ -1075,7 +1074,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg=
github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc=
@@ -1142,8 +1140,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
@@ -2393,8 +2391,8 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3 h1:/4/IJi5iyTdh6mqOUaASW148HQpujYiHl0Wl78dSOSc=
github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3/go.mod h1:aJIMhRsunltJR926EB2MUg8qHemFQDreSB33pyto2Ps=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE=
github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+1
View File
@@ -23,6 +23,7 @@ use (
./apps/plugins
./apps/preferences
./apps/provisioning
./apps/quotas
./apps/scope
./apps/secret
./apps/shorturl
+6 -14
View File
@@ -267,6 +267,8 @@ gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06
git.sr.ht/~sbinet/gg v0.6.0 h1:RIzgkizAk+9r7uPzf/VfbJHBMKUr0F5hRFxTUGMnt38=
git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU=
github.com/Azure/azure-amqp-common-go/v3 v3.2.3 h1:uDF62mbd9bypXWi19V1bN5NZEO84JqgmI5G73ibAmrk=
github.com/Azure/azure-amqp-common-go/v3 v3.2.3/go.mod h1:7rPmbSfszeovxGfc5fSAXE4ehlXQZHpMja2OtxC2Tas=
@@ -602,6 +604,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk=
github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo=
@@ -680,8 +684,6 @@ github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/efficientgo/tools/core v0.0.0-20220225185207-fe763185946b h1:ZHiD4/yE4idlbqvAO6iYCOYRzOMRpxkW+FKasRA3tsQ=
github.com/efficientgo/tools/core v0.0.0-20220225185207-fe763185946b/go.mod h1:OmVcnJopJL8d3X3sSXTiypGoUSgFq1aDGmlrdi9dn/M=
github.com/elastic/elastic-transport-go/v8 v8.6.1 h1:h2jQRqH6eLGiBSN4eZbQnJLtL4bC5b4lfVFRjw2R4e4=
@@ -873,7 +875,6 @@ github.com/grafana/grafana-app-sdk v0.41.0 h1:SYHN3U7B1myRKY3UZZDkFsue9TDmAOap0U
github.com/grafana/grafana-app-sdk v0.41.0/go.mod h1:Wg/3vEZfok1hhIWiHaaJm+FwkosfO98o8KbeLFEnZpY=
github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc=
github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA=
github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk=
github.com/grafana/grafana-app-sdk/logging v0.38.0/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk=
github.com/grafana/grafana-app-sdk/logging v0.39.0 h1:3GgN5+dUZYqq74Q+GT9/ET+yo+V54zWQk/Q2/JsJQB4=
github.com/grafana/grafana-app-sdk/logging v0.39.0/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk=
@@ -887,9 +888,10 @@ github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5
github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
github.com/grafana/grafana-app-sdk/logging v0.48.0 h1:xolkQxBlA2LQF4hprKIAeu+zUem1DigYZ6XC1TOhFJE=
github.com/grafana/grafana-app-sdk/logging v0.48.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
github.com/grafana/grafana-app-sdk/logging v0.48.2 h1:tI+a9slUvxKUgweXDzUqkca2LWV3g1UdaSvwt8nQNHg=
github.com/grafana/grafana-app-sdk/logging v0.48.2/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
github.com/grafana/grafana-app-sdk/logging v0.48.5 h1:vWiTZrsSscbC5IQq2heWXm0dXhvg40nXIeUTCdE9qsc=
github.com/grafana/grafana-app-sdk/logging v0.48.5/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
github.com/grafana/grafana-app-sdk/plugin v0.41.0 h1:ShUvGpAVzM3UxcsfwS6l/lwW4ytDeTbCQXf8w2P8Yp8=
github.com/grafana/grafana-app-sdk/plugin v0.41.0/go.mod h1:YIhimVfAqtOp3kdhxOanaSZjypVKh/bYxf9wfFfhDm0=
github.com/grafana/grafana-aws-sdk v0.38.2 h1:TzQD0OpWsNjtldi5G5TLDlBRk8OyDf+B5ujcoAu4Dp0=
@@ -956,7 +958,6 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9K
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
@@ -1375,7 +1376,6 @@ github.com/richardartoul/molecule v1.0.0/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+u
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
@@ -1412,8 +1412,6 @@ github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKl
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE=
github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
@@ -1593,7 +1591,6 @@ go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5queth
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/collector v0.121.0/go.mod h1:M4TlnmkjIgishm2DNCk9K3hMKTmAsY9w8cNFsp9EchM=
go.opentelemetry.io/collector v0.124.0/go.mod h1:QzERYfmHUedawjr8Ph/CBEEkVqWS8IlxRLAZt+KHlCg=
go.opentelemetry.io/collector/client v1.29.0/go.mod h1:LCUoEV2KCTKA1i+/txZaGsSPVWUcqeOV6wCfNsAippE=
@@ -1880,7 +1877,6 @@ go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v8
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
@@ -2081,7 +2077,6 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw=
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822 h1:zWFRixYR5QlotL+Uv3YfsPRENIrQFXiGs+iwqel6fOQ=
@@ -2112,7 +2107,6 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
@@ -2135,7 +2129,6 @@ google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7E
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE=
google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI=
google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0=
@@ -2250,7 +2243,6 @@ sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ih
sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk=
sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/structured-merge-diff/v6 v6.2.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
+6
View File
@@ -218,6 +218,8 @@ lineage: schemas: [{
// Optional field, if you want to extract part of a series name or metric node segment.
// Named capture groups can be used to separate the display text and value.
regex?: string
// Determine whether regex applies to variable value or display text
regexApplyTo?: #VariableRegexApplyTo
// Additional static options for query variable
staticOptions?: [...#VariableOption]
// Ordering of static options in relation to options returned from data source for query variable
@@ -245,6 +247,10 @@ lineage: schemas: [{
// Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing), 3 (show under the controls dropdown menu).
#VariableHide: 0 | 1 | 2 | 3 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable|inControlsMenu") @grafana(TSVeneer="type")
// Determine whether regex applies to variable value or display text
// Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users)
#VariableRegexApplyTo: "value" | "text" @cuetsy(kind="type")
// Sort variable options
// Accepted values are:
// `0`: No sorting
+2 -2
View File
@@ -296,8 +296,8 @@
"@grafana/plugin-ui": "^0.11.1",
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "6.47.1",
"@grafana/scenes-react": "6.47.1",
"@grafana/scenes": "6.49.0",
"@grafana/scenes-react": "6.49.0",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
+1
View File
@@ -521,6 +521,7 @@ export {
VariableRefresh,
VariableSort,
VariableHide,
type VariableRegexApplyTo,
type VariableType,
type VariableModel,
type TypedVariableModel,
+8
View File
@@ -261,6 +261,10 @@ export interface FeatureToggles {
*/
kubernetesCorrelations?: boolean;
/**
* Adds support for Kubernetes unified storage quotas
*/
kubernetesUnifiedStorageQuotas?: boolean;
/**
* Adds support for Kubernetes logs drilldown
*/
kubernetesLogsDrilldown?: boolean;
@@ -1220,4 +1224,8 @@ export interface FeatureToggles {
* @default false
*/
useMTPlugins?: boolean;
/**
* Enables support for variables whose values can have multiple properties
*/
multiPropsVariables?: boolean;
}
@@ -32,6 +32,8 @@ export enum VariableRefresh {
onTimeRangeChanged,
}
export type VariableRegexApplyTo = 'value' | 'text';
export enum VariableSort {
disabled,
alphabeticalAsc,
@@ -117,6 +119,7 @@ export interface QueryVariableModel extends VariableWithMultiSupport {
queryValue?: string;
query: any;
regex: string;
regexApplyTo?: VariableRegexApplyTo;
refresh: VariableRefresh;
staticOptions?: VariableOption[];
staticOptionsOrder?: 'before' | 'after' | 'sorted';
@@ -508,6 +508,9 @@ export const versionedPages = {
queryOptionsRegExInputV2: {
[MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Query RegEx field',
},
queryOptionsRegExApplyToSelectV2: {
[MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Query RegExApplyTo select',
},
queryOptionsSortSelect: {
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Sort select',
},
+1
View File
@@ -12,6 +12,7 @@ export type {
AnnotationTarget,
AnnotationPanelFilter,
VariableOption,
VariableRegexApplyTo,
DashboardLink,
DashboardLinkType,
DashboardLinkPlacement,
@@ -187,6 +187,10 @@ export interface VariableModel {
* Named capture groups can be used to separate the display text and value.
*/
regex?: string;
/**
* Determine whether regex applies to variable value or display text
*/
regexApplyTo?: VariableRegexApplyTo;
/**
* Whether the variable value should be managed by URL query params or not
*/
@@ -259,6 +263,12 @@ export enum VariableHide {
inControlsMenu = 3,
}
/**
* Determine whether regex applies to variable value or display text
* Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users)
*/
export type VariableRegexApplyTo = ('value' | 'text');
/**
* Sort variable options
* Accepted values are:
@@ -293,6 +293,7 @@ export const handyTestingSchema: Spec = {
},
refresh: 'onDashboardLoad',
regex: 'regex1',
regexApplyTo: 'value',
skipUrlSync: false,
sort: 'disabled',
allowCustomValue: true,
@@ -1105,6 +1105,7 @@ export interface QueryVariableSpec {
datasource?: DataSourceRef;
query: DataQueryKind;
regex: string;
regexApplyTo?: VariableRegexApplyTo;
sort: VariableSort;
definition?: string;
options: VariableOption[];
@@ -1125,6 +1126,7 @@ export const defaultQueryVariableSpec = (): QueryVariableSpec => ({
skipUrlSync: false,
query: defaultDataQueryKind(),
regex: "",
regexApplyTo: "value",
sort: "disabled",
options: [],
multi: false,
@@ -1161,6 +1163,12 @@ export type VariableRefresh = "never" | "onDashboardLoad" | "onTimeRangeChanged"
export const defaultVariableRefresh = (): VariableRefresh => ("never");
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
export type VariableRegexApplyTo = "value" | "text";
export const defaultVariableRegexApplyTo = (): VariableRegexApplyTo => ("value");
// Sort variable options
// Accepted values are:
// `disabled`: No sorting
@@ -1111,6 +1111,7 @@ export interface QueryVariableSpec {
description?: string;
query: DataQueryKind;
regex: string;
regexApplyTo?: VariableRegexApplyTo;
sort: VariableSort;
definition?: string;
options: VariableOption[];
@@ -1131,6 +1132,7 @@ export const defaultQueryVariableSpec = (): QueryVariableSpec => ({
skipUrlSync: false,
query: defaultDataQueryKind(),
regex: "",
regexApplyTo: "value",
sort: "disabled",
options: [],
multi: false,
@@ -1167,6 +1169,12 @@ export type VariableRefresh = "never" | "onDashboardLoad" | "onTimeRangeChanged"
export const defaultVariableRefresh = (): VariableRefresh => ("never");
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
export type VariableRegexApplyTo = "value" | "text";
export const defaultVariableRegexApplyTo = (): VariableRegexApplyTo => ("value");
// Sort variable options
// Accepted values are:
// `disabled`: No sorting
+2 -1
View File
@@ -56,7 +56,8 @@ import (
_ "github.com/grafana/e2e"
_ "github.com/grafana/gofpdf"
_ "github.com/grafana/gomemcache/memcache"
_ "github.com/grafana/tempo/pkg/traceql"
_ "github.com/grafana/grafana/apps/alerting/alertenrichment/pkg/apis/alertenrichment/v1beta1"
_ "github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1"
_ "github.com/grafana/tempo/pkg/traceql"
)
+11
View File
@@ -834,6 +834,8 @@ type VariableModel struct {
// Optional field, if you want to extract part of a series name or metric node segment.
// Named capture groups can be used to separate the display text and value.
Regex *string `json:"regex,omitempty"`
// Determine whether regex applies to variable value or display text
RegexApplyTo *VariableRegexApplyTo `json:"regexApplyTo,omitempty"`
// Additional static options for query variable
StaticOptions []VariableOption `json:"staticOptions,omitempty"`
// Ordering of static options in relation to options returned from data source for query variable
@@ -942,6 +944,15 @@ const (
VariableSortNaturalDesc VariableSort = 8
)
// Determine whether regex applies to variable value or display text
// Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users)
type VariableRegexApplyTo string
const (
VariableRegexApplyToValue VariableRegexApplyTo = "value"
VariableRegexApplyToText VariableRegexApplyTo = "text"
)
// Contains the list of annotations that are associated with the dashboard.
// Annotations are used to overlay event markers and overlay event tags on graphs.
// Grafana comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the HTTP API.
@@ -95,3 +95,7 @@ func (d *directResourceClient) BulkProcess(ctx context.Context, opts ...grpc.Cal
func (b *directResourceClient) RebuildIndexes(ctx context.Context, req *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) {
return nil, fmt.Errorf("not implemented")
}
func (b *directResourceClient) GetQuotaUsage(ctx context.Context, req *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error) {
return nil, fmt.Errorf("not implemented")
}
@@ -1103,3 +1103,7 @@ func (m *MockClient) BulkProcess(ctx context.Context, opts ...grpc.CallOption) (
func (m *MockClient) UpdateIndex(ctx context.Context, reason string) error {
return nil
}
func (m *MockClient) GetQuotaUsage(ctx context.Context, req *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error) {
return nil, nil
}
@@ -284,3 +284,6 @@ func (m *MockClient) BulkProcess(ctx context.Context, opts ...grpc.CallOption) (
func (m *MockClient) UpdateIndex(ctx context.Context, reason string) error {
return nil
}
func (m *MockClient) GetQuotaUsage(ctx context.Context, in *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error) {
return nil, nil
}
+7
View File
@@ -3,6 +3,8 @@ package appregistry
import (
"context"
"github.com/grafana/grafana/pkg/registry/apps/quotas"
"github.com/open-feature/go-sdk/openfeature"
"k8s.io/client-go/rest"
"github.com/grafana/grafana-app-sdk/app"
@@ -44,12 +46,17 @@ func ProvideAppInstallers(
exampleAppInstaller *example.ExampleAppInstaller,
advisorAppInstaller *advisor.AdvisorAppInstaller,
alertingHistorianAppInstaller *historian.AlertingHistorianAppInstaller,
quotasAppInstaller *quotas.QuotasAppInstaller,
) []appsdkapiserver.AppInstaller {
featureClient := openfeature.NewDefaultClient()
installers := []appsdkapiserver.AppInstaller{
playlistAppInstaller,
pluginsApplInstaller,
exampleAppInstaller,
}
if featureClient.Boolean(context.Background(), featuremgmt.FlagKubernetesUnifiedStorageQuotas, false, openfeature.TransactionContext(context.Background())) {
installers = append(installers, quotasAppInstaller)
}
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesShortURLs) {
installers = append(installers, shorturlAppInstaller)
+3 -1
View File
@@ -3,6 +3,7 @@ package appregistry
import (
"testing"
"github.com/grafana/grafana/pkg/registry/apps/quotas"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/registry/apps/advisor"
@@ -27,6 +28,7 @@ func TestProvideAppInstallers_Table(t *testing.T) {
exampleAppInstaller := &example.ExampleAppInstaller{}
advisorAppInstaller := &advisor.AdvisorAppInstaller{}
historianAppInstaller := &historian.AlertingHistorianAppInstaller{}
quotasAppInstaller := &quotas.QuotasAppInstaller{}
tests := []struct {
name string
@@ -43,7 +45,7 @@ func TestProvideAppInstallers_Table(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
features := featuremgmt.WithFeatures(tt.flags...)
got := ProvideAppInstallers(features, playlistInstaller, pluginsInstaller, nil, tt.rulesInst, correlationsAppInstaller, notificationsAppInstaller, nil, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, historianAppInstaller)
got := ProvideAppInstallers(features, playlistInstaller, pluginsInstaller, nil, tt.rulesInst, correlationsAppInstaller, notificationsAppInstaller, nil, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, historianAppInstaller, quotasAppInstaller)
if tt.expectRulesApp {
require.Contains(t, got, tt.rulesInst)
} else {
+50
View File
@@ -0,0 +1,50 @@
package quotas
import (
"github.com/grafana/grafana/apps/quotas/pkg/apis"
"github.com/grafana/grafana/pkg/storage/unified/resource"
restclient "k8s.io/client-go/rest"
"github.com/grafana/grafana-app-sdk/app"
appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver"
"github.com/grafana/grafana-app-sdk/simple"
quotasapp "github.com/grafana/grafana/apps/quotas/pkg/app"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
)
var (
_ appsdkapiserver.AppInstaller = (*QuotasAppInstaller)(nil)
)
type QuotasAppInstaller struct {
appsdkapiserver.AppInstaller
cfg *setting.Cfg
}
func RegisterAppInstaller(
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
resourceClient resource.ResourceClient,
) (*QuotasAppInstaller, error) {
installer := &QuotasAppInstaller{
cfg: cfg,
}
specificConfig := &quotasapp.QuotasAppConfig{
ResourceClient: resourceClient,
}
provider := simple.NewAppProvider(apis.LocalManifest(), specificConfig, quotasapp.New)
appConfig := app.Config{
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
ManifestData: *apis.LocalManifest().ManifestData,
SpecificConfig: specificConfig,
}
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, apis.NewGoTypeAssociator())
if err != nil {
return nil, err
}
installer.AppInstaller = i
return installer, nil
}
+2
View File
@@ -2,6 +2,7 @@ package appregistry
import (
"github.com/google/wire"
"github.com/grafana/grafana/pkg/registry/apps/quotas"
"github.com/grafana/grafana/pkg/registry/apps/alerting/historian"
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications"
@@ -29,5 +30,6 @@ var WireSet = wire.NewSet(
historian.RegisterAppInstaller,
logsdrilldown.RegisterAppInstaller,
annotation.RegisterAppInstaller,
quotas.RegisterAppInstaller,
example.RegisterAppInstaller,
)
+11 -2
View File
@@ -89,6 +89,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apps/logsdrilldown"
"github.com/grafana/grafana/pkg/registry/apps/playlist"
"github.com/grafana/grafana/pkg/registry/apps/plugins"
"github.com/grafana/grafana/pkg/registry/apps/quotas"
"github.com/grafana/grafana/pkg/registry/apps/shorturl"
"github.com/grafana/grafana/pkg/registry/backgroundsvcs"
"github.com/grafana/grafana/pkg/registry/usagestatssvcs"
@@ -824,7 +825,11 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
if err != nil {
return nil, err
}
v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller)
quotasAppInstaller, err := quotas.RegisterAppInstaller(cfg, featureToggles, resourceClient)
if err != nil {
return nil, err
}
v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller, quotasAppInstaller)
builderMetrics := builder.ProvideBuilderMetrics(registerer)
apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics)
if err != nil {
@@ -1477,7 +1482,11 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
if err != nil {
return nil, err
}
v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller)
quotasAppInstaller, err := quotas.RegisterAppInstaller(cfg, featureToggles, resourceClient)
if err != nil {
return nil, err
}
v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, appInstaller, shortURLAppInstaller, alertingRulesAppInstaller, correlationsAppInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, annotationAppInstaller, exampleAppInstaller, advisorAppInstaller, alertingHistorianAppInstaller, quotasAppInstaller)
builderMetrics := builder.ProvideBuilderMetrics(registerer)
apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics)
if err != nil {
+14
View File
@@ -414,6 +414,13 @@ var (
Owner: grafanaDataProSquad,
RequiresRestart: true,
},
{
Name: "kubernetesUnifiedStorageQuotas",
Description: "Adds support for Kubernetes unified storage quotas",
Stage: FeatureStageExperimental,
Owner: grafanaSearchAndStorageSquad,
RequiresRestart: true,
},
{
Name: "kubernetesLogsDrilldown",
Description: "Adds support for Kubernetes logs drilldown",
@@ -2015,6 +2022,13 @@ var (
FrontendOnly: true,
Expression: "false",
},
{
Name: "multiPropsVariables",
Description: "Enables support for variables whose values can have multiple properties",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaDashboardsSquad,
},
}
)
@@ -244,7 +244,6 @@ addFieldFromCalculationStatFunctions,2023-11-03T14:39:58Z,2025-11-17T15:58:43Z,6
pdfTables,2023-11-06T13:39:22Z,,95b48339f89c7d267bce7d894404d26ccd75d0e3,Agnès Toulet
newVizTooltips,2023-11-06T16:35:59Z,2024-04-03T00:32:01Z,6b4b7127544865b78f712d907e6f1719595f4232,Adela Almasan
ssoSettingsApi,2023-11-08T09:50:01Z,2025-07-03T08:53:33Z,5285e9503be5702680acb2b52a6bda0632f4603d,Misi
logsInfiniteScrolling,2023-11-09T10:54:03Z,,174c2ab45a2af912519153c5c3e671f04396d7d7,Matias Chomicki
flameGraphItemCollapsing,2023-11-09T14:31:07Z,2024-07-15T12:45:41Z,494a07b522df4e3ff9512b47767745a94c15f080,Andrej Ocenas
alertingDetailsViewV2,2023-11-09T17:35:03Z,2024-03-14T14:18:01Z,323ee7c38ceb18b8e71c780d797fabac7041a673,Gilles De Mey
alertingSimplifiedRouting,2023-11-10T13:14:39Z,2025-05-09T13:30:56Z,68e37c3925080cf64a5e7570eabb042f19ca2dbf,Sonia Aguilar
1 #name created deleted hash author
244 pdfTables 2023-11-06T13:39:22Z 95b48339f89c7d267bce7d894404d26ccd75d0e3 Agnès Toulet
245 newVizTooltips 2023-11-06T16:35:59Z 2024-04-03T00:32:01Z 6b4b7127544865b78f712d907e6f1719595f4232 Adela Almasan
246 ssoSettingsApi 2023-11-08T09:50:01Z 2025-07-03T08:53:33Z 5285e9503be5702680acb2b52a6bda0632f4603d Misi
logsInfiniteScrolling 2023-11-09T10:54:03Z 174c2ab45a2af912519153c5c3e671f04396d7d7 Matias Chomicki
247 flameGraphItemCollapsing 2023-11-09T14:31:07Z 2024-07-15T12:45:41Z 494a07b522df4e3ff9512b47767745a94c15f080 Andrej Ocenas
248 alertingDetailsViewV2 2023-11-09T17:35:03Z 2024-03-14T14:18:01Z 323ee7c38ceb18b8e71c780d797fabac7041a673 Gilles De Mey
249 alertingSimplifiedRouting 2023-11-10T13:14:39Z 2025-05-09T13:30:56Z 68e37c3925080cf64a5e7570eabb042f19ca2dbf Sonia Aguilar
+2
View File
@@ -56,6 +56,7 @@ kubernetesShortURLs,experimental,@grafana/grafana-app-platform-squad,false,true,
useKubernetesShortURLsAPI,experimental,@grafana/sharing-squad,false,false,true
kubernetesAlertingRules,experimental,@grafana/alerting-squad,false,true,false
kubernetesCorrelations,experimental,@grafana/datapro,false,true,false
kubernetesUnifiedStorageQuotas,experimental,@grafana/search-and-storage,false,true,false
kubernetesLogsDrilldown,experimental,@grafana/observability-logs,false,true,false
kubernetesQueryCaching,experimental,@grafana/grafana-operator-experience-squad,false,true,false
dashboardDisableSchemaValidationV1,experimental,@grafana/grafana-app-platform-squad,false,false,false
@@ -273,3 +274,4 @@ lokiQueryLimitsContext,experimental,@grafana/observability-logs,false,false,true
rudderstackUpgrade,experimental,@grafana/grafana-frontend-platform,false,false,true
kubernetesAlertingHistorian,experimental,@grafana/alerting-squad,false,true,false
useMTPlugins,experimental,@grafana/plugins-platform-backend,false,false,true
multiPropsVariables,experimental,@grafana/dashboards-squad,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
56 useKubernetesShortURLsAPI experimental @grafana/sharing-squad false false true
57 kubernetesAlertingRules experimental @grafana/alerting-squad false true false
58 kubernetesCorrelations experimental @grafana/datapro false true false
59 kubernetesUnifiedStorageQuotas experimental @grafana/search-and-storage false true false
60 kubernetesLogsDrilldown experimental @grafana/observability-logs false true false
61 kubernetesQueryCaching experimental @grafana/grafana-operator-experience-squad false true false
62 dashboardDisableSchemaValidationV1 experimental @grafana/grafana-app-platform-squad false false false
274 rudderstackUpgrade experimental @grafana/grafana-frontend-platform false false true
275 kubernetesAlertingHistorian experimental @grafana/alerting-squad false true false
276 useMTPlugins experimental @grafana/plugins-platform-backend false false true
277 multiPropsVariables experimental @grafana/dashboards-squad false false true
+4
View File
@@ -183,6 +183,10 @@ const (
// Adds support for Kubernetes correlations
FlagKubernetesCorrelations = "kubernetesCorrelations"
// FlagKubernetesUnifiedStorageQuotas
// Adds support for Kubernetes unified storage quotas
FlagKubernetesUnifiedStorageQuotas = "kubernetesUnifiedStorageQuotas"
// FlagKubernetesLogsDrilldown
// Adds support for Kubernetes logs drilldown
FlagKubernetesLogsDrilldown = "kubernetesLogsDrilldown"
+26
View File
@@ -2039,6 +2039,19 @@
"requiresRestart": true
}
},
{
"metadata": {
"name": "kubernetesUnifiedStorageQuotas",
"resourceVersion": "1764965198011",
"creationTimestamp": "2025-12-05T20:06:38Z"
},
"spec": {
"description": "Adds support for Kubernetes unified storage quotas",
"stage": "experimental",
"codeowner": "@grafana/search-and-storage",
"requiresRestart": true
}
},
{
"metadata": {
"name": "localeFormatPreference",
@@ -2259,6 +2272,19 @@
"codeowner": "@grafana/alerting-squad"
}
},
{
"metadata": {
"name": "multiPropsVariables",
"resourceVersion": "1765293230587",
"creationTimestamp": "2025-12-09T15:13:50Z"
},
"spec": {
"description": "Enables support for variables whose values can have multiple properties",
"stage": "experimental",
"codeowner": "@grafana/dashboards-squad",
"frontend": true
}
},
{
"metadata": {
"name": "multiTenantTempCredentials",
@@ -166,6 +166,7 @@ type resourceClientMock struct {
resourcepb.BulkStoreClient
resourcepb.BlobStoreClient
resourcepb.DiagnosticsClient
resourcepb.QuotasClient
}
// always return GRPC Unauthenticated code
+21
View File
@@ -587,6 +587,22 @@ message ResourceTableRow {
bytes object = 4;
}
message QuotaUsageRequest {
// Namespace (tenant)
ResourceKey key = 1;
}
message QuotaUsageResponse {
// Error details
ErrorResult error = 1;
// Current usage
int64 usage = 2;
// Current limit
int64 limit = 3;
}
// This provides the CRUD+List+Watch support needed for a k8s apiserver
// The semantics and behaviors of this service are constrained by kubernetes
// This does not understand the resource schemas, only deals with json bytes
@@ -631,3 +647,8 @@ service Diagnostics {
// Check if the service is healthy
rpc IsHealthy(HealthCheckRequest) returns (HealthCheckResponse);
}
service Quotas {
// Get current quota usage and limits
rpc GetQuotaUsage(QuotaUsageRequest) returns (QuotaUsageResponse);
}
+4
View File
@@ -39,6 +39,7 @@ type ResourceClient interface {
resourcepb.BulkStoreClient
resourcepb.BlobStoreClient
resourcepb.DiagnosticsClient
resourcepb.QuotasClient
}
// Internal implementation
@@ -49,6 +50,7 @@ type resourceClient struct {
resourcepb.BulkStoreClient
resourcepb.BlobStoreClient
resourcepb.DiagnosticsClient
resourcepb.QuotasClient
}
func NewResourceClient(conn, indexConn grpc.ClientConnInterface, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer trace.Tracer) (ResourceClient, error) {
@@ -76,6 +78,7 @@ func newResourceClient(storageCc grpc.ClientConnInterface, indexCc grpc.ClientCo
BulkStoreClient: resourcepb.NewBulkStoreClient(storageCc),
BlobStoreClient: resourcepb.NewBlobStoreClient(storageCc),
DiagnosticsClient: resourcepb.NewDiagnosticsClient(storageCc),
QuotasClient: resourcepb.NewQuotasClient(storageCc),
}
}
@@ -102,6 +105,7 @@ func NewLocalResourceClient(server ResourceServer) ResourceClient {
&resourcepb.BlobStore_ServiceDesc,
&resourcepb.BulkStore_ServiceDesc,
&resourcepb.Diagnostics_ServiceDesc,
&resourcepb.Quotas_ServiceDesc,
} {
channel.RegisterService(
grpchan.InterceptServer(
@@ -394,6 +394,80 @@ func (_c *MockResourceClient_GetBlob_Call) RunAndReturn(run func(context.Context
return _c
}
// GetQuotaUsage provides a mock function with given fields: ctx, in, opts
func (_m *MockResourceClient) GetQuotaUsage(ctx context.Context, in *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error) {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, in)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
if len(ret) == 0 {
panic("no return value specified for GetQuotaUsage")
}
var r0 *resourcepb.QuotaUsageResponse
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error)); ok {
return rf(ctx, in, opts...)
}
if rf, ok := ret.Get(0).(func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) *resourcepb.QuotaUsageResponse); ok {
r0 = rf(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*resourcepb.QuotaUsageResponse)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) error); ok {
r1 = rf(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockResourceClient_GetQuotaUsage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetQuotaUsage'
type MockResourceClient_GetQuotaUsage_Call struct {
*mock.Call
}
// GetQuotaUsage is a helper method to define mock.On call
// - ctx context.Context
// - in *resourcepb.QuotaUsageRequest
// - opts ...grpc.CallOption
func (_e *MockResourceClient_Expecter) GetQuotaUsage(ctx interface{}, in interface{}, opts ...interface{}) *MockResourceClient_GetQuotaUsage_Call {
return &MockResourceClient_GetQuotaUsage_Call{Call: _e.mock.On("GetQuotaUsage",
append([]interface{}{ctx, in}, opts...)...)}
}
func (_c *MockResourceClient_GetQuotaUsage_Call) Run(run func(ctx context.Context, in *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption)) *MockResourceClient_GetQuotaUsage_Call {
_c.Call.Run(func(args mock.Arguments) {
variadicArgs := make([]grpc.CallOption, len(args)-2)
for i, a := range args[2:] {
if a != nil {
variadicArgs[i] = a.(grpc.CallOption)
}
}
run(args[0].(context.Context), args[1].(*resourcepb.QuotaUsageRequest), variadicArgs...)
})
return _c
}
func (_c *MockResourceClient_GetQuotaUsage_Call) Return(_a0 *resourcepb.QuotaUsageResponse, _a1 error) *MockResourceClient_GetQuotaUsage_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockResourceClient_GetQuotaUsage_Call) RunAndReturn(run func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error)) *MockResourceClient_GetQuotaUsage_Call {
_c.Call.Return(run)
return _c
}
// GetStats provides a mock function with given fields: ctx, in, opts
func (_m *MockResourceClient) GetStats(ctx context.Context, in *resourcepb.ResourceStatsRequest, opts ...grpc.CallOption) (*resourcepb.ResourceStatsResponse, error) {
_va := make([]interface{}, len(opts))
+33
View File
@@ -41,6 +41,7 @@ type ResourceServer interface {
resourcepb.ManagedObjectIndexServer
resourcepb.BlobStoreServer
resourcepb.DiagnosticsServer
resourcepb.QuotasServer
}
type ListIterator interface {
@@ -1466,6 +1467,38 @@ func (s *server) PutBlob(ctx context.Context, req *resourcepb.PutBlobRequest) (*
return rsp, nil
}
func (s *server) GetQuotaUsage(ctx context.Context, req *resourcepb.QuotaUsageRequest) (*resourcepb.QuotaUsageResponse, error) {
if s.overridesService == nil {
return &resourcepb.QuotaUsageResponse{Error: &resourcepb.ErrorResult{
Message: "overrides service not configured on resource server",
Code: http.StatusNotImplemented,
}}, nil
}
nsr := NamespacedResource{
Namespace: req.Key.Namespace,
Group: req.Key.Group,
Resource: req.Key.Resource,
}
usage, err := s.backend.GetResourceStats(ctx, nsr, 0)
if err != nil {
return &resourcepb.QuotaUsageResponse{Error: AsErrorResult(err)}, nil
}
limit, err := s.overridesService.GetQuota(ctx, nsr)
if err != nil {
return &resourcepb.QuotaUsageResponse{Error: AsErrorResult(err)}, nil
}
// handle case where no resources exist yet - very unlikely but possible
rsp := &resourcepb.QuotaUsageResponse{Limit: int64(limit.Limit)}
if len(usage) <= 0 {
rsp.Usage = 0
} else {
rsp.Usage = usage[0].Count
}
return rsp, nil
}
func (s *server) getPartialObject(ctx context.Context, key *resourcepb.ResourceKey, rv int64) (utils.GrafanaMetaAccessor, *resourcepb.ErrorResult) {
if r := verifyRequestKey(key); r != nil {
return nil, r
@@ -5,6 +5,8 @@ import (
"encoding/json"
"errors"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"testing"
@@ -22,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/util/scheduler"
)
@@ -614,3 +617,70 @@ func TestArtificialDelayAfterSuccessfulOperation(t *testing.T) {
check(t, false, &resourcepb.UpdateResponse{Error: AsErrorResult(errors.New("some error"))}, nil)
check(t, false, &resourcepb.DeleteResponse{Error: AsErrorResult(errors.New("some error"))}, nil)
}
func TestGetQuotaUsage(t *testing.T) {
ctx := context.Background()
t.Run("returns error when overrides service is not configured", func(t *testing.T) {
s := &server{
overridesService: nil,
log: log.NewNopLogger(),
}
resp, err := s.GetQuotaUsage(ctx, &resourcepb.QuotaUsageRequest{
Key: &resourcepb.ResourceKey{
Namespace: "stacks-123",
Group: "dashboard.grafana.app",
Resource: "dashboards",
},
})
require.NoError(t, err)
require.NotNil(t, resp.Error)
assert.Equal(t, int32(http.StatusNotImplemented), resp.Error.Code)
assert.Equal(t, "overrides service not configured on resource server", resp.Error.Message)
})
t.Run("returns usage and limit successfully", func(t *testing.T) {
// Create a temporary overrides config file
tmpFile := filepath.Join(t.TempDir(), "overrides.yaml")
content := `"123":
quotas:
dashboard.grafana.app/dashboards:
limit: 500
`
require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644))
// Create a real OverridesService with the temp file
overridesService, err := NewOverridesService(ctx, log.NewNopLogger(), prometheus.NewRegistry(), tracing.NewNoopTracerService(), ReloadOptions{
FilePath: tmpFile,
})
require.NoError(t, err)
require.NoError(t, overridesService.init(ctx))
defer func() {
_ = overridesService.stop(ctx)
}()
// Create a mock backend that returns resource stats (reusing mockStorageBackend from search_test.go)
mockBackend := &mockStorageBackend{
resourceStats: []ResourceStats{{Count: 42}},
}
s := &server{
backend: mockBackend,
overridesService: overridesService,
log: log.NewNopLogger(),
}
resp, err := s.GetQuotaUsage(ctx, &resourcepb.QuotaUsageRequest{
Key: &resourcepb.ResourceKey{
Namespace: "stacks-123",
Group: "dashboard.grafana.app",
Resource: "dashboards",
},
})
require.NoError(t, err)
require.Nil(t, resp.Error)
assert.Equal(t, int64(42), resp.Usage)
assert.Equal(t, int64(500), resp.Limit)
})
}
+249 -118
View File
@@ -2484,6 +2484,114 @@ func (x *ResourceTableRow) GetObject() []byte {
return nil
}
type QuotaUsageRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Namespace (tenant)
Key *ResourceKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *QuotaUsageRequest) Reset() {
*x = QuotaUsageRequest{}
mi := &file_resource_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *QuotaUsageRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*QuotaUsageRequest) ProtoMessage() {}
func (x *QuotaUsageRequest) ProtoReflect() protoreflect.Message {
mi := &file_resource_proto_msgTypes[30]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use QuotaUsageRequest.ProtoReflect.Descriptor instead.
func (*QuotaUsageRequest) Descriptor() ([]byte, []int) {
return file_resource_proto_rawDescGZIP(), []int{30}
}
func (x *QuotaUsageRequest) GetKey() *ResourceKey {
if x != nil {
return x.Key
}
return nil
}
type QuotaUsageResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Error details
Error *ErrorResult `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
// Current usage
Usage int64 `protobuf:"varint,2,opt,name=usage,proto3" json:"usage,omitempty"`
// Current limit
Limit int64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *QuotaUsageResponse) Reset() {
*x = QuotaUsageResponse{}
mi := &file_resource_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *QuotaUsageResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*QuotaUsageResponse) ProtoMessage() {}
func (x *QuotaUsageResponse) ProtoReflect() protoreflect.Message {
mi := &file_resource_proto_msgTypes[31]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use QuotaUsageResponse.ProtoReflect.Descriptor instead.
func (*QuotaUsageResponse) Descriptor() ([]byte, []int) {
return file_resource_proto_rawDescGZIP(), []int{31}
}
func (x *QuotaUsageResponse) GetError() *ErrorResult {
if x != nil {
return x.Error
}
return nil
}
func (x *QuotaUsageResponse) GetUsage() int64 {
if x != nil {
return x.Usage
}
return 0
}
func (x *QuotaUsageResponse) GetLimit() int64 {
if x != nil {
return x.Limit
}
return 0
}
type WatchEvent_Resource struct {
state protoimpl.MessageState `protogen:"open.v1"`
Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
@@ -2494,7 +2602,7 @@ type WatchEvent_Resource struct {
func (x *WatchEvent_Resource) Reset() {
*x = WatchEvent_Resource{}
mi := &file_resource_proto_msgTypes[30]
mi := &file_resource_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2506,7 +2614,7 @@ func (x *WatchEvent_Resource) String() string {
func (*WatchEvent_Resource) ProtoMessage() {}
func (x *WatchEvent_Resource) ProtoReflect() protoreflect.Message {
mi := &file_resource_proto_msgTypes[30]
mi := &file_resource_proto_msgTypes[32]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2553,7 +2661,7 @@ type BulkResponse_Summary struct {
func (x *BulkResponse_Summary) Reset() {
*x = BulkResponse_Summary{}
mi := &file_resource_proto_msgTypes[31]
mi := &file_resource_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2565,7 +2673,7 @@ func (x *BulkResponse_Summary) String() string {
func (*BulkResponse_Summary) ProtoMessage() {}
func (x *BulkResponse_Summary) ProtoReflect() protoreflect.Message {
mi := &file_resource_proto_msgTypes[31]
mi := &file_resource_proto_msgTypes[33]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2649,7 +2757,7 @@ type BulkResponse_Rejected struct {
func (x *BulkResponse_Rejected) Reset() {
*x = BulkResponse_Rejected{}
mi := &file_resource_proto_msgTypes[32]
mi := &file_resource_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2661,7 +2769,7 @@ func (x *BulkResponse_Rejected) String() string {
func (*BulkResponse_Rejected) ProtoMessage() {}
func (x *BulkResponse_Rejected) ProtoReflect() protoreflect.Message {
mi := &file_resource_proto_msgTypes[32]
mi := &file_resource_proto_msgTypes[34]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2718,7 +2826,7 @@ type ListManagedObjectsResponse_Item struct {
func (x *ListManagedObjectsResponse_Item) Reset() {
*x = ListManagedObjectsResponse_Item{}
mi := &file_resource_proto_msgTypes[33]
mi := &file_resource_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2730,7 +2838,7 @@ func (x *ListManagedObjectsResponse_Item) String() string {
func (*ListManagedObjectsResponse_Item) ProtoMessage() {}
func (x *ListManagedObjectsResponse_Item) ProtoReflect() protoreflect.Message {
mi := &file_resource_proto_msgTypes[33]
mi := &file_resource_proto_msgTypes[35]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2801,7 +2909,7 @@ type CountManagedObjectsResponse_ResourceCount struct {
func (x *CountManagedObjectsResponse_ResourceCount) Reset() {
*x = CountManagedObjectsResponse_ResourceCount{}
mi := &file_resource_proto_msgTypes[34]
mi := &file_resource_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2813,7 +2921,7 @@ func (x *CountManagedObjectsResponse_ResourceCount) String() string {
func (*CountManagedObjectsResponse_ResourceCount) ProtoMessage() {}
func (x *CountManagedObjectsResponse_ResourceCount) ProtoReflect() protoreflect.Message {
mi := &file_resource_proto_msgTypes[34]
mi := &file_resource_proto_msgTypes[36]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2886,7 +2994,7 @@ type ResourceTableColumnDefinition_Properties struct {
func (x *ResourceTableColumnDefinition_Properties) Reset() {
*x = ResourceTableColumnDefinition_Properties{}
mi := &file_resource_proto_msgTypes[35]
mi := &file_resource_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2898,7 +3006,7 @@ func (x *ResourceTableColumnDefinition_Properties) String() string {
func (*ResourceTableColumnDefinition_Properties) ProtoMessage() {}
func (x *ResourceTableColumnDefinition_Properties) ProtoReflect() protoreflect.Message {
mi := &file_resource_proto_msgTypes[35]
mi := &file_resource_proto_msgTypes[37]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3320,68 +3428,85 @@ var file_resource_proto_rawDesc = string([]byte{
0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
0x12, 0x14, 0x0a, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52,
0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74,
0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x2a, 0x49,
0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43,
0x41, 0x54, 0x45, 0x44, 0x5f, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61,
0x6e, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43, 0x41, 0x54, 0x45,
0x44, 0x5f, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x2a, 0x4d, 0x0a, 0x16, 0x52, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63,
0x68, 0x56, 0x32, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00,
0x12, 0x09, 0x0a, 0x05, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45,
0x78, 0x61, 0x63, 0x74, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64,
0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x03, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65,
0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65,
0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b,
0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64,
0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44,
0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18,
0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74,
0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63,
0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0x4b, 0x0a, 0x09, 0x42, 0x75, 0x6c, 0x6b,
0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x42, 0x75, 0x6c, 0x6b, 0x50, 0x72, 0x6f,
0x63, 0x65, 0x73, 0x73, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x28, 0x01, 0x32, 0xd9, 0x01, 0x0a, 0x12, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x62, 0x0a, 0x13,
0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65,
0x63, 0x74, 0x73, 0x12, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43,
0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63,
0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x5f, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f,
0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x23, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a,
0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67,
0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x32, 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73,
0x12, 0x48, 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43,
0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65,
0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3b, 0x5a, 0x39, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61,
0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f,
0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0x3c,
0x0a, 0x11, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x6d, 0x0a, 0x12,
0x51, 0x75, 0x6f, 0x74, 0x61, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72,
0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12,
0x14, 0x0a, 0x05, 0x75, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05,
0x75, 0x73, 0x61, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03,
0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x2a, 0x49, 0x0a, 0x14, 0x52,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61,
0x74, 0x63, 0x68, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43, 0x41, 0x54, 0x45,
0x44, 0x5f, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00,
0x12, 0x14, 0x0a, 0x10, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43, 0x41, 0x54, 0x45, 0x44, 0x5f, 0x45,
0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x2a, 0x4d, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x56, 0x32,
0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a,
0x05, 0x55, 0x6e, 0x73, 0x65, 0x74, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63,
0x74, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54,
0x68, 0x61, 0x6e, 0x10, 0x03, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12,
0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b,
0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x55,
0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18,
0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65,
0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65,
0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x15, 0x2e,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x05,
0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76,
0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0x4b, 0x0a, 0x09, 0x42, 0x75, 0x6c, 0x6b, 0x53, 0x74, 0x6f,
0x72, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x42, 0x75, 0x6c, 0x6b, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73,
0x73, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c,
0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x2e, 0x42, 0x75, 0x6c, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x28, 0x01, 0x32, 0xd9, 0x01, 0x0a, 0x12, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62,
0x6a, 0x65, 0x63, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x62, 0x0a, 0x13, 0x43, 0x6f, 0x75,
0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73,
0x12, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e,
0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62,
0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5f, 0x0a,
0x12, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65,
0x63, 0x74, 0x73, 0x12, 0x23, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c,
0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74,
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x4f,
0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57,
0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a,
0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63,
0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x54, 0x0a, 0x06, 0x51, 0x75, 0x6f, 0x74, 0x61,
0x73, 0x12, 0x4a, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x55, 0x73, 0x61,
0x67, 0x65, 0x12, 0x1b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x51, 0x75,
0x6f, 0x74, 0x61, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x51, 0x75, 0x6f, 0x74, 0x61,
0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3b, 0x5a,
0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66,
0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f,
0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
})
var (
@@ -3397,7 +3522,7 @@ func file_resource_proto_rawDescGZIP() []byte {
}
var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 7)
var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 36)
var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 38)
var file_resource_proto_goTypes = []any{
(ResourceVersionMatch)(0), // 0: resource.ResourceVersionMatch
(ResourceVersionMatchV2)(0), // 1: resource.ResourceVersionMatchV2
@@ -3436,12 +3561,14 @@ var file_resource_proto_goTypes = []any{
(*ResourceTable)(nil), // 34: resource.ResourceTable
(*ResourceTableColumnDefinition)(nil), // 35: resource.ResourceTableColumnDefinition
(*ResourceTableRow)(nil), // 36: resource.ResourceTableRow
(*WatchEvent_Resource)(nil), // 37: resource.WatchEvent.Resource
(*BulkResponse_Summary)(nil), // 38: resource.BulkResponse.Summary
(*BulkResponse_Rejected)(nil), // 39: resource.BulkResponse.Rejected
(*ListManagedObjectsResponse_Item)(nil), // 40: resource.ListManagedObjectsResponse.Item
(*CountManagedObjectsResponse_ResourceCount)(nil), // 41: resource.CountManagedObjectsResponse.ResourceCount
(*ResourceTableColumnDefinition_Properties)(nil), // 42: resource.ResourceTableColumnDefinition.Properties
(*QuotaUsageRequest)(nil), // 37: resource.QuotaUsageRequest
(*QuotaUsageResponse)(nil), // 38: resource.QuotaUsageResponse
(*WatchEvent_Resource)(nil), // 39: resource.WatchEvent.Resource
(*BulkResponse_Summary)(nil), // 40: resource.BulkResponse.Summary
(*BulkResponse_Rejected)(nil), // 41: resource.BulkResponse.Rejected
(*ListManagedObjectsResponse_Item)(nil), // 42: resource.ListManagedObjectsResponse.Item
(*CountManagedObjectsResponse_ResourceCount)(nil), // 43: resource.CountManagedObjectsResponse.ResourceCount
(*ResourceTableColumnDefinition_Properties)(nil), // 44: resource.ResourceTableColumnDefinition.Properties
}
var file_resource_proto_depIdxs = []int32{
10, // 0: resource.ErrorResult.details:type_name -> resource.ErrorDetails
@@ -3465,51 +3592,55 @@ var file_resource_proto_depIdxs = []int32{
9, // 18: resource.ListResponse.error:type_name -> resource.ErrorResult
21, // 19: resource.WatchRequest.options:type_name -> resource.ListOptions
3, // 20: resource.WatchEvent.type:type_name -> resource.WatchEvent.Type
37, // 21: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource
37, // 22: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource
39, // 21: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource
39, // 22: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource
7, // 23: resource.BulkRequest.key:type_name -> resource.ResourceKey
4, // 24: resource.BulkRequest.action:type_name -> resource.BulkRequest.Action
9, // 25: resource.BulkResponse.error:type_name -> resource.ErrorResult
38, // 26: resource.BulkResponse.summary:type_name -> resource.BulkResponse.Summary
39, // 27: resource.BulkResponse.rejected:type_name -> resource.BulkResponse.Rejected
40, // 28: resource.ListManagedObjectsResponse.items:type_name -> resource.ListManagedObjectsResponse.Item
40, // 26: resource.BulkResponse.summary:type_name -> resource.BulkResponse.Summary
41, // 27: resource.BulkResponse.rejected:type_name -> resource.BulkResponse.Rejected
42, // 28: resource.ListManagedObjectsResponse.items:type_name -> resource.ListManagedObjectsResponse.Item
9, // 29: resource.ListManagedObjectsResponse.error:type_name -> resource.ErrorResult
41, // 30: resource.CountManagedObjectsResponse.items:type_name -> resource.CountManagedObjectsResponse.ResourceCount
43, // 30: resource.CountManagedObjectsResponse.items:type_name -> resource.CountManagedObjectsResponse.ResourceCount
9, // 31: resource.CountManagedObjectsResponse.error:type_name -> resource.ErrorResult
5, // 32: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus
35, // 33: resource.ResourceTable.columns:type_name -> resource.ResourceTableColumnDefinition
36, // 34: resource.ResourceTable.rows:type_name -> resource.ResourceTableRow
6, // 35: resource.ResourceTableColumnDefinition.type:type_name -> resource.ResourceTableColumnDefinition.ColumnType
42, // 36: resource.ResourceTableColumnDefinition.properties:type_name -> resource.ResourceTableColumnDefinition.Properties
44, // 36: resource.ResourceTableColumnDefinition.properties:type_name -> resource.ResourceTableColumnDefinition.Properties
7, // 37: resource.ResourceTableRow.key:type_name -> resource.ResourceKey
7, // 38: resource.BulkResponse.Rejected.key:type_name -> resource.ResourceKey
4, // 39: resource.BulkResponse.Rejected.action:type_name -> resource.BulkRequest.Action
7, // 40: resource.ListManagedObjectsResponse.Item.object:type_name -> resource.ResourceKey
18, // 41: resource.ResourceStore.Read:input_type -> resource.ReadRequest
12, // 42: resource.ResourceStore.Create:input_type -> resource.CreateRequest
14, // 43: resource.ResourceStore.Update:input_type -> resource.UpdateRequest
16, // 44: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest
22, // 45: resource.ResourceStore.List:input_type -> resource.ListRequest
24, // 46: resource.ResourceStore.Watch:input_type -> resource.WatchRequest
26, // 47: resource.BulkStore.BulkProcess:input_type -> resource.BulkRequest
30, // 48: resource.ManagedObjectIndex.CountManagedObjects:input_type -> resource.CountManagedObjectsRequest
28, // 49: resource.ManagedObjectIndex.ListManagedObjects:input_type -> resource.ListManagedObjectsRequest
32, // 50: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest
19, // 51: resource.ResourceStore.Read:output_type -> resource.ReadResponse
13, // 52: resource.ResourceStore.Create:output_type -> resource.CreateResponse
15, // 53: resource.ResourceStore.Update:output_type -> resource.UpdateResponse
17, // 54: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse
23, // 55: resource.ResourceStore.List:output_type -> resource.ListResponse
25, // 56: resource.ResourceStore.Watch:output_type -> resource.WatchEvent
27, // 57: resource.BulkStore.BulkProcess:output_type -> resource.BulkResponse
31, // 58: resource.ManagedObjectIndex.CountManagedObjects:output_type -> resource.CountManagedObjectsResponse
29, // 59: resource.ManagedObjectIndex.ListManagedObjects:output_type -> resource.ListManagedObjectsResponse
33, // 60: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse
51, // [51:61] is the sub-list for method output_type
41, // [41:51] is the sub-list for method input_type
41, // [41:41] is the sub-list for extension type_name
41, // [41:41] is the sub-list for extension extendee
0, // [0:41] is the sub-list for field type_name
7, // 38: resource.QuotaUsageRequest.key:type_name -> resource.ResourceKey
9, // 39: resource.QuotaUsageResponse.error:type_name -> resource.ErrorResult
7, // 40: resource.BulkResponse.Rejected.key:type_name -> resource.ResourceKey
4, // 41: resource.BulkResponse.Rejected.action:type_name -> resource.BulkRequest.Action
7, // 42: resource.ListManagedObjectsResponse.Item.object:type_name -> resource.ResourceKey
18, // 43: resource.ResourceStore.Read:input_type -> resource.ReadRequest
12, // 44: resource.ResourceStore.Create:input_type -> resource.CreateRequest
14, // 45: resource.ResourceStore.Update:input_type -> resource.UpdateRequest
16, // 46: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest
22, // 47: resource.ResourceStore.List:input_type -> resource.ListRequest
24, // 48: resource.ResourceStore.Watch:input_type -> resource.WatchRequest
26, // 49: resource.BulkStore.BulkProcess:input_type -> resource.BulkRequest
30, // 50: resource.ManagedObjectIndex.CountManagedObjects:input_type -> resource.CountManagedObjectsRequest
28, // 51: resource.ManagedObjectIndex.ListManagedObjects:input_type -> resource.ListManagedObjectsRequest
32, // 52: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest
37, // 53: resource.Quotas.GetQuotaUsage:input_type -> resource.QuotaUsageRequest
19, // 54: resource.ResourceStore.Read:output_type -> resource.ReadResponse
13, // 55: resource.ResourceStore.Create:output_type -> resource.CreateResponse
15, // 56: resource.ResourceStore.Update:output_type -> resource.UpdateResponse
17, // 57: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse
23, // 58: resource.ResourceStore.List:output_type -> resource.ListResponse
25, // 59: resource.ResourceStore.Watch:output_type -> resource.WatchEvent
27, // 60: resource.BulkStore.BulkProcess:output_type -> resource.BulkResponse
31, // 61: resource.ManagedObjectIndex.CountManagedObjects:output_type -> resource.CountManagedObjectsResponse
29, // 62: resource.ManagedObjectIndex.ListManagedObjects:output_type -> resource.ListManagedObjectsResponse
33, // 63: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse
38, // 64: resource.Quotas.GetQuotaUsage:output_type -> resource.QuotaUsageResponse
54, // [54:65] is the sub-list for method output_type
43, // [43:54] is the sub-list for method input_type
43, // [43:43] is the sub-list for extension type_name
43, // [43:43] is the sub-list for extension extendee
0, // [0:43] is the sub-list for field type_name
}
func init() { file_resource_proto_init() }
@@ -3524,9 +3655,9 @@ func file_resource_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_resource_proto_rawDesc), len(file_resource_proto_rawDesc)),
NumEnums: 7,
NumMessages: 36,
NumMessages: 38,
NumExtensions: 0,
NumServices: 4,
NumServices: 5,
},
GoTypes: file_resource_proto_goTypes,
DependencyIndexes: file_resource_proto_depIdxs,
@@ -709,3 +709,94 @@ var Diagnostics_ServiceDesc = grpc.ServiceDesc{
Streams: []grpc.StreamDesc{},
Metadata: "resource.proto",
}
const (
Quotas_GetQuotaUsage_FullMethodName = "/resource.Quotas/GetQuotaUsage"
)
// QuotasClient is the client API for Quotas service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type QuotasClient interface {
// Get current quota usage and limits
GetQuotaUsage(ctx context.Context, in *QuotaUsageRequest, opts ...grpc.CallOption) (*QuotaUsageResponse, error)
}
type quotasClient struct {
cc grpc.ClientConnInterface
}
func NewQuotasClient(cc grpc.ClientConnInterface) QuotasClient {
return &quotasClient{cc}
}
func (c *quotasClient) GetQuotaUsage(ctx context.Context, in *QuotaUsageRequest, opts ...grpc.CallOption) (*QuotaUsageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(QuotaUsageResponse)
err := c.cc.Invoke(ctx, Quotas_GetQuotaUsage_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// QuotasServer is the server API for Quotas service.
// All implementations should embed UnimplementedQuotasServer
// for forward compatibility
type QuotasServer interface {
// Get current quota usage and limits
GetQuotaUsage(context.Context, *QuotaUsageRequest) (*QuotaUsageResponse, error)
}
// UnimplementedQuotasServer should be embedded to have forward compatible implementations.
type UnimplementedQuotasServer struct {
}
func (UnimplementedQuotasServer) GetQuotaUsage(context.Context, *QuotaUsageRequest) (*QuotaUsageResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetQuotaUsage not implemented")
}
// UnsafeQuotasServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to QuotasServer will
// result in compilation errors.
type UnsafeQuotasServer interface {
mustEmbedUnimplementedQuotasServer()
}
func RegisterQuotasServer(s grpc.ServiceRegistrar, srv QuotasServer) {
s.RegisterService(&Quotas_ServiceDesc, srv)
}
func _Quotas_GetQuotaUsage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(QuotaUsageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(QuotasServer).GetQuotaUsage(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Quotas_GetQuotaUsage_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(QuotasServer).GetQuotaUsage(ctx, req.(*QuotaUsageRequest))
}
return interceptor(ctx, in, info, handler)
}
// Quotas_ServiceDesc is the grpc.ServiceDesc for Quotas service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Quotas_ServiceDesc = grpc.ServiceDesc{
ServiceName: "resource.Quotas",
HandlerType: (*QuotasServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetQuotaUsage",
Handler: _Quotas_GetQuotaUsage_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "resource.proto",
}
+1
View File
@@ -312,6 +312,7 @@ func (s *service) starting(ctx context.Context) error {
resourcepb.RegisterManagedObjectIndexServer(srv, server)
resourcepb.RegisterBlobStoreServer(srv, server)
resourcepb.RegisterDiagnosticsServer(srv, server)
resourcepb.RegisterQuotasServer(srv, server)
grpc_health_v1.RegisterHealthServer(srv, healthService)
// register reflection service
@@ -0,0 +1,202 @@
package identity
import (
"context"
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"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 TestIntegrationTeamSearch(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
// TODO: Add rest.Mode3 and rest.Mode4 when they're supported
modes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2}
for _, mode := range modes {
t.Run(fmt.Sprintf("Team search with dual writer mode %d", mode), func(t *testing.T) {
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
AppModeProduction: false,
DisableAnonymous: true,
APIServerStorageType: "unified",
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
"teams.iam.grafana.app": {
DualWriterMode: mode,
},
},
EnableFeatureToggles: []string{
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
featuremgmt.FlagKubernetesAuthnMutation,
},
})
doTeamSearchTests(t, helper)
})
}
}
func doTeamSearchTests(t *testing.T, helper *apis.K8sTestHelper) {
ctx := context.Background()
namespace := helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID())
// Create teams for testing
teamClient := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
Namespace: namespace,
GVR: gvrTeams,
})
team1, err := teamClient.Resource.Create(ctx, helper.LoadYAMLOrJSONFile("testdata/team-test-create-v0.yaml"), metav1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, team1)
// Create a second team with a different name
team2YAML := helper.LoadYAMLOrJSONFile("testdata/team-test-create-v0.yaml")
team2YAML.Object["metadata"].(map[string]interface{})["name"] = "testteam2"
team2YAML.Object["spec"].(map[string]interface{})["title"] = "Another Team"
team2YAML.Object["spec"].(map[string]interface{})["email"] = "anotherteam@example.com"
team2, err := teamClient.Resource.Create(ctx, team2YAML, metav1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, team2)
t.Run("should search teams without query parameter", func(t *testing.T) {
path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams", namespace)
var result iamv0alpha1.TeamSearchResults
response := apis.DoRequest(helper, apis.RequestParams{
User: helper.Org1.Admin,
Method: http.MethodGet,
Path: path,
}, &result)
require.NotNil(t, response)
require.Equal(t, http.StatusOK, response.Response.StatusCode)
require.NotNil(t, response.Result)
require.GreaterOrEqual(t, result.TotalHits, int64(2), "should find at least 2 teams")
require.GreaterOrEqual(t, len(result.Hits), 2, "should return at least 2 hits")
for _, hit := range result.Hits {
if hit.Name == team1.GetName() {
require.Equal(t, "Test Team 1", hit.Title)
require.Equal(t, "testteam1@example123.com", hit.Email)
}
if hit.Name == team2.GetName() {
require.Equal(t, "Another Team", hit.Title)
require.Equal(t, "anotherteam@example.com", hit.Email)
}
}
})
t.Run("should search teams with query parameter", func(t *testing.T) {
path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?query=another", namespace)
var result iamv0alpha1.TeamSearchResults
response := apis.DoRequest(helper, apis.RequestParams{
User: helper.Org1.Admin,
Method: http.MethodGet,
Path: path,
}, &result)
require.NotNil(t, response)
require.Equal(t, http.StatusOK, response.Response.StatusCode)
require.NotNil(t, response.Result)
require.Equal(t, result.TotalHits, int64(1), "should find 1 team matching 'another'")
require.Equal(t, len(result.Hits), 1, "should return 1 hit")
require.Equal(t, result.Hits[0].Name, team2.GetName())
require.Equal(t, result.Hits[0].Title, "Another Team")
require.Equal(t, result.Hits[0].Email, "anotherteam@example.com")
})
t.Run("should return no results when query does not match any teams", func(t *testing.T) {
path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?query=nonexistent", namespace)
var result iamv0alpha1.TeamSearchResults
response := apis.DoRequest(helper, apis.RequestParams{
User: helper.Org1.Admin,
Method: http.MethodGet,
Path: path,
}, &result)
require.NotNil(t, response)
require.Equal(t, http.StatusOK, response.Response.StatusCode)
require.NotNil(t, response.Result)
require.Equal(t, int64(0), result.TotalHits, "should return 0 hits when query does not match any teams")
require.Equal(t, 0, len(result.Hits), "should return 0 hits when query does not match any teams")
})
t.Run("should search teams with limit parameter", func(t *testing.T) {
path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?limit=1", namespace)
var result iamv0alpha1.TeamSearchResults
response := apis.DoRequest(helper, apis.RequestParams{
User: helper.Org1.Admin,
Method: http.MethodGet,
Path: path,
}, &result)
require.NotNil(t, response)
require.Equal(t, http.StatusOK, response.Response.StatusCode)
require.NotNil(t, response.Result)
require.Equal(t, 1, len(result.Hits), "should return 1 hit when limit is 1")
})
t.Run("should search teams with pagination", func(t *testing.T) {
// First page
path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?limit=1&page=1", namespace)
var result1 iamv0alpha1.TeamSearchResults
response1 := apis.DoRequest(helper, apis.RequestParams{
User: helper.Org1.Admin,
Method: http.MethodGet,
Path: path,
}, &result1)
require.NotNil(t, response1)
require.Equal(t, http.StatusOK, response1.Response.StatusCode)
require.NotNil(t, response1.Result)
require.Equal(t, int64(0), result1.Offset, "first page should have offset 0")
// Second page
path2 := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?limit=1&page=2", namespace)
var result2 iamv0alpha1.TeamSearchResults
response2 := apis.DoRequest(helper, apis.RequestParams{
User: helper.Org1.Admin,
Method: http.MethodGet,
Path: path2,
}, &result2)
require.NotNil(t, response2)
require.Equal(t, http.StatusOK, response2.Response.StatusCode)
require.NotNil(t, response2.Result)
require.Equal(t, int64(1), result2.Offset, "second page should have offset 1")
})
t.Run("should search teams with offset parameter", func(t *testing.T) {
path := fmt.Sprintf("/apis/iam.grafana.app/v0alpha1/namespaces/%s/searchTeams?offset=1&limit=1", namespace)
var result iamv0alpha1.TeamSearchResults
response := apis.DoRequest(helper, apis.RequestParams{
User: helper.Org1.Admin,
Method: http.MethodGet,
Path: path,
}, &result)
require.NotNil(t, response)
require.Equal(t, http.StatusOK, response.Response.StatusCode)
require.NotNil(t, response.Result)
require.GreaterOrEqual(t, result.TotalHits, int64(2), "should find at least 2 teams")
require.Equal(t, 1, len(result.Hits), "should return 1 hit")
require.Equal(t, int64(1), result.Offset, "should return offset 1")
})
}
@@ -3020,6 +3020,9 @@
"type": "string",
"default": ""
},
"regexApplyTo": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRegexApplyTo"
},
"skipUrlSync": {
"type": "boolean",
"default": false
@@ -3930,6 +3933,14 @@
"onTimeRangeChanged"
]
},
"com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRegexApplyTo": {
"description": "Determine whether regex applies to variable value or display text\nAccepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)",
"type": "string",
"enum": [
"value",
"text"
]
},
"com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableSort": {
"description": "Sort variable options\nAccepted values are:\n`disabled`: No sorting\n`alphabeticalAsc`: Alphabetical ASC\n`alphabeticalDesc`: Alphabetical DESC\n`numericalAsc`: Numerical ASC\n`numericalDesc`: Numerical DESC\n`alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC\n`alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC\n`naturalAsc`: Natural ASC\n`naturalDesc`: Natural DESC\nVariableSort enum with default value",
"type": "string",
@@ -3047,6 +3047,9 @@
"type": "string",
"default": ""
},
"regexApplyTo": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableRegexApplyTo"
},
"skipUrlSync": {
"type": "boolean",
"default": false
@@ -3957,6 +3960,14 @@
"onTimeRangeChanged"
]
},
"com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableRegexApplyTo": {
"description": "Determine whether regex applies to variable value or display text\nAccepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)",
"type": "string",
"enum": [
"value",
"text"
]
},
"com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableSort": {
"description": "Sort variable options\nAccepted values are:\n`disabled`: No sorting\n`alphabeticalAsc`: Alphabetical ASC\n`alphabeticalDesc`: Alphabetical DESC\n`numericalAsc`: Numerical ASC\n`numericalDesc`: Numerical DESC\n`alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC\n`alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC\n`naturalAsc`: Natural ASC\n`naturalDesc`: Natural DESC\nVariableSort enum with default value",
"type": "string",
@@ -323,6 +323,7 @@ exports[`Given a scene with custom quick ranges should save quick ranges to save
},
"refresh": 1,
"regex": "",
"regexApplyTo": "value",
"type": "query",
},
{
@@ -1049,6 +1050,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
},
"refresh": 1,
"regex": "",
"regexApplyTo": "value",
"type": "query",
},
{
@@ -1408,6 +1410,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
},
"refresh": 1,
"regex": "",
"regexApplyTo": "value",
"type": "query",
},
{
@@ -173,6 +173,7 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model
},
"refresh": "onDashboardLoad",
"regex": "regex1",
"regexApplyTo": "value",
"skipUrlSync": false,
"sort": "alphabeticalDesc",
},
@@ -141,6 +141,7 @@ describe('sceneVariablesSetToVariables', () => {
"query": "query",
"refresh": 1,
"regex": "",
"regexApplyTo": "value",
"staticOptions": [
{
"text": "test",
@@ -205,6 +206,7 @@ describe('sceneVariablesSetToVariables', () => {
"query": "query",
"refresh": 1,
"regex": "",
"regexApplyTo": "value",
"staticOptions": [
{
"text": "test",
@@ -1084,6 +1086,7 @@ describe('sceneVariablesSetToVariables', () => {
},
"refresh": "onDashboardLoad",
"regex": "",
"regexApplyTo": "value",
"skipUrlSync": false,
"sort": "disabled",
"staticOptions": [
@@ -84,6 +84,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
sort: variable.state.sort,
refresh: variable.state.refresh,
regex: variable.state.regex,
regexApplyTo: variable.state.regexApplyTo,
allValue: variable.state.allValue,
includeAll: variable.state.includeAll,
multi: variable.state.isMulti,
@@ -375,6 +376,7 @@ export function sceneVariablesSetToSchemaV2Variables(
sort: transformSortVariableToEnum(variable.state.sort),
refresh: transformVariableRefreshToEnum(variable.state.refresh),
regex: variable.state.regex ?? '',
regexApplyTo: variable.state.regexApplyTo ?? 'value',
allValue: variable.state.allValue,
includeAll: variable.state.includeAll || false,
multi: variable.state.isMulti || false,
@@ -366,6 +366,7 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S
sort: transformSortVariableToEnumV1(variable.spec.sort),
refresh: transformVariableRefreshToEnumV1(variable.spec.refresh),
regex: variable.spec.regex,
regexApplyTo: variable.spec.regexApplyTo,
allValue: variable.spec.allValue || undefined,
includeAll: variable.spec.includeAll,
defaultToAll: Boolean(variable.spec.includeAll),
@@ -283,6 +283,7 @@ describe('transformSceneToSaveModelSchemaV2', () => {
sort: VariableSortV1.alphabeticalDesc,
refresh: VariableRefresh.onDashboardLoad,
regex: 'regex1',
regexApplyTo: 'value',
allValue: '*',
includeAll: true,
isMulti: true,
@@ -71,6 +71,7 @@ describe('QueryVariableEditorForm', () => {
const mockOnQueryChange = jest.fn();
const mockOnLegacyQueryChange = jest.fn();
const mockOnRegExChange = jest.fn();
const mockOnRegexApplyToChange = jest.fn();
const mockOnSortChange = jest.fn();
const mockOnRefreshChange = jest.fn();
const mockOnMultiChange = jest.fn();
@@ -89,6 +90,8 @@ describe('QueryVariableEditorForm', () => {
timeRange: getDefaultTimeRange(),
regex: '.*',
onRegExChange: mockOnRegExChange,
regexApplyTo: 'value',
onRegexApplyToChange: mockOnRegexApplyToChange,
sort: VariableSort.alphabeticalAsc,
onSortChange: mockOnSortChange,
refresh: VariableRefresh.onDashboardLoad,
@@ -126,6 +129,9 @@ describe('QueryVariableEditorForm', () => {
const regexInput = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2
);
const regexApplyToSelect = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2
);
const sortSelect = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2
);
@@ -154,6 +160,8 @@ describe('QueryVariableEditorForm', () => {
expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source');
expect(regexInput).toBeInTheDocument();
expect(regexInput).toHaveValue('.*');
expect(regexApplyToSelect).toBeInTheDocument();
expect(getByRole('radio', { name: 'Variable value' })).toBeChecked();
expect(sortSelect).toBeInTheDocument();
expect(sortSelect).toHaveTextContent('Alphabetical (asc)');
expect(refreshSelect).toBeInTheDocument();
@@ -213,6 +221,21 @@ describe('QueryVariableEditorForm', () => {
).toBe('.?');
});
it('should call onRegexApplyToChange when selecting the regex apply to option', async () => {
const {
renderer: { getByTestId },
} = await setup();
const regexApplyToSelect = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2
);
await userEvent.click(regexApplyToSelect);
const anotherOption = screen.getByText('Display text');
await userEvent.click(anotherOption);
expect(mockOnRegexApplyToChange).toHaveBeenCalledTimes(1);
expect(mockOnRegexApplyToChange).toHaveBeenCalledWith('text');
});
it('should call onSortChange when changing the sort', async () => {
const {
renderer: { getByTestId },
@@ -1,14 +1,15 @@
import { FormEvent, useCallback } from 'react';
import { useAsync } from 'react-use';
import { DataSourceInstanceSettings, SelectableValue, TimeRange } from '@grafana/data';
import { DataSourceInstanceSettings, SelectableValue, TimeRange, VariableRegexApplyTo } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { getDataSourceSrv } from '@grafana/runtime';
import { QueryVariable } from '@grafana/scenes';
import { DataSourceRef, VariableRefresh, VariableSort } from '@grafana/schema';
import { Field, TextLink } from '@grafana/ui';
import { Field } from '@grafana/ui';
import { QueryEditor } from 'app/features/dashboard-scene/settings/variables/components/QueryEditor';
import { QueryVariableRegexForm } from 'app/features/dashboard-scene/settings/variables/components/QueryVariableRegexForm';
import { SelectionOptionsForm } from 'app/features/dashboard-scene/settings/variables/components/SelectionOptionsForm';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor';
@@ -21,7 +22,6 @@ import {
} from 'app/features/variables/query/QueryVariableStaticOptions';
import { VariableLegend } from './VariableLegend';
import { VariableTextAreaField } from './VariableTextAreaField';
type VariableQueryType = QueryVariable['state']['query'];
@@ -34,6 +34,8 @@ interface QueryVariableEditorFormProps {
timeRange: TimeRange;
regex: string | null;
onRegExChange: (event: FormEvent<HTMLTextAreaElement>) => void;
regexApplyTo?: VariableRegexApplyTo;
onRegexApplyToChange?: (event: VariableRegexApplyTo) => void;
sort: VariableSort;
onSortChange: (option: SelectableValue<VariableSort>) => void;
refresh: VariableRefresh;
@@ -61,6 +63,8 @@ export function QueryVariableEditorForm({
timeRange,
regex,
onRegExChange,
regexApplyTo,
onRegexApplyToChange,
sort,
onSortChange,
refresh,
@@ -131,32 +135,11 @@ export function QueryVariableEditorForm({
/>
)}
<VariableTextAreaField
defaultValue={regex ?? ''}
name={t('dashboard-scene.query-variable-editor-form.name-regex', 'Regex')}
description={
<div>
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-optional">
Optional, if you want to extract part of a series name or metric node segment.
</Trans>
<br />
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-examples">
Named capture groups can be used to separate the display text and value (
<TextLink
href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex#filter-and-modify-using-named-text-and-value-capture-groups"
external
>
see examples
</TextLink>
).
</Trans>
</div>
}
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
onBlur={onRegExChange}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
width={52}
<QueryVariableRegexForm
regex={regex}
regexApplyTo={regexApplyTo}
onRegExChange={onRegExChange}
onRegexApplyToChange={onRegexApplyToChange}
/>
<QueryVariableSortSelect
@@ -0,0 +1,96 @@
import { render, fireEvent } from '@testing-library/react';
import { selectors } from '@grafana/e2e-selectors';
import { QueryVariableRegexForm } from './QueryVariableRegexForm';
describe('QueryVariableRegexForm', () => {
const onRegExChange = jest.fn();
const onRegexApplyToChange = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('should render the form fields correctly', () => {
const { getByTestId, getByRole } = render(
<QueryVariableRegexForm
regex=".*test.*"
regexApplyTo="value"
onRegExChange={onRegExChange}
onRegexApplyToChange={onRegexApplyToChange}
/>
);
const regexInput = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2
);
const regexApplyToField = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2
);
expect(regexInput).toBeInTheDocument();
expect(regexInput).toHaveValue('.*test.*');
expect(regexApplyToField).toBeInTheDocument();
expect(getByRole('radio', { name: 'Variable value' })).toBeChecked();
expect(getByRole('radio', { name: 'Display text' })).not.toBeChecked();
});
it('should render with "Display text" option selected', () => {
const { getByRole } = render(
<QueryVariableRegexForm
regex=""
regexApplyTo="text"
onRegExChange={onRegExChange}
onRegexApplyToChange={onRegexApplyToChange}
/>
);
expect(getByRole('radio', { name: 'Display text' })).toBeChecked();
expect(getByRole('radio', { name: 'Variable value' })).not.toBeChecked();
});
it('should default to "Variable value" when regexApplyTo is not provided', () => {
const { getByRole } = render(
<QueryVariableRegexForm regex="" onRegExChange={onRegExChange} onRegexApplyToChange={onRegexApplyToChange} />
);
expect(getByRole('radio', { name: 'Variable value' })).toBeChecked();
});
it('should call onRegExChange when regex input is blurred', () => {
const { getByTestId } = render(
<QueryVariableRegexForm
regex=".*"
regexApplyTo="value"
onRegExChange={onRegExChange}
onRegexApplyToChange={onRegexApplyToChange}
/>
);
const regexInput = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2
);
fireEvent.blur(regexInput);
expect(onRegExChange).toHaveBeenCalledTimes(1);
});
it('should call onRegexApplyToChange when radio option is changed', () => {
const { getByRole } = render(
<QueryVariableRegexForm
regex=""
regexApplyTo="value"
onRegExChange={onRegExChange}
onRegexApplyToChange={onRegexApplyToChange}
/>
);
const displayTextOption = getByRole('radio', { name: 'Display text' });
fireEvent.click(displayTextOption);
expect(onRegexApplyToChange).toHaveBeenCalledTimes(1);
expect(onRegexApplyToChange).toHaveBeenCalledWith('text');
});
});
@@ -0,0 +1,91 @@
import { useMemo, FormEvent } from 'react';
import { VariableRegexApplyTo, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { Field, Stack, TextLink, RadioButtonGroup, Box } from '@grafana/ui';
import { VariableTextAreaField } from '../components/VariableTextAreaField';
interface Props {
regex: string | null;
onRegExChange: (event: FormEvent<HTMLTextAreaElement>) => void;
regexApplyTo?: VariableRegexApplyTo;
onRegexApplyToChange?: (option: VariableRegexApplyTo) => void;
}
export function QueryVariableRegexForm({ regex, regexApplyTo, onRegExChange, onRegexApplyToChange }: Props) {
const APPLY_REGEX_TO_OPTIONS: Array<SelectableValue<VariableRegexApplyTo>> = useMemo(
() => [
{
label: t('dashboard-scene.query-variable-editor-form.regex-apply-to-options.label.value', 'Variable value'),
value: 'value',
},
{
label: t('dashboard-scene.query-variable-editor-form.regex-apply-to-options.label.text', 'Display text'),
value: 'text',
},
],
[]
);
const regexApplyToValue = useMemo(
() => APPLY_REGEX_TO_OPTIONS.find((o) => o.value === regexApplyTo)?.value ?? APPLY_REGEX_TO_OPTIONS[0].value,
[regexApplyTo, APPLY_REGEX_TO_OPTIONS]
);
return (
<Box marginBottom={2}>
<Stack direction="column" gap={2}>
<VariableTextAreaField
defaultValue={regex ?? ''}
name={t('dashboard-scene.query-variable-editor-form.name-regex', 'Regex')}
description={
<div>
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-optional">
Optional, if you want to extract part of a series name or metric node segment.
</Trans>
<br />
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-examples">
Named capture groups can be used to separate the display text and value (
<TextLink
href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex#filter-and-modify-using-named-text-and-value-capture-groups"
external
>
see examples
</TextLink>
).
</Trans>
</div>
}
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
onBlur={onRegExChange}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
width={52}
noMargin
/>
{onRegexApplyToChange && (
<Field
label={t('dashboard-scene.query-variable-editor-form.label-regex-apply-to', 'Apply regex to')}
description={t(
'dashboard-scene.query-variable-editor-form.description-regex-apply-to',
'Choose whether to apply the regex pattern to the variable value or display text'
)}
data-testid={
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2
}
noMargin
>
<RadioButtonGroup
options={APPLY_REGEX_TO_OPTIONS}
onChange={onRegexApplyToChange}
value={regexApplyToValue}
/>
</Field>
)}
</Stack>
</Box>
);
}
@@ -1,7 +1,6 @@
import { css } from '@emotion/css';
import { useId } from '@react-aria/utils';
import { FormEvent, PropsWithChildren, ReactElement } from 'react';
import * as React from 'react';
import { FormEvent, PropsWithChildren, ReactElement, ReactNode } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Field, TextArea, useStyles2 } from '@grafana/ui';
@@ -17,7 +16,8 @@ interface VariableTextAreaFieldProps {
required?: boolean;
testId?: string;
onBlur?: (event: FormEvent<HTMLTextAreaElement>) => void;
description?: React.ReactNode;
description?: ReactNode;
noMargin?: boolean;
}
export function VariableTextAreaField({
@@ -31,13 +31,14 @@ export function VariableTextAreaField({
ariaLabel,
required,
width,
noMargin,
testId,
}: PropsWithChildren<VariableTextAreaFieldProps>): ReactElement {
const styles = useStyles2(getStyles);
const id = useId();
return (
<Field label={name} description={description} htmlFor={id}>
<Field label={name} description={description} htmlFor={id} noMargin={noMargin}>
<TextArea
id={id}
rows={2}
@@ -73,6 +73,7 @@ describe('QueryVariableEditor', () => {
},
query: 'my-query',
regex: '.*',
regexApplyTo: 'value',
sort: VariableSort.alphabeticalAsc,
refresh: VariableRefresh.onDashboardLoad,
isMulti: true,

Some files were not shown because too many files have changed in this diff Show More